story-research-zapwall/lib/notificationService.ts
2026-01-07 03:10:40 +01:00

207 lines
6.1 KiB
TypeScript

/**
* Notification service - stores and manages notifications in IndexedDB
*/
import { createIndexedDBHelper, type IndexedDBHelper } from './helpers/indexedDBHelper'
const DB_NAME = 'nostr_notifications'
const DB_VERSION = 1
const STORE_NAME = 'notifications'
export type NotificationType =
| 'purchase' // Achat
| 'review' // Avis
| 'sponsoring' // Sponsoring
| 'review_tip' // Remerciement (tip sur un avis)
| 'payment_note' // Note de paiement
| 'published' // Objet publié avec succès (passé de published: false à liste de relais)
export interface Notification {
id: string // Unique notification ID
type: NotificationType
objectType: string // Type d'objet (author, series, publication, etc.)
objectId: string // ID de l'objet dans objectCache
eventId: string // ID de l'événement Nostr
timestamp: number // Date de création de la notification (milliseconds)
read: boolean // Si la notification a été lue
data?: Record<string, unknown> // Données supplémentaires (relais publiés, montant, etc.)
// Compatibilité avec l'ancien format
title?: string
message?: string
articleId?: string
articleTitle?: string
amount?: number
fromPubkey?: string
}
class NotificationService {
private readonly dbHelper: IndexedDBHelper
constructor() {
this.dbHelper = createIndexedDBHelper({
dbName: DB_NAME,
version: DB_VERSION,
storeName: STORE_NAME,
keyPath: 'id',
indexes: [
{ name: 'type', keyPath: 'type', unique: false },
{ name: 'objectId', keyPath: 'objectId', unique: false },
{ name: 'eventId', keyPath: 'eventId', unique: false },
{ name: 'timestamp', keyPath: 'timestamp', unique: false },
{ name: 'read', keyPath: 'read', unique: false },
{ name: 'objectType', keyPath: 'objectType', unique: false },
],
})
}
/**
* Create a new notification
* Utilise writeService pour écrire via Web Worker
*/
async createNotification(params: {
type: NotificationType
objectType: string
objectId: string
eventId: string
data?: Record<string, unknown>
}): Promise<void> {
try {
const { type, objectType, objectId, eventId, data } = params
// Check if notification already exists for this event
const existing = await this.getNotificationByEventId(eventId)
if (existing) {
return // Notification already exists
}
// Utiliser writeService pour créer la notification via Web Worker
const { writeService } = await import('./writeService')
await writeService.createNotification(type, objectType, objectId, eventId, data)
} catch (error) {
console.error('[NotificationService] Error creating notification:', error)
}
}
/**
* Get notification by event ID
*/
async getNotificationByEventId(eventId: string): Promise<Notification | null> {
try {
return await this.dbHelper.getByIndex<Notification>('eventId', eventId)
} catch (error) {
console.error('[NotificationService] Error getting notification by event ID:', error)
return null
}
}
/**
* Get all notifications for a user
*/
async getAllNotifications(limit: number = 100): Promise<Notification[]> {
try {
const notifications: Notification[] = []
const store = await this.dbHelper.getStore('readonly')
const index = store.index('timestamp')
return new Promise<Notification[]>((resolve, reject) => {
const request = index.openCursor(null, 'prev') // Descending order (newest first)
request.onsuccess = (event: globalThis.Event): void => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
if (cursor) {
notifications.push(cursor.value as Notification)
if (notifications.length < limit) {
cursor.continue()
} else {
resolve(notifications)
}
} else {
resolve(notifications)
}
}
request.onerror = (): void => {
if (request.error) {
reject(request.error)
} else {
reject(new Error('Unknown error opening cursor'))
}
}
})
} catch (error) {
console.error('[NotificationService] Error getting all notifications:', error)
return []
}
}
/**
* Get unread notifications count
*/
async getUnreadCount(): Promise<number> {
try {
return await this.dbHelper.countByIndex('read', IDBKeyRange.only(false))
} catch (error) {
console.error('[NotificationService] Error getting unread count:', error)
return 0
}
}
/**
* Mark notification as read
*/
async markAsRead(notificationId: string): Promise<void> {
try {
const notification = await this.dbHelper.get<Notification>(notificationId)
if (!notification) {
throw new Error('Notification not found')
}
const updatedNotification: Notification = {
...notification,
read: true,
}
await this.dbHelper.put(updatedNotification)
} catch (error) {
console.error('[NotificationService] Error marking notification as read:', error)
throw error
}
}
/**
* Mark all notifications as read
*/
async markAllAsRead(): Promise<void> {
try {
const notifications = await this.getAllNotifications(10000)
const unreadNotifications = notifications.filter((n) => !n.read)
await Promise.all(
unreadNotifications.map((notification) => {
const updatedNotification: Notification = {
...notification,
read: true,
}
return this.dbHelper.put(updatedNotification)
})
)
} catch (error) {
console.error('[NotificationService] Error marking all notifications as read:', error)
throw error
}
}
/**
* Delete notification
*/
async deleteNotification(notificationId: string): Promise<void> {
try {
await this.dbHelper.delete(notificationId)
} catch (error) {
console.error('[NotificationService] Error deleting notification:', error)
throw error
}
}
}
export const notificationService = new NotificationService()