diff --git a/src/lib/nostrIdentifier.ts b/src/lib/nostrIdentifier.ts index bfac9253..0d5c186e 100644 --- a/src/lib/nostrIdentifier.ts +++ b/src/lib/nostrIdentifier.ts @@ -5,6 +5,11 @@ import { nip19 } from 'nostr-tools'; */ const NIP19_PREFIXES = ['npub1', 'nprofile1', 'note1', 'nevent1', 'naddr1']; +/** + * Matches a 64-character lowercase hex string (a raw Nostr event ID or pubkey). + */ +const HEX_64_RE = /^[0-9a-f]{64}$/; + /** * Checks whether a string looks like a NIP-05 identifier (user@domain.com) * or a bare domain (e.g. fiatjaf.com). Strips a leading `@` if present. @@ -46,6 +51,11 @@ export function getNostrIdentifierPath(input: string): string | null { } } + // Try raw 64-char hex (event ID or pubkey) — route handles it directly + if (HEX_64_RE.test(value)) { + return `/${value}`; + } + // Try NIP-05 if (looksLikeNip05(value)) { // Strip leading @ for the URL path diff --git a/src/pages/NIP19Page.tsx b/src/pages/NIP19Page.tsx index 210b5c99..a4ec483e 100644 --- a/src/pages/NIP19Page.tsx +++ b/src/pages/NIP19Page.tsx @@ -1,10 +1,14 @@ import { nip19 } from 'nostr-tools'; import { useParams } from 'react-router-dom'; +import { useNostr } from '@nostrify/react'; +import { useQuery } from '@tanstack/react-query'; import NotFound from './NotFound'; import { ProfilePage } from './ProfilePage'; import { PostDetailPage, AddrPostDetailPage } from './PostDetailPage'; import type { AddressPointer } from 'nostr-tools/nip19'; +const HEX_64_RE = /^[0-9a-f]{64}$/; + /** * Returns true if the identifier looks like a NIP-05 username (contains @). */ @@ -12,12 +16,50 @@ function isNip05Like(id: string): boolean { return id.includes('@'); } +/** + * Resolves a raw 64-char hex string to either an event ID or a pubkey. + * Tries to find an event with that ID first; if none is found, checks + * whether a kind-0 profile exists for it as a pubkey. + */ +function HexIdentifierPage({ hex }: { hex: string }) { + const { nostr } = useNostr(); + + const { data: resolved, isLoading } = useQuery({ + queryKey: ['hex-resolve', hex], + queryFn: async () => { + // Try as event ID first + const events = await nostr.query([{ ids: [hex], limit: 1 }]); + if (events.length > 0) { + return 'event' as const; + } + // Try as pubkey (look for kind 0 profile) + const profiles = await nostr.query([{ kinds: [0], authors: [hex], limit: 1 }]); + if (profiles.length > 0) { + return 'pubkey' as const; + } + return 'event' as const; // default to event — PostDetailPage will handle "not found" + }, + staleTime: Infinity, + }); + + if (isLoading || !resolved) { + return null; + } + + if (resolved === 'pubkey') { + return ; + } + + return ; +} + /** * Universal route handler for `/:param`. * * Dispatches based on the shape of the identifier: * - NIP-19 (`npub1...`, `note1...`, `nevent1...`, `naddr1...`, `nprofile1...`) * - NIP-05 (`user@domain.com`) → profile + * - Raw 64-char hex → event ID or pubkey (resolved via relay query) */ export function NIP19Page() { const { nip19: identifier } = useParams<{ nip19: string }>(); @@ -31,6 +73,11 @@ export function NIP19Page() { return ; } + // Raw 64-char hex — could be event ID or pubkey, need to resolve + if (HEX_64_RE.test(identifier)) { + return ; + } + // Try NIP-19 decoding let decoded; try {