story-research-zapwall/lib/notificationService.ts
2026-01-07 01:51:26 +01:00

346 lines
9.9 KiB
TypeScript

/**
* 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<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 db: IDBDatabase | null = null
private initPromise: Promise<void> | null = null
private async init(): Promise<void> {
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<void> {
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<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 {
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<Notification[]> {
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<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 => {
reject(request.error)
}
})
} catch (error) {
console.error('[NotificationService] Error getting all notifications:', error)
return []
}
}
/**
* Get unread notifications count
*/
async getUnreadCount(): Promise<number> {
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<IDBCursorWithValue>).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<void> {
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<void>((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<void> {
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<void>((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<void> {
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<void>((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()