346 lines
9.9 KiB
TypeScript
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()
|