/** * Notification detector - scans IndexedDB for new events and creates notifications * Runs in a service worker or main thread */ import { objectCache } from './objectCache' import { notificationService, type NotificationType } from './notificationService' import type { CachedObject } from './objectCache' const USER_OBJECT_NOTIFICATION_TYPES: Array<{ type: string; notificationType: NotificationType }> = [ { type: 'purchase', notificationType: 'purchase' }, { type: 'review', notificationType: 'review' }, { type: 'sponsoring', notificationType: 'sponsoring' }, { type: 'review_tip', notificationType: 'review_tip' }, { type: 'payment_note', notificationType: 'payment_note' }, ] interface ObjectChange { objectType: string objectId: string eventId: string oldPublished: false | string[] newPublished: false | string[] } class NotificationDetector { private lastScanTime: number = 0 private scanInterval: number | null = null private isScanning = false private userPubkey: string | null = null /** * Start scanning for notifications */ start(userPubkey: string): void { if (this.scanInterval) { return // Already started } this.userPubkey = userPubkey this.lastScanTime = Date.now() // Scan immediately void this.scan() // Then scan periodically (every 30 seconds) this.scanInterval = window.setInterval(() => { void this.scan() }, 30000) } /** * Stop scanning */ stop(): void { if (this.scanInterval) { clearInterval(this.scanInterval) this.scanInterval = null } this.userPubkey = null } /** * Scan IndexedDB for new events that should trigger notifications */ async scan(): Promise { if (this.isScanning || !this.userPubkey) { return } this.isScanning = true try { // Scan for user-related objects await this.scanUserObjects() // Scan for published status changes await this.scanPublishedStatusChanges() this.lastScanTime = Date.now() } catch (error) { console.error('[NotificationDetector] Error scanning:', error) } finally { this.isScanning = false } } /** * Scan for user-related objects (purchases, reviews, sponsoring, review_tips, payment_notes) */ private async scanUserObjects(): Promise { if (!this.userPubkey) { return } for (const cfg of USER_OBJECT_NOTIFICATION_TYPES) { await this.scanUserObjectsOfType(cfg) } } private async scanUserObjectsOfType(params: { type: string; notificationType: NotificationType }): Promise { try { const {userPubkey} = this if (!userPubkey) { return } const allObjects = await objectCache.getAll(params.type as Parameters[0]) const userObjects = filterUserRelatedObjects({ type: params.type, allObjects: allObjects as CachedObject[], userPubkey }) await this.createNotificationsForNewObjects({ type: params.type, notificationType: params.notificationType, objects: userObjects }) } catch (error) { console.error(`[NotificationDetector] Error scanning ${params.type}:`, error) } } private async createNotificationsForNewObjects(params: { type: string notificationType: NotificationType objects: CachedObject[] }): Promise { for (const obj of params.objects) { if (obj.createdAt * 1000 > this.lastScanTime) { const eventId = obj.id.split(':')[1] ?? obj.id await notificationService.createNotification({ type: params.notificationType, objectType: params.type, objectId: obj.id, eventId, data: { object: obj }, }) } } } /** * Scan for published status changes (published: false -> list of relays) */ private async scanPublishedStatusChanges(): Promise { if (!this.userPubkey) { return } try { // Get all object types that can be published const objectTypes = ['author', 'series', 'publication', 'review', 'purchase', 'sponsoring', 'review_tip', 'payment_note'] const oneHourAgo = Date.now() - 60 * 60 * 1000 for (const objectType of objectTypes) { try { const allObjects = await objectCache.getAll(objectType as Parameters[0]) const userObjects = (allObjects as CachedObject[]).filter((obj: CachedObject) => { // Check if object belongs to user - need to check parsed object for pubkey const parsed = obj.parsed as { pubkey?: string } | undefined return parsed?.pubkey === this.userPubkey }) for (const obj of userObjects) { await this.maybeCreatePublishedNotification({ obj, objectType, oneHourAgo, }) } } catch (error) { console.error(`[NotificationDetector] Error scanning published status for ${objectType}:`, error) } } } catch (error) { console.error('[NotificationDetector] Error scanning published status changes:', error) } } private async maybeCreatePublishedNotification(params: { obj: CachedObject objectType: string oneHourAgo: number }): Promise { if (!Array.isArray(params.obj.published) || params.obj.published.length === 0) { return } if (params.obj.createdAt * 1000 <= params.oneHourAgo) { return } const eventId = params.obj.id.split(':')[1] ?? params.obj.id const existing = await notificationService.getNotificationByEventId(eventId) if (existing?.type === 'published') { return } const relays = params.obj.published await notificationService.createNotification({ type: 'published', objectType: params.objectType, objectId: params.obj.id, eventId, data: { relays, object: params.obj, title: 'Publication réussie', message: `Votre contenu a été publié sur ${relays.length} relais`, }, }) } /** * Manually check for a specific object change */ async checkObjectChange(change: ObjectChange): Promise { if (!this.userPubkey) { return } try { // Check if published status changed from false to array if (change.oldPublished === false && Array.isArray(change.newPublished) && change.newPublished.length > 0) { // Get the object to check if it belongs to user const obj = await objectCache.get(change.objectType as Parameters[0], change.objectId) if (obj) { const cachedObj = obj as CachedObject const parsed = cachedObj.parsed as { pubkey?: string } | undefined if (parsed?.pubkey === this.userPubkey) { const relays = change.newPublished await notificationService.createNotification({ type: 'published', objectType: change.objectType, objectId: change.objectId, eventId: change.eventId, data: { relays, object: obj, title: 'Publication réussie', message: `Votre contenu a été publié sur ${relays.length} relais`, }, }) } } } } catch (error) { console.error('[NotificationDetector] Error checking object change:', error) } } } function filterUserRelatedObjects(params: { type: string; allObjects: CachedObject[]; userPubkey: string }): CachedObject[] { return params.allObjects.filter((obj: CachedObject) => { if (params.type === 'purchase' || params.type === 'sponsoring' || params.type === 'payment_note') { return (obj as { targetPubkey?: string }).targetPubkey === params.userPubkey } if (params.type === 'review' || params.type === 'review_tip') { return true } return false }) } export const notificationDetector = new NotificationDetector()