Finish Notifications -> Conversation
This commit is contained in:
@@ -55,4 +55,11 @@ class DynamicLoadController extends Controller
|
|||||||
|
|
||||||
return response()->json( ['success' => true] );
|
return response()->json( ['success' => true] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getConversations( Request $request ){
|
||||||
|
$service = app(XenforoApiService::class);
|
||||||
|
$data = $service->getConversations(\Auth::user()->user_id);
|
||||||
|
|
||||||
|
return response()->json( $data );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,8 +49,9 @@ class XenforoApiService {
|
|||||||
|
|
||||||
public function getUserAlerts(int $userId): mixed
|
public function getUserAlerts(int $userId): mixed
|
||||||
{
|
{
|
||||||
if( app(XenforoService::class)->getXfUser($userId)?->alerts_unviewed > 0 )
|
if( app(XenforoService::class)->getXfUser($userId)?->alerts_unviewed > 0 ) {
|
||||||
return $this->get("alerts?page=1&cutoff=7days", $userId);
|
Cache::forget("xf_alerts_{$userId}");
|
||||||
|
}
|
||||||
|
|
||||||
return Cache::remember("xf_alerts_{$userId}", 60, function() use($userId) {
|
return Cache::remember("xf_alerts_{$userId}", 60, function() use($userId) {
|
||||||
return $this->get("alerts?page=1&cutoff=7days", $userId);
|
return $this->get("alerts?page=1&cutoff=7days", $userId);
|
||||||
@@ -63,4 +64,11 @@ class XenforoApiService {
|
|||||||
$this->post("alerts/marl-all", $userId );
|
$this->post("alerts/marl-all", $userId );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getConversations(int $userId): mixed
|
||||||
|
{
|
||||||
|
return Cache::remember("xf_conversations_{$userId}", 60, function() use($userId) {
|
||||||
|
return $this->get("conversations?page=1&receiver_id={$userId}", $userId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1554,7 +1554,7 @@ ul {
|
|||||||
|
|
||||||
|
|
||||||
/* File: resources/css/components/notifications.css */
|
/* File: resources/css/components/notifications.css */
|
||||||
.\$notifications {
|
.\$notifications, .\$conversations {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 8px);
|
top: calc(100% + 8px);
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.notifications {
|
.notifications, .conversations {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 8px);
|
top: calc(100% + 8px);
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import "easymde/dist/easymde.min.css";
|
|||||||
import { calculate as calculateHashes } from "./hashes.js";
|
import { calculate as calculateHashes } from "./hashes.js";
|
||||||
import hovercard from "./hovercard.js";
|
import hovercard from "./hovercard.js";
|
||||||
import notifications from "./notifications.js";
|
import notifications from "./notifications.js";
|
||||||
|
import conversations from "./conversations.js";
|
||||||
|
|
||||||
|
|
||||||
// Lucide icons.
|
// Lucide icons.
|
||||||
@@ -27,3 +28,6 @@ Alpine.store('hovercard', hovercard() );
|
|||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
Alpine.store('notifications', notifications() );
|
Alpine.store('notifications', notifications() );
|
||||||
|
|
||||||
|
// Conversations
|
||||||
|
Alpine.store('conversations', conversations() );
|
||||||
|
|||||||
103
resources/js/conversations.js
Normal file
103
resources/js/conversations.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/** @typedef { import('types/ConversationResponseItem.js').ConversationResponseItem} ConversationResponseItem */
|
||||||
|
|
||||||
|
export default function conversations() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
start: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {ConversationResponseItem[]}
|
||||||
|
*/
|
||||||
|
data: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
error: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
unviewed: 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request for getting notifications.
|
||||||
|
*
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
async getConversations() {
|
||||||
|
if( this.loading )
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const RESPONSE = await fetch('/api/dynamic/conversations', { credentials: "include", headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||||
|
|
||||||
|
if( !RESPONSE.ok )
|
||||||
|
throw new Error(RESPONSE.status)
|
||||||
|
|
||||||
|
let json = await RESPONSE.json()
|
||||||
|
if( !json.conversations )
|
||||||
|
throw new Error(RESPONSE.status);
|
||||||
|
|
||||||
|
this.data = json.conversations;
|
||||||
|
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.error = true;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} anchorEl
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
async open( anchorEl ){
|
||||||
|
|
||||||
|
if( this.start ){
|
||||||
|
this.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.start = !this.start;
|
||||||
|
if( this.start && !this.data ){
|
||||||
|
await this.getConversations();
|
||||||
|
}
|
||||||
|
|
||||||
|
if( this.start ){
|
||||||
|
Alpine.nextTick(() => window.refreshIcons(document.querySelector('.notifications')))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get unread(){
|
||||||
|
if( !this.data ){
|
||||||
|
return this.unviewed;
|
||||||
|
}
|
||||||
|
return this.data.filter(a => a.is_unread === 0 ).length;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
close(){
|
||||||
|
this.start = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
29
resources/js/types/ConversationResponseItem.js
Normal file
29
resources/js/types/ConversationResponseItem.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {Object} ConversationResponseItem
|
||||||
|
*
|
||||||
|
* @see app/Http/DynamicLoadController.php
|
||||||
|
*
|
||||||
|
* @property {string} username
|
||||||
|
* @property {object} recipients
|
||||||
|
* @property {boolean} is_starred
|
||||||
|
* @property {boolean} is_unread
|
||||||
|
* @property {boolean} can_edit
|
||||||
|
* @property {boolean} can_reply
|
||||||
|
* @property {boolean} can_invite
|
||||||
|
* @property {boolean} can_upload_attachment
|
||||||
|
* @property {boolean} view_url
|
||||||
|
* @property {number} conversation_id
|
||||||
|
* @property {string} title
|
||||||
|
* @property {number} user_id
|
||||||
|
* @property {number} start_date
|
||||||
|
* @property {boolean} open_invite
|
||||||
|
* @property {boolean} conversation_open
|
||||||
|
* @property {number} reply_count
|
||||||
|
* @property {number} recipient_count
|
||||||
|
* @property {number} first_message_id
|
||||||
|
* @property {number} last_message_date
|
||||||
|
* @property {number} last_message_id
|
||||||
|
* @property {number} last_message_user_id
|
||||||
|
* @property {Object} Starter
|
||||||
|
*/
|
||||||
|
export {}
|
||||||
68
resources/views/components/conversations.blade.php
Normal file
68
resources/views/components/conversations.blade.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<div
|
||||||
|
x-data
|
||||||
|
x-cloak
|
||||||
|
x-show="$store.conversations.start"
|
||||||
|
x-translation:enter="dropdown-enter"
|
||||||
|
x-transition:leave="dropdown-leave"
|
||||||
|
class="conversations"
|
||||||
|
@click.outside="$store.conversations.close()"
|
||||||
|
@keydown.escape.window="$store.conversations.close()"
|
||||||
|
>
|
||||||
|
<div class="notifications-header">
|
||||||
|
<span class="notifications-header-title">Private Messages</span>
|
||||||
|
<div class="notifications-header-actions">
|
||||||
|
<a href="{{ xfRoute('direct-messages/add') }}" class="btn">
|
||||||
|
<i data-lucide="plus" size="14"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ xfRoute('direct-messages') }}" class="btn">
|
||||||
|
<i data-lucide="external-link" size="14"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template x-if="$store.conversations.loading">
|
||||||
|
<div class="notifications-loading">
|
||||||
|
<i data-lucide="loader-2" class="spin"></i>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="$store.conversations.error">
|
||||||
|
<div class="notifications-empty">
|
||||||
|
<i data-lucide="alert-circle" size="24"></i>
|
||||||
|
<span>Failed to load messages.</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="$store.conversations.data && !$store.conversations.loading">
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<template x-if="$store.conversations.data.length === 0">
|
||||||
|
<div class="notifications-empty">
|
||||||
|
<i data-lucide="mail-off" size="24"></i>
|
||||||
|
<span>No new conversations.</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-for="conv in $store.conversations.data" :key="conv.conversation_id">
|
||||||
|
<a :href="`{{ xfRoute('direct-messages')}}/${conv.conversation_id}/#convMessage-${conv.last_message_id}`" class="notifications-item" :class="{ 'unread': conv.is_unread }">
|
||||||
|
<div class="notifications-avatar">
|
||||||
|
<template x-if="conv.Starter?.avatar_urls?.s">
|
||||||
|
<img :src="conv.Starter.avatar_urls.s" :alt="conv.username">
|
||||||
|
</template>
|
||||||
|
<template x-if="!conv.Starter?.avatar_urls?.s">
|
||||||
|
<span x-text="conv.username?.charAt(0).toUpperCase()"></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="notifications-content">
|
||||||
|
<span class="notifications-text" x-text="conv.title"></span>
|
||||||
|
<span class="notifications-date" x-text="new Date(conv.last_message_date * 1000).toLocaleDateString()"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="notifications-unread-dot" x-show="conv.is_unread"></div>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<button type="button" class="btn" x-show="$store.notifications.unread > 0" @click="$store.notifications.markAllRead()" title="Mark all as read">
|
<button type="button" class="btn" x-show="$store.notifications.unread > 0" @click="$store.notifications.markAllRead()" title="Mark all as read">
|
||||||
<i data-lucide="check-circle" size="14"></i>
|
<i data-lucide="check-circle" size="14"></i>
|
||||||
</button>
|
</button>
|
||||||
<a href="{{ xfRoute('account.alerts') }}" class="btn">
|
<a href="{{ xfRoute('account/alerts') }}" class="btn">
|
||||||
<i data-lucide="external-link" size="14"></i>
|
<i data-lucide="external-link" size="14"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,9 +62,19 @@
|
|||||||
|
|
||||||
@include('components.notifications')
|
@include('components.notifications')
|
||||||
</div>
|
</div>
|
||||||
<button class="btn">
|
<div x-data x-init="$store.conversations.unviewed = {{ \Auth::user()->conversations_unread }}" style="position:relative">
|
||||||
<i data-lucide="mail" size="18"></i>
|
<button type="button" class="btn" :class="{ 'active': $store.conversations.start }" @click="$store.conversations.open($el)" @click.outside="$store.conversations.close()">
|
||||||
</button>
|
<i data-lucide="mail" size="18"></i>
|
||||||
|
<span
|
||||||
|
class="topbar-badge"
|
||||||
|
:class="$store.conversations.unread > 9 ? 'topbar-badge--overflow' : ''"
|
||||||
|
x-show="$store.conversations.unread > 0"
|
||||||
|
x-text="$store.conversations.unread > 99 ? '99+' : $store.conversations.unread"
|
||||||
|
></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@include('components.conversations')
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<button class="btn">
|
<button class="btn">
|
||||||
<i data-lucide="settings" size="18"></i>
|
<i data-lucide="settings" size="18"></i>
|
||||||
|
|||||||
@@ -62,4 +62,5 @@ Route::get( '/api/dynamic/hovercard/{user_id}', [ \App\Http\Controllers\DynamicL
|
|||||||
Route::middleware('xf.auth')->controller(\App\Http\Controllers\DynamicLoadController::class)->name('dynamic.')->prefix('/api/dynamic/')->group(function(){
|
Route::middleware('xf.auth')->controller(\App\Http\Controllers\DynamicLoadController::class)->name('dynamic.')->prefix('/api/dynamic/')->group(function(){
|
||||||
Route::get('/notifications', 'getNotifications' )->name('notifications');
|
Route::get('/notifications', 'getNotifications' )->name('notifications');
|
||||||
Route::post('/notifications/mark-all-read', 'markAllRead' )->name('markallread');
|
Route::post('/notifications/mark-all-read', 'markAllRead' )->name('markallread');
|
||||||
|
Route::get('/conversations', 'getConversations' )->name('conversations');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user