/** * 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' 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 } const objectTypes: 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' }, ] for (const { type, notificationType } of objectTypes) { try { const allObjects = await objectCache.getAll(type as Parameters[0]) const userObjects = (allObjects as CachedObject[]).filter((obj: CachedObject) => { // Check if object is related to the user // For purchases: targetPubkey === userPubkey // For reviews: targetEventId points to user's article // For sponsoring: targetPubkey === userPubkey // For review_tips: targetEventId points to user's review // For payment_notes: targetPubkey === userPubkey if (type === 'purchase' || type === 'sponsoring' || type === 'payment_note') { return (obj as { targetPubkey?: string }).targetPubkey === this.userPubkey } if (type === 'review' || type === 'review_tip') { // Need to check if the target event belongs to the user // This is more complex and may require checking the article/review // For now, we'll create notifications for all reviews/tips // The UI can filter them if needed return true } return false }) // Create notifications for objects created after last scan for (const obj of userObjects) { const cachedObj = obj if (cachedObj.createdAt * 1000 > this.lastScanTime) { const eventId = cachedObj.id.split(':')[1] ?? cachedObj.id await notificationService.createNotification({ type: notificationType, objectType: type, objectId: cachedObj.id, eventId, data: { object: obj }, }) } } } catch (error) { console.error(`[NotificationDetector] Error scanning ${type}:`, error) } } } /** * 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) { if (Array.isArray(obj.published) && obj.published.length > 0) { const eventId = obj.id.split(':')[1] ?? obj.id const existing = await notificationService.getNotificationByEventId(eventId) const alreadyNotified = existing?.type === 'published' const recentlyCreated = obj.createdAt * 1000 > oneHourAgo if (!alreadyNotified && recentlyCreated) { const relays = obj.published await notificationService.createNotification({ type: 'published', objectType, objectId: obj.id, 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 scanning published status for ${objectType}:`, error) } } } catch (error) { console.error('[NotificationDetector] Error scanning published status changes:', error) } } /** * 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) } } } export const notificationDetector = new NotificationDetector()