SkateSpots/src/hooks/useAuthor.ts
shakespeare.diy a46be4be68 Fix generic npub names: query profile aggregators in parallel in useAuthor
Previously useAuthor only hit the global profile-aggregator relays as a
fallback when the main pool returned zero kind 0 events. On the curated skate
relay many followed users have no profile, so names fell back to genUserName.
Now query the main pool and aggregators (purplepag.es, nostr.band, nos.lol,
damus) in parallel and keep the newest kind 0, so real display names resolve.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-25 14:01:55 +07:00

79 lines
2.8 KiB
TypeScript

import { type NostrEvent, type NostrMetadata, NSchema as n } from '@nostrify/nostrify';
import { useNostr } from '@nostrify/react';
import { useQuery } from '@tanstack/react-query';
// Fallback relays for profile aggregation — global indexers
const FALLBACK_RELAYS = [
'wss://purplepag.es',
'wss://relay.nostr.band',
'wss://nos.lol',
'wss://relay.damus.io',
];
export function useAuthor(pubkey: string | undefined) {
const { nostr } = useNostr();
return useQuery<{ event?: NostrEvent; metadata?: NostrMetadata }>({
queryKey: ['author', pubkey ?? ''],
queryFn: async ({ signal }) => {
if (!pubkey) {
return {};
}
const querySignal = AbortSignal.any([signal, AbortSignal.timeout(15000)]);
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);
return fallbackGroup.query(
[{ kinds: [0], authors: [pubkey], limit: 1 }],
{ signal: AbortSignal.timeout(8000) },
);
})(),
]);
// 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 {};
}
try {
const metadata = n.json().pipe(n.metadata()).parse(event.content);
return { metadata, event };
} catch {
return { event };
}
} catch (error) {
// Network error or timeout - return empty to avoid breaking the UI
console.warn('[useAuthor] Failed to fetch profile for', pubkey?.slice(0, 8), error);
return {};
}
},
staleTime: 60 * 1000, // 1 minute — allows NostrSync to invalidate and refetch quickly
retry: 5,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
});
}