Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
374542414f
  1. 114
      src/components/Embedded/EmbeddedNote.tsx
  2. 29
      src/components/ParentNotePreview/index.tsx
  3. 44
      src/hooks/useFetchProfile.tsx
  4. 4
      src/i18n/locales/ar.ts
  5. 4
      src/i18n/locales/de.ts
  6. 4
      src/i18n/locales/en.ts
  7. 4
      src/i18n/locales/es.ts
  8. 4
      src/i18n/locales/fa.ts
  9. 4
      src/i18n/locales/fr.ts
  10. 4
      src/i18n/locales/hi.ts
  11. 4
      src/i18n/locales/it.ts
  12. 4
      src/i18n/locales/ja.ts
  13. 4
      src/i18n/locales/ko.ts
  14. 4
      src/i18n/locales/pl.ts
  15. 4
      src/i18n/locales/pt-BR.ts
  16. 4
      src/i18n/locales/pt-PT.ts
  17. 4
      src/i18n/locales/ru.ts
  18. 4
      src/i18n/locales/th.ts
  19. 4
      src/i18n/locales/zh.ts
  20. 4
      src/pages/secondary/NotePage/NotFound.tsx
  21. 67
      src/services/client-events.service.ts

114
src/components/Embedded/EmbeddedNote.tsx

@ -18,6 +18,32 @@ import { contentParserService } from '@/services/content-parser.service'
import { useSmartNoteNavigation } from '@/PageManager' import { useSmartNoteNavigation } from '@/PageManager'
import { toNote } from '@/lib/link' import { toNote } from '@/lib/link'
/** Embedded `noteId` is often raw hex from parsers; must accept A–F and normalize for REQ `ids`. */
function hexEventIdFromNoteId(noteId: string): string | null {
const trimmed = noteId.trim()
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
return trimmed.toLowerCase()
}
try {
const { type, data } = nip19.decode(noteId)
if (type === 'note') return data
if (type === 'nevent') return data.id
return null
} catch {
return null
}
}
/** True if `fetchEventWithExternalRelays(noteId, …)` can build a REQ filter (hex, note, nevent, naddr). */
function canSearchOnExternalRelays(noteId: string): boolean {
if (hexEventIdFromNoteId(noteId)) return true
try {
return nip19.decode(noteId.trim()).type === 'naddr'
} catch {
return false
}
}
export function EmbeddedNote({ export function EmbeddedNote({
noteId, noteId,
className, className,
@ -140,6 +166,9 @@ function EmbeddedNoteNotFound({
const [triedExternal, setTriedExternal] = useState(false) const [triedExternal, setTriedExternal] = useState(false)
const [externalRelays, setExternalRelays] = useState<string[]>([]) const [externalRelays, setExternalRelays] = useState<string[]>([])
const [hexEventId, setHexEventId] = useState<string | null>(null) const [hexEventId, setHexEventId] = useState<string | null>(null)
const [externalSearchDetail, setExternalSearchDetail] = useState<
null | 'unparseable' | 'no_relays' | 'searched'
>(null)
// Calculate which external relays would be tried when user clicks "Try external relays". // Calculate which external relays would be tried when user clicks "Try external relays".
// IMPORTANT: For embedded events, we should search: // IMPORTANT: For embedded events, we should search:
@ -179,11 +208,13 @@ function EmbeddedNoteNotFound({
} }
} }
// 2. Extract hints from bech32 ID and embedded event author // 2. Hex id (any case) or bech32; hints from nevent/naddr for extra relays
if (!/^[0-9a-f]{64}$/.test(noteId)) { const quickHex = hexEventIdFromNoteId(noteId)
if (quickHex) {
extractedHexEventId = quickHex
}
try { try {
const { type, data } = nip19.decode(noteId) const { type, data } = nip19.decode(noteId)
if (type === 'nevent') { if (type === 'nevent') {
extractedHexEventId = data.id extractedHexEventId = data.id
if (data.relays) hintRelays.push(...data.relays) if (data.relays) hintRelays.push(...data.relays)
@ -198,11 +229,8 @@ function EmbeddedNoteNotFound({
} else if (type === 'note') { } else if (type === 'note') {
extractedHexEventId = data extractedHexEventId = data
} }
} catch (err) { } catch {
logger.error('Failed to parse external relays', { error: err, noteId }) // Plain hex ids are not valid bech32 — already handled via quickHex
}
} else {
extractedHexEventId = noteId
} }
setHexEventId(extractedHexEventId) setHexEventId(extractedHexEventId)
@ -244,61 +272,81 @@ function EmbeddedNoteNotFound({
} }
getExternalRelays() getExternalRelays()
}, [noteId]) // containingEvent supplies e/a/q relay hints + author NIP-65 list — must rerun when parent loads
}, [noteId, containingEvent?.id])
const handleTryExternalRelays = async () => { const handleTryExternalRelays = async () => {
if (!hexEventId || isSearchingExternal) return if (isSearchingExternal) return
if (!canSearchOnExternalRelays(noteId)) {
logger.warn('External relay search skipped: unsupported note id', { noteId })
setExternalSearchDetail('unparseable')
setTriedExternal(true)
return
}
if (externalRelays.length === 0) { if (externalRelays.length === 0) {
logger.warn('No external relays to search', { noteId, hexEventId }) logger.warn('No external relays to search', { noteId })
setExternalSearchDetail('no_relays')
setTriedExternal(true) setTriedExternal(true)
return return
} }
setIsSearchingExternal(true) setIsSearchingExternal(true)
setExternalSearchDetail(null)
let found: Event | undefined
try { try {
const idLog = hexEventId ?? hexEventIdFromNoteId(noteId) ?? noteId.slice(0, 16)
logger.info('Searching external relays', { logger.info('Searching external relays', {
noteId, noteId,
hexEventId, hexOrHint: idLog,
relayCount: externalRelays.length, relayCount: externalRelays.length,
relays: externalRelays.slice(0, 5) // Log first 5 relays relays: externalRelays.slice(0, 5)
}) })
const event = await client.fetchEventWithExternalRelays(hexEventId, externalRelays) const event = await client.fetchEventWithExternalRelays(noteId, externalRelays)
if (event) { if (event) {
logger.info('Event found on external relay', { noteId, hexEventId }) logger.info('Event found on external relay', { noteId })
if (onEventFound) { found = event
onEventFound(event) onEventFound?.(event)
}
} else { } else {
logger.info('Event not found on external relays', { logger.info('Event not found on external relays', {
noteId, noteId,
hexEventId,
relayCount: externalRelays.length relayCount: externalRelays.length
}) })
setExternalSearchDetail('searched')
} }
} catch (error) { } catch (error) {
logger.error('External relay fetch failed', { error, noteId, hexEventId, externalRelays }) logger.error('External relay fetch failed', { error, noteId, externalRelays })
setExternalSearchDetail('searched')
} finally { } finally {
setIsSearchingExternal(false) setIsSearchingExternal(false)
if (!found) {
setTriedExternal(true) setTriedExternal(true)
} }
} }
}
const hasExternalRelays = externalRelays.length > 0 const hasExternalRelays = externalRelays.length > 0
const showExternalTryButton =
!triedExternal && hasExternalRelays && canSearchOnExternalRelays(noteId)
return ( return (
<div className={cn('text-left p-3 border rounded-lg', className)}> <div className={cn('text-left p-3 border rounded-lg', className)}>
<div className="flex flex-col items-center text-muted-foreground gap-3"> <div className="flex flex-col items-center text-muted-foreground gap-3">
<div className="text-sm font-medium">{t('Note not found')}</div> <div className="text-sm font-medium">{t('Note not found')}</div>
{!triedExternal && hasExternalRelays && ( {showExternalTryButton && (
<div className="flex flex-col items-center gap-2 w-full"> <div className="flex flex-col items-center gap-2 w-full">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handleTryExternalRelays} onClick={(e) => {
e.stopPropagation()
e.preventDefault()
void handleTryExternalRelays()
}}
disabled={isSearchingExternal} disabled={isSearchingExternal}
className="gap-2 w-full" className="gap-2 w-full"
> >
@ -333,7 +381,27 @@ function EmbeddedNoteNotFound({
<div className="text-xs text-center">{t('No external relay hints available')}</div> <div className="text-xs text-center">{t('No external relay hints available')}</div>
)} )}
{triedExternal && ( {!triedExternal && hasExternalRelays && !canSearchOnExternalRelays(noteId) && (
<div className="text-xs text-center text-muted-foreground">
{t('External relay search is not available for this link type')}
</div>
)}
{triedExternal && externalSearchDetail === 'unparseable' && (
<div className="text-xs text-center">{t('External relay search is not available for this link type')}</div>
)}
{triedExternal && externalSearchDetail === 'no_relays' && (
<div className="text-xs text-center">{t('No external relay hints available')}</div>
)}
{triedExternal && externalSearchDetail === 'searched' && (
<div className="text-xs text-center">
{t('Searched external relays not found', { count: externalRelays.length })}
</div>
)}
{triedExternal && !externalSearchDetail && (
<div className="text-xs text-center">{t('Note could not be found anywhere')}</div> <div className="text-xs text-center">{t('Note could not be found anywhere')}</div>
)} )}

29
src/components/ParentNotePreview/index.tsx

@ -5,7 +5,7 @@ import { cn } from '@/lib/utils'
import client from '@/services/client.service' import client from '@/services/client.service'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { Event, nip19 } from 'nostr-tools' import { Event } from 'nostr-tools'
import ContentPreview from '../ContentPreview' import ContentPreview from '../ContentPreview'
import UserAvatar from '../UserAvatar' import UserAvatar from '../UserAvatar'
import logger from '@/lib/logger' import logger from '@/lib/logger'
@ -26,34 +26,13 @@ export default function ParentNotePreview({
/** One automatic searchable-relay attempt per eventId; without this, the effect re-fires forever after each 20s timeout. */ /** One automatic searchable-relay attempt per eventId; without this, the effect re-fires forever after each 20s timeout. */
const autoSearchableAttemptedRef = useRef(false) const autoSearchableAttemptedRef = useRef(false)
// Helper function to decode event ID // Helper function to fetch from searchable relays (hex, note1, nevent1, naddr1)
const getHexEventId = (id: string): string | null => {
if (/^[0-9a-f]{64}$/.test(id)) {
return id
}
try {
const { type, data } = nip19.decode(id)
if (type === 'note') {
return data
} else if (type === 'nevent') {
return data.id
}
// Can't fetch naddr with fetchEventWithExternalRelays
return null
} catch (err) {
// Invalid bech32 or already hex
return null
}
}
// Helper function to fetch from searchable relays
const fetchFromSearchableRelays = useCallback(async () => { const fetchFromSearchableRelays = useCallback(async () => {
const hexEventId = getHexEventId(eventId) if (!eventId?.trim()) return
if (!hexEventId) return
setIsFetchingFallback(true) setIsFetchingFallback(true)
try { try {
const foundEvent = await client.fetchEventWithExternalRelays(hexEventId, SEARCHABLE_RELAY_URLS) const foundEvent = await client.fetchEventWithExternalRelays(eventId, SEARCHABLE_RELAY_URLS)
if (foundEvent) { if (foundEvent) {
setFallbackEvent(foundEvent) setFallbackEvent(foundEvent)
} }

44
src/hooks/useFetchProfile.tsx

@ -73,29 +73,12 @@ export function useFetchProfile(id?: string, skipCache = false) {
pubkey: pubkey.substring(0, 8) pubkey: pubkey.substring(0, 8)
}) })
try { try {
// Add timeout to prevent waiting forever on a stuck promise // Await the shared promise only — it already races fetchProfileEvent with
const timeoutPromise = new Promise<null>((resolve) => { // PROFILE_FETCH_PROMISE_TIMEOUT_MS. Per-waiter Promise.race timers caused N identical
setTimeout(() => { // "timeout" warnings (one per mounted component) and premature map deletion.
logger.warn('[useFetchProfile] Existing promise timeout, not starting duplicate fetch', { const existingProfile = await existingPromise
pubkey: pubkey.substring(0, 8)
})
resolve(null)
}, PROFILE_FETCH_PROMISE_TIMEOUT_MS)
})
const existingProfile = await Promise.race([existingPromise, timeoutPromise])
if (cancelled.current) return null if (cancelled.current) return null
// If timeout won: do NOT start a new fetch (avoids pile-up of parallel fetches for same pubkey).
// Return null so caller can show fallback; the original fetch may still complete and update cache.
// Set a cooldown period to prevent immediate retries from other components
if (existingProfile === null && !cancelled.current) {
globalFetchPromises.delete(pubkey)
globalFetchingPubkeys.delete(pubkey)
// Set cooldown for 10 seconds to prevent cascade of duplicate fetches
globalFetchCooldowns.set(pubkey, Date.now() + 10000)
return null
}
if (existingProfile) { if (existingProfile) {
// Update state for this instance // Update state for this instance
setProfile(existingProfile) setProfile(existingProfile)
@ -130,26 +113,9 @@ export function useFetchProfile(id?: string, skipCache = false) {
const retryPromise = globalFetchPromises.get(pubkey) const retryPromise = globalFetchPromises.get(pubkey)
if (retryPromise) { if (retryPromise) {
try { try {
// Add timeout protection here too const retryProfile = await retryPromise
const timeoutPromise = new Promise<null>((resolve) => {
setTimeout(() => {
logger.warn('[useFetchProfile] Retry promise timeout, not starting duplicate fetch', {
pubkey: pubkey.substring(0, 8)
})
resolve(null)
}, PROFILE_FETCH_PROMISE_TIMEOUT_MS)
})
const retryProfile = await Promise.race([retryPromise, timeoutPromise])
if (cancelled.current) return null if (cancelled.current) return null
if (retryProfile === null && !cancelled.current) {
globalFetchPromises.delete(pubkey)
globalFetchingPubkeys.delete(pubkey)
// Set cooldown for 10 seconds to prevent cascade of duplicate fetches
globalFetchCooldowns.set(pubkey, Date.now() + 10000)
return null
}
if (retryProfile) { if (retryProfile) {
// Update state for this instance // Update state for this instance
setProfile(retryProfile) setProfile(retryProfile)

4
src/i18n/locales/ar.ts

@ -404,6 +404,10 @@ export default {
'Searching...': 'Searching...', 'Searching...': 'Searching...',
'Show relays': 'Show relays', 'Show relays': 'Show relays',
'No external relay hints available': 'No external relay hints available', 'No external relay hints available': 'No external relay hints available',
'External relay search is not available for this link type':
'External relay search is not available for this link type.',
'Searched external relays not found':
'Searched {{count}} external relays; the note was still not found.',
'no more replies': 'لا توجد مزيد من الردود', 'no more replies': 'لا توجد مزيد من الردود',
'Relay sets': 'مجموعات الريلاي', 'Relay sets': 'مجموعات الريلاي',
'Favorite Relays': 'الريلايات المفضلة', 'Favorite Relays': 'الريلايات المفضلة',

4
src/i18n/locales/de.ts

@ -413,6 +413,10 @@ export default {
'Searching...': 'Searching...', 'Searching...': 'Searching...',
'Show relays': 'Show relays', 'Show relays': 'Show relays',
'No external relay hints available': 'No external relay hints available', 'No external relay hints available': 'No external relay hints available',
'External relay search is not available for this link type':
'Externe Relay-Suche ist für diesen Linktyp nicht verfügbar.',
'Searched external relays not found':
'{{count}} externe Relays durchsucht; die Note wurde nicht gefunden.',
'no more replies': 'keine weiteren Antworten', 'no more replies': 'keine weiteren Antworten',
'Relay sets': 'Relay-Sets', 'Relay sets': 'Relay-Sets',
'Favorite Relays': 'Lieblings-Relays', 'Favorite Relays': 'Lieblings-Relays',

4
src/i18n/locales/en.ts

@ -405,6 +405,10 @@ export default {
'Searching...': 'Searching...', 'Searching...': 'Searching...',
'Show relays': 'Show relays', 'Show relays': 'Show relays',
'No external relay hints available': 'No external relay hints available', 'No external relay hints available': 'No external relay hints available',
'External relay search is not available for this link type':
'External relay search is not available for this link type.',
'Searched external relays not found':
'Searched {{count}} external relays; the note was still not found.',
'no more replies': 'no more replies', 'no more replies': 'no more replies',
'Relay sets': 'Relay sets', 'Relay sets': 'Relay sets',
'Favorite Relays': 'Favorite Relays', 'Favorite Relays': 'Favorite Relays',

4
src/i18n/locales/es.ts

@ -408,6 +408,10 @@ export default {
'Searching...': 'Searching...', 'Searching...': 'Searching...',
'Show relays': 'Show relays', 'Show relays': 'Show relays',
'No external relay hints available': 'No external relay hints available', 'No external relay hints available': 'No external relay hints available',
'External relay search is not available for this link type':
'External relay search is not available for this link type.',
'Searched external relays not found':
'Searched {{count}} external relays; the note was still not found.',
'no more replies': 'no hay más respuestas', 'no more replies': 'no hay más respuestas',
'Relay sets': 'Conjuntos de relés', 'Relay sets': 'Conjuntos de relés',
'Favorite Relays': 'Relés favoritos', 'Favorite Relays': 'Relés favoritos',

4
src/i18n/locales/fa.ts

@ -407,6 +407,10 @@ export default {
'Searching...': 'Searching...', 'Searching...': 'Searching...',
'Show relays': 'Show relays', 'Show relays': 'Show relays',
'No external relay hints available': 'No external relay hints available', 'No external relay hints available': 'No external relay hints available',
'External relay search is not available for this link type':
'External relay search is not available for this link type.',
'Searched external relays not found':
'Searched {{count}} external relays; the note was still not found.',
'no more replies': 'پاسخ بیشتری وجود ندارد', 'no more replies': 'پاسخ بیشتری وجود ندارد',
'Relay sets': 'مجموعههای رله', 'Relay sets': 'مجموعههای رله',
'Favorite Relays': 'رلههای مورد علاقه', 'Favorite Relays': 'رلههای مورد علاقه',

4
src/i18n/locales/fr.ts

@ -407,6 +407,10 @@ export default {
'Searching...': 'Searching...', 'Searching...': 'Searching...',
'Show relays': 'Show relays', 'Show relays': 'Show relays',
'No external relay hints available': 'No external relay hints available', 'No external relay hints available': 'No external relay hints available',
'External relay search is not available for this link type':
'External relay search is not available for this link type.',
'Searched external relays not found':
'Searched {{count}} external relays; the note was still not found.',
'no more replies': 'aucune autre réponse', 'no more replies': 'aucune autre réponse',
'Relay sets': 'Groupes de relais', 'Relay sets': 'Groupes de relais',
'Favorite Relays': 'Relais favoris', 'Favorite Relays': 'Relais favoris',

4
src/i18n/locales/hi.ts

@ -408,6 +408,10 @@ export default {
'Searching...': 'Searching...', 'Searching...': 'Searching...',
'Show relays': 'Show relays', 'Show relays': 'Show relays',
'No external relay hints available': 'No external relay hints available', 'No external relay hints available': 'No external relay hints available',
'External relay search is not available for this link type':
'External relay search is not available for this link type.',
'Searched external relays not found':
'Searched {{count}} external relays; the note was still not found.',
'no more replies': 'कई और उततर नह', 'no more replies': 'कई और उततर नह',
'Relay sets': 'रिट', 'Relay sets': 'रिट',
'Favorite Relays': 'पसि', 'Favorite Relays': 'पसि',

4
src/i18n/locales/it.ts

@ -408,6 +408,10 @@ export default {
'Searching...': 'Searching...', 'Searching...': 'Searching...',
'Show relays': 'Show relays', 'Show relays': 'Show relays',
'No external relay hints available': 'No external relay hints available', 'No external relay hints available': 'No external relay hints available',
'External relay search is not available for this link type':
'External relay search is not available for this link type.',
'Searched external relays not found':
'Searched {{count}} external relays; the note was still not found.',
'no more replies': 'niente più repliche', 'no more replies': 'niente più repliche',
'Relay sets': 'Set di Relay', 'Relay sets': 'Set di Relay',
'Favorite Relays': 'Relay preferiti', 'Favorite Relays': 'Relay preferiti',

4
src/i18n/locales/ja.ts

@ -405,6 +405,10 @@ export default {
'Searching...': 'Searching...', 'Searching...': 'Searching...',
'Show relays': 'Show relays', 'Show relays': 'Show relays',
'No external relay hints available': 'No external relay hints available', 'No external relay hints available': 'No external relay hints available',
'External relay search is not available for this link type':
'External relay search is not available for this link type.',
'Searched external relays not found':
'Searched {{count}} external relays; the note was still not found.',
'no more replies': 'これ以上の返信はありません', 'no more replies': 'これ以上の返信はありません',
'Relay sets': 'リレイセット', 'Relay sets': 'リレイセット',
'Favorite Relays': 'お気に入りのリレイ', 'Favorite Relays': 'お気に入りのリレイ',

4
src/i18n/locales/ko.ts

@ -404,6 +404,10 @@ export default {
'Searching...': 'Searching...', 'Searching...': 'Searching...',
'Show relays': 'Show relays', 'Show relays': 'Show relays',
'No external relay hints available': 'No external relay hints available', 'No external relay hints available': 'No external relay hints available',
'External relay search is not available for this link type':
'External relay search is not available for this link type.',
'Searched external relays not found':
'Searched {{count}} external relays; the note was still not found.',
'no more replies': '더 이상 답글 없음', 'no more replies': '더 이상 답글 없음',
'Relay sets': '릴레이 세트', 'Relay sets': '릴레이 세트',
'Favorite Relays': '즐겨찾는 릴레이', 'Favorite Relays': '즐겨찾는 릴레이',

4
src/i18n/locales/pl.ts

@ -405,6 +405,10 @@ export default {
'Searching...': 'Searching...', 'Searching...': 'Searching...',
'Show relays': 'Show relays', 'Show relays': 'Show relays',
'No external relay hints available': 'No external relay hints available', 'No external relay hints available': 'No external relay hints available',
'External relay search is not available for this link type':
'External relay search is not available for this link type.',
'Searched external relays not found':
'Searched {{count}} external relays; the note was still not found.',
'no more replies': 'brak kolejnych odpowiedzi', 'no more replies': 'brak kolejnych odpowiedzi',
'Relay sets': 'Zestawy transmiterów', 'Relay sets': 'Zestawy transmiterów',
'Favorite Relays': 'Ulubione transmitery', 'Favorite Relays': 'Ulubione transmitery',

4
src/i18n/locales/pt-BR.ts

@ -407,6 +407,10 @@ export default {
'Searching...': 'Searching...', 'Searching...': 'Searching...',
'Show relays': 'Show relays', 'Show relays': 'Show relays',
'No external relay hints available': 'No external relay hints available', 'No external relay hints available': 'No external relay hints available',
'External relay search is not available for this link type':
'External relay search is not available for this link type.',
'Searched external relays not found':
'Searched {{count}} external relays; the note was still not found.',
'no more replies': 'não há mais respostas', 'no more replies': 'não há mais respostas',
'Relay sets': 'Conjuntos de relay', 'Relay sets': 'Conjuntos de relay',
'Favorite Relays': 'Relays favoritos', 'Favorite Relays': 'Relays favoritos',

4
src/i18n/locales/pt-PT.ts

@ -407,6 +407,10 @@ export default {
'Searching...': 'Searching...', 'Searching...': 'Searching...',
'Show relays': 'Show relays', 'Show relays': 'Show relays',
'No external relay hints available': 'No external relay hints available', 'No external relay hints available': 'No external relay hints available',
'External relay search is not available for this link type':
'External relay search is not available for this link type.',
'Searched external relays not found':
'Searched {{count}} external relays; the note was still not found.',
'no more replies': 'não há mais respostas', 'no more replies': 'não há mais respostas',
'Relay sets': 'Conjuntos de Relé', 'Relay sets': 'Conjuntos de Relé',
'Favorite Relays': 'Relés Favoritos', 'Favorite Relays': 'Relés Favoritos',

4
src/i18n/locales/ru.ts

@ -408,6 +408,10 @@ export default {
'Searching...': 'Searching...', 'Searching...': 'Searching...',
'Show relays': 'Show relays', 'Show relays': 'Show relays',
'No external relay hints available': 'No external relay hints available', 'No external relay hints available': 'No external relay hints available',
'External relay search is not available for this link type':
'External relay search is not available for this link type.',
'Searched external relays not found':
'Searched {{count}} external relays; the note was still not found.',
'no more replies': 'больше нет ответов', 'no more replies': 'больше нет ответов',
'Relay sets': 'Наборы ретрансляторов', 'Relay sets': 'Наборы ретрансляторов',
'Favorite Relays': 'Избранные ретрансляторы', 'Favorite Relays': 'Избранные ретрансляторы',

4
src/i18n/locales/th.ts

@ -404,6 +404,10 @@ export default {
'Searching...': 'Searching...', 'Searching...': 'Searching...',
'Show relays': 'Show relays', 'Show relays': 'Show relays',
'No external relay hints available': 'No external relay hints available', 'No external relay hints available': 'No external relay hints available',
'External relay search is not available for this link type':
'External relay search is not available for this link type.',
'Searched external relays not found':
'Searched {{count}} external relays; the note was still not found.',
'no more replies': 'ไมการตอบกลบเพมเตม', 'no more replies': 'ไมการตอบกลบเพมเตม',
'Relay sets': 'ชดรเลย', 'Relay sets': 'ชดรเลย',
'Favorite Relays': 'รเลยโปรด', 'Favorite Relays': 'รเลยโปรด',

4
src/i18n/locales/zh.ts

@ -403,6 +403,10 @@ export default {
'Searching...': 'Searching...', 'Searching...': 'Searching...',
'Show relays': 'Show relays', 'Show relays': 'Show relays',
'No external relay hints available': 'No external relay hints available', 'No external relay hints available': 'No external relay hints available',
'External relay search is not available for this link type':
'External relay search is not available for this link type.',
'Searched external relays not found':
'Searched {{count}} external relays; the note was still not found.',
'no more replies': '没有更多回复了', 'no more replies': '没有更多回复了',
'Relay sets': '服务器组', 'Relay sets': '服务器组',
'Favorite Relays': '收藏的服务器', 'Favorite Relays': '收藏的服务器',

4
src/pages/secondary/NotePage/NotFound.tsx

@ -135,7 +135,7 @@ export default function NotFound({
}, [bech32Id]) }, [bech32Id])
const handleTryExternalRelays = async () => { const handleTryExternalRelays = async () => {
if (!hexEventId || isSearchingExternal) return if (!bech32Id || isSearchingExternal) return
if (externalRelays.length === 0) { if (externalRelays.length === 0) {
logger.warn('No external relays to search (NotFound)', { bech32Id, hexEventId }) logger.warn('No external relays to search (NotFound)', { bech32Id, hexEventId })
@ -152,7 +152,7 @@ export default function NotFound({
relays: externalRelays.slice(0, 5) // Log first 5 relays relays: externalRelays.slice(0, 5) // Log first 5 relays
}) })
const event = await client.fetchEventWithExternalRelays(hexEventId, externalRelays) const event = await client.fetchEventWithExternalRelays(bech32Id, externalRelays)
if (event) { if (event) {
logger.info('Event found on external relay (NotFound)', { bech32Id, hexEventId }) logger.info('Event found on external relay (NotFound)', { bech32Id, hexEventId })

67
src/services/client-events.service.ts

@ -10,8 +10,8 @@ import { isReplaceableEvent } from '@/lib/event'
import { buildComprehensiveRelayList } from '@/lib/relay-list-builder' import { buildComprehensiveRelayList } from '@/lib/relay-list-builder'
/** /**
* Build comprehensive relay list: author's outboxes + user's inboxes + relay hints + defaults * Build comprehensive relay list for event-by-id fetch: user's inboxes (+ cache), relay hints,
* Uses the shared relay list builder utility * author outboxes/inboxes when known, FAST_READ_RELAY_URLS, and SEARCHABLE_RELAY_URLS.
*/ */
async function buildComprehensiveRelayListForEvents( async function buildComprehensiveRelayListForEvents(
authorPubkey: string | undefined, authorPubkey: string | undefined,
@ -26,6 +26,7 @@ async function buildComprehensiveRelayListForEvents(
seenRelays, seenRelays,
containingEventRelays, containingEventRelays,
includeFastReadRelays: true, includeFastReadRelays: true,
includeSearchableRelays: true,
includeLocalRelays: true includeLocalRelays: true
}) })
} }
@ -86,35 +87,69 @@ export class EventService {
} }
/** /**
* Fetch event with external relays * REQ filter for searching a note/nevent/naddr/hex id on arbitrary relays.
*/ */
async fetchEventWithExternalRelays(eventId: string, externalRelays: string[]): Promise<NEvent | undefined> { private filterForExternalRelayFetch(noteId: string): Filter | null {
const trimmed = noteId.trim()
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
return { ids: [trimmed.toLowerCase()], limit: 1 }
}
try {
const { type, data } = nip19.decode(trimmed)
if (type === 'note') return { ids: [data], limit: 1 }
if (type === 'nevent') return { ids: [data.id], limit: 1 }
if (type === 'naddr') {
return {
kinds: [data.kind],
authors: [data.pubkey],
'#d': [data.identifier],
limit: 1
}
}
} catch {
/* invalid id */
}
return null
}
/**
* Fetch event with external relays (hex, note1, nevent1, or naddr1)
*/
async fetchEventWithExternalRelays(noteId: string, externalRelays: string[]): Promise<NEvent | undefined> {
if (!externalRelays || externalRelays.length === 0) { if (!externalRelays || externalRelays.length === 0) {
logger.warn('fetchEventWithExternalRelays: No external relays provided', { eventId }) logger.warn('fetchEventWithExternalRelays: No external relays provided', { noteId })
return undefined
}
const filter = this.filterForExternalRelayFetch(noteId)
if (!filter) {
logger.warn('fetchEventWithExternalRelays: unparseable note id', {
noteIdPrefix: noteId.slice(0, 24)
})
return undefined return undefined
} }
const logKey =
'ids' in filter && filter.ids?.[0]
? filter.ids[0].slice(0, 8)
: `${filter.kinds?.[0]}:${(filter.authors?.[0] ?? '').slice(0, 8)}`
logger.debug('fetchEventWithExternalRelays: Starting search', { logger.debug('fetchEventWithExternalRelays: Starting search', {
eventId: eventId.substring(0, 8), noteIdKey: logKey,
relayCount: externalRelays.length, relayCount: externalRelays.length,
relays: externalRelays relays: externalRelays
}) })
const startTime = Date.now() const startTime = Date.now()
const events = await this.queryService.query( const events = await this.queryService.query(externalRelays, filter, undefined, {
externalRelays,
{ ids: [eventId], limit: 1 },
undefined,
{
eoseTimeout: 10000, eoseTimeout: 10000,
globalTimeout: 20000, globalTimeout: 20000,
immediateReturn: true immediateReturn: true
} })
)
const duration = Date.now() - startTime const duration = Date.now() - startTime
logger.debug('fetchEventWithExternalRelays: Search completed', { logger.debug('fetchEventWithExternalRelays: Search completed', {
eventId: eventId.substring(0, 8), noteIdKey: logKey,
relayCount: externalRelays.length, relayCount: externalRelays.length,
eventsFound: events.length, eventsFound: events.length,
durationMs: duration durationMs: duration
@ -271,7 +306,7 @@ export class EventService {
/** /**
* Private: Try harder to fetch event from relays * Private: Try harder to fetch event from relays
* ALWAYS uses: author's outboxes + user's inboxes + relay hints + seen relays + defaults * Uses: hints, seen, author relays when known, user's inboxes + cache, fast read + searchable relays.
*/ */
private async tryHarderToFetchEvent( private async tryHarderToFetchEvent(
relayHints: string[], relayHints: string[],
@ -328,7 +363,7 @@ export class EventService {
/** /**
* Private: Fetch events from big relays (batch) * Private: Fetch events from big relays (batch)
* Uses comprehensive relay list: user's inboxes + defaults * Uses same comprehensive list as single-event fetch (inboxes, fast read, searchable, cache).
*/ */
private async fetchEventsFromBigRelays(ids: readonly string[]): Promise<(NEvent | undefined)[]> { private async fetchEventsFromBigRelays(ids: readonly string[]): Promise<(NEvent | undefined)[]> {
// Build comprehensive relay list (user's inboxes + defaults) // Build comprehensive relay list (user's inboxes + defaults)

Loading…
Cancel
Save