You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
208 lines
7.6 KiB
208 lines
7.6 KiB
import ClientSelect from '@/components/ClientSelect' |
|
import { Button } from '@/components/ui/button' |
|
import { FAST_READ_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants' |
|
import { normalizeUrl } from '@/lib/url' |
|
import client from '@/services/client.service' |
|
import { AlertCircle, Search } from 'lucide-react' |
|
import { nip19 } from 'nostr-tools' |
|
import { useEffect, useState } from 'react' |
|
import { useTranslation } from 'react-i18next' |
|
import logger from '@/lib/logger' |
|
|
|
export default function NotFound({ |
|
bech32Id, |
|
onEventFound |
|
}: { |
|
bech32Id?: string |
|
onEventFound?: (event: any) => void |
|
}) { |
|
const { t } = useTranslation() |
|
const [isSearchingExternal, setIsSearchingExternal] = useState(false) |
|
const [triedExternal, setTriedExternal] = useState(false) |
|
const [externalRelays, setExternalRelays] = useState<string[]>([]) |
|
const [hexEventId, setHexEventId] = useState<string | null>(null) |
|
|
|
// Calculate which external relays would be tried (excluding already-tried relays) |
|
useEffect(() => { |
|
if (!bech32Id) return |
|
|
|
const getExternalRelays = async () => { |
|
// Get all relays that have already been tried (FAST_READ_RELAY_URLS) |
|
// These are the relays used in the initial fetch |
|
const alreadyTriedRelaysSet = new Set<string>() |
|
;[...FAST_READ_RELAY_URLS].forEach(url => { |
|
const normalized = normalizeUrl(url) |
|
if (normalized) alreadyTriedRelaysSet.add(normalized) |
|
}) |
|
|
|
let hintRelays: string[] = [] |
|
let extractedHexEventId: string | null = null |
|
|
|
// Parse relay hints and author from bech32 ID |
|
if (!/^[0-9a-f]{64}$/.test(bech32Id)) { |
|
try { |
|
const { type, data } = nip19.decode(bech32Id) |
|
|
|
if (type === 'nevent') { |
|
extractedHexEventId = data.id |
|
if (data.relays) hintRelays.push(...data.relays) |
|
if (data.author) { |
|
const authorRelayList = await client.fetchRelayList(data.author).catch(() => ({ read: [] as string[], write: [] as string[] })) |
|
hintRelays.push(...(authorRelayList.read ?? []).slice(0, 4), ...(authorRelayList.write ?? []).slice(0, 4)) |
|
} |
|
} else if (type === 'naddr') { |
|
if (data.relays) hintRelays.push(...data.relays) |
|
const authorRelayList = await client.fetchRelayList(data.pubkey).catch(() => ({ read: [] as string[], write: [] as string[] })) |
|
hintRelays.push(...(authorRelayList.read ?? []).slice(0, 4), ...(authorRelayList.write ?? []).slice(0, 4)) |
|
} else if (type === 'note') { |
|
extractedHexEventId = data |
|
} |
|
} catch (err) { |
|
logger.error('Failed to parse external relays', { error: err, bech32Id }) |
|
} |
|
} else { |
|
extractedHexEventId = bech32Id |
|
} |
|
|
|
setHexEventId(extractedHexEventId) |
|
|
|
// Get relays where this event was seen |
|
const seenOn = extractedHexEventId ? client.getSeenEventRelayUrls(extractedHexEventId) : [] |
|
hintRelays.push(...seenOn) |
|
|
|
// Normalize all hint relays |
|
const normalizedHints = hintRelays |
|
.map(url => normalizeUrl(url)) |
|
.filter((url): url is string => Boolean(url)) |
|
|
|
// Combine hints with SEARCHABLE_RELAY_URLS (always include as fallback) |
|
// Normalize SEARCHABLE_RELAY_URLS for comparison |
|
const normalizedSearchableRelays = SEARCHABLE_RELAY_URLS |
|
.map(url => normalizeUrl(url)) |
|
.filter((url): url is string => Boolean(url)) |
|
|
|
// Combine all potential relays (hints + searchable) |
|
const allPotentialRelays = new Set([...normalizedHints, ...normalizedSearchableRelays]) |
|
|
|
// Filter out relays that were already tried |
|
const externalRelays = Array.from(allPotentialRelays).filter( |
|
relay => !alreadyTriedRelaysSet.has(relay) |
|
) |
|
|
|
// Deduplicate final relay list |
|
setExternalRelays(externalRelays) |
|
|
|
logger.debug('External relays calculated (NotFound)', { |
|
bech32Id, |
|
hintRelaysCount: normalizedHints.length, |
|
searchableRelaysCount: normalizedSearchableRelays.length, |
|
alreadyTriedCount: alreadyTriedRelaysSet.size, |
|
externalRelaysCount: externalRelays.length, |
|
externalRelays: externalRelays.slice(0, 10) // Log first 10 |
|
}) |
|
} |
|
|
|
getExternalRelays() |
|
}, [bech32Id]) |
|
|
|
const handleTryExternalRelays = async () => { |
|
if (!hexEventId || isSearchingExternal) return |
|
|
|
if (externalRelays.length === 0) { |
|
logger.warn('No external relays to search (NotFound)', { bech32Id, hexEventId }) |
|
setTriedExternal(true) |
|
return |
|
} |
|
|
|
setIsSearchingExternal(true) |
|
try { |
|
logger.info('Searching external relays (NotFound)', { |
|
bech32Id, |
|
hexEventId, |
|
relayCount: externalRelays.length, |
|
relays: externalRelays.slice(0, 5) // Log first 5 relays |
|
}) |
|
|
|
const event = await client.fetchEventWithExternalRelays(hexEventId, externalRelays) |
|
|
|
if (event) { |
|
logger.info('Event found on external relay (NotFound)', { bech32Id, hexEventId }) |
|
if (onEventFound) { |
|
onEventFound(event) |
|
} |
|
} else { |
|
logger.info('Event not found on external relays (NotFound)', { |
|
bech32Id, |
|
hexEventId, |
|
relayCount: externalRelays.length |
|
}) |
|
} |
|
} catch (error) { |
|
logger.error('External relay fetch failed (NotFound)', { error, bech32Id, hexEventId, externalRelays }) |
|
} finally { |
|
setIsSearchingExternal(false) |
|
setTriedExternal(true) |
|
} |
|
} |
|
|
|
const hasExternalRelays = externalRelays.length > 0 |
|
|
|
return ( |
|
<div className="text-muted-foreground w-full h-full flex flex-col items-center justify-center gap-4 p-4"> |
|
<AlertCircle className="w-12 h-12 text-muted-foreground/50" /> |
|
<div className="text-lg font-medium">{t('Note not found')}</div> |
|
|
|
{bech32Id && !triedExternal && hasExternalRelays && ( |
|
<div className="flex flex-col items-center gap-3 max-w-md"> |
|
<div className="text-sm text-center text-muted-foreground"> |
|
{t('The note was not found on your relays or default relays.')} |
|
</div> |
|
|
|
<Button |
|
variant="default" |
|
onClick={handleTryExternalRelays} |
|
disabled={isSearchingExternal} |
|
className="gap-2" |
|
> |
|
{isSearchingExternal ? ( |
|
<> |
|
<Search className="w-4 h-4 animate-spin" /> |
|
{t('Searching external relays...')} |
|
</> |
|
) : ( |
|
<> |
|
<Search className="w-4 h-4" /> |
|
{t('Try external relays')} |
|
</> |
|
)} |
|
</Button> |
|
|
|
<details className="text-xs text-muted-foreground w-full"> |
|
<summary className="cursor-pointer hover:text-foreground text-center list-none"> |
|
{t('Show relays')} ({externalRelays.length}) |
|
</summary> |
|
<div className="mt-2 space-y-1 max-h-32 overflow-y-auto"> |
|
{externalRelays.map((relay, i) => ( |
|
<div key={i} className="font-mono text-[10px] truncate px-2 py-1 bg-muted/50 rounded"> |
|
{relay} |
|
</div> |
|
))} |
|
</div> |
|
</details> |
|
</div> |
|
)} |
|
|
|
{bech32Id && !triedExternal && !hasExternalRelays && ( |
|
<div className="text-sm text-muted-foreground"> |
|
{t('No external relay hints available')} |
|
</div> |
|
)} |
|
|
|
{triedExternal && ( |
|
<div className="text-sm">{t('Note could not be found anywhere')}</div> |
|
)} |
|
|
|
<ClientSelect originalNoteId={bech32Id} /> |
|
</div> |
|
) |
|
}
|
|
|