Files
eranos/src/hooks/useEvent.ts
T

152 lines
5.6 KiB
TypeScript

import { useNostr } from '@nostrify/react';
import { useQuery } from '@tanstack/react-query';
import type { NostrEvent, NostrFilter } from '@nostrify/nostrify';
import { ZAPSTORE_RELAY } from '@/lib/appRelays';
/** Kinds whose canonical home is the Zapstore relay. */
const ZAPSTORE_KINDS = [32267, 30063];
/**
* Extract write relay URLs from a NIP-65 (kind 10002) relay list event.
* Write relays are where the author publishes their content.
* Tags with no marker are both read+write; tags with "write" are write-only.
*/
function extractWriteRelays(event: NostrEvent): string[] {
const relays = new Set<string>();
for (const [name, url, marker] of event.tags) {
if (name !== 'r' || marker === 'read' || !url) continue;
try {
const parsed = new URL(url);
if (parsed.protocol === 'wss:') {
relays.add(parsed.href);
}
} catch {
// skip malformed URLs
}
}
return [...relays];
}
/**
* Last-resort: fetch the author's NIP-65 relay list and query their write relays
* for the target event. Returns the event if found, or null.
*/
async function queryAuthorRelays(
nostr: { query: (filters: NostrFilter[], opts?: { signal?: AbortSignal }) => Promise<NostrEvent[]>; group: (urls: string[]) => { query: (filters: NostrFilter[], opts?: { signal?: AbortSignal }) => Promise<NostrEvent[]> } },
authorPubkey: string,
eventFilter: NostrFilter[],
signal: AbortSignal,
): Promise<NostrEvent | null> {
try {
// Fetch the author's NIP-65 relay list from our connected relays
const relayListSignal = AbortSignal.any([signal, AbortSignal.timeout(5000)]);
const relayListEvents = await nostr.query(
[{ kinds: [10002], authors: [authorPubkey], limit: 1 }],
{ signal: relayListSignal },
);
if (relayListEvents.length === 0) return null;
const writeRelays = extractWriteRelays(relayListEvents[0]).slice(0, 5);
if (writeRelays.length === 0) return null;
// Query the author's write relays for the target event
const authorRelaySignal = AbortSignal.any([signal, AbortSignal.timeout(6000)]);
const events = await nostr.group(writeRelays).query(eventFilter, { signal: authorRelaySignal });
return events.length > 0 ? events[0] : null;
} catch {
return null;
}
}
/** Fetches a single Nostr event by its hex ID, optionally querying relay hints. */
export function useEvent(eventId: string | undefined, relays?: string[], authorHint?: string) {
const { nostr } = useNostr();
return useQuery<NostrEvent | null>({
queryKey: ['event', eventId ?? ''],
queryFn: async () => {
if (!eventId) return null;
const filter: NostrFilter[] = [{ ids: [eventId], limit: 1 }];
// 1. Query the user's configured relays first (batched automatically)
const events = await nostr.query(filter, { signal: AbortSignal.timeout(5000) });
if (events.length > 0) return events[0];
// 2. If not found and we have relay hints, try those relays directly
if (relays && relays.length > 0) {
try {
const hintEvents = await nostr.group(relays).query(filter, { signal: AbortSignal.timeout(5000) });
if (hintEvents.length > 0) return hintEvents[0];
} catch {
// relay hint query failed — fall through
}
}
// 3. Last resort: if we have the author's pubkey, fetch their NIP-65 relay
// list and try their write relays (where they publish content)
if (authorHint) {
const found = await queryAuthorRelays(nostr, authorHint, filter, AbortSignal.timeout(10000));
if (found) return found;
}
return null;
},
enabled: !!eventId,
staleTime: 5 * 60 * 1000,
});
}
/** Coordinates for an addressable event (naddr). */
export interface AddrCoords {
kind: number;
pubkey: string;
identifier: string;
}
/** Fetches a single addressable Nostr event by kind + pubkey + d-tag, optionally querying relay hints. */
export function useAddrEvent(addr: AddrCoords | undefined, relays?: string[]) {
const { nostr } = useNostr();
return useQuery<NostrEvent | null>({
queryKey: ['addr-event', addr?.kind ?? 0, addr?.pubkey ?? '', addr?.identifier ?? ''],
queryFn: async () => {
if (!addr) return null;
const filter: NostrFilter[] = [{ kinds: [addr.kind], authors: [addr.pubkey], '#d': [addr.identifier], limit: 1 }];
// For Zapstore kinds, try the canonical relay first for fastest results
if (ZAPSTORE_KINDS.includes(addr.kind)) {
try {
const zapEvents = await nostr.relay(ZAPSTORE_RELAY).query(filter, { signal: AbortSignal.timeout(5000) });
if (zapEvents.length > 0) return zapEvents[0];
} catch {
// zapstore relay failed — fall through to normal flow
}
}
// 1. Query the user's configured relays
const events = await nostr.query(filter, { signal: AbortSignal.timeout(5000) });
if (events.length > 0) return events[0];
// 2. If not found and we have relay hints, try those relays directly
if (relays && relays.length > 0) {
try {
const hintEvents = await nostr.group(relays).query(filter, { signal: AbortSignal.timeout(5000) });
if (hintEvents.length > 0) return hintEvents[0];
} catch {
// relay hint query failed — fall through
}
}
// 3. Last resort: fetch the author's NIP-65 relay list and try their
// write relays (naddr always includes the author pubkey)
const found = await queryAuthorRelays(nostr, addr.pubkey, filter, AbortSignal.timeout(10000));
if (found) return found;
return null;
},
enabled: !!addr,
staleTime: 5 * 60 * 1000,
});
}