feat: add in-app notifications system with bell icon, polling, and notifications page
This commit is contained in:
@@ -761,6 +761,44 @@ class DirectusService {
|
||||
return `${this.baseUrl}/assets/${fileId}${queryString ? '?' + queryString : ''}`
|
||||
}
|
||||
|
||||
// ── Notifications ──
|
||||
|
||||
async getNotifications(userHash, options = {}) {
|
||||
const params = {
|
||||
fields: ['id', 'type', 'reference_id', 'read', 'date_created', 'user_hash'],
|
||||
filter: { user_hash: { _eq: userHash } },
|
||||
sort: ['-date_created'],
|
||||
limit: options.limit || 50
|
||||
}
|
||||
if (options.unreadOnly) {
|
||||
params.filter.read = { _eq: false }
|
||||
}
|
||||
const response = await this.get('/items/notifications', params)
|
||||
return response.data
|
||||
}
|
||||
|
||||
async getUnreadCount(userHash) {
|
||||
const response = await this.get('/items/notifications', {
|
||||
filter: { user_hash: { _eq: userHash }, read: { _eq: false } },
|
||||
aggregate: { count: 'id' }
|
||||
})
|
||||
return parseInt(response.data?.[0]?.count?.id || '0', 10)
|
||||
}
|
||||
|
||||
async markNotificationRead(id) {
|
||||
return this.patch(`/items/notifications/${id}`, { read: true })
|
||||
}
|
||||
|
||||
async markAllNotificationsRead(userHash) {
|
||||
const unread = await this.getNotifications(userHash, { unreadOnly: true })
|
||||
const updates = unread.map(n => this.markNotificationRead(n.id))
|
||||
return Promise.all(updates)
|
||||
}
|
||||
|
||||
async deleteNotification(id) {
|
||||
return this.delete(`/items/notifications/${id}`)
|
||||
}
|
||||
|
||||
getThumbnailUrl(fileId, size = 300) {
|
||||
return this.getFileUrl(fileId, { width: size, height: size, fit: 'cover' })
|
||||
}
|
||||
|
||||
115
js/services/notifications.js
Normal file
115
js/services/notifications.js
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Notifications Service - Polls and manages user notifications
|
||||
*/
|
||||
|
||||
import { directus } from './directus.js'
|
||||
import { auth } from './auth.js'
|
||||
|
||||
class NotificationsService {
|
||||
constructor() {
|
||||
this.unreadCount = 0
|
||||
this.notifications = []
|
||||
this.listeners = new Set()
|
||||
this.pollTimer = null
|
||||
this.pollInterval = 60 * 1000 // 60 seconds
|
||||
this.userHash = null
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!auth.isLoggedIn()) return
|
||||
|
||||
this.userHash = await this._getUserHash()
|
||||
if (!this.userHash) return
|
||||
|
||||
await this.refresh()
|
||||
this.startPolling()
|
||||
}
|
||||
|
||||
async _getUserHash() {
|
||||
const user = await auth.getUser()
|
||||
return user?.id || null
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
if (!this.userHash) return
|
||||
|
||||
try {
|
||||
const [count, notifications] = await Promise.all([
|
||||
directus.getUnreadCount(this.userHash),
|
||||
directus.getNotifications(this.userHash)
|
||||
])
|
||||
this.unreadCount = count
|
||||
this.notifications = notifications
|
||||
this.notify()
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch notifications:', e)
|
||||
}
|
||||
}
|
||||
|
||||
startPolling() {
|
||||
this.stopPolling()
|
||||
this.pollTimer = setInterval(() => this.refresh(), this.pollInterval)
|
||||
|
||||
document.addEventListener('visibilitychange', this._onVisibility)
|
||||
}
|
||||
|
||||
stopPolling() {
|
||||
if (this.pollTimer) {
|
||||
clearInterval(this.pollTimer)
|
||||
this.pollTimer = null
|
||||
}
|
||||
document.removeEventListener('visibilitychange', this._onVisibility)
|
||||
}
|
||||
|
||||
_onVisibility = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
this.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
async markRead(id) {
|
||||
await directus.markNotificationRead(id)
|
||||
const n = this.notifications.find(n => n.id === id)
|
||||
if (n) {
|
||||
n.read = true
|
||||
this.unreadCount = Math.max(0, this.unreadCount - 1)
|
||||
this.notify()
|
||||
}
|
||||
}
|
||||
|
||||
async markAllRead() {
|
||||
if (!this.userHash) return
|
||||
await directus.markAllNotificationsRead(this.userHash)
|
||||
this.notifications.forEach(n => n.read = true)
|
||||
this.unreadCount = 0
|
||||
this.notify()
|
||||
}
|
||||
|
||||
async remove(id) {
|
||||
await directus.deleteNotification(id)
|
||||
this.notifications = this.notifications.filter(n => n.id !== id)
|
||||
this.unreadCount = this.notifications.filter(n => !n.read).length
|
||||
this.notify()
|
||||
}
|
||||
|
||||
subscribe(callback) {
|
||||
this.listeners.add(callback)
|
||||
return () => this.listeners.delete(callback)
|
||||
}
|
||||
|
||||
notify() {
|
||||
for (const cb of this.listeners) {
|
||||
cb(this.unreadCount, this.notifications)
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.stopPolling()
|
||||
this.listeners.clear()
|
||||
this.notifications = []
|
||||
this.unreadCount = 0
|
||||
this.userHash = null
|
||||
}
|
||||
}
|
||||
|
||||
export const notificationsService = new NotificationsService()
|
||||
Reference in New Issue
Block a user