289 lines
9.6 KiB
TypeScript
289 lines
9.6 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
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<void> {
|
|
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<typeof objectCache.getAll>[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<void> {
|
|
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<typeof objectCache.getAll>[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<void> {
|
|
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<typeof objectCache.get>[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()
|