import { useQuery } from '@tanstack/react-query'; import { useNostr } from '@nostrify/react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useResolvedDmRelays } from '@/hooks/useDirectMessages'; /** * Lightweight DM "envelope" watcher — the id and timestamp of every NIP-17 gift * wrap addressed to the user, **without decrypting anything**. * * This is what makes DM notifications possible without ever prompting Amber: * the seal/rumor are encrypted, so we only see that *something* arrived. No * sender, no content, no signer round-trip. The preview is intentionally generic * ("New direct message") — the honest ceiling when we refuse to decrypt in the * background. * * NIP-17 randomises the outer wrap timestamp (up to 2 days in the past), so we * cannot reliably tell "new" from `created_at`. Callers should diff on the * stable `id` instead. */ const GIFT_WRAP_KIND = 1059; export interface DmEnvelope { id: string; createdAt: number; } export function useDmEnvelopes(enabled: boolean) { const { nostr } = useNostr(); const { user } = useCurrentUser(); const { relays, key } = useResolvedDmRelays(); return useQuery({ queryKey: ['dm-envelopes', user?.pubkey, key], enabled: enabled && !!user?.pubkey && relays.length > 0, staleTime: 60_000, refetchInterval: 90_000, queryFn: async (c): Promise => { const pubkey = user!.pubkey; const batches = await Promise.all( relays.map(async (relayUrl) => { try { const relay = nostr.relay(relayUrl); return await relay.query( [{ kinds: [GIFT_WRAP_KIND], '#p': [pubkey], limit: 60 }], { signal: AbortSignal.any([c.signal, AbortSignal.timeout(8000)]) }, ); } catch { // One relay failing must not stop the others. return []; } }), ); const unique = new Map(); for (const event of batches.flat()) unique.set(event.id, event.created_at); return [...unique].map(([id, createdAt]) => ({ id, createdAt })); }, }); }