Fix 404 on nevent/naddr detail pages by using relay hints

When clicking a nevent or naddr link, the NIP19Page was discarding the
relay hints encoded in the identifier. The useEvent/useAddrEvent hooks
would only query the user's configured relays, which may not have the
event — resulting in a 404 even though the event appeared in the feed.

Now relay hints from nevent/naddr identifiers are passed through to the
hooks, which first try the user's relays, then fall back to querying
the hint relays directly via nostr.group().

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy
2026-02-18 00:44:51 -06:00
parent 269cc4f552
commit 7d2c788ab0
3 changed files with 53 additions and 21 deletions
+42 -14
View File
@@ -2,19 +2,33 @@ import { useNostr } from '@nostrify/react';
import { useQuery } from '@tanstack/react-query';
import type { NostrEvent } from '@nostrify/nostrify';
/** Fetches a single Nostr event by its hex ID. */
export function useEvent(eventId: string | undefined) {
/** Fetches a single Nostr event by its hex ID, optionally querying relay hints. */
export function useEvent(eventId: string | undefined, relays?: string[]) {
const { nostr } = useNostr();
return useQuery<NostrEvent | null>({
queryKey: ['event', eventId ?? ''],
queryFn: async ({ signal }) => {
if (!eventId) return null;
const events = await nostr.query(
[{ ids: [eventId], limit: 1 }],
{ signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]) },
);
return events[0] ?? null;
const querySignal = AbortSignal.any([signal, AbortSignal.timeout(5000)]);
const filter = [{ ids: [eventId], limit: 1 }];
// Query the user's configured relays first
const events = await nostr.query(filter, { signal: querySignal });
if (events.length > 0) return events[0];
// If not found and we have relay hints, try those relays directly
if (relays && relays.length > 0) {
try {
const hintSignal = AbortSignal.any([signal, AbortSignal.timeout(5000)]);
const hintEvents = await nostr.group(relays).query(filter, { signal: hintSignal });
if (hintEvents.length > 0) return hintEvents[0];
} catch {
// relay hint query failed — fall through
}
}
return null;
},
enabled: !!eventId,
staleTime: 5 * 60 * 1000,
@@ -28,19 +42,33 @@ export interface AddrCoords {
identifier: string;
}
/** Fetches a single addressable Nostr event by kind + pubkey + d-tag. */
export function useAddrEvent(addr: AddrCoords | undefined) {
/** 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 ({ signal }) => {
if (!addr) return null;
const events = await nostr.query(
[{ kinds: [addr.kind], authors: [addr.pubkey], '#d': [addr.identifier], limit: 1 }],
{ signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]) },
);
return events[0] ?? null;
const querySignal = AbortSignal.any([signal, AbortSignal.timeout(5000)]);
const filter = [{ kinds: [addr.kind], authors: [addr.pubkey], '#d': [addr.identifier], limit: 1 }];
// Query the user's configured relays first
const events = await nostr.query(filter, { signal: querySignal });
if (events.length > 0) return events[0];
// If not found and we have relay hints, try those relays directly
if (relays && relays.length > 0) {
try {
const hintSignal = AbortSignal.any([signal, AbortSignal.timeout(5000)]);
const hintEvents = await nostr.group(relays).query(filter, { signal: hintSignal });
if (hintEvents.length > 0) return hintEvents[0];
} catch {
// relay hint query failed — fall through
}
}
return null;
},
enabled: !!addr,
staleTime: 5 * 60 * 1000,
+5 -3
View File
@@ -29,12 +29,14 @@ export function NIP19Page() {
case 'note':
return <PostDetailPage eventId={decoded.data as string} />;
case 'nevent':
return <PostDetailPage eventId={(decoded.data as { id: string }).id} />;
case 'nevent': {
const neventData = decoded.data as { id: string; relays?: string[] };
return <PostDetailPage eventId={neventData.id} relays={neventData.relays} />;
}
case 'naddr': {
const addr = decoded.data as AddressPointer;
return <AddrPostDetailPage addr={{ kind: addr.kind, pubkey: addr.pubkey, identifier: addr.identifier }} />;
return <AddrPostDetailPage addr={{ kind: addr.kind, pubkey: addr.pubkey, identifier: addr.identifier }} relays={addr.relays} />;
}
default:
+6 -4
View File
@@ -36,10 +36,12 @@ import NotFound from './NotFound';
interface PostDetailPageProps {
eventId: string;
relays?: string[];
}
interface AddrPostDetailPageProps {
addr: AddrCoords;
relays?: string[];
}
/** Formats a sats amount into a compact human-readable string. */
@@ -123,8 +125,8 @@ function getParentEventId(event: NostrEvent): string | undefined {
return undefined;
}
export function PostDetailPage({ eventId }: PostDetailPageProps) {
const { data: event, isLoading, isError } = useEvent(eventId);
export function PostDetailPage({ eventId, relays }: PostDetailPageProps) {
const { data: event, isLoading, isError } = useEvent(eventId, relays);
useSeoMeta({
title: event ? 'Post Details - Mew' : 'Loading... - Mew',
@@ -154,8 +156,8 @@ export function PostDetailPage({ eventId }: PostDetailPageProps) {
}
/** Detail page for addressable events (naddr). Same layout as PostDetailPage. */
export function AddrPostDetailPage({ addr }: AddrPostDetailPageProps) {
const { data: event, isLoading, isError } = useAddrEvent(addr);
export function AddrPostDetailPage({ addr, relays }: AddrPostDetailPageProps) {
const { data: event, isLoading, isError } = useAddrEvent(addr, relays);
useSeoMeta({
title: event