+ {!hideMetadata && metadata.title &&
{metadata.title}
}
+ {!hideMetadata && metadata.summary && (
+
+ {metadata.summary}
+
+ )}
+ {/* Show title inline when metadata is hidden (for nested content) */}
+ {hideMetadata && metadata.title && (
+
{metadata.title}
+ )}
+ {!hideMetadata && metadata.image && (() => {
// Find the index of the metadata image in allImages
const cleanedMetadataImage = cleanUrl(metadata.image)
const metadataImageIndex = cleanedMetadataImage
diff --git a/src/components/Note/PublicationIndex/PublicationIndex.tsx b/src/components/Note/PublicationIndex/PublicationIndex.tsx
index 228dd6b..73c0688 100644
--- a/src/components/Note/PublicationIndex/PublicationIndex.tsx
+++ b/src/components/Note/PublicationIndex/PublicationIndex.tsx
@@ -9,7 +9,7 @@ import { generateBech32IdFromATag } from '@/lib/tag'
import client from '@/services/client.service'
import logger from '@/lib/logger'
import { Button } from '@/components/ui/button'
-import { MoreVertical, RefreshCw } from 'lucide-react'
+import { MoreVertical, RefreshCw, ArrowUp } from 'lucide-react'
import indexedDb from '@/services/indexed-db.service'
import { isReplaceableEvent } from '@/lib/event'
import { useSecondaryPage } from '@/PageManager'
@@ -91,79 +91,316 @@ export default function PublicationIndex({
const [failedReferences, setFailedReferences] = useState
([])
const maxRetries = 5
+ // Extract references from 'a' tags (addressable events) and 'e' tags (event IDs)
+ const referencesData = useMemo(() => {
+ const refs: PublicationReference[] = []
+ for (const tag of event.tags) {
+ if (tag[0] === 'a' && tag[1]) {
+ // Addressable event (kind:pubkey:identifier)
+ const [kindStr, pubkey, identifier] = tag[1].split(':')
+ const kind = parseInt(kindStr)
+ if (!isNaN(kind)) {
+ refs.push({
+ type: 'a',
+ coordinate: tag[1],
+ kind,
+ pubkey,
+ identifier: identifier || '',
+ relay: tag[2],
+ eventId: tag[3] // Optional event ID for version tracking
+ })
+ }
+ } else if (tag[0] === 'e' && tag[1]) {
+ // Event ID reference
+ refs.push({
+ type: 'e',
+ eventId: tag[1],
+ relay: tag[2]
+ })
+ }
+ }
+ return refs
+ }, [event])
+
+ // Recursive helper function to build ToC item from a reference
+ const buildToCItemFromRef = useCallback((ref: PublicationReference, allReferences: PublicationReference[], visitedCoords: Set = new Set()): ToCItem | null => {
+ if (!ref.event) {
+ // If no event but we have a coordinate/eventId, create placeholder
+ if (ref.coordinate || ref.eventId) {
+ return {
+ title: 'Loading...',
+ coordinate: ref.coordinate || ref.eventId || '',
+ kind: ref.kind || 0
+ }
+ }
+ return null
+ }
+
+ // Extract title from the event
+ const title = ref.event.tags.find(tag => tag[0] === 'title')?.[1] ||
+ ref.event.tags.find(tag => tag[0] === 'd')?.[1] ||
+ 'Untitled'
+
+ const coordinate = ref.coordinate || ref.eventId || ''
+ const coordKey = ref.coordinate || ref.eventId || ''
+
+ // Prevent infinite recursion
+ if (visitedCoords.has(coordKey)) {
+ return null
+ }
+ visitedCoords.add(coordKey)
+
+ const tocItem: ToCItem = {
+ title,
+ coordinate,
+ event: ref.event,
+ kind: ref.kind || ref.event?.kind || 0
+ }
+
+ // Build children recursively - check both nestedRefs and event tags
+ const children: ToCItem[] = []
+ const processedCoords = new Set()
+
+ // First, process discovered nestedRefs if they exist
+ if (ref.nestedRefs && ref.nestedRefs.length > 0) {
+ for (const nestedRef of ref.nestedRefs) {
+ const nestedCoord = nestedRef.coordinate || nestedRef.eventId || ''
+ if (nestedCoord && !processedCoords.has(nestedCoord)) {
+ processedCoords.add(nestedCoord)
+
+ // Look up the full reference (with fetched event) from allReferences
+ const fullNestedRef = allReferences.find(r =>
+ (r.coordinate && nestedRef.coordinate && r.coordinate === nestedRef.coordinate) ||
+ (r.eventId && nestedRef.eventId && r.eventId === nestedRef.eventId) ||
+ (r.event && nestedRef.event && r.event.id === nestedRef.event.id)
+ ) || nestedRef
+
+ const nestedToCItem = buildToCItemFromRef(fullNestedRef, allReferences, new Set(visitedCoords))
+ if (nestedToCItem) {
+ children.push(nestedToCItem)
+ }
+ }
+ }
+ }
+
+ // Also process tags from publication events (for publications that reference other publications)
+ if ((ref.kind === ExtendedKind.PUBLICATION || ref.event?.kind === ExtendedKind.PUBLICATION) && ref.event) {
+ for (const tag of ref.event.tags) {
+ if (tag[0] === 'a' && tag[1]) {
+ const [kindStr, , identifier] = tag[1].split(':')
+ const kind = parseInt(kindStr)
+
+ if (!isNaN(kind) && (kind === ExtendedKind.PUBLICATION_CONTENT ||
+ kind === ExtendedKind.WIKI_ARTICLE ||
+ kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN ||
+ kind === ExtendedKind.PUBLICATION)) {
+ const tagCoord = tag[1]
+ if (!processedCoords.has(tagCoord)) {
+ processedCoords.add(tagCoord)
+
+ // Look up the fetched event from allReferences
+ const fetchedNestedEvent = allReferences.find(r =>
+ r.coordinate === tagCoord ||
+ (r.type === 'a' && r.coordinate === tagCoord) ||
+ (r.event && r.event.kind === kind && r.event.pubkey && tagCoord.includes(r.event.pubkey))
+ )
+
+ if (fetchedNestedEvent) {
+ const nestedToCItem = buildToCItemFromRef(fetchedNestedEvent, allReferences, new Set(visitedCoords))
+ if (nestedToCItem) {
+ children.push(nestedToCItem)
+ }
+ } else {
+ // Event not fetched yet, create placeholder
+ children.push({
+ title: identifier || 'Untitled',
+ coordinate: tagCoord,
+ kind
+ })
+ }
+ }
+ }
+ } else if (tag[0] === 'e' && tag[1]) {
+ const eventId = tag[1]
+ if (!processedCoords.has(eventId)) {
+ processedCoords.add(eventId)
+
+ // Look up the fetched event from allReferences
+ const fetchedNestedEvent = allReferences.find(r =>
+ (r.type === 'e' && r.eventId === eventId) ||
+ (r.event && r.event.id === eventId) ||
+ (r.coordinate === eventId) ||
+ (r.eventId === eventId)
+ )
+
+ if (fetchedNestedEvent) {
+ const nestedToCItem = buildToCItemFromRef(fetchedNestedEvent, allReferences, new Set(visitedCoords))
+ if (nestedToCItem) {
+ children.push(nestedToCItem)
+ }
+ } else {
+ // Event not fetched yet, create placeholder
+ children.push({
+ title: 'Loading...',
+ coordinate: eventId,
+ kind: 0
+ })
+ }
+ }
+ }
+ }
+ }
+
+ if (children.length > 0) {
+ tocItem.children = children
+ }
+
+ return tocItem
+ }, [])
+
// Build table of contents from references
+ // Also include items from main event tags if references haven't loaded yet
const tableOfContents = useMemo(() => {
const toc: ToCItem[] = []
+ // Build ToC from all fetched references (recursively)
for (const ref of references) {
- if (!ref.event) continue
-
- // Extract title from the event
- const title = ref.event.tags.find(tag => tag[0] === 'title')?.[1] ||
- ref.event.tags.find(tag => tag[0] === 'd')?.[1] ||
- 'Untitled'
+ // Only add top-level references (those directly in the main event)
+ const isTopLevel = referencesData.some(rd =>
+ (rd.coordinate && ref.coordinate && rd.coordinate === ref.coordinate) ||
+ (rd.eventId && ref.eventId && rd.eventId === ref.eventId)
+ )
- const tocItem: ToCItem = {
- title,
- coordinate: ref.coordinate || ref.eventId || '',
- event: ref.event,
- kind: ref.kind || ref.event?.kind || 0
+ if (isTopLevel) {
+ const tocItem = buildToCItemFromRef(ref, references)
+ if (tocItem) {
+ toc.push(tocItem)
+ }
}
-
- // For nested 30040 publications, recursively get their ToC
- if ((ref.kind === ExtendedKind.PUBLICATION || ref.event?.kind === ExtendedKind.PUBLICATION) && ref.event) {
- const nestedRefs: ToCItem[] = []
-
- // Parse nested references from this publication (both 'a' and 'e' tags)
- for (const tag of ref.event.tags) {
- if (tag[0] === 'a' && tag[1]) {
- const [kindStr, , identifier] = tag[1].split(':')
- const kind = parseInt(kindStr)
-
- if (!isNaN(kind) && kind === ExtendedKind.PUBLICATION_CONTENT ||
+ }
+
+ // If no references have loaded yet, build initial ToC from main event tags
+ if (toc.length === 0 && references.length === 0) {
+ // Parse a and e tags from the main event
+ for (const tag of event.tags) {
+ if (tag[0] === 'a' && tag[1]) {
+ const [kindStr, , identifier] = tag[1].split(':')
+ const kind = parseInt(kindStr)
+
+ if (!isNaN(kind) && (kind === ExtendedKind.PUBLICATION_CONTENT ||
kind === ExtendedKind.WIKI_ARTICLE ||
kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN ||
- kind === ExtendedKind.PUBLICATION) {
- // For this simplified version, we'll just extract the title from the coordinate
- const nestedTitle = identifier || 'Untitled'
-
- nestedRefs.push({
- title: nestedTitle,
- coordinate: tag[1],
- kind
- })
- }
- } else if (tag[0] === 'e' && tag[1]) {
- // For 'e' tags, we can't extract title from the tag alone
- // The title will come from the fetched event if available
- const nestedTitle = ref.event?.tags.find(t => t[0] === 'title')?.[1] || 'Untitled'
-
- nestedRefs.push({
- title: nestedTitle,
- coordinate: tag[1], // Use event ID as coordinate
- kind: ref.event?.kind
+ kind === ExtendedKind.PUBLICATION)) {
+ toc.push({
+ title: identifier || 'Untitled',
+ coordinate: tag[1],
+ kind
})
}
- }
-
- if (nestedRefs.length > 0) {
- tocItem.children = nestedRefs
+ } else if (tag[0] === 'e' && tag[1]) {
+ // For e-tags without fetched event, use event ID as placeholder
+ toc.push({
+ title: 'Loading...',
+ coordinate: tag[1],
+ kind: 0
+ })
}
}
-
- toc.push(tocItem)
}
return toc
- }, [references])
+ }, [references, event, referencesData, buildToCItemFromRef])
- // Scroll to section
- const scrollToSection = (coordinate: string) => {
- const element = document.getElementById(`section-${coordinate.replace(/:/g, '-')}`)
- if (element) {
- element.scrollIntoView({ behavior: 'smooth', block: 'start' })
+ // Scroll to ToC
+ const scrollToToc = useCallback(() => {
+ const tocElement = document.getElementById('publication-toc')
+ if (tocElement) {
+ const rect = tocElement.getBoundingClientRect()
+ const elementTop = rect.top + window.scrollY
+ const offset = 96
+ const scrollPosition = Math.max(0, elementTop - offset)
+ window.scrollTo({ top: scrollPosition, behavior: 'smooth' })
}
- }
+ }, [])
+
+ // Scroll to section with offset for fixed header
+ const scrollToSection = useCallback((coordinate: string) => {
+ if (!coordinate) {
+ console.warn('[PublicationIndex] Cannot scroll: coordinate is empty')
+ return
+ }
+
+ // For e-tags, coordinate is the event ID (hex string, no colons)
+ // For a-tags, coordinate is kind:pubkey:identifier (has colons)
+ const sectionId = `section-${coordinate.replace(/:/g, '-')}`
+
+ console.log('[PublicationIndex] Scrolling to section:', { coordinate, sectionId })
+
+ // Try to find the element, with retry for elements that might still be rendering
+ const findAndScroll = (attempt = 0) => {
+ // Try the primary section ID format
+ let element = document.getElementById(sectionId)
+
+ // Try alternative ID formats if primary not found
+ if (!element) {
+ const altId1 = `section-${coordinate}`
+ element = document.getElementById(altId1)
+ if (element) {
+ console.log('[PublicationIndex] Found section with alternative ID:', altId1)
+ }
+ }
+
+ if (element) {
+ // Get the element's position relative to the viewport
+ const rect = element.getBoundingClientRect()
+ const elementTop = rect.top + window.scrollY
+
+ // Account for fixed header/titlebar (typically around 4rem/64px, using 6rem/96px for safety)
+ const offset = 96
+ const scrollPosition = Math.max(0, elementTop - offset)
+
+ console.log('[PublicationIndex] Found section element, scrolling to:', {
+ scrollPosition,
+ elementTop,
+ elementId: element.id,
+ windowScrollY: window.scrollY,
+ rectTop: rect.top
+ })
+
+ // Use requestAnimationFrame for smoother scrolling
+ requestAnimationFrame(() => {
+ window.scrollTo({ top: scrollPosition, behavior: 'smooth' })
+ })
+ return true
+ }
+
+ // Retry up to 10 times with increasing delay if element not found (might still be rendering)
+ if (attempt < 10) {
+ const delay = Math.min(100 * (attempt + 1), 1000) // Max 1 second delay
+ setTimeout(() => findAndScroll(attempt + 1), delay)
+ return false
+ }
+
+ // Log all section IDs for debugging
+ const allSectionIds = Array.from(document.querySelectorAll('[id^="section-"]'))
+ .map(el => el.id)
+ .filter(id => id) // Filter out empty IDs
+
+ console.warn('[PublicationIndex] Section not found after retries:', {
+ coordinate,
+ sectionId,
+ altId1: `section-${coordinate}`,
+ attempt,
+ availableSections: allSectionIds.slice(0, 20) // Limit output
+ })
+
+ // Show a helpful message
+ console.warn(`[PublicationIndex] Could not find section with ID: ${sectionId}. Available sections:`, allSectionIds.slice(0, 10))
+ return false
+ }
+
+ findAndScroll()
+ }, [])
// Export publication as AsciiDoc
const exportPublication = async () => {
@@ -203,37 +440,6 @@ export default function PublicationIndex({
}
}
- // Extract references from 'a' tags (addressable events) and 'e' tags (event IDs)
- const referencesData = useMemo(() => {
- const refs: PublicationReference[] = []
- for (const tag of event.tags) {
- if (tag[0] === 'a' && tag[1]) {
- // Addressable event (kind:pubkey:identifier)
- const [kindStr, pubkey, identifier] = tag[1].split(':')
- const kind = parseInt(kindStr)
- if (!isNaN(kind)) {
- refs.push({
- type: 'a',
- coordinate: tag[1],
- kind,
- pubkey,
- identifier: identifier || '',
- relay: tag[2],
- eventId: tag[3] // Optional event ID for version tracking
- })
- }
- } else if (tag[0] === 'e' && tag[1]) {
- // Event ID reference
- refs.push({
- type: 'e',
- eventId: tag[1],
- relay: tag[2]
- })
- }
- }
- return refs
- }, [event])
-
// Add current event to visited set
const currentCoordinate = useMemo(() => {
const dTag = event.tags.find(tag => tag[0] === 'd')?.[1] || ''
@@ -249,6 +455,147 @@ export default function PublicationIndex({
})
}, [currentCoordinate, event])
+ // Helper function to build comprehensive relay list
+ const buildComprehensiveRelayList = useCallback(async (
+ additionalRelays: string[] = []
+ ): Promise => {
+ const { FAST_READ_RELAY_URLS, BIG_RELAY_URLS, SEARCHABLE_RELAY_URLS } = await import('@/constants')
+ const relayUrls = new Set()
+
+ // Add FAST_READ_RELAY_URLS
+ FAST_READ_RELAY_URLS.forEach(url => {
+ const normalized = normalizeUrl(url)
+ if (normalized) relayUrls.add(normalized)
+ })
+
+ // Add additional relays (from tag relay hints)
+ additionalRelays.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 BIG_RELAY_URLS as fallback
+ BIG_RELAY_URLS.forEach(url => {
+ const normalized = normalizeUrl(url)
+ if (normalized) relayUrls.add(normalized)
+ })
+
+ // Add SEARCHABLE_RELAY_URLS
+ SEARCHABLE_RELAY_URLS.forEach(url => {
+ const normalized = normalizeUrl(url)
+ if (normalized) relayUrls.add(normalized)
+ })
+
+ return Array.from(relayUrls)
+ }, [])
+
+ // Helper function to fetch event using subscription-style query with comprehensive relay list
+ const fetchEventWithSubscription = useCallback(async (
+ filter: any,
+ relayUrls: string[],
+ logPrefix: string
+ ): Promise => {
+ try {
+ let foundEvent: Event | undefined = undefined
+ let hasEosed = false
+ let subscriptionClosed = false
+
+ const { closer } = await client.subscribeTimeline(
+ [{ urls: relayUrls, filter }],
+ {
+ onEvents: (events, eosed) => {
+ if (events.length > 0 && !foundEvent) {
+ foundEvent = events[0]
+ logger.debug(`[PublicationIndex] Found event via ${logPrefix} subscription`)
+ }
+ 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) {
+ return foundEvent
+ }
+ } catch (subError) {
+ logger.warn(`[PublicationIndex] Subscription error for ${logPrefix}, falling back to fetchEvents:`, subError)
+ // Fallback to regular fetchEvents if subscription fails
+ const events = await client.fetchEvents(relayUrls, [filter])
+ if (events.length > 0) {
+ logger.debug(`[PublicationIndex] Found event via ${logPrefix} fetchEvents fallback`)
+ return events[0]
+ }
+ }
+
+ return undefined
+ }, [])
+
+ // Unified method to fetch event for both a and e tags
+ const fetchEventFromRelay = useCallback(async (
+ filter: any,
+ additionalRelays: string[],
+ logPrefix: string
+ ): Promise => {
+ // Build comprehensive relay list
+ const finalRelayUrls = await buildComprehensiveRelayList(additionalRelays)
+ logger.debug(`[PublicationIndex] Using ${finalRelayUrls.length} relays for ${logPrefix} query`)
+
+ // Fetch using subscription-style query with comprehensive relay list
+ return await fetchEventWithSubscription(filter, finalRelayUrls, logPrefix)
+ }, [buildComprehensiveRelayList, fetchEventWithSubscription])
+
// Fetch a single reference with retry logic
const fetchSingleReference = useCallback(async (
ref: PublicationReference,
@@ -292,127 +639,9 @@ export default function PublicationIndex({
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()
-
- // 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)
- }
- }
+ // Build comprehensive relay list and fetch using unified method
+ const additionalRelays = decoded.data.relays || []
+ fetchedEvent = await fetchEventFromRelay(filter, additionalRelays, 'naddr')
}
} catch (error) {
logger.warn('[PublicationIndex] Error trying naddr filter query:', error)
@@ -438,26 +667,56 @@ export default function PublicationIndex({
logger.warn('[PublicationIndex] Could not generate bech32 ID for:', ref.coordinate)
}
} 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)
+ // Handle event ID reference (e tag) - same as a tags
+ // First check indexedDb PUBLICATION_EVENTS store (events cached as part of publications)
+ const hexId = ref.eventId.length === 64 ? ref.eventId : undefined
+ if (hexId) {
+ try {
+ // Check PUBLICATION_EVENTS store first (for non-replaceable events stored with master)
+ fetchedEvent = await indexedDb.getEventFromPublicationStore(hexId)
+ if (fetchedEvent) {
+ logger.debug('[PublicationIndex] Loaded from indexedDb PUBLICATION_EVENTS store by event ID:', ref.eventId)
+ }
+ } catch (error) {
+ logger.debug('[PublicationIndex] PUBLICATION_EVENTS store lookup failed:', error)
+ }
+
+ // Also check if it's a replaceable event (check by pubkey and kind if we have them)
+ if (!fetchedEvent && ref.kind && ref.pubkey && isReplaceableEvent(ref.kind)) {
+ try {
+ const replaceableEvent = await indexedDb.getReplaceableEvent(ref.pubkey, ref.kind)
+ if (replaceableEvent && replaceableEvent.id === hexId) {
+ fetchedEvent = replaceableEvent
+ logger.debug('[PublicationIndex] Loaded from indexedDb replaceable cache by event ID:', ref.eventId)
+ }
+ } catch (error) {
+ logger.debug('[PublicationIndex] Replaceable cache lookup failed:', error)
+ }
+ }
}
- if (fetchedEvent) {
- // Check if this is a replaceable event kind
- if (isReplaceableEvent(fetchedEvent.kind)) {
- // Save to cache as replaceable event (will be linked to master via putPublicationWithNestedEvents)
- await indexedDb.putReplaceableEvent(fetchedEvent)
- logger.debug('[PublicationIndex] Cached replaceable event with ID:', ref.eventId)
- } else {
- // For non-replaceable events, we'll link them to master later via putPublicationWithNestedEvents
- logger.debug('[PublicationIndex] Cached non-replaceable event with ID (will link to master):', ref.eventId)
+ // If not found in indexedDb cache, try to fetch from relay using unified method
+ if (!fetchedEvent) {
+ // Build comprehensive relay list and fetch using unified method
+ const additionalRelays = ref.relay ? [ref.relay] : []
+ const filter = { ids: [hexId || ref.eventId], limit: 1 }
+ fetchedEvent = await fetchEventFromRelay(filter, additionalRelays, 'e tag')
+
+ // Cache the fetched event if found
+ if (fetchedEvent) {
+ // Check if this is a replaceable event kind
+ if (isReplaceableEvent(fetchedEvent.kind)) {
+ // Save to cache as replaceable event (will be linked to master via putPublicationWithNestedEvents)
+ await indexedDb.putReplaceableEvent(fetchedEvent)
+ logger.debug('[PublicationIndex] Cached replaceable event with ID:', ref.eventId)
+ } else {
+ // For non-replaceable events, we'll link them to master later via putPublicationWithNestedEvents
+ logger.debug('[PublicationIndex] Fetched non-replaceable event with ID (will link to master):', ref.eventId)
+ }
}
- } else {
+ }
+
+ if (!fetchedEvent) {
logger.warn('[PublicationIndex] Could not fetch event for ID:', ref.eventId)
}
}
@@ -511,13 +770,28 @@ export default function PublicationIndex({
}
}
- return { ...ref, event: fetchedEvent, nestedRefs }
+ // For e-tags, ensure coordinate is set to eventId if not already set
+ const updatedRef = { ...ref, event: fetchedEvent, nestedRefs }
+ if (ref.type === 'e' && ref.eventId && !updatedRef.coordinate) {
+ updatedRef.coordinate = ref.eventId
+ }
+ return updatedRef
} else {
- return { ...ref, event: undefined }
+ // For e-tags, ensure coordinate is set to eventId even if fetch failed
+ const updatedRef = { ...ref, event: undefined }
+ if (ref.type === 'e' && ref.eventId && !updatedRef.coordinate) {
+ updatedRef.coordinate = ref.eventId
+ }
+ return updatedRef
}
} catch (error) {
logger.error('[PublicationIndex] Error fetching reference:', error)
- return { ...ref, event: undefined }
+ // For e-tags, ensure coordinate is set to eventId even on error
+ const updatedRef = { ...ref, event: undefined }
+ if (ref.type === 'e' && ref.eventId && !updatedRef.coordinate) {
+ updatedRef.coordinate = ref.eventId
+ }
+ return updatedRef
}
}, [referencesData])
@@ -826,7 +1100,7 @@ export default function PublicationIndex({
{/* Table of Contents - only show for top-level publications */}
{!isNested && !isLoading && tableOfContents.length > 0 && (
-