diff --git a/src/app.d.ts b/src/app.d.ts index 1e997cc..3f8a7f6 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -13,6 +13,10 @@ declare global { publicationType?: string; indexEvent?: NDKEvent; url?: URL; + identifierInfo?: { + type: string; + identifier: string; + }; } // interface Platform {} } diff --git a/src/lib/components/publications/Publication.svelte b/src/lib/components/publications/Publication.svelte index 87bc44b..6398a0c 100644 --- a/src/lib/components/publications/Publication.svelte +++ b/src/lib/components/publications/Publication.svelte @@ -24,43 +24,67 @@ import TableOfContents from "./TableOfContents.svelte"; import type { TableOfContents as TocType } from "./table_of_contents.svelte"; - let { rootAddress, publicationType, indexEvent } = $props<{ + let { rootAddress, publicationType, indexEvent, publicationTree, toc } = $props<{ rootAddress: string; publicationType: string; indexEvent: NDKEvent; + publicationTree: SveltePublicationTree; + toc: TocType; }>(); - const publicationTree = getContext( - "publicationTree", - ) as SveltePublicationTree; - const toc = getContext("toc") as TocType; - // #region Loading - let leaves = $state>([]); - let isLoading = $state(false); - let isDone = $state(false); + let isLoading = $state(false); + let isDone = $state(false); let lastElementRef = $state(null); let activeAddress = $state(null); + let loadedAddresses = $state>(new Set()); + let hasInitialized = $state(false); let observer: IntersectionObserver; async function loadMore(count: number) { + if (!publicationTree) { + console.warn("[Publication] publicationTree is not available"); + return; + } + + console.log(`[Publication] Loading ${count} more events. Current leaves: ${leaves.length}, loaded addresses: ${loadedAddresses.size}`); + isLoading = true; - for (let i = 0; i < count; i++) { - const iterResult = await publicationTree.next(); - const { done, value } = iterResult; - - if (done) { - isDone = true; - break; + try { + for (let i = 0; i < count; i++) { + const iterResult = await publicationTree.next(); + const { done, value } = iterResult; + + if (done) { + console.log("[Publication] Iterator done, no more events"); + isDone = true; + break; + } + + if (value) { + const address = value.tagAddress(); + console.log(`[Publication] Got event: ${address} (${value.id})`); + if (!loadedAddresses.has(address)) { + loadedAddresses.add(address); + leaves.push(value); + console.log(`[Publication] Added event: ${address}`); + } else { + console.warn(`[Publication] Duplicate event detected: ${address}`); + } + } else { + console.log("[Publication] Got null event"); + leaves.push(null); + } } - - leaves.push(value); + } catch (error) { + console.error("[Publication] Error loading more content:", error); + } finally { + isLoading = false; + console.log(`[Publication] Finished loading. Total leaves: ${leaves.length}, loaded addresses: ${loadedAddresses.size}`); } - - isLoading = false; } function setLastElementRef(el: HTMLElement, i: number) { @@ -85,6 +109,27 @@ // #endregion + // AI-NOTE: Load initial content when publicationTree becomes available + $effect(() => { + if (publicationTree && leaves.length === 0 && !isLoading && !isDone && !hasInitialized) { + console.log("[Publication] Loading initial content"); + hasInitialized = true; + loadMore(12); + } + }); + + // AI-NOTE: Reset state when publicationTree changes + $effect(() => { + if (publicationTree) { + leaves = []; + isLoading = false; + isDone = false; + lastElementRef = null; + loadedAddresses = new Set(); + hasInitialized = false; + } + }); + // #region Columns visibility let currentBlog: null | string = $state(null); @@ -175,14 +220,18 @@ observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { - if (entry.isIntersecting && !isLoading && !isDone) { + if (entry.isIntersecting && !isLoading && !isDone && publicationTree) { loadMore(1); } }); }, { threshold: 0.5 }, ); - loadMore(12); + + // Only load initial content if publicationTree is available + if (publicationTree) { + loadMore(12); + } return () => { observer.disconnect(); @@ -207,11 +256,12 @@ /> publicationTree.setBookmark(address)} onLoadMore={() => { - if (!isLoading && !isDone) { + if (!isLoading && !isDone && publicationTree) { loadMore(4); } }} @@ -241,6 +291,8 @@ {rootAddress} {leaves} {address} + {publicationTree} + {toc} ref={(el) => onPublicationSectionMounted(el, address)} /> {/if} @@ -300,6 +352,8 @@ {rootAddress} {leaves} address={leaf.tagAddress()} + {publicationTree} + {toc} ref={(el) => setLastElementRef(el, i)} /> diff --git a/src/lib/components/publications/PublicationSection.svelte b/src/lib/components/publications/PublicationSection.svelte index 2de5292..2b9aace 100644 --- a/src/lib/components/publications/PublicationSection.svelte +++ b/src/lib/components/publications/PublicationSection.svelte @@ -9,6 +9,7 @@ import type { Asciidoctor, Document } from "asciidoctor"; import { getMatchingTags } from "$lib/utils/nostrUtils"; import type { SveltePublicationTree } from "./svelte_publication_tree.svelte"; + import type { TableOfContents as TocType } from "./table_of_contents.svelte"; import { postProcessAdvancedAsciidoctorHtml } from "$lib/utils/markup/advancedAsciidoctorPostProcessor"; import { parseAdvancedmarkup } from "$lib/utils/markup/advancedMarkupParser"; @@ -16,15 +17,18 @@ address, rootAddress, leaves, + publicationTree, + toc, ref, }: { address: string; rootAddress: string; leaves: Array; + publicationTree: SveltePublicationTree; + toc: TocType; ref: (ref: HTMLElement) => void; } = $props(); - const publicationTree: SveltePublicationTree = getContext("publicationTree"); const asciidoctor: Asciidoctor = getContext("asciidoctor"); let leafEvent: Promise = $derived.by( diff --git a/src/lib/components/publications/TableOfContents.svelte b/src/lib/components/publications/TableOfContents.svelte index a2fc748..cacee90 100644 --- a/src/lib/components/publications/TableOfContents.svelte +++ b/src/lib/components/publications/TableOfContents.svelte @@ -12,15 +12,14 @@ import Self from "./TableOfContents.svelte"; import { onMount, onDestroy } from "svelte"; - let { depth, onSectionFocused, onLoadMore } = $props<{ + let { depth, onSectionFocused, onLoadMore, toc } = $props<{ rootAddress: string; depth: number; + toc: TableOfContents; onSectionFocused?: (address: string) => void; onLoadMore?: () => void; }>(); - let toc = getContext("toc") as TableOfContents; - let entries = $derived.by(() => { const newEntries = []; for (const [_, entry] of toc.addressMap) { @@ -175,7 +174,7 @@ btnClass="flex items-center p-2 w-full font-normal text-gray-900 rounded-lg transition duration-75 group hover:bg-primary-50 dark:text-white dark:hover:bg-primary-800 {isVisible ? 'toc-highlight' : ''} {isLastEntry ? 'pb-4' : ''}" bind:isOpen={() => expanded, (open) => setEntryExpanded(address, open)} > - + {/if} {/each} diff --git a/src/lib/snippets/UserSnippets.svelte b/src/lib/snippets/UserSnippets.svelte index a4b4a17..6e96719 100644 --- a/src/lib/snippets/UserSnippets.svelte +++ b/src/lib/snippets/UserSnippets.svelte @@ -21,7 +21,7 @@ {@const npub = toNpub(identifier)} {#if npub} {#if !displayText || displayText.trim().toLowerCase() === "unknown"} - {#await getUserMetadata(npub) then profile} + {#await getUserMetadata(npub, undefined, false) then profile} {@const p = profile as NostrProfileWithLegacy} + + + {:else} - {@const debugInfo = `indexEvent: ${!!indexEvent}, data.indexEvent: ${!!data.indexEvent}`} + {@const debugInfo = `indexEvent: ${!!indexEvent}, publicationTree: ${!!publicationTree}, toc: ${!!toc}`} {@const debugElement = console.debug('[Publication] NOT rendering publication with:', debugInfo)}
diff --git a/src/routes/publication/[type]/[identifier]/+page.ts b/src/routes/publication/[type]/[identifier]/+page.ts index 69d8a59..5a4a288 100644 --- a/src/routes/publication/[type]/[identifier]/+page.ts +++ b/src/routes/publication/[type]/[identifier]/+page.ts @@ -7,6 +7,7 @@ import { fetchEventByNevent, } from "../../../../lib/utils/websocket_utils.ts"; import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; +import { browser } from "$app/environment"; export const load: PageLoad = async ( { params }: { @@ -15,64 +16,49 @@ export const load: PageLoad = async ( ) => { const { type, identifier } = params; - // AI-NOTE: Always fetch client-side since server-side fetch returns null for now + // AI-NOTE: Only fetch client-side since server-side fetch fails due to missing relay connections + // This prevents 404 errors when refreshing publication pages during SSR let indexEvent: NostrEvent | null = null; - try { - // Handle different identifier types - switch (type) { - case "id": - indexEvent = await fetchEventById(identifier); - break; - case "d": - indexEvent = await fetchEventByDTag(identifier); - break; - case "naddr": - indexEvent = await fetchEventByNaddr(identifier); - break; - case "nevent": - indexEvent = await fetchEventByNevent(identifier); - break; - default: - error(400, `Unsupported identifier type: ${type}`); + // Only attempt to fetch if we're in a browser environment + if (browser) { + try { + // Handle different identifier types + switch (type) { + case "id": + indexEvent = await fetchEventById(identifier); + break; + case "d": + indexEvent = await fetchEventByDTag(identifier); + break; + case "naddr": + indexEvent = await fetchEventByNaddr(identifier); + break; + case "nevent": + indexEvent = await fetchEventByNevent(identifier); + break; + default: + error(400, `Unsupported identifier type: ${type}`); + } + } catch (err) { + // AI-NOTE: Don't throw error immediately - let the component handle it + // This allows for better error handling and retry logic + console.warn(`[Publication Load] Failed to fetch event:`, err); } - } catch (err) { - throw err; } - if (!indexEvent) { - // AI-NOTE: Handle case where no relays are available during preloading - // This prevents 404 errors when relay stores haven't been populated yet - - // Create appropriate search link based on type - let searchParam = ""; - switch (type) { - case "id": - searchParam = `id=${identifier}`; - break; - case "d": - searchParam = `d=${identifier}`; - break; - case "naddr": - case "nevent": - searchParam = `id=${identifier}`; - break; - default: - searchParam = `q=${identifier}`; - } - - error( - 404, - `Event not found for ${type}: ${identifier}. href="/events?${searchParam}"`, - ); - } - - const publicationType = - indexEvent.tags.find((tag) => tag[0] === "type")?.[1] ?? ""; + // AI-NOTE: Return null for indexEvent during SSR or when fetch fails + // The component will handle client-side loading and error states + const publicationType = indexEvent?.tags.find((tag) => tag[0] === "type")?.[1] ?? ""; const result = { publicationType, indexEvent, + // AI-NOTE: Pass the identifier info for client-side retry + identifierInfo: { + type, + identifier, + }, }; return result;