Browse Source

bug-fixed publication pages

imwald
Silberengel 5 months ago
parent
commit
3ef2153c57
  1. 579
      src/components/Note/PublicationIndex/PublicationIndex.tsx
  2. 80
      src/services/client.service.ts

579
src/components/Note/PublicationIndex/PublicationIndex.tsx

@ -1,16 +1,18 @@ @@ -1,16 +1,18 @@
import { ExtendedKind } from '@/constants'
import { Event } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react'
import { Event, nip19 } from 'nostr-tools'
import { useEffect, useMemo, useState, useCallback } from 'react'
import { cn } from '@/lib/utils'
import { normalizeUrl } from '@/lib/url'
import AsciidocArticle from '../AsciidocArticle/AsciidocArticle'
import MarkdownArticle from '../MarkdownArticle/MarkdownArticle'
import { generateBech32IdFromATag } from '@/lib/tag'
import client from '@/services/client.service'
import logger from '@/lib/logger'
import { Button } from '@/components/ui/button'
import { MoreVertical } from 'lucide-react'
import { MoreVertical, RefreshCw } from 'lucide-react'
import indexedDb from '@/services/indexed-db.service'
import { isReplaceableEvent } from '@/lib/event'
import { useSecondaryPage } from '@/PageManager'
interface PublicationReference {
coordinate?: string
@ -21,6 +23,7 @@ interface PublicationReference { @@ -21,6 +23,7 @@ interface PublicationReference {
identifier?: string
relay?: string
type: 'a' | 'e' // 'a' for addressable (coordinate), 'e' for event ID
nestedRefs?: PublicationReference[] // Discovered nested references
}
interface ToCItem {
@ -48,6 +51,7 @@ export default function PublicationIndex({ @@ -48,6 +51,7 @@ export default function PublicationIndex({
event: Event
className?: string
}) {
const { push } = useSecondaryPage()
// Parse publication metadata from event tags
const metadata = useMemo<PublicationMetadata>(() => {
const meta: PublicationMetadata = { tags: [] }
@ -80,6 +84,10 @@ export default function PublicationIndex({ @@ -80,6 +84,10 @@ export default function PublicationIndex({
const [references, setReferences] = useState<PublicationReference[]>([])
const [visitedIndices, setVisitedIndices] = useState<Set<string>>(new Set())
const [isLoading, setIsLoading] = useState(true)
const [retryCount, setRetryCount] = useState(0)
const [isRetrying, setIsRetrying] = useState(false)
const [failedReferences, setFailedReferences] = useState<PublicationReference[]>([])
const maxRetries = 5
// Build table of contents from references
const tableOfContents = useMemo<ToCItem[]>(() => {
@ -239,35 +247,17 @@ export default function PublicationIndex({ @@ -239,35 +247,17 @@ export default function PublicationIndex({
})
}, [currentCoordinate, event])
// Fetch referenced events
useEffect(() => {
let isMounted = true
const fetchReferences = async () => {
setIsLoading(true)
const fetchedRefs: PublicationReference[] = []
// Capture current visitedIndices at the start of the fetch
const currentVisited = visitedIndices
// Add a timeout to prevent infinite loading on mobile
const timeout = setTimeout(() => {
if (isMounted) {
logger.warn('[PublicationIndex] Fetch timeout reached, setting loaded state')
setIsLoading(false)
}
}, 30000) // 30 second timeout
try {
for (const ref of referencesData) {
if (!isMounted) break
// Fetch a single reference with retry logic
const fetchSingleReference = useCallback(async (
ref: PublicationReference,
currentVisited: Set<string>,
isRetry = false
): Promise<PublicationReference | null> => {
// Skip if this is a 30040 event we've already visited (prevent circular references)
if (ref.type === 'a' && ref.kind === ExtendedKind.PUBLICATION && ref.coordinate) {
if (currentVisited.has(ref.coordinate)) {
logger.debug('[PublicationIndex] Skipping visited 30040 index:', ref.coordinate)
fetchedRefs.push({ ...ref, event: undefined })
continue
return { ...ref, event: undefined }
}
}
@ -285,8 +275,156 @@ export default function PublicationIndex({ @@ -285,8 +275,156 @@ export default function PublicationIndex({
// If not found, try to fetch from relay
if (!fetchedEvent) {
// For naddr, always use subscription-style query with comprehensive relay list (more reliable)
if (bech32Id.startsWith('naddr1')) {
try {
const { nip19 } = await import('nostr-tools')
const decoded = nip19.decode(bech32Id)
if (decoded.type === 'naddr') {
const filter: any = {
authors: [decoded.data.pubkey],
kinds: [decoded.data.kind],
limit: 1
}
if (decoded.data.identifier) {
filter['#d'] = [decoded.data.identifier]
}
// Use comprehensive relay list (same as initial fetch in client.service)
// Build relay list: FAST_READ_RELAY_URLS, user's favorite relays, user's relay list, decoded relays, BIG_RELAY_URLS
const { FAST_READ_RELAY_URLS, BIG_RELAY_URLS } = await import('@/constants')
const relayUrls = new Set<string>()
// Add FAST_READ_RELAY_URLS
FAST_READ_RELAY_URLS.forEach(url => {
const normalized = normalizeUrl(url)
if (normalized) relayUrls.add(normalized)
})
// Add user's favorite relays (kind 10012) and relay list (kind 10002) if logged in
try {
const userPubkey = (client as any).pubkey
if (userPubkey) {
// Fetch user's relay list (includes cache relays)
const userRelayList = await client.fetchRelayList(userPubkey)
if (userRelayList?.read) {
userRelayList.read.forEach((url: string) => {
const normalized = normalizeUrl(url)
if (normalized) relayUrls.add(normalized)
})
}
// Fetch user's favorite relays (kind 10012)
try {
const { ExtendedKind } = await import('@/constants')
const favoriteRelaysEvent = await (client as any).fetchReplaceableEvent?.(userPubkey, ExtendedKind.FAVORITE_RELAYS)
if (favoriteRelaysEvent) {
favoriteRelaysEvent.tags.forEach(([tagName, tagValue]: [string, string]) => {
if (tagName === 'relay' && tagValue) {
const normalized = normalizeUrl(tagValue)
if (normalized) relayUrls.add(normalized)
}
})
}
} catch (error) {
// Ignore if favorite relays can't be fetched
}
}
} catch (error) {
// Ignore if user relay list can't be fetched
}
// Add relays from decoded naddr if available
if (decoded.data.relays && decoded.data.relays.length > 0) {
decoded.data.relays.forEach((url: string) => {
const normalized = normalizeUrl(url)
if (normalized) relayUrls.add(normalized)
})
}
// Add BIG_RELAY_URLS as fallback
BIG_RELAY_URLS.forEach(url => {
const normalized = normalizeUrl(url)
if (normalized) relayUrls.add(normalized)
})
// Add SEARCHABLE_RELAY_URLS (important for finding events that search page finds)
const { SEARCHABLE_RELAY_URLS } = await import('@/constants')
SEARCHABLE_RELAY_URLS.forEach(url => {
const normalized = normalizeUrl(url)
if (normalized) relayUrls.add(normalized)
})
const finalRelayUrls = Array.from(relayUrls)
logger.debug('[PublicationIndex] Using', finalRelayUrls.length, 'relays for naddr query')
// Fetch using subscription-style query (more reliable for naddr)
// Use subscribeTimeline approach for better reliability (waits for eosed signals)
// This is the same approach NoteListPage uses, which successfully finds events
try {
let foundEvent: Event | undefined = undefined
let hasEosed = false
let subscriptionClosed = false
const { closer } = await client.subscribeTimeline(
[{ urls: finalRelayUrls, filter }],
{
onEvents: (events, eosed) => {
if (events.length > 0 && !foundEvent) {
foundEvent = events[0]
logger.debug('[PublicationIndex] Found event via naddr subscription:', ref.coordinate)
}
if (eosed) {
hasEosed = true
}
// Close subscription once we have an event and eosed
if ((foundEvent || hasEosed) && !subscriptionClosed) {
subscriptionClosed = true
closer()
}
},
onNew: () => {} // Not needed for one-time fetch
},
{ needSort: false }
)
// Wait for up to 10 seconds for events to arrive or eosed
const startTime = Date.now()
while (!foundEvent && !hasEosed && Date.now() - startTime < 10000) {
await new Promise(resolve => setTimeout(resolve, 100))
}
// Close subscription if still open
if (!subscriptionClosed) {
closer()
}
if (foundEvent) {
fetchedEvent = foundEvent
}
} catch (subError) {
logger.warn('[PublicationIndex] Subscription error, falling back to fetchEvents:', subError)
// Fallback to regular fetchEvents if subscription fails
const events = await client.fetchEvents(finalRelayUrls, [filter])
if (events.length > 0) {
fetchedEvent = events[0]
logger.debug('[PublicationIndex] Found event via naddr fetchEvents fallback:', ref.coordinate)
}
}
}
} catch (error) {
logger.warn('[PublicationIndex] Error trying naddr filter query:', error)
}
} else {
// For non-naddr (nevent/note), try fetchEvent first, then force retry
if (isRetry) {
fetchedEvent = await client.fetchEventForceRetry(bech32Id)
} else {
fetchedEvent = await client.fetchEvent(bech32Id)
// Save to cache as replaceable event
}
}
// Save to cache as replaceable event if we fetched it
if (fetchedEvent) {
await indexedDb.putReplaceableEvent(fetchedEvent)
logger.debug('[PublicationIndex] Cached event with coordinate:', ref.coordinate)
@ -300,7 +438,12 @@ export default function PublicationIndex({ @@ -300,7 +438,12 @@ export default function PublicationIndex({
} else if (ref.type === 'e' && ref.eventId) {
// Handle event ID reference (e tag)
// Try to fetch by event ID first
if (isRetry) {
// On retry, use force retry to try more relays
fetchedEvent = await client.fetchEventForceRetry(ref.eventId)
} else {
fetchedEvent = await client.fetchEvent(ref.eventId)
}
if (fetchedEvent) {
// Check if this is a replaceable event kind
@ -310,7 +453,6 @@ export default function PublicationIndex({ @@ -310,7 +453,6 @@ export default function PublicationIndex({
logger.debug('[PublicationIndex] Cached replaceable event with ID:', ref.eventId)
} else {
// For non-replaceable events, we'll link them to master later via putPublicationWithNestedEvents
// Just cache them for now without master link - they'll be properly linked when we call putPublicationWithNestedEvents
logger.debug('[PublicationIndex] Cached non-replaceable event with ID (will link to master):', ref.eventId)
}
} else {
@ -318,24 +460,159 @@ export default function PublicationIndex({ @@ -318,24 +460,159 @@ export default function PublicationIndex({
}
}
if (fetchedEvent && isMounted) {
fetchedRefs.push({ ...ref, event: fetchedEvent })
} else if (isMounted) {
const identifier = ref.type === 'a' ? ref.coordinate : ref.eventId
logger.warn('[PublicationIndex] Could not fetch event for:', identifier || 'unknown')
fetchedRefs.push({ ...ref, event: undefined })
if (fetchedEvent) {
// Check if this event has nested references we haven't seen yet
const nestedRefs: PublicationReference[] = []
for (const tag of fetchedEvent.tags) {
if (tag[0] === 'a' && tag[1]) {
const [kindStr, pubkey, identifier] = tag[1].split(':')
const kind = parseInt(kindStr)
if (!isNaN(kind)) {
const coordinate = tag[1]
const nestedRef: PublicationReference = {
type: 'a',
coordinate,
kind,
pubkey,
identifier: identifier || '',
relay: tag[2],
eventId: tag[3]
}
// Check if we already have this reference
const existingRef = referencesData.find(r =>
r.coordinate === coordinate ||
(r.type === 'a' && r.coordinate === coordinate)
)
if (!existingRef && !currentVisited.has(coordinate)) {
nestedRefs.push(nestedRef)
}
}
} else if (tag[0] === 'e' && tag[1]) {
const eventId = tag[1]
const nestedRef: PublicationReference = {
type: 'e',
eventId,
relay: tag[2]
}
// Check if we already have this reference
const existingRef = referencesData.find(r =>
r.eventId === eventId ||
(r.type === 'e' && r.eventId === eventId)
)
if (!existingRef) {
nestedRefs.push(nestedRef)
}
}
}
return { ...ref, event: fetchedEvent, nestedRefs }
} else {
return { ...ref, event: undefined }
}
} catch (error) {
logger.error('[PublicationIndex] Error fetching reference:', error)
return { ...ref, event: undefined }
}
}, [referencesData])
// Fetch referenced events
useEffect(() => {
let isMounted = true
const fetchReferences = async (isManualRetry = false) => {
if (isManualRetry) {
setIsRetrying(true)
} else {
setIsLoading(true)
}
const fetchedRefs: PublicationReference[] = []
const failedRefs: PublicationReference[] = []
const discoveredRefs: PublicationReference[] = []
// Capture current visitedIndices at the start of the fetch
const currentVisited = visitedIndices
// Add a timeout to prevent infinite loading on mobile
const timeout = setTimeout(() => {
if (isMounted) {
fetchedRefs.push({ ...ref, event: undefined })
logger.warn('[PublicationIndex] Fetch timeout reached, setting loaded state')
setIsLoading(false)
setIsRetrying(false)
}
}, 30000) // 30 second timeout
try {
// Combine original references with failed references if this is a retry
const refsToFetch = isManualRetry && failedReferences.length > 0
? [...referencesData, ...failedReferences]
: referencesData
for (const ref of refsToFetch) {
if (!isMounted) break
const result = await fetchSingleReference(ref, currentVisited, isManualRetry)
if (!isMounted) break
if (result) {
if (result.event) {
fetchedRefs.push(result)
// Collect discovered nested references
if ((result as any).nestedRefs && (result as any).nestedRefs.length > 0) {
for (const nestedRef of (result as any).nestedRefs) {
// Check if we already have this reference
const existingRef = fetchedRefs.find(r =>
(r.coordinate && nestedRef.coordinate && r.coordinate === nestedRef.coordinate) ||
(r.eventId && nestedRef.eventId && r.eventId === nestedRef.eventId)
)
if (!existingRef && !discoveredRefs.find(r =>
(r.coordinate && nestedRef.coordinate && r.coordinate === nestedRef.coordinate) ||
(r.eventId && nestedRef.eventId && r.eventId === nestedRef.eventId)
)) {
discoveredRefs.push(nestedRef)
}
}
}
} else {
// Failed to fetch
failedRefs.push(result)
fetchedRefs.push(result)
}
}
}
// Fetch discovered nested references
if (discoveredRefs.length > 0 && isMounted) {
logger.info('[PublicationIndex] Found', discoveredRefs.length, 'new nested references')
for (const nestedRef of discoveredRefs) {
if (!isMounted) break
const result = await fetchSingleReference(nestedRef, currentVisited, isManualRetry)
if (!isMounted) break
if (result) {
if (result.event) {
fetchedRefs.push(result)
} else {
failedRefs.push(result)
fetchedRefs.push(result)
}
}
}
}
if (isMounted) {
setReferences(fetchedRefs)
setFailedReferences(failedRefs.filter(ref => !ref.event))
setIsLoading(false)
setIsRetrying(false)
// Store master publication with all nested events
const nestedEvents = fetchedRefs.filter(ref => ref.event).map(ref => ref.event!).filter((e): e is Event => e !== undefined)
@ -345,13 +622,31 @@ export default function PublicationIndex({ @@ -345,13 +622,31 @@ export default function PublicationIndex({
})
}
}
} catch (error) {
logger.error('[PublicationIndex] Error in fetchReferences:', error)
if (isMounted) {
setIsLoading(false)
setIsRetrying(false)
}
} finally {
clearTimeout(timeout)
}
}
if (referencesData.length > 0) {
fetchReferences()
fetchReferences(false).then(() => {
// Auto-retry failed references after initial load
setFailedReferences(prevFailed => {
if (prevFailed.length > 0 && retryCount < maxRetries) {
const delay = Math.min(1000 * Math.pow(2, retryCount), 10000) // Exponential backoff, max 10s
setTimeout(() => {
setRetryCount(prev => prev + 1)
fetchReferences(true)
}, delay)
}
return prevFailed
})
})
} else {
setIsLoading(false)
}
@ -359,7 +654,129 @@ export default function PublicationIndex({ @@ -359,7 +654,129 @@ export default function PublicationIndex({
return () => {
isMounted = false
}
}, [referencesData, visitedIndices]) // Now include visitedIndices but capture it inside
}, [referencesData, visitedIndices, fetchSingleReference]) // Include fetchSingleReference in dependencies
// Manual retry function
const handleManualRetry = useCallback(async () => {
setRetryCount(0)
setIsRetrying(true)
const fetchReferences = async () => {
const updatedRefs: Map<string, PublicationReference> = new Map()
const newRefs: PublicationReference[] = []
const failedRefs: PublicationReference[] = []
const discoveredRefs: PublicationReference[] = []
const currentVisited = visitedIndices
// Create a map of existing references for quick lookup
references.forEach(ref => {
const id = ref.coordinate || ref.eventId || ''
if (id) {
updatedRefs.set(id, ref)
}
})
// Only retry failed references, not all references
const refsToRetry = failedReferences.length > 0 ? failedReferences : references.filter(ref => !ref.event)
if (refsToRetry.length === 0) {
setIsRetrying(false)
return
}
logger.info('[PublicationIndex] Retrying', refsToRetry.length, 'failed references')
for (const ref of refsToRetry) {
const result = await fetchSingleReference(ref, currentVisited, true)
if (result) {
const id = result.coordinate || result.eventId || ''
if (result.event) {
// Successfully fetched - update existing reference or add new one
if (id) {
updatedRefs.set(id, result)
} else {
newRefs.push(result)
}
// Collect discovered nested references
if ((result as any).nestedRefs && (result as any).nestedRefs.length > 0) {
for (const nestedRef of (result as any).nestedRefs) {
const nestedId = nestedRef.coordinate || nestedRef.eventId || ''
if (!nestedId) continue
// Check if we already have this reference
const existingInMap = updatedRefs.has(nestedId)
const existingInNew = newRefs.find(r => {
const rid = r.coordinate || r.eventId || ''
return rid === nestedId
})
const existingInDiscovered = discoveredRefs.find(r => {
const rid = r.coordinate || r.eventId || ''
return rid === nestedId
})
if (!existingInMap && !existingInNew && !existingInDiscovered) {
discoveredRefs.push(nestedRef)
}
}
}
} else {
// Still failed
if (id) {
updatedRefs.set(id, result)
} else {
failedRefs.push(result)
}
}
}
}
// Fetch discovered nested references
if (discoveredRefs.length > 0) {
logger.info('[PublicationIndex] Found', discoveredRefs.length, 'new nested references on retry')
for (const nestedRef of discoveredRefs) {
const result = await fetchSingleReference(nestedRef, currentVisited, true)
if (result) {
const id = result.coordinate || result.eventId || ''
if (result.event) {
if (id) {
updatedRefs.set(id, result)
} else {
newRefs.push(result)
}
} else {
if (id) {
updatedRefs.set(id, result)
} else {
failedRefs.push(result)
}
}
}
}
}
// Update state with merged results
const finalRefs = Array.from(updatedRefs.values()).concat(newRefs)
const stillFailed = finalRefs.filter(ref => !ref.event)
setReferences(finalRefs)
setFailedReferences(stillFailed)
setIsRetrying(false)
// Store master publication with all nested events
const nestedEvents = finalRefs.filter(ref => ref.event).map(ref => ref.event!).filter((e): e is Event => e !== undefined)
if (nestedEvents.length > 0) {
indexedDb.putPublicationWithNestedEvents(event, nestedEvents).catch(err => {
logger.error('[PublicationIndex] Error caching publication with nested events:', err)
})
}
}
await fetchReferences()
}, [failedReferences, visitedIndices, fetchSingleReference, references, event])
return (
<div className={cn('space-y-6', className)}>
@ -422,6 +839,26 @@ export default function PublicationIndex({ @@ -422,6 +839,26 @@ export default function PublicationIndex({
</div>
)}
{/* Failed References Banner */}
{!isLoading && failedReferences.length > 0 && references.length > 0 && (
<div className="p-4 border rounded-lg bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800">
<div className="flex items-center justify-between gap-4">
<div className="text-sm text-yellow-800 dark:text-yellow-200">
{failedReferences.length} reference{failedReferences.length !== 1 ? 's' : ''} failed to load. Click retry to attempt loading again.
</div>
<Button
variant="outline"
size="sm"
onClick={handleManualRetry}
disabled={isRetrying}
>
<RefreshCw className={cn("h-4 w-4 mr-2", isRetrying && "animate-spin")} />
Retry All
</Button>
</div>
</div>
)}
{/* Content - render referenced events */}
{isLoading ? (
<div className="text-muted-foreground">
@ -431,18 +868,78 @@ export default function PublicationIndex({ @@ -431,18 +868,78 @@ export default function PublicationIndex({
) : references.length === 0 ? (
<div className="p-6 border rounded-lg bg-muted/30 text-center">
<div className="text-lg font-semibold mb-2">No content loaded</div>
<div className="text-sm text-muted-foreground">
<div className="text-sm text-muted-foreground mb-4">
Unable to load publication content. The referenced events may not be available on the current relays.
</div>
<Button
variant="outline"
onClick={handleManualRetry}
disabled={isRetrying}
>
<RefreshCw className={cn("h-4 w-4 mr-2", isRetrying && "animate-spin")} />
Retry Loading
</Button>
</div>
) : (
<div className="space-y-8">
{references.map((ref, index) => {
if (!ref.event) {
// Generate naddr from coordinate or eventId for link
let notesLink: string | null = null
if (ref.coordinate) {
const aTag = ['a', ref.coordinate, ref.relay || '', ref.eventId || '']
const bech32Id = generateBech32IdFromATag(aTag)
if (bech32Id) {
// Construct URL as /notes?events=naddr1...
notesLink = `/notes?events=${encodeURIComponent(bech32Id)}`
}
} else if (ref.eventId) {
// For event IDs, try to construct a note/nevent, otherwise use as-is
if (ref.eventId.startsWith('note1') || ref.eventId.startsWith('nevent1') || ref.eventId.startsWith('naddr1')) {
notesLink = `/notes?events=${encodeURIComponent(ref.eventId)}`
} else if (/^[0-9a-f]{64}$/i.test(ref.eventId)) {
// Hex event ID - try to create nevent
try {
const nevent = nip19.neventEncode({ id: ref.eventId })
notesLink = `/notes?events=${encodeURIComponent(nevent)}`
} catch {
// Fallback to hex ID
notesLink = `/notes?events=${encodeURIComponent(ref.eventId)}`
}
}
}
return (
<div key={index} className="p-4 border rounded-lg bg-muted/50">
<div className="flex items-center justify-between gap-2">
<div className="text-sm text-muted-foreground">
Reference {index + 1}: Unable to load event {ref.coordinate || ref.eventId || 'unknown'}
Reference {index + 1}: Unable to load event{' '}
{notesLink ? (
<a
href={notesLink}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
push(notesLink!)
}}
className="text-primary hover:underline cursor-pointer"
>
{ref.coordinate || ref.eventId || 'unknown'}
</a>
) : (
<span>{ref.coordinate || ref.eventId || 'unknown'}</span>
)}
</div>
<Button
variant="outline"
size="sm"
onClick={handleManualRetry}
disabled={isRetrying}
className="shrink-0"
>
<RefreshCw className={cn("h-4 w-4", isRetrying && "animate-spin")} />
Retry
</Button>
</div>
</div>
)

80
src/services/client.service.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { BIG_RELAY_URLS, ExtendedKind, PROFILE_FETCH_RELAY_URLS, PROFILE_RELAY_URLS } from '@/constants'
import { BIG_RELAY_URLS, ExtendedKind, FAST_READ_RELAY_URLS, PROFILE_FETCH_RELAY_URLS, PROFILE_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants'
import {
compareEvents,
getReplaceableCoordinate,
@ -942,14 +942,90 @@ class ClientService extends EventTarget { @@ -942,14 +942,90 @@ class ClientService extends EventTarget {
} else if (!relayUrls.length && !alreadyFetchedFromBigRelays) {
relayUrls = BIG_RELAY_URLS
}
if (!relayUrls.length) {
// Final fallback to searchable relays
relayUrls = SEARCHABLE_RELAY_URLS
}
if (!relayUrls.length) return
const events = await this.query(relayUrls, filter)
return events.sort((a, b) => b.created_at - a.created_at)[0]
}
/**
* Get user's favorite relays from kind 10012 event
*/
private async getUserFavoriteRelays(): Promise<string[]> {
if (!this.pubkey) return []
try {
const favoriteRelaysEvent = await this.fetchReplaceableEvent(this.pubkey, ExtendedKind.FAVORITE_RELAYS)
if (!favoriteRelaysEvent) return []
const relays: string[] = []
favoriteRelaysEvent.tags.forEach(([tagName, tagValue]) => {
if (tagName === 'relay' && tagValue && isWebsocketUrl(tagValue)) {
const normalizedUrl = normalizeUrl(tagValue)
if (normalizedUrl && !relays.includes(normalizedUrl)) {
relays.push(normalizedUrl)
}
}
})
return relays
} catch (error) {
console.warn('[ClientService] Error fetching user favorite relays:', error)
return []
}
}
/**
* Build initial relay list for fetching events
* Priority: FAST_READ_RELAY_URLS, user's favorite relays (10012), user's relay list read relays (10002) including cache relays (10432)
* All relays are normalized and deduplicated
*/
private async buildInitialRelayList(): Promise<string[]> {
const relaySet = new Set<string>()
// Add FAST_READ_RELAY_URLS
FAST_READ_RELAY_URLS.forEach(url => {
const normalized = normalizeUrl(url)
if (normalized) relaySet.add(normalized)
})
// Add user's favorite relays (kind 10012)
if (this.pubkey) {
const favoriteRelays = await this.getUserFavoriteRelays()
favoriteRelays.forEach(url => {
const normalized = normalizeUrl(url)
if (normalized) relaySet.add(normalized)
})
// Add user's relay list read relays (kind 10002) and cache relays (kind 10432)
// fetchRelayList already merges cache relays with regular relay list
try {
const relayList = await this.fetchRelayList(this.pubkey)
if (relayList?.read) {
relayList.read.forEach(url => {
const normalized = normalizeUrl(url)
if (normalized) relaySet.add(normalized)
})
}
} catch (error) {
console.warn('[ClientService] Error fetching user relay list:', error)
}
}
// Return deduplicated array (normalization already handled, Set ensures deduplication)
return Array.from(relaySet)
}
private async fetchEventsFromBigRelays(ids: readonly string[]) {
const events = await this.query(BIG_RELAY_URLS, {
// Use optimized initial relay list instead of BIG_RELAY_URLS
const initialRelays = await this.buildInitialRelayList()
const relayUrls = initialRelays.length > 0 ? initialRelays : BIG_RELAY_URLS
const events = await this.query(relayUrls, {
ids: Array.from(new Set(ids)),
limit: ids.length
})

Loading…
Cancel
Save