Browse Source

fixed publications

master
silberengel 7 months ago
parent
commit
fc34d68a83
  1. 4
      src/app.d.ts
  2. 100
      src/lib/components/publications/Publication.svelte
  3. 6
      src/lib/components/publications/PublicationSection.svelte
  4. 7
      src/lib/components/publications/TableOfContents.svelte
  5. 2
      src/lib/snippets/UserSnippets.svelte
  6. 165
      src/routes/publication/[type]/[identifier]/+page.svelte
  7. 84
      src/routes/publication/[type]/[identifier]/+page.ts

4
src/app.d.ts vendored

@ -13,6 +13,10 @@ declare global { @@ -13,6 +13,10 @@ declare global {
publicationType?: string;
indexEvent?: NDKEvent;
url?: URL;
identifierInfo?: {
type: string;
identifier: string;
};
}
// interface Platform {}
}

100
src/lib/components/publications/Publication.svelte

@ -24,43 +24,67 @@ @@ -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<Array<NDKEvent | null>>([]);
let isLoading = $state<boolean>(false);
let isDone = $state<boolean>(false);
let isLoading = $state(false);
let isDone = $state(false);
let lastElementRef = $state<HTMLElement | null>(null);
let activeAddress = $state<string | null>(null);
let loadedAddresses = $state<Set<string>>(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 @@ @@ -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 @@ @@ -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 @@ @@ -207,11 +256,12 @@
/>
<TableOfContents
{rootAddress}
{toc}
depth={2}
onSectionFocused={(address: string) =>
publicationTree.setBookmark(address)}
onLoadMore={() => {
if (!isLoading && !isDone) {
if (!isLoading && !isDone && publicationTree) {
loadMore(4);
}
}}
@ -241,6 +291,8 @@ @@ -241,6 +291,8 @@
{rootAddress}
{leaves}
{address}
{publicationTree}
{toc}
ref={(el) => onPublicationSectionMounted(el, address)}
/>
{/if}
@ -300,6 +352,8 @@ @@ -300,6 +352,8 @@
{rootAddress}
{leaves}
address={leaf.tagAddress()}
{publicationTree}
{toc}
ref={(el) => setLastElementRef(el, i)}
/>

6
src/lib/components/publications/PublicationSection.svelte

@ -9,6 +9,7 @@ @@ -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 @@ @@ -16,15 +17,18 @@
address,
rootAddress,
leaves,
publicationTree,
toc,
ref,
}: {
address: string;
rootAddress: string;
leaves: Array<NDKEvent | null>;
publicationTree: SveltePublicationTree;
toc: TocType;
ref: (ref: HTMLElement) => void;
} = $props();
const publicationTree: SveltePublicationTree = getContext("publicationTree");
const asciidoctor: Asciidoctor = getContext("asciidoctor");
let leafEvent: Promise<NDKEvent | null> = $derived.by(

7
src/lib/components/publications/TableOfContents.svelte

@ -12,15 +12,14 @@ @@ -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<TocEntry[]>(() => {
const newEntries = [];
for (const [_, entry] of toc.addressMap) {
@ -175,7 +174,7 @@ @@ -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)}
>
<Self rootAddress={address} depth={childDepth} {onSectionFocused} {onLoadMore} />
<Self rootAddress={address} depth={childDepth} {toc} {onSectionFocused} {onLoadMore} />
</SidebarDropdownWrapper>
{/if}
{/each}

2
src/lib/snippets/UserSnippets.svelte

@ -21,7 +21,7 @@ @@ -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}
<span class="inline-flex items-center gap-0.5">
<button

165
src/routes/publication/[type]/[identifier]/+page.svelte

@ -9,35 +9,98 @@ @@ -9,35 +9,98 @@
import { page } from "$app/state";
import { goto } from "$app/navigation";
import { createNDKEvent } from "$lib/utils/nostrUtils";
import { browser } from "$app/environment";
import {
fetchEventByDTag,
fetchEventById,
fetchEventByNaddr,
fetchEventByNevent,
} from "$lib/utils/websocket_utils.ts";
import type { NostrEvent } from "$lib/utils/websocket_utils.ts";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
let { data }: PageProps = $props();
// data.indexEvent can be null from server-side rendering
// We need to handle this case properly
// AI-NOTE: Always create NDK event since we now ensure NDK is available
console.debug('[Publication] data.indexEvent:', data.indexEvent);
console.debug('[Publication] data.ndk:', data.ndk);
const indexEvent = data.indexEvent && data.ndk
? createNDKEvent(data.ndk, data.indexEvent)
: null; // No event if no NDK or no event data
console.debug('[Publication] indexEvent created:', indexEvent);
// Only create publication tree if we have a valid index event
const publicationTree = indexEvent ? new SveltePublicationTree(indexEvent, data.ndk) : null;
const toc = indexEvent ? new TableOfContents(
indexEvent.tagAddress(),
publicationTree!,
page.url.pathname ?? "",
) : null;
setContext("publicationTree", publicationTree);
setContext("toc", toc);
setContext("asciidoctor", Processor());
// AI-NOTE: Handle client-side loading when event is not available during SSR
let indexEvent = $state<NDKEvent | null>(null);
let loading = $state(false);
let error = $state<string | null>(null);
let publicationTree = $state<SveltePublicationTree | null>(null);
let toc = $state<TableOfContents | null>(null);
let initialized = $state(false);
// AI-NOTE: Initialize with server-side data if available
$effect(() => {
if (initialized) return; // Prevent re-initialization
if (data.indexEvent && data.ndk) {
const serverEvent = createNDKEvent(data.ndk, data.indexEvent);
indexEvent = serverEvent;
initializePublicationComponents(serverEvent);
initialized = true;
} else if (browser && data.identifierInfo && !loading) {
// AI-NOTE: Client-side loading when server-side data is not available
loadEventClientSide();
}
});
async function loadEventClientSide() {
if (!browser || !data.identifierInfo || loading) return;
loading = true;
error = null;
try {
const { type, identifier } = data.identifierInfo;
let fetchedEvent: NostrEvent | null = null;
// Handle different identifier types
switch (type) {
case "id":
fetchedEvent = await fetchEventById(identifier);
break;
case "d":
fetchedEvent = await fetchEventByDTag(identifier);
break;
case "naddr":
fetchedEvent = await fetchEventByNaddr(identifier);
break;
case "nevent":
fetchedEvent = await fetchEventByNevent(identifier);
break;
default:
throw new Error(`Unsupported identifier type: ${type}`);
}
// Only set up bookmark handling if we have a valid publication tree
if (publicationTree && indexEvent) {
if (fetchedEvent && data.ndk) {
const clientEvent = createNDKEvent(data.ndk, fetchedEvent);
indexEvent = clientEvent;
initializePublicationComponents(clientEvent);
initialized = true;
} else {
throw new Error("Failed to fetch event from relays");
}
} catch (err) {
console.error("[Publication] Client-side loading failed:", err);
error = err instanceof Error ? err.message : "Failed to load publication";
} finally {
loading = false;
}
}
function initializePublicationComponents(event: NDKEvent) {
if (!data.ndk) return;
console.log("[Publication] Initializing publication components for event:", event.tagAddress());
publicationTree = new SveltePublicationTree(event, data.ndk);
toc = new TableOfContents(
event.tagAddress(),
publicationTree,
page.url.pathname ?? "",
);
// Set up bookmark handling
publicationTree.onBookmarkMoved((address) => {
goto(`#${address}`, {
replaceState: true,
@ -55,12 +118,27 @@ @@ -55,12 +118,27 @@
db.onsuccess = () => {
const transaction = db.result.transaction(["bookmarks"], "readwrite");
const store = transaction.objectStore("bookmarks");
const bookmarkKey = `${indexEvent.tagAddress()}`;
const bookmarkKey = `${event.tagAddress()}`;
store.put({ key: bookmarkKey, address });
};
});
}
// AI-NOTE: Set context values reactively to avoid capturing initial null values
$effect(() => {
if (publicationTree) {
setContext("publicationTree", publicationTree);
}
});
$effect(() => {
if (toc) {
setContext("toc", toc);
}
});
setContext("asciidoctor", Processor());
onMount(() => {
// Only handle bookmarks if we have valid components
if (!publicationTree || !indexEvent) return;
@ -77,11 +155,11 @@ @@ -77,11 +155,11 @@
db.onsuccess = () => {
const transaction = db.result.transaction(["bookmarks"], "readonly");
const store = transaction.objectStore("bookmarks");
const bookmarkKey = `${indexEvent.tagAddress()}`;
const bookmarkKey = `${indexEvent!.tagAddress()}`;
const request = store.get(bookmarkKey);
request.onsuccess = () => {
if (request.result?.address) {
if (request.result?.address && publicationTree && indexEvent) {
// Set the bookmark in the publication tree
publicationTree.setBookmark(request.result.address);
@ -99,12 +177,12 @@ @@ -99,12 +177,12 @@
});
</script>
{#if indexEvent && data.indexEvent}
{@const debugInfo = `indexEvent: ${!!indexEvent}, data.indexEvent: ${!!data.indexEvent}`}
{#if indexEvent && publicationTree && toc}
{@const debugInfo = `indexEvent: ${!!indexEvent}, publicationTree: ${!!publicationTree}, toc: ${!!toc}`}
{@const debugElement = console.debug('[Publication] Rendering publication with:', debugInfo)}
<ArticleNav
publicationType={data.publicationType}
rootId={data.indexEvent.id}
rootId={indexEvent.id}
indexEvent={indexEvent}
/>
@ -113,10 +191,33 @@ @@ -113,10 +191,33 @@
rootAddress={indexEvent.tagAddress()}
publicationType={data.publicationType}
indexEvent={indexEvent}
publicationTree={publicationTree}
toc={toc}
/>
</main>
{:else if loading}
<main class="publication">
<div class="flex items-center justify-center min-h-screen">
<p class="text-gray-600 dark:text-gray-400">Loading publication...</p>
</div>
</main>
{:else if error}
<main class="publication">
<div class="flex items-center justify-center min-h-screen">
<div class="text-center">
<p class="text-red-600 dark:text-red-400 mb-4">Failed to load publication</p>
<p class="text-gray-600 dark:text-gray-400 mb-4">{error}</p>
<button
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
onclick={loadEventClientSide}
>
Try Again
</button>
</div>
</div>
</main>
{: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)}
<main class="publication">
<div class="flex items-center justify-center min-h-screen">

84
src/routes/publication/[type]/[identifier]/+page.ts

@ -7,6 +7,7 @@ import { @@ -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 ( @@ -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;

Loading…
Cancel
Save