From 03b3005eee35f0311600c48862e97dac33056eca Mon Sep 17 00:00:00 2001 From: Silberengel Date: Fri, 31 Oct 2025 20:25:24 +0100 Subject: [PATCH] bug-fix publications --- .../Note/AsciidocArticle/AsciidocArticle.tsx | 5 + .../Note/MarkdownArticle/MarkdownArticle.tsx | 26 +- .../PublicationIndex/PublicationIndex.tsx | 797 +++++++++++++----- src/services/indexed-db.service.ts | 28 + 4 files changed, 612 insertions(+), 244 deletions(-) diff --git a/src/components/Note/AsciidocArticle/AsciidocArticle.tsx b/src/components/Note/AsciidocArticle/AsciidocArticle.tsx index 51e6fca..d90011f 100644 --- a/src/components/Note/AsciidocArticle/AsciidocArticle.tsx +++ b/src/components/Note/AsciidocArticle/AsciidocArticle.tsx @@ -365,6 +365,11 @@ export default function AsciidocArticle({ )} + {/* Show title inline when used as nested content */} + {hideImagesAndInfo && metadata.title && ( +

{metadata.title}

+ )} + {/* Render AsciiDoc content (everything is now processed as AsciiDoc) */}
-
- {!hideMetadata && metadata.title &&

{metadata.title}

} - {!hideMetadata && metadata.summary && ( -
-

{metadata.summary}

-
- )} - {!hideMetadata && metadata.image && (() => { +
+ {!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 && ( -
+

Table of Contents