diff --git a/src/hooks/useAuthor.ts b/src/hooks/useAuthor.ts index e0aea66..c5da199 100644 --- a/src/hooks/useAuthor.ts +++ b/src/hooks/useAuthor.ts @@ -23,25 +23,38 @@ export function useAuthor(pubkey: string | undefined) { const querySignal = AbortSignal.any([signal, AbortSignal.timeout(15000)]); try { - // Try main relays first - let [event] = await nostr.query( - [{ kinds: [0], authors: [pubkey], limit: 1 }], - { signal: querySignal }, - ); - - // If not found, try fallback profile aggregator relays - if (!event) { - try { + // Query the main pool AND the global profile aggregators IN PARALLEL, + // then keep the NEWEST kind 0 across all of them. Doing both at once + // (rather than only falling back when the main pool is empty) means a + // followed user's real name resolves even when their profile lives on a + // big indexer but not on the app's curated skate relay — the previous + // "generic npub name" bug. + const results = await Promise.allSettled([ + nostr.query( + [{ kinds: [0], authors: [pubkey], limit: 1 }], + { signal: querySignal }, + ), + (async () => { const fallbackGroup = nostr.group(FALLBACK_RELAYS); - [event] = await fallbackGroup.query( + return fallbackGroup.query( [{ kinds: [0], authors: [pubkey], limit: 1 }], - { signal: AbortSignal.timeout(5000) }, + { signal: AbortSignal.timeout(8000) }, ); - } catch { - // Fallback failed, continue with no event + })(), + ]); + + // Collect every kind 0 event we found and pick the most recent one. + const candidates: NostrEvent[] = []; + for (const r of results) { + if (r.status === 'fulfilled' && Array.isArray(r.value)) { + candidates.push(...r.value); } } + const event = candidates + .filter((e) => e.kind === 0 && e.pubkey === pubkey) + .sort((a, b) => b.created_at - a.created_at)[0]; + if (!event) { // No profile found - this is okay, user may not have published one yet return {};