/** * Notification service - stores and manages notifications in IndexedDB */ 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 // 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 db: IDBDatabase | null = null private initPromise: Promise | null = null private async init(): Promise { if (this.db) { return } if (this.initPromise) { return this.initPromise } this.initPromise = this.openDatabase() try { await this.initPromise } catch (error) { this.initPromise = null throw error } } private openDatabase(): Promise { return new Promise((resolve, reject) => { if (typeof window === 'undefined' || !window.indexedDB) { reject(new Error('IndexedDB is not available')) return } const request = window.indexedDB.open(DB_NAME, DB_VERSION) request.onerror = (): void => { reject(new Error(`Failed to open IndexedDB: ${request.error}`)) } request.onsuccess = (): void => { this.db = request.result resolve() } request.onupgradeneeded = (event: IDBVersionChangeEvent): void => { const db = (event.target as IDBOpenDBRequest).result if (!db.objectStoreNames.contains(STORE_NAME)) { const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' }) store.createIndex('type', 'type', { unique: false }) store.createIndex('objectId', 'objectId', { unique: false }) store.createIndex('eventId', 'eventId', { unique: false }) store.createIndex('timestamp', 'timestamp', { unique: false }) store.createIndex('read', 'read', { unique: false }) store.createIndex('objectType', '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 }): Promise { 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 { try { await this.init() if (!this.db) { return null } const transaction = this.db.transaction([STORE_NAME], 'readonly') const store = transaction.objectStore(STORE_NAME) const index = store.index('eventId') return new Promise((resolve, reject) => { const request = index.get(eventId) request.onsuccess = (): void => { resolve((request.result as Notification) ?? null) } request.onerror = (): void => { reject(request.error) } }) } 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 { try { await this.init() if (!this.db) { return [] } const transaction = this.db.transaction([STORE_NAME], 'readonly') const store = transaction.objectStore(STORE_NAME) const index = store.index('timestamp') return new Promise((resolve, reject) => { const request = index.openCursor(null, 'prev') // Descending order (newest first) const notifications: Notification[] = [] request.onsuccess = (event: globalThis.Event): void => { const cursor = (event.target as IDBRequest).result if (cursor) { notifications.push(cursor.value as Notification) if (notifications.length < limit) { cursor.continue() } else { resolve(notifications) } } else { resolve(notifications) } } request.onerror = (): void => { reject(request.error) } }) } catch (error) { console.error('[NotificationService] Error getting all notifications:', error) return [] } } /** * Get unread notifications count */ async getUnreadCount(): Promise { try { await this.init() if (!this.db) { return 0 } const transaction = this.db.transaction([STORE_NAME], 'readonly') const store = transaction.objectStore(STORE_NAME) const index = store.index('read') return new Promise((resolve, reject) => { const request = index.openCursor(IDBKeyRange.only(false)) let count = 0 request.onsuccess = (event: globalThis.Event): void => { const cursor = (event.target as IDBRequest).result if (cursor) { count++ cursor.continue() } else { resolve(count) } } request.onerror = (): void => { reject(request.error) } }) } catch (error) { console.error('[NotificationService] Error getting unread count:', error) return 0 } } /** * Mark notification as read */ async markAsRead(notificationId: string): Promise { try { await this.init() if (!this.db) { throw new Error('Database not initialized') } const transaction = this.db.transaction([STORE_NAME], 'readwrite') const store = transaction.objectStore(STORE_NAME) const request = store.get(notificationId) await new Promise((resolve, reject) => { request.onsuccess = (): void => { const notification = request.result as Notification | undefined if (!notification) { reject(new Error('Notification not found')) return } const updatedNotification: Notification = { ...notification, read: true, } const updateRequest = store.put(updatedNotification) updateRequest.onsuccess = (): void => { resolve() } updateRequest.onerror = (): void => { reject(new Error(`Failed to update notification: ${updateRequest.error}`)) } } request.onerror = (): void => { reject(request.error) } }) } catch (error) { console.error('[NotificationService] Error marking notification as read:', error) throw error } } /** * Mark all notifications as read */ async markAllAsRead(): Promise { try { await this.init() if (!this.db) { throw new Error('Database not initialized') } const notifications = await this.getAllNotifications(10000) const transaction = this.db.transaction([STORE_NAME], 'readwrite') const store = transaction.objectStore(STORE_NAME) await Promise.all( notifications .filter((n) => !n.read) .map( (notification) => new Promise((resolve, reject) => { const updatedNotification: Notification = { ...notification, read: true, } const request = store.put(updatedNotification) request.onsuccess = (): void => { resolve() } request.onerror = (): void => { reject(request.error) } }) ) ) } catch (error) { console.error('[NotificationService] Error marking all notifications as read:', error) throw error } } /** * Delete notification */ async deleteNotification(notificationId: string): Promise { try { await this.init() if (!this.db) { throw new Error('Database not initialized') } const transaction = this.db.transaction([STORE_NAME], 'readwrite') const store = transaction.objectStore(STORE_NAME) await new Promise((resolve, reject) => { const request = store.delete(notificationId) request.onsuccess = (): void => { resolve() } request.onerror = (): void => { reject(new Error(`Failed to delete notification: ${request.error}`)) } }) } catch (error) { console.error('[NotificationService] Error deleting notification:', error) throw error } } } export const notificationService = new NotificationService()