/** * 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'] 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) { // Check if object was recently published (published changed from false to array) if (Array.isArray(obj.published) && obj.published.length > 0) { // Check if we already created a notification for this const eventId = obj.id.split(':')[1] ?? obj.id const existing = await notificationService.getNotificationByEventId(eventId) if (existing?.type !== 'published') { // Check if this is a recent change (within last 5 minutes) // We can't track old/new state easily, so we'll create notification // if object was published recently (created in last hour and published) const oneHourAgo = Date.now() - 60 * 60 * 1000 const cachedObj = obj if (cachedObj.createdAt * 1000 > oneHourAgo) { const relays = cachedObj.published as string[] await notificationService.createNotification({ type: 'published', objectType, objectId: cachedObj.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) } } /** * Get notification title based on type */ private getNotificationTitle(type: NotificationType, _obj: CachedObject): string { switch (type) { case 'purchase': return 'Nouvel achat' case 'review': return 'Nouvel avis' case 'sponsoring': return 'Nouveau sponsoring' case 'review_tip': return 'Nouveau remerciement' case 'payment_note': return 'Nouvelle note de paiement' case 'published': return 'Publication réussie' default: return 'Nouvelle notification' } } /** * Get notification message based on type */ private getNotificationMessage(type: NotificationType, _obj: CachedObject): string { switch (type) { case 'purchase': return `Vous avez acheté un article` case 'review': return `Un nouvel avis a été publié` case 'sponsoring': return `Vous avez reçu un sponsoring` case 'review_tip': return `Vous avez reçu un remerciement` case 'payment_note': return `Une note de paiement a été ajoutée` case 'published': const cachedObj = _obj const relays = Array.isArray(cachedObj.published) ? cachedObj.published : [] return `Votre contenu a été publié sur ${relays.length} relais` default: return 'Nouvelle notification' } } } export const notificationDetector = new NotificationDetector()