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>
79 lines
2.8 KiB
TypeScript
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),
|
|
});
|
|
}
|