|
|
|
|
@ -9,7 +9,7 @@ import { generateBech32IdFromATag } from '@/lib/tag'
@@ -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({
@@ -91,79 +91,316 @@ export default function PublicationIndex({
|
|
|
|
|
const [failedReferences, setFailedReferences] = useState<PublicationReference[]>([]) |
|
|
|
|
const maxRetries = 5 |
|
|
|
|
|
|
|
|
|
// Build table of contents from references
|
|
|
|
|
const tableOfContents = useMemo<ToCItem[]>(() => { |
|
|
|
|
const toc: ToCItem[] = [] |
|
|
|
|
// 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]) |
|
|
|
|
|
|
|
|
|
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' |
|
|
|
|
|
|
|
|
|
const tocItem: ToCItem = { |
|
|
|
|
title, |
|
|
|
|
coordinate: ref.coordinate || ref.eventId || '', |
|
|
|
|
event: ref.event, |
|
|
|
|
kind: ref.kind || ref.event?.kind || 0 |
|
|
|
|
// Recursive helper function to build ToC item from a reference
|
|
|
|
|
const buildToCItemFromRef = useCallback((ref: PublicationReference, allReferences: PublicationReference[], visitedCoords: Set<string> = 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 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// For nested 30040 publications, recursively get their ToC
|
|
|
|
|
if ((ref.kind === ExtendedKind.PUBLICATION || ref.event?.kind === ExtendedKind.PUBLICATION) && ref.event) { |
|
|
|
|
const nestedRefs: ToCItem[] = [] |
|
|
|
|
// 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' |
|
|
|
|
|
|
|
|
|
// 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) |
|
|
|
|
const coordinate = ref.coordinate || ref.eventId || '' |
|
|
|
|
const coordKey = ref.coordinate || ref.eventId || '' |
|
|
|
|
|
|
|
|
|
if (!isNaN(kind) && kind === ExtendedKind.PUBLICATION_CONTENT ||
|
|
|
|
|
// 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<string>() |
|
|
|
|
|
|
|
|
|
// 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) { |
|
|
|
|
// 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 |
|
|
|
|
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 |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
} 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 |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (nestedRefs.length > 0) { |
|
|
|
|
tocItem.children = nestedRefs |
|
|
|
|
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<ToCItem[]>(() => { |
|
|
|
|
const toc: ToCItem[] = [] |
|
|
|
|
|
|
|
|
|
// Build ToC from all fetched references (recursively)
|
|
|
|
|
for (const ref of references) { |
|
|
|
|
// 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) |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
if (isTopLevel) { |
|
|
|
|
const tocItem = buildToCItemFromRef(ref, references) |
|
|
|
|
if (tocItem) { |
|
|
|
|
toc.push(tocItem) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 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) |
|
|
|
|
|
|
|
|
|
toc.push(tocItem) |
|
|
|
|
if (!isNaN(kind) && (kind === ExtendedKind.PUBLICATION_CONTENT ||
|
|
|
|
|
kind === ExtendedKind.WIKI_ARTICLE ||
|
|
|
|
|
kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN || |
|
|
|
|
kind === ExtendedKind.PUBLICATION)) { |
|
|
|
|
toc.push({ |
|
|
|
|
title: identifier || 'Untitled', |
|
|
|
|
coordinate: tag[1], |
|
|
|
|
kind |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
} 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 |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return toc |
|
|
|
|
}, [references]) |
|
|
|
|
}, [references, event, referencesData, buildToCItemFromRef]) |
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
const scrollToSection = (coordinate: string) => { |
|
|
|
|
const element = document.getElementById(`section-${coordinate.replace(/:/g, '-')}`) |
|
|
|
|
if (element) { |
|
|
|
|
element.scrollIntoView({ behavior: 'smooth', block: 'start' }) |
|
|
|
|
// 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({
@@ -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({
@@ -249,6 +455,147 @@ export default function PublicationIndex({
|
|
|
|
|
}) |
|
|
|
|
}, [currentCoordinate, event]) |
|
|
|
|
|
|
|
|
|
// Helper function to build comprehensive relay list
|
|
|
|
|
const buildComprehensiveRelayList = useCallback(async ( |
|
|
|
|
additionalRelays: string[] = [] |
|
|
|
|
): Promise<string[]> => { |
|
|
|
|
const { FAST_READ_RELAY_URLS, BIG_RELAY_URLS, SEARCHABLE_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 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<Event | undefined> => { |
|
|
|
|
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<Event | undefined> => { |
|
|
|
|
// 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({
@@ -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<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) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
// 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({
@@ -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({
@@ -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({
@@ -826,7 +1100,7 @@ export default function PublicationIndex({
|
|
|
|
|
|
|
|
|
|
{/* Table of Contents - only show for top-level publications */} |
|
|
|
|
{!isNested && !isLoading && tableOfContents.length > 0 && ( |
|
|
|
|
<div className="border rounded-lg p-6 bg-muted/30"> |
|
|
|
|
<div id="publication-toc" className="border rounded-lg p-6 bg-muted/30 scroll-mt-24"> |
|
|
|
|
<h2 className="text-xl font-semibold mb-4">Table of Contents</h2> |
|
|
|
|
<nav> |
|
|
|
|
<ul className="space-y-2"> |
|
|
|
|
@ -950,28 +1224,76 @@ export default function PublicationIndex({
@@ -950,28 +1224,76 @@ export default function PublicationIndex({
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Render based on event kind
|
|
|
|
|
// Use the same coordinate logic as ToC: coordinate || eventId
|
|
|
|
|
// For e-tags, coordinate might be empty, so use eventId
|
|
|
|
|
// For a-tags, coordinate is set (kind:pubkey:identifier)
|
|
|
|
|
const coordinate = ref.coordinate || ref.eventId || '' |
|
|
|
|
const sectionId = `section-${coordinate.replace(/:/g, '-')}` |
|
|
|
|
const eventKind = ref.kind || ref.event.kind |
|
|
|
|
|
|
|
|
|
// Debug: log section ID generation
|
|
|
|
|
logger.debug('[PublicationIndex] Rendering section:', {
|
|
|
|
|
coordinate,
|
|
|
|
|
sectionId,
|
|
|
|
|
hasCoordinate: !!ref.coordinate,
|
|
|
|
|
hasEventId: !!ref.eventId, |
|
|
|
|
eventId: ref.eventId?.substring(0, 16) + '...' |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
if (eventKind === ExtendedKind.PUBLICATION) { |
|
|
|
|
// Recursively render nested 30040 publication index
|
|
|
|
|
return ( |
|
|
|
|
<div key={index} id={sectionId} className="border-l-4 border-primary pl-6 scroll-mt-4"> |
|
|
|
|
<div key={index} id={sectionId} className="border-l-4 border-primary pl-6 scroll-mt-24 pt-6 relative"> |
|
|
|
|
{!isNested && ( |
|
|
|
|
<Button |
|
|
|
|
variant="ghost" |
|
|
|
|
size="sm" |
|
|
|
|
className="absolute top-0 right-0 opacity-70 hover:opacity-100" |
|
|
|
|
onClick={scrollToToc} |
|
|
|
|
title="Back to Table of Contents" |
|
|
|
|
> |
|
|
|
|
<ArrowUp className="h-4 w-4 mr-2" /> |
|
|
|
|
ToC |
|
|
|
|
</Button> |
|
|
|
|
)} |
|
|
|
|
<PublicationIndex event={ref.event} isNested={true} /> |
|
|
|
|
</div> |
|
|
|
|
) |
|
|
|
|
} else if (eventKind === ExtendedKind.PUBLICATION_CONTENT || eventKind === ExtendedKind.WIKI_ARTICLE) { |
|
|
|
|
// Render 30041 or 30818 content as AsciidocArticle
|
|
|
|
|
return ( |
|
|
|
|
<div key={index} id={sectionId} className="scroll-mt-4"> |
|
|
|
|
<div key={index} id={sectionId} className="scroll-mt-24 pt-6 relative"> |
|
|
|
|
{!isNested && ( |
|
|
|
|
<Button |
|
|
|
|
variant="ghost" |
|
|
|
|
size="sm" |
|
|
|
|
className="absolute top-0 right-0 opacity-70 hover:opacity-100" |
|
|
|
|
onClick={scrollToToc} |
|
|
|
|
title="Back to Table of Contents" |
|
|
|
|
> |
|
|
|
|
<ArrowUp className="h-4 w-4 mr-2" /> |
|
|
|
|
ToC |
|
|
|
|
</Button> |
|
|
|
|
)} |
|
|
|
|
<AsciidocArticle event={ref.event} hideImagesAndInfo={true} /> |
|
|
|
|
</div> |
|
|
|
|
) |
|
|
|
|
} else if (eventKind === ExtendedKind.WIKI_ARTICLE_MARKDOWN) { |
|
|
|
|
// Render 30817 content as MarkdownArticle
|
|
|
|
|
return ( |
|
|
|
|
<div key={index} id={sectionId} className="scroll-mt-4"> |
|
|
|
|
<div key={index} id={sectionId} className="scroll-mt-24 pt-6 relative"> |
|
|
|
|
{!isNested && ( |
|
|
|
|
<Button |
|
|
|
|
variant="ghost" |
|
|
|
|
size="sm" |
|
|
|
|
className="absolute top-0 right-0 opacity-70 hover:opacity-100" |
|
|
|
|
onClick={scrollToToc} |
|
|
|
|
title="Back to Table of Contents" |
|
|
|
|
> |
|
|
|
|
<ArrowUp className="h-4 w-4 mr-2" /> |
|
|
|
|
ToC |
|
|
|
|
</Button> |
|
|
|
|
)} |
|
|
|
|
<MarkdownArticle event={ref.event} showImageGallery={false} hideMetadata={true} /> |
|
|
|
|
</div> |
|
|
|
|
) |
|
|
|
|
@ -1004,10 +1326,19 @@ function ToCItemComponent({
@@ -1004,10 +1326,19 @@ function ToCItemComponent({
|
|
|
|
|
}) { |
|
|
|
|
const indentClass = level > 0 ? `ml-${level * 4}` : '' |
|
|
|
|
|
|
|
|
|
const handleClick = () => { |
|
|
|
|
if (!item.coordinate) { |
|
|
|
|
console.warn('[PublicationIndex] ToC item has no coordinate:', item) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
console.debug('[PublicationIndex] ToC item clicked:', { title: item.title, coordinate: item.coordinate }) |
|
|
|
|
onItemClick(item.coordinate) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<li className={cn('list-none', indentClass)}> |
|
|
|
|
<button |
|
|
|
|
onClick={() => onItemClick(item.coordinate)} |
|
|
|
|
onClick={handleClick} |
|
|
|
|
className="text-left text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline cursor-pointer" |
|
|
|
|
> |
|
|
|
|
{item.title} |
|
|
|
|
|