175 lines
4.8 KiB
TypeScript
175 lines
4.8 KiB
TypeScript
import type { Event } from 'nostr-tools'
|
|
import { nostrService } from './nostr'
|
|
import { zapVerificationService } from './zapVerification'
|
|
import type { Notification } from '@/types/notifications'
|
|
import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
|
|
import { getPrimaryRelaySync } from './config'
|
|
|
|
function createZapReceiptFilters(userPubkey: string) {
|
|
return [
|
|
{
|
|
kinds: [9735], // Zap receipt
|
|
'#p': [userPubkey], // Receipts targeting this user
|
|
},
|
|
]
|
|
}
|
|
|
|
async function buildPaymentNotification(event: Event, userPubkey: string): Promise<Notification | null> {
|
|
const paymentInfo = zapVerificationService.extractPaymentInfo(event)
|
|
|
|
if (paymentInfo?.recipient !== userPubkey) {
|
|
return null
|
|
}
|
|
|
|
let articleTitle: string | undefined
|
|
if (paymentInfo.articleId) {
|
|
try {
|
|
const article = await nostrService.getArticleById(paymentInfo.articleId)
|
|
articleTitle = article?.title
|
|
} catch (e) {
|
|
console.error('Error loading article for notification:', e)
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: event.id,
|
|
type: 'payment',
|
|
title: 'New Payment Received',
|
|
message: articleTitle
|
|
? `Vous avez reçu un zap de ${paymentInfo.amount} sats pour "${articleTitle}"`
|
|
: `Vous avez reçu un zap de ${paymentInfo.amount} sats`,
|
|
timestamp: event.created_at,
|
|
read: false,
|
|
...(paymentInfo.articleId ? { articleId: paymentInfo.articleId } : {}),
|
|
...(articleTitle ? { articleTitle } : {}),
|
|
amount: paymentInfo.amount,
|
|
fromPubkey: paymentInfo.payer,
|
|
}
|
|
}
|
|
|
|
function registerZapSubscription(
|
|
sub: ReturnType<SimplePoolWithSub['sub']>,
|
|
userPubkey: string,
|
|
onNotification: (notification: Notification) => void
|
|
) {
|
|
sub.on('event', (event: Event) => {
|
|
void buildPaymentNotification(event, userPubkey)
|
|
.then((notification) => {
|
|
if (notification) {
|
|
onNotification(notification)
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error('Error processing zap receipt notification:', error)
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Service for monitoring and managing notifications
|
|
*/
|
|
export class NotificationService {
|
|
private subscriptions: Map<string, () => void> = new Map()
|
|
|
|
/**
|
|
* Subscribe to zap receipts (payments) for a user's articles
|
|
*/
|
|
subscribeToPayments(
|
|
userPubkey: string,
|
|
onNotification: (notification: Notification) => void
|
|
): () => void {
|
|
const pool = nostrService.getPool()
|
|
if (!pool) {
|
|
return () => {}
|
|
}
|
|
|
|
const filters = createZapReceiptFilters(userPubkey)
|
|
const poolWithSub = pool as SimplePoolWithSub
|
|
const relayUrl = getPrimaryRelaySync()
|
|
const sub = poolWithSub.sub([relayUrl], filters)
|
|
|
|
registerZapSubscription(sub, userPubkey, onNotification)
|
|
|
|
const unsubscribe = () => {
|
|
sub.unsub()
|
|
}
|
|
|
|
return unsubscribe
|
|
}
|
|
|
|
/**
|
|
* Stop all subscriptions
|
|
*/
|
|
stopAll(): void {
|
|
this.subscriptions.forEach((unsubscribe) => unsubscribe())
|
|
this.subscriptions.clear()
|
|
}
|
|
}
|
|
|
|
export const notificationService = new NotificationService()
|
|
|
|
/**
|
|
* Load stored notifications from IndexedDB
|
|
*/
|
|
export async function loadStoredNotifications(userPubkey: string): Promise<Notification[]> {
|
|
try {
|
|
const { storageService } = await import('./storage/indexedDB')
|
|
const key = `notifications_${userPubkey}`
|
|
const stored = await storageService.get<Notification[]>(key, 'notifications_storage')
|
|
return stored ?? []
|
|
} catch (error) {
|
|
console.error('Error loading stored notifications:', error)
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save notifications to IndexedDB
|
|
*/
|
|
export async function saveNotifications(userPubkey: string, notifications: Notification[]): Promise<void> {
|
|
try {
|
|
const { storageService } = await import('./storage/indexedDB')
|
|
const key = `notifications_${userPubkey}`
|
|
await storageService.set(key, notifications, 'notifications_storage')
|
|
} catch (error) {
|
|
console.error('Error saving notifications:', error)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mark notification as read
|
|
*/
|
|
export function markNotificationAsRead(
|
|
userPubkey: string,
|
|
notificationId: string,
|
|
notifications: Notification[]
|
|
): Notification[] {
|
|
const updated = notifications.map((n) =>
|
|
n.id === notificationId ? { ...n, read: true } : n
|
|
)
|
|
saveNotifications(userPubkey, updated)
|
|
return updated
|
|
}
|
|
|
|
/**
|
|
* Mark all notifications as read
|
|
*/
|
|
export function markAllAsRead(userPubkey: string, notifications: Notification[]): Notification[] {
|
|
const updated = notifications.map((n) => ({ ...n, read: true }))
|
|
saveNotifications(userPubkey, updated)
|
|
return updated
|
|
}
|
|
|
|
/**
|
|
* Delete a notification
|
|
*/
|
|
export function deleteNotification(
|
|
userPubkey: string,
|
|
notificationId: string,
|
|
notifications: Notification[]
|
|
): Notification[] {
|
|
const updated = notifications.filter((n) => n.id !== notificationId)
|
|
saveNotifications(userPubkey, updated)
|
|
return updated
|
|
}
|