From 872592dd766839bd2d5b85c27af83fd896b0d760 Mon Sep 17 00:00:00 2001 From: limina1 Date: Mon, 28 Jul 2025 15:59:10 -0400 Subject: [PATCH 01/17] feat: add My Notes page with tag filtering and AsciiDoc rendering - Add My Notes navigation link - Create My Notes page with 30041 event fetching - Implement tag filtering system with sidebar - Add AsciiDoc rendering for note content - Include Playwright tests for layout validation - Fix NDKFilter import issues (type imports) - Update layout to prevent horizontal scroll - Add publisher service for note publishing --- playwright.config.ts | 12 +- src/lib/components/Navigation.svelte | 1 + src/lib/services/publisher.ts | 91 +++++++++ src/lib/utils/event_search.ts | 3 +- src/lib/utils/search_types.ts | 3 +- src/routes/+layout.svelte | 2 +- src/routes/my-notes/+page.svelte | 276 +++++++++++++++++++++++++++ tests/e2e/my_notes_layout.pw.spec.ts | 103 ++++++++++ 8 files changed, 482 insertions(+), 9 deletions(-) create mode 100644 src/routes/my-notes/+page.svelte create mode 100644 tests/e2e/my_notes_layout.pw.spec.ts diff --git a/playwright.config.ts b/playwright.config.ts index 4ef00bd..5779001 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -27,7 +27,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://127.0.0.1:3000', + baseURL: 'http://localhost:5173', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", @@ -72,11 +72,11 @@ export default defineConfig({ ], /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://127.0.0.1:3000', - // reuseExistingServer: !process.env.CI, - // }, + webServer: { + command: 'npm run dev', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + }, // Glob patterns or regular expressions to ignore test files. // testIgnore: '*test-assets', diff --git a/src/lib/components/Navigation.svelte b/src/lib/components/Navigation.svelte index fdcfe32..e155c03 100644 --- a/src/lib/components/Navigation.svelte +++ b/src/lib/components/Navigation.svelte @@ -31,6 +31,7 @@ Visualize Getting Started Events + My Notes About Contact diff --git a/src/lib/services/publisher.ts b/src/lib/services/publisher.ts index 4bfc033..3d5e9fe 100644 --- a/src/lib/services/publisher.ts +++ b/src/lib/services/publisher.ts @@ -3,6 +3,7 @@ import { ndkInstance } from "../ndk.ts"; import { getMimeTags } from "../utils/mime.ts"; import { parseAsciiDocSections } from "../utils/ZettelParser.ts"; import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk"; +import { nip19 } from "nostr-tools"; export interface PublishResult { success: boolean; @@ -103,6 +104,96 @@ export async function publishZettel( } } +/** + * Publishes all AsciiDoc sections as separate Nostr events + * @param options - Publishing options + * @returns Promise resolving to array of publish results + */ +export async function publishMultipleZettels( + options: PublishOptions, +): Promise { + const { content, kind = 30041, onError } = options; + + if (!content.trim()) { + const error = 'Please enter some content'; + onError?.(error); + return [{ success: false, error }]; + } + + const ndk = get(ndkInstance); + if (!ndk?.activeUser) { + const error = 'Please log in first'; + onError?.(error); + return [{ success: false, error }]; + } + + try { + const sections = parseAsciiDocSections(content, 2); + if (sections.length === 0) { + throw new Error('No valid sections found in content'); + } + + const allRelayUrls = Array.from(ndk.pool?.relays.values() || []).map((r) => r.url); + if (allRelayUrls.length === 0) { + throw new Error('No relays available in NDK pool'); + } + const relaySet = NDKRelaySet.fromRelayUrls(allRelayUrls, ndk); + + const results: PublishResult[] = []; + const publishedEvents: NDKEvent[] = []; + for (const section of sections) { + const title = section.title; + const cleanContent = section.content; + const sectionTags = section.tags || []; + const dTag = generateDTag(title); + const [mTag, MTag] = getMimeTags(kind); + const tags: string[][] = [["d", dTag], mTag, MTag, ["title", title]]; + if (sectionTags) { + tags.push(...sectionTags); + } + const ndkEvent = new NDKEvent(ndk); + ndkEvent.kind = kind; + ndkEvent.created_at = Math.floor(Date.now() / 1000); + ndkEvent.tags = tags; + ndkEvent.content = cleanContent; + ndkEvent.pubkey = ndk.activeUser.pubkey; + try { + await ndkEvent.sign(); + const publishedToRelays = await ndkEvent.publish(relaySet); + if (publishedToRelays.size > 0) { + results.push({ success: true, eventId: ndkEvent.id }); + publishedEvents.push(ndkEvent); + } else { + results.push({ success: false, error: 'Failed to publish to any relays' }); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error'; + results.push({ success: false, error: errorMessage }); + } + } + // Debug: extract and log 'e' and 'a' tags from all published events + publishedEvents.forEach(ev => { + // Extract d-tag from tags + const dTagEntry = ev.tags.find(t => t[0] === 'd'); + const dTag = dTagEntry ? dTagEntry[1] : ''; + const aTag = `${ev.kind}:${ev.pubkey}:${dTag}`; + console.log(`Event ${ev.id} tags:`); + console.log(' e:', ev.id); + console.log(' a:', aTag); + // Print nevent and naddr using nip19 + const nevent = nip19.neventEncode({ id: ev.id }); + const naddr = nip19.naddrEncode({ kind: ev.kind, pubkey: ev.pubkey, identifier: dTag }); + console.log(' nevent:', nevent); + console.log(' naddr:', naddr); + }); + return results; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + onError?.(errorMessage); + return [{ success: false, error: errorMessage }]; + } +} + function generateDTag(title: string): string { return title .toLowerCase() diff --git a/src/lib/utils/event_search.ts b/src/lib/utils/event_search.ts index 25319c0..aa1e9a7 100644 --- a/src/lib/utils/event_search.ts +++ b/src/lib/utils/event_search.ts @@ -1,7 +1,8 @@ import { ndkInstance } from "../ndk.ts"; import { fetchEventWithFallback } from "./nostrUtils.ts"; import { nip19 } from "nostr-tools"; -import { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk"; +import { NDKEvent } from "@nostr-dev-kit/ndk"; +import type { NDKFilter } from "@nostr-dev-kit/ndk"; import { get } from "svelte/store"; import { wellKnownUrl, isValidNip05Address } from "./search_utils.ts"; import { TIMEOUTS, VALIDATION } from "./search_constants.ts"; diff --git a/src/lib/utils/search_types.ts b/src/lib/utils/search_types.ts index 134ceff..167472e 100644 --- a/src/lib/utils/search_types.ts +++ b/src/lib/utils/search_types.ts @@ -1,4 +1,5 @@ -import { NDKEvent, NDKFilter, NDKSubscription } from "@nostr-dev-kit/ndk"; +import { NDKEvent, NDKSubscription } from "@nostr-dev-kit/ndk"; +import type { NDKFilter } from "@nostr-dev-kit/ndk"; /** * Extended NostrProfile interface for search results diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 47be24c..90335f6 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -45,7 +45,7 @@ -
+
diff --git a/src/routes/my-notes/+page.svelte b/src/routes/my-notes/+page.svelte new file mode 100644 index 0000000..0163a77 --- /dev/null +++ b/src/routes/my-notes/+page.svelte @@ -0,0 +1,276 @@ + + +
+ + + + +
+

My Notes

+ {#if loading} +
Loading…
+ {:else if error} +
{error}
+ {:else if filteredEvents.length === 0} +
No notes found.
+ {:else} +
    + {#each filteredEvents as event} +
  • +
    +
    {getTitle(event)}
    + +
    + {#if showTags[event.id]} +
    + {#each getTags(event) as tag} + + {tag[0]}: + {tag[1]} + + {/each} +
    + {/if} +
    + {event.created_at + ? new Date(event.created_at * 1000).toLocaleString() + : ""} +
    +
    + {@html renderedContent[event.id] || ""} +
    +
  • + {/each} +
+ {/if} +
+
diff --git a/tests/e2e/my_notes_layout.pw.spec.ts b/tests/e2e/my_notes_layout.pw.spec.ts new file mode 100644 index 0000000..0a17d75 --- /dev/null +++ b/tests/e2e/my_notes_layout.pw.spec.ts @@ -0,0 +1,103 @@ +import { test, expect } from '@playwright/test'; + +// Utility to check for horizontal scroll bar +async function hasHorizontalScroll(page, selector) { + return await page.evaluate((sel) => { + const el = document.querySelector(sel); + if (!el) return false; + return el.scrollWidth > el.clientWidth; + }, selector); +} + +test.describe('My Notes Layout', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/my-notes'); + await page.waitForSelector('h1:text("My Notes")'); + }); + + test('no horizontal scroll bar for all tag type and tag filter combinations', async ({ page }) => { + // Helper to check scroll for current state + async function assertNoScroll() { + const hasScroll = await hasHorizontalScroll(page, 'main, body, html'); + expect(hasScroll).toBeFalsy(); + } + + // Check default (no tag type selected) + await assertNoScroll(); + + // Get all tag type buttons + const tagTypeButtons = await page.locator('aside button').all(); + // Only consider tag type buttons (first N) + const tagTypeCount = await page.locator('aside > div.flex.flex-wrap.gap-2.mb-6 > button').count(); + // For each single tag type + for (let i = 0; i < tagTypeCount; i++) { + // Click tag type button + await tagTypeButtons[i].click(); + await page.waitForTimeout(100); // Wait for UI update + await assertNoScroll(); + // Get tag filter buttons (after tag type buttons) + const tagFilterButtons = await page.locator('aside > div.flex.flex-wrap.gap-2.mb-4 > button').all(); + // Try all single tag filter selections + for (let j = 0; j < tagFilterButtons.length; j++) { + await tagFilterButtons[j].click(); + await page.waitForTimeout(100); + await assertNoScroll(); + // Deselect + await tagFilterButtons[j].click(); + await page.waitForTimeout(50); + } + // Try all pairs of tag filter selections + for (let j = 0; j < tagFilterButtons.length; j++) { + for (let k = j + 1; k < tagFilterButtons.length; k++) { + await tagFilterButtons[j].click(); + await tagFilterButtons[k].click(); + await page.waitForTimeout(100); + await assertNoScroll(); + // Deselect + await tagFilterButtons[j].click(); + await tagFilterButtons[k].click(); + await page.waitForTimeout(50); + } + } + // Deselect tag type + await tagTypeButtons[i].click(); + await page.waitForTimeout(100); + } + + // Try all pairs of tag type selections (multi-select) + for (let i = 0; i < tagTypeCount; i++) { + for (let j = i + 1; j < tagTypeCount; j++) { + await tagTypeButtons[i].click(); + await tagTypeButtons[j].click(); + await page.waitForTimeout(100); + await assertNoScroll(); + // Get tag filter buttons for this combination + const tagFilterButtons = await page.locator('aside > div.flex.flex-wrap.gap-2.mb-4 > button').all(); + // Try all single tag filter selections + for (let k = 0; k < tagFilterButtons.length; k++) { + await tagFilterButtons[k].click(); + await page.waitForTimeout(100); + await assertNoScroll(); + await tagFilterButtons[k].click(); + await page.waitForTimeout(50); + } + // Try all pairs of tag filter selections + for (let k = 0; k < tagFilterButtons.length; k++) { + for (let l = k + 1; l < tagFilterButtons.length; l++) { + await tagFilterButtons[k].click(); + await tagFilterButtons[l].click(); + await page.waitForTimeout(100); + await assertNoScroll(); + await tagFilterButtons[k].click(); + await tagFilterButtons[l].click(); + await page.waitForTimeout(50); + } + } + // Deselect tag types + await tagTypeButtons[i].click(); + await tagTypeButtons[j].click(); + await page.waitForTimeout(100); + } + } + }); +}); \ No newline at end of file From fa0d2376f1b310c586193c1c7806eab268f53493 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 3 Aug 2025 08:57:55 +0200 Subject: [PATCH 02/17] added standard relay set to fetchNostrEvent, as a temporary fix. --- package-lock.json | 6 +++--- src/lib/utils/websocket_utils.ts | 30 +++++++++++++++++++++++----- tests/e2e/my_notes_layout.pw.spec.ts | 6 +++--- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 95e0f71..ed96156 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6422,9 +6422,9 @@ } }, "node_modules/svelte": { - "version": "5.37.2", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.37.2.tgz", - "integrity": "sha512-SAakJiy04/OvXRAUnGxRACGzw6GB9kmxYIjuMO/zTcTL6psqc54Y0O/yR6I3OLqFqn79EPd23qsCGkKozvYYbQ==", + "version": "5.37.3", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.37.3.tgz", + "integrity": "sha512-7t/ejshehHd+95z3Z7ebS7wsqHDQxi/8nBTuTRwpMgNegfRBfuitCSKTUDKIBOExqfT2+DhQ2VLG8Xn+cBXoaQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/lib/utils/websocket_utils.ts b/src/lib/utils/websocket_utils.ts index ab6ef5b..c95001e 100644 --- a/src/lib/utils/websocket_utils.ts +++ b/src/lib/utils/websocket_utils.ts @@ -1,6 +1,8 @@ import { WebSocketPool } from "../data_structures/websocket_pool.ts"; import { error } from "@sveltejs/kit"; import { naddrDecode, neventDecode } from "../utils.ts"; +import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts"; +import { get } from "svelte/store"; export interface NostrEvent { id: string; @@ -25,8 +27,9 @@ export interface NostrFilter { type ResolveCallback = (value: T | PromiseLike) => void; type RejectCallback = (reason?: any) => void; type EventHandler = (ev: Event) => void; +type MessageEventHandler = (ev: MessageEvent) => void; type EventHandlerReject = (reject: RejectCallback) => EventHandler; -type EventHandlerResolve = (resolve: ResolveCallback) => EventHandlerReject; +type EventHandlerResolve = (resolve: ResolveCallback) => (reject: RejectCallback) => MessageEventHandler; function handleMessage( ev: MessageEvent, @@ -67,14 +70,31 @@ function handleError( } export async function fetchNostrEvent(filter: NostrFilter): Promise { - // TODO: Improve relay selection when relay management is implemented. - const ws = await WebSocketPool.instance.acquire("wss://thecitadel.nostr1.com"); + // AI-NOTE: Updated to use active relay stores instead of hardcoded relay URL + // This ensures the function uses the user's configured relays and can find events + // across multiple relays rather than being limited to a single hardcoded relay. + + // Get available relays from the active relay stores + const inboxRelays = get(activeInboxRelays); + const outboxRelays = get(activeOutboxRelays); + + // Combine all available relays, prioritizing inbox relays + const availableRelays = [...inboxRelays, ...outboxRelays]; + + if (availableRelays.length === 0) { + throw new Error("[WebSocket Utils]: No relays available for fetching events"); + } + + // Select a relay - prefer inbox relays if available, otherwise use any available relay + const selectedRelay = inboxRelays.length > 0 ? inboxRelays[0] : availableRelays[0]; + + const ws = await WebSocketPool.instance.acquire(selectedRelay); const subId = crypto.randomUUID(); // AI-NOTE: Currying is used here to abstract the internal handler logic away from the WebSocket // handling logic. The message and error handlers themselves can be refactored without affecting // the WebSocket handling logic. - const curriedMessageHandler: (subId: string) => EventHandlerResolve = + const curriedMessageHandler: (subId: string) => (resolve: ResolveCallback) => (reject: RejectCallback) => MessageEventHandler = (subId) => (resolve) => (reject) => @@ -87,7 +107,7 @@ export async function fetchNostrEvent(filter: NostrFilter): Promise // AI-NOTE: These variables store references to partially-applied handlers so that the `finally` // block receives the correct references to clean up the listeners. - let messageHandler: EventHandler; + let messageHandler: MessageEventHandler; let errorHandler: EventHandler; const res = new Promise((resolve, reject) => { diff --git a/tests/e2e/my_notes_layout.pw.spec.ts b/tests/e2e/my_notes_layout.pw.spec.ts index 0a17d75..23db168 100644 --- a/tests/e2e/my_notes_layout.pw.spec.ts +++ b/tests/e2e/my_notes_layout.pw.spec.ts @@ -1,8 +1,8 @@ -import { test, expect } from '@playwright/test'; +import { test, expect, type Page } from '@playwright/test'; // Utility to check for horizontal scroll bar -async function hasHorizontalScroll(page, selector) { - return await page.evaluate((sel) => { +async function hasHorizontalScroll(page: Page, selector: string) { + return await page.evaluate((sel: string) => { const el = document.querySelector(sel); if (!el) return false; return el.scrollWidth > el.clientWidth; From 521db62f60201ca8ac6ec0ebe0ed21a5da324b5e Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 3 Aug 2025 09:09:11 +0200 Subject: [PATCH 03/17] Use websocketpool in publication feed --- .../publications/PublicationFeed.svelte | 68 ++++++++++++++----- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/src/lib/components/publications/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte index 44db458..701551b 100644 --- a/src/lib/components/publications/PublicationFeed.svelte +++ b/src/lib/components/publications/PublicationFeed.svelte @@ -7,10 +7,9 @@ import { onMount, onDestroy } from "svelte"; import { getMatchingTags, - NDKRelaySetFromNDK, - type NDKEvent, - type NDKRelaySet, } from "$lib/utils/nostrUtils"; + import { WebSocketPool } from "$lib/data_structures/websocket_pool"; + import { NDKEvent } from "@nostr-dev-kit/ndk"; import { searchCache } from "$lib/utils/searchCache"; import { indexEventCache } from "$lib/utils/indexEventCache"; import { isValidNip05Address } from "$lib/utils/search_utility"; @@ -139,21 +138,54 @@ async function fetchFromRelay(relay: string): Promise { try { console.debug(`[PublicationFeed] Fetching from relay: ${relay}`); - const relaySet = NDKRelaySetFromNDK.fromRelayUrls([relay], ndk); - let eventSet = await ndk - .fetchEvents( - { - kinds: [indexKind], - limit: 1000, // Increased limit to get more events - }, - { - groupable: false, - skipVerification: false, - skipValidation: false, - }, - relaySet, - ) - .withTimeout(5000); // Reduced timeout to 5 seconds for faster response + + // Use WebSocketPool to get a pooled connection + const ws = await WebSocketPool.instance.acquire(relay); + const subId = crypto.randomUUID(); + + // Create a promise that resolves with the events + const eventPromise = new Promise>((resolve, reject) => { + const events = new Set(); + + const messageHandler = (ev: MessageEvent) => { + try { + const data = JSON.parse(ev.data); + + if (data[0] === "EVENT" && data[1] === subId) { + const event = new NDKEvent(ndk, data[2]); + events.add(event); + } else if (data[0] === "EOSE" && data[1] === subId) { + resolve(events); + } + } catch (error) { + console.error(`[PublicationFeed] Error parsing message from ${relay}:`, error); + } + }; + + const errorHandler = (ev: Event) => { + reject(new Error(`WebSocket error for ${relay}: ${ev}`)); + }; + + ws.addEventListener("message", messageHandler); + ws.addEventListener("error", errorHandler); + + // Send the subscription request + ws.send(JSON.stringify([ + "REQ", + subId, + { kinds: [indexKind], limit: 1000 } + ])); + + // Set up cleanup + setTimeout(() => { + ws.removeEventListener("message", messageHandler); + ws.removeEventListener("error", errorHandler); + WebSocketPool.instance.release(ws); + resolve(events); + }, 5000); + }); + + let eventSet = await eventPromise; console.debug(`[PublicationFeed] Raw events from ${relay}:`, eventSet.size); eventSet = filterValidIndexEvents(eventSet); From b119c0010afa54cfc88d930bddfd76c58a2a0fc1 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 3 Aug 2025 09:37:03 +0200 Subject: [PATCH 04/17] improved error handling for missing publications --- src/lib/utils/websocket_utils.ts | 16 ++- src/routes/publication/+error.svelte | 123 ++++++++++++++++-- .../publication/[type]/[identifier]/+page.ts | 23 +++- 3 files changed, 142 insertions(+), 20 deletions(-) diff --git a/src/lib/utils/websocket_utils.ts b/src/lib/utils/websocket_utils.ts index c95001e..f835408 100644 --- a/src/lib/utils/websocket_utils.ts +++ b/src/lib/utils/websocket_utils.ts @@ -69,7 +69,7 @@ function handleError( reject(ev); } -export async function fetchNostrEvent(filter: NostrFilter): Promise { +export async function fetchNostrEvent(filter: NostrFilter): Promise { // AI-NOTE: Updated to use active relay stores instead of hardcoded relay URL // This ensures the function uses the user's configured relays and can find events // across multiple relays rather than being limited to a single hardcoded relay. @@ -82,7 +82,11 @@ export async function fetchNostrEvent(filter: NostrFilter): Promise const availableRelays = [...inboxRelays, ...outboxRelays]; if (availableRelays.length === 0) { - throw new Error("[WebSocket Utils]: No relays available for fetching events"); + // AI-NOTE: Return null instead of throwing error when no relays are available + // This allows the publication routes to handle the case gracefully during preloading + // when relay stores haven't been populated yet + console.warn("[WebSocket Utils]: No relays available for fetching events, returning null"); + return null; } // Select a relay - prefer inbox relays if available, otherwise use any available relay @@ -135,7 +139,7 @@ export async function fetchEventById(id: string): Promise { try { const event = await fetchNostrEvent({ ids: [id], limit: 1 }); if (!event) { - error(404, `Event not found for ID: ${id}`); + error(404, `Event not found for ID: ${id}. href="/events?id=${id}"`); } return event; } catch (err) { @@ -153,7 +157,7 @@ export async function fetchEventByDTag(dTag: string): Promise { try { const event = await fetchNostrEvent({ "#d": [dTag], limit: 1 }); if (!event) { - error(404, `Event not found for d-tag: ${dTag}`); + error(404, `Event not found for d-tag: ${dTag}. href="/events?d=${dTag}"`); } return event; } catch (err) { @@ -177,7 +181,7 @@ export async function fetchEventByNaddr(naddr: string): Promise { }; const event = await fetchNostrEvent(filter); if (!event) { - error(404, `Event not found for naddr: ${naddr}`); + error(404, `Event not found for naddr: ${naddr}. href="/events?id=${naddr}"`); } return event; } catch (err) { @@ -196,7 +200,7 @@ export async function fetchEventByNevent(nevent: string): Promise { const decoded = neventDecode(nevent); const event = await fetchNostrEvent({ ids: [decoded.id], limit: 1 }); if (!event) { - error(404, `Event not found for nevent: ${nevent}`); + error(404, `Event not found for nevent: ${nevent}. href="/events?id=${nevent}"`); } return event; } catch (err) { diff --git a/src/routes/publication/+error.svelte b/src/routes/publication/+error.svelte index 9d0d347..c9d1ce2 100644 --- a/src/routes/publication/+error.svelte +++ b/src/routes/publication/+error.svelte @@ -3,28 +3,125 @@ import { Alert, P, Button } from "flowbite-svelte"; import { ExclamationCircleOutline } from "flowbite-svelte-icons"; import { page } from "$app/state"; + + // Parse error message to extract search parameters and format it nicely + function parseErrorMessage(message: string): { + errorType: string; + identifier: string; + searchUrl?: string; + shortIdentifier?: string; + } { + const searchLinkMatch = message.match(/href="([^"]+)"/); + let searchUrl: string | undefined; + let baseMessage = message; + + if (searchLinkMatch) { + searchUrl = searchLinkMatch[1]; + baseMessage = message.replace(/href="[^"]+"/, '').trim(); + } + + // Extract error type and identifier from the message + const match = baseMessage.match(/Event not found for (\w+): (.+)/); + if (match) { + const errorType = match[1]; + const fullIdentifier = match[2]; + const shortIdentifier = fullIdentifier.length > 50 + ? fullIdentifier.substring(0, 47) + '...' + : fullIdentifier; + + return { + errorType, + identifier: fullIdentifier, + searchUrl, + shortIdentifier + }; + } + + return { + errorType: 'unknown', + identifier: baseMessage, + searchUrl, + shortIdentifier: baseMessage.length > 50 + ? baseMessage.substring(0, 47) + '...' + : baseMessage + }; + } + + $: errorInfo = page.error?.message ? parseErrorMessage(page.error.message) : { + errorType: 'unknown', + identifier: '', + shortIdentifier: '' + }; -
- -
- - Failed to load publication. +
+ +
+ + + Failed to load publication +
-

- Alexandria failed to find one or more of the events comprising this - publication. -

-

- {page.error?.message} + +

+ Alexandria failed to find one or more of the events comprising this publication.

+ +
+
+ + Error Type: + + + {errorInfo.errorType} + +
+ +
+ + Identifier: + +
+
+ {errorInfo.shortIdentifier} +
+ {#if errorInfo.identifier.length > 50} +
+ + Show full identifier + +
+ {errorInfo.identifier} +
+
+ {/if} +
+
+
+ + {#if errorInfo.searchUrl} +
+ +
+ {/if} +
diff --git a/src/routes/publication/[type]/[identifier]/+page.ts b/src/routes/publication/[type]/[identifier]/+page.ts index 1c00099..51c7d55 100644 --- a/src/routes/publication/[type]/[identifier]/+page.ts +++ b/src/routes/publication/[type]/[identifier]/+page.ts @@ -27,7 +27,28 @@ export const load: PageLoad = async ({ params }: { params: { type: string; ident } if (!indexEvent) { - error(404, `Event not found for ${type}: ${identifier}`); + // AI-NOTE: Handle case where no relays are available during preloading + // This prevents 404 errors when relay stores haven't been populated yet + console.warn(`[Publication Load] Event not found for ${type}: ${identifier} - may be due to no relays available`); + + // 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] ?? ""; From 0e153b1161bd28a46952646678c57f89156cde0d Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 3 Aug 2025 09:54:32 +0200 Subject: [PATCH 05/17] sped up relays and made connections more robust for fetchNostrEvent function --- src/lib/utils/websocket_utils.ts | 137 +++++++++++++++++++++---------- 1 file changed, 93 insertions(+), 44 deletions(-) diff --git a/src/lib/utils/websocket_utils.ts b/src/lib/utils/websocket_utils.ts index f835408..5c12c98 100644 --- a/src/lib/utils/websocket_utils.ts +++ b/src/lib/utils/websocket_utils.ts @@ -79,57 +79,106 @@ export async function fetchNostrEvent(filter: NostrFilter): Promise 0 ? inboxRelays[0] : availableRelays[0]; - - const ws = await WebSocketPool.instance.acquire(selectedRelay); - const subId = crypto.randomUUID(); - - // AI-NOTE: Currying is used here to abstract the internal handler logic away from the WebSocket - // handling logic. The message and error handlers themselves can be refactored without affecting - // the WebSocket handling logic. - const curriedMessageHandler: (subId: string) => (resolve: ResolveCallback) => (reject: RejectCallback) => MessageEventHandler = - (subId) => - (resolve) => + // Try all available relays in parallel and return the first result + const relayPromises = availableRelays.map(async (relay) => { + try { + console.debug(`[WebSocket Utils]: Trying relay: ${relay}`); + + const ws = await WebSocketPool.instance.acquire(relay); + const subId = crypto.randomUUID(); + + // AI-NOTE: Currying is used here to abstract the internal handler logic away from the WebSocket + // handling logic. The message and error handlers themselves can be refactored without affecting + // the WebSocket handling logic. + const curriedMessageHandler: (subId: string) => (resolve: ResolveCallback) => (reject: RejectCallback) => MessageEventHandler = + (subId) => + (resolve) => + (reject) => + (ev: MessageEvent) => + handleMessage(ev, subId, resolve, reject); + const curriedErrorHandler: EventHandlerReject = (reject) => - (ev: MessageEvent) => - handleMessage(ev, subId, resolve, reject); - const curriedErrorHandler: EventHandlerReject = - (reject) => - (ev: Event) => - handleError(ev, reject); - - // AI-NOTE: These variables store references to partially-applied handlers so that the `finally` - // block receives the correct references to clean up the listeners. - let messageHandler: MessageEventHandler; - let errorHandler: EventHandler; - - const res = new Promise((resolve, reject) => { - messageHandler = curriedMessageHandler(subId)(resolve)(reject); - errorHandler = curriedErrorHandler(reject); - - ws.addEventListener("message", messageHandler); - ws.addEventListener("error", errorHandler); - }) - .withTimeout(2000) - .finally(() => { - ws.removeEventListener("message", messageHandler); - ws.removeEventListener("error", errorHandler); - WebSocketPool.instance.release(ws); + (ev: Event) => + handleError(ev, reject); + + // AI-NOTE: These variables store references to partially-applied handlers so that the `finally` + // block receives the correct references to clean up the listeners. + let messageHandler: MessageEventHandler; + let errorHandler: EventHandler; + + const res = new Promise((resolve, reject) => { + messageHandler = curriedMessageHandler(subId)(resolve)(reject); + errorHandler = curriedErrorHandler(reject); + + ws.addEventListener("message", messageHandler); + ws.addEventListener("error", errorHandler); + }) + .withTimeout(2000) + .finally(() => { + ws.removeEventListener("message", messageHandler); + ws.removeEventListener("error", errorHandler); + WebSocketPool.instance.release(ws); + }); + + ws.send(JSON.stringify(["REQ", subId, filter])); + + const result = await res; + if (result) { + console.debug(`[WebSocket Utils]: Found event on relay: ${relay}`); + return result; + } + + console.debug(`[WebSocket Utils]: No event found on relay: ${relay}`); + return null; + } catch (err) { + console.warn(`[WebSocket Utils]: Failed to fetch from relay ${relay}:`, err); + return null; + } }); - ws.send(JSON.stringify(["REQ", subId, filter])); - return res; + // Wait for the first successful result or all to fail with timeout + const timeoutPromise = new Promise((resolve) => { + setTimeout(() => { + console.warn("[WebSocket Utils]: Fetch timeout reached"); + resolve(null); + }, 5000); // 5 second timeout for the entire fetch operation + }); + + const fetchPromise = Promise.allSettled(relayPromises).then((results) => { + // Find the first successful result + for (const result of results) { + if (result.status === 'fulfilled' && result.value) { + return result.value; + } + } + return null; + }); + + // Race between the fetch and the timeout + const result = await Promise.race([fetchPromise, timeoutPromise]); + + if (result) { + return result; + } + + console.warn("[WebSocket Utils]: Failed to fetch event from all relays (timeout or no results)"); + return null; } /** From 56a0dbb4325656d4bdeec3e1829123a27412416a Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 3 Aug 2025 09:59:24 +0200 Subject: [PATCH 06/17] return first result --- src/lib/utils/websocket_utils.ts | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/lib/utils/websocket_utils.ts b/src/lib/utils/websocket_utils.ts index 5c12c98..659580e 100644 --- a/src/lib/utils/websocket_utils.ts +++ b/src/lib/utils/websocket_utils.ts @@ -152,7 +152,7 @@ export async function fetchNostrEvent(filter: NostrFilter): Promise((resolve) => { setTimeout(() => { console.warn("[WebSocket Utils]: Fetch timeout reached"); @@ -160,18 +160,12 @@ export async function fetchNostrEvent(filter: NostrFilter): Promise { - // Find the first successful result - for (const result of results) { - if (result.status === 'fulfilled' && result.value) { - return result.value; - } - } - return null; - }); - - // Race between the fetch and the timeout - const result = await Promise.race([fetchPromise, timeoutPromise]); + // Race between individual relay results and the timeout + const result = await Promise.race([ + // Wait for the first successful result from any relay + Promise.race(relayPromises.filter(p => p !== null)), + timeoutPromise + ]); if (result) { return result; From 12cf16b36d7afd6f0e6689d1fb8088836b119288 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 3 Aug 2025 22:04:01 +0200 Subject: [PATCH 07/17] fixed the publication loading --- src/lib/utils.ts | 21 +++++ src/lib/utils/websocket_utils.ts | 36 ++++---- .../[type]/[identifier]/+layout.server.ts | 39 ++++----- .../[type]/[identifier]/+layout.svelte | 2 + .../[type]/[identifier]/+page.svelte | 10 ++- .../publication/[type]/[identifier]/+page.ts | 82 ++++++++++++++----- 6 files changed, 127 insertions(+), 63 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 2171d53..ee44929 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -2,6 +2,7 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { nip19 } from "nostr-tools"; import { getMatchingTags } from "./utils/nostrUtils.ts"; import type { AddressPointer, EventPointer } from "nostr-tools/nip19"; +import type { NostrEvent } from "./utils/websocket_utils.ts"; export class DecodeError extends Error { constructor(message: string) { @@ -40,6 +41,26 @@ export function naddrEncode(event: NDKEvent, relays: string[]) { }); } +/** + * Creates a tag address from a raw Nostr event (for compatibility with NDK events) + * @param event The raw Nostr event + * @param relays Optional relay list for the address + * @returns A tag address string + */ +export function createTagAddress(event: NostrEvent, relays: string[] = []): string { + const dTag = event.tags.find((tag: string[]) => tag[0] === "d")?.[1]; + if (!dTag) { + throw new Error("Event does not have a d tag"); + } + + return nip19.naddrEncode({ + identifier: dTag, + pubkey: event.pubkey, + kind: event.kind, + relays, + }); +} + export function nprofileEncode(pubkey: string, relays: string[]) { return nip19.nprofileEncode({ pubkey, relays }); } diff --git a/src/lib/utils/websocket_utils.ts b/src/lib/utils/websocket_utils.ts index 659580e..3d6b608 100644 --- a/src/lib/utils/websocket_utils.ts +++ b/src/lib/utils/websocket_utils.ts @@ -152,26 +152,25 @@ export async function fetchNostrEvent(filter: NostrFilter): Promise((resolve) => { - setTimeout(() => { - console.warn("[WebSocket Utils]: Fetch timeout reached"); - resolve(null); - }, 5000); // 5 second timeout for the entire fetch operation - }); - - // Race between individual relay results and the timeout - const result = await Promise.race([ - // Wait for the first successful result from any relay - Promise.race(relayPromises.filter(p => p !== null)), - timeoutPromise - ]); + // Wait for all relay results and find the first successful one + const results = await Promise.allSettled(relayPromises); - if (result) { - return result; + // Find the first successful result + for (const result of results) { + if (result.status === 'fulfilled' && result.value) { + console.debug(`[WebSocket Utils]: Returning successful result from relay`); + return result.value; + } } - console.warn("[WebSocket Utils]: Failed to fetch event from all relays (timeout or no results)"); + // Debug: log all results to see what happened + console.debug(`[WebSocket Utils]: All relay results:`, results.map((r, i) => ({ + relay: availableRelays[i], + status: r.status, + value: r.status === 'fulfilled' ? r.value : r.reason + }))); + + console.warn("[WebSocket Utils]: Failed to fetch event from all relays (no successful results)"); return null; } @@ -222,12 +221,15 @@ export async function fetchEventByNaddr(naddr: string): Promise { authors: [decoded.pubkey], "#d": [decoded.identifier], }; + console.debug(`[fetchEventByNaddr] Calling fetchNostrEvent with filter:`, filter); const event = await fetchNostrEvent(filter); + console.debug(`[fetchEventByNaddr] fetchNostrEvent returned:`, event ? 'success' : 'null'); if (!event) { error(404, `Event not found for naddr: ${naddr}. href="/events?id=${naddr}"`); } return event; } catch (err) { + console.error(`[fetchEventByNaddr] Error:`, err); if (err && typeof err === "object" && "status" in err) { throw err; } diff --git a/src/routes/publication/[type]/[identifier]/+layout.server.ts b/src/routes/publication/[type]/[identifier]/+layout.server.ts index 26e28b4..2a90624 100644 --- a/src/routes/publication/[type]/[identifier]/+layout.server.ts +++ b/src/routes/publication/[type]/[identifier]/+layout.server.ts @@ -1,40 +1,29 @@ import { error } from "@sveltejs/kit"; import type { LayoutServerLoad } from "./$types"; -import { fetchEventByDTag, fetchEventById, fetchEventByNaddr, fetchEventByNevent } from "../../../../lib/utils/websocket_utils.ts"; import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; +// AI-NOTE: Server-side event fetching for SEO metadata +async function fetchEventServerSide(type: string, identifier: string): Promise { + // For now, return null to indicate server-side fetch not implemented + // This will fall back to client-side fetching + return null; +} + export const load: LayoutServerLoad = async ({ params, url }) => { const { type, identifier } = params; - let indexEvent: NostrEvent; - - // 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}`); - } + // Try to fetch event server-side for metadata + const indexEvent = await fetchEventServerSide(type, identifier); - // Extract metadata for meta tags - const title = indexEvent.tags.find((tag) => tag[0] === "title")?.[1] || "Alexandria Publication"; - const summary = indexEvent.tags.find((tag) => tag[0] === "summary")?.[1] || + // Extract metadata for meta tags (use fallbacks if no event found) + const title = indexEvent?.tags.find((tag) => tag[0] === "title")?.[1] || "Alexandria Publication"; + const summary = indexEvent?.tags.find((tag) => tag[0] === "summary")?.[1] || "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages."; - const image = indexEvent.tags.find((tag) => tag[0] === "image")?.[1] || "/screenshots/old_books.jpg"; + const image = indexEvent?.tags.find((tag) => tag[0] === "image")?.[1] || "/screenshots/old_books.jpg"; const currentUrl = `${url.origin}${url.pathname}`; return { - indexEvent, + indexEvent, // Will be null, triggering client-side fetch metadata: { title, summary, diff --git a/src/routes/publication/[type]/[identifier]/+layout.svelte b/src/routes/publication/[type]/[identifier]/+layout.svelte index a3b7be6..c14d288 100644 --- a/src/routes/publication/[type]/[identifier]/+layout.svelte +++ b/src/routes/publication/[type]/[identifier]/+layout.svelte @@ -3,6 +3,8 @@ import type { LayoutProps } from "./$types"; let { data, children }: LayoutProps = $props(); + + // AI-NOTE: Use metadata from server-side load for SEO and social sharing const { metadata } = data; diff --git a/src/routes/publication/[type]/[identifier]/+page.svelte b/src/routes/publication/[type]/[identifier]/+page.svelte index 11fd1f8..4452a48 100644 --- a/src/routes/publication/[type]/[identifier]/+page.svelte +++ b/src/routes/publication/[type]/[identifier]/+page.svelte @@ -9,12 +9,20 @@ import { page } from "$app/state"; import { goto } from "$app/navigation"; import { createNDKEvent } from "$lib/utils/nostrUtils"; + import { createTagAddress } from "$lib/utils"; + import { get } from "svelte/store"; + import { activeInboxRelays } from "$lib/ndk"; let { data }: PageProps = $props(); // data.indexEvent can be null from server-side rendering // We need to handle this case properly - const indexEvent = data.indexEvent ? createNDKEvent(data.ndk, data.indexEvent) : null; + // AI-NOTE: Always create NDK event since we now ensure NDK is available + const indexEvent = data.indexEvent && data.ndk + ? createNDKEvent(data.ndk, data.indexEvent) + : null; // No event if no NDK or no event data + + // Only create publication tree if we have a valid index event const publicationTree = indexEvent ? new SveltePublicationTree(indexEvent, data.ndk) : null; diff --git a/src/routes/publication/[type]/[identifier]/+page.ts b/src/routes/publication/[type]/[identifier]/+page.ts index 51c7d55..b2aefd4 100644 --- a/src/routes/publication/[type]/[identifier]/+page.ts +++ b/src/routes/publication/[type]/[identifier]/+page.ts @@ -3,29 +3,46 @@ import type { PageLoad } from "./$types"; import { fetchEventByDTag, fetchEventById, fetchEventByNaddr, fetchEventByNevent } from "../../../../lib/utils/websocket_utils.ts"; import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; -export const load: PageLoad = async ({ params }: { params: { type: string; identifier: string } }) => { +export const load: PageLoad = async ({ params, parent }: { params: { type: string; identifier: string }; parent: any }) => { + console.debug(`[Publication Load] Page load function called with params:`, params); const { type, identifier } = params; + console.debug(`[Publication Load] About to call parent()...`); + + // Get layout data (no server-side data since SSR is disabled) + const layoutData = await parent(); + console.debug(`[Publication Load] Layout data received:`, layoutData ? 'success' : 'null'); - let indexEvent: NostrEvent | null; + // AI-NOTE: Always fetch client-side since server-side fetch returns null for now + let indexEvent: NostrEvent | null = null; + console.debug(`[Publication Load] Fetching client-side for: ${identifier}`); + + try { + // Handle different identifier types + switch (type) { + case 'id': + indexEvent = await fetchEventById(identifier); + break; + case 'd': + indexEvent = await fetchEventByDTag(identifier); + break; + case 'naddr': + console.debug(`[Publication Load] Calling fetchEventByNaddr for: ${identifier}`); + indexEvent = await fetchEventByNaddr(identifier); + console.debug(`[Publication Load] fetchEventByNaddr returned:`, indexEvent ? 'success' : 'null'); + break; + case 'nevent': + indexEvent = await fetchEventByNevent(identifier); + break; + default: + error(400, `Unsupported identifier type: ${type}`); + } - // 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}`); + console.debug(`[Publication Load] Client-side indexEvent after fetch:`, indexEvent ? 'success' : 'null'); + } catch (err) { + console.error(`[Publication Load] Error fetching event client-side:`, 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 @@ -51,10 +68,35 @@ export const load: PageLoad = async ({ params }: { params: { type: string; ident error(404, `Event not found for ${type}: ${identifier}. href="/events?${searchParam}"`); } + console.debug(`[Publication Load] indexEvent details:`, { + id: indexEvent.id, + kind: indexEvent.kind, + pubkey: indexEvent.pubkey, + tags: indexEvent.tags.length, + contentLength: indexEvent.content.length + }); + const publicationType = indexEvent.tags.find((tag) => tag[0] === "type")?.[1] ?? ""; + + console.debug(`[Publication Load] publicationType:`, publicationType); + + // AI-NOTE: Use proper NDK instance from layout or create one with relays + let ndk = layoutData?.ndk; + if (!ndk) { + console.debug(`[Publication Load] Layout NDK not available, creating NDK instance with relays`); + // Import NDK dynamically to avoid SSR issues + const NDK = (await import("@nostr-dev-kit/ndk")).default; + // Import initNdk to get properly configured NDK with relays + const { initNdk } = await import("$lib/ndk"); + ndk = initNdk(); + } - return { + const result = { publicationType, indexEvent, + ndk, // Use minimal NDK instance }; + + console.debug(`[Publication Load] Returning result:`, result); + return result; }; From 8ce21e250a176ecc01d69ea6822debe2227da9c0 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 3 Aug 2025 22:09:56 +0200 Subject: [PATCH 08/17] removed border around relays --- src/lib/components/RelayStatus.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/RelayStatus.svelte b/src/lib/components/RelayStatus.svelte index 949c000..fba24c3 100644 --- a/src/lib/components/RelayStatus.svelte +++ b/src/lib/components/RelayStatus.svelte @@ -136,7 +136,7 @@ import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
{#each relayStatuses as status} -
+
{status.url}
From b01592693e41cb458c3e9d649528a6bf713d8397 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 3 Aug 2025 22:18:35 +0200 Subject: [PATCH 09/17] fixed landing page grid --- src/app.css | 22 +++++++++++++++++++ .../publications/PublicationFeed.svelte | 2 +- .../publications/PublicationHeader.svelte | 18 +++++++-------- src/routes/+layout.svelte | 2 +- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/app.css b/src/app.css index 4e2c9b2..b5169ae 100644 --- a/src/app.css +++ b/src/app.css @@ -247,6 +247,28 @@ @apply text-base font-semibold; } + /* Line clamp utilities for text truncation */ + .line-clamp-1 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + } + + .line-clamp-2 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + } + + .line-clamp-3 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + } + /* Lists */ .ol-leather li a, .ul-leather li a { diff --git a/src/lib/components/publications/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte index 701551b..48e4eba 100644 --- a/src/lib/components/publications/PublicationFeed.svelte +++ b/src/lib/components/publications/PublicationFeed.svelte @@ -396,7 +396,7 @@
{#if loading && eventsInView.length === 0} {#each getSkeletonIds() as id} diff --git a/src/lib/components/publications/PublicationHeader.svelte b/src/lib/components/publications/PublicationHeader.svelte index c293fc9..c1c6222 100644 --- a/src/lib/components/publications/PublicationHeader.svelte +++ b/src/lib/components/publications/PublicationHeader.svelte @@ -47,9 +47,9 @@ {#if title != null && href != null} - +
{#if image} -
-
- -
-

{title}

-

+
+ diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 90335f6..2fff8a9 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -45,7 +45,7 @@ -
+
From 51d1377968863b35f8f0bf8950abc0e2a152e32a Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 3 Aug 2025 22:23:49 +0200 Subject: [PATCH 10/17] suppress ToC, except for 30040s --- src/lib/components/util/ArticleNav.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/components/util/ArticleNav.svelte b/src/lib/components/util/ArticleNav.svelte index f2c986c..7928b66 100644 --- a/src/lib/components/util/ArticleNav.svelte +++ b/src/lib/components/util/ArticleNav.svelte @@ -27,6 +27,7 @@ indexEvent.getMatchingTags("p")[0]?.[1] ?? null, ); let isLeaf: boolean = $derived(indexEvent.kind === 30041); + let isIndexEvent: boolean = $derived(indexEvent.kind === 30040); let lastScrollY = $state(0); let isVisible = $state(true); @@ -140,7 +141,7 @@ {/if} - {#if !isLeaf} + {#if isIndexEvent} {#if publicationType === "blog"}

{:else} + {@const debugInfo = `indexEvent: ${!!indexEvent}, data.indexEvent: ${!!data.indexEvent}`} + {@const debugElement = console.debug('[Publication] NOT rendering publication with:', debugInfo)}

Loading publication...

From 88cbd7c4d97ae7c395a7d96334fd620d31fe133f Mon Sep 17 00:00:00 2001 From: silberengel Date: Mon, 4 Aug 2025 01:13:48 +0200 Subject: [PATCH 17/17] fixed publication link on redirect --- .../util/ViewPublicationLink.svelte | 69 ++++++++++++------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/src/lib/components/util/ViewPublicationLink.svelte b/src/lib/components/util/ViewPublicationLink.svelte index d29cb7b..6217bce 100644 --- a/src/lib/components/util/ViewPublicationLink.svelte +++ b/src/lib/components/util/ViewPublicationLink.svelte @@ -21,52 +21,69 @@ return getEventType(event.kind || 0) === "addressable"; } - function getNaddrAddress(event: NDKEvent): string | null { - if (!isAddressableEvent(event)) { - return null; - } - try { - return naddrEncode(event, $activeInboxRelays); - } catch { - return null; - } - } - + // AI-NOTE: Always ensure the returned address is a valid naddr1... string. + // If the tag value is a raw coordinate (kind:pubkey:d-tag), encode it. + // If it's already naddr1..., use as-is. Otherwise, fallback to event's own naddr. function getViewPublicationNaddr(event: NDKEvent): string | null { // First, check for a-tags with 'defer' - these indicate the event is deferring to someone else's version const aTags = getMatchingTags(event, "a"); for (const tag of aTags) { if (tag.length >= 2 && tag.includes("defer")) { - // This is a deferral to someone else's addressable event - return tag[1]; // Return the addressable event address + const value = tag[1]; + if (value.startsWith("naddr1")) { + return value; + } + // Check for coordinate format: kind:pubkey:d-tag + const coordMatch = value.match(/^(\d+):([0-9a-fA-F]{64}):(.+)$/); + if (coordMatch) { + const [_, kind, pubkey, dTag] = coordMatch; + try { + return naddrEncode({ kind: Number(kind), pubkey, tags: [["d", dTag]] } as NDKEvent, $activeInboxRelays); + } catch { + return null; + } + } + // Fallback: if not naddr1 or coordinate, ignore } } // For deferred events with deferral tag, use the deferral naddr instead of the event's own naddr const deferralNaddr = getDeferralNaddr(event); if (deferralNaddr) { - return deferralNaddr; + if (deferralNaddr.startsWith("naddr1")) { + return deferralNaddr; + } + const coordMatch = deferralNaddr.match(/^(\d+):([0-9a-fA-F]{64}):(.+)$/); + if (coordMatch) { + const [_, kind, pubkey, dTag] = coordMatch; + try { + return naddrEncode({ kind: Number(kind), pubkey, tags: [["d", dTag]] } as NDKEvent, $activeInboxRelays); + } catch { + return null; + } + } } // Otherwise, use the event's own naddr if it's addressable return getNaddrAddress(event); } + function getNaddrAddress(event: NDKEvent): string | null { + if (!isAddressableEvent(event)) { + return null; + } + try { + return naddrEncode(event, $activeInboxRelays); + } catch { + return null; + } + } + function navigateToPublication() { const naddrAddress = getViewPublicationNaddr(event); - console.log("ViewPublicationLink: navigateToPublication called", { - eventKind: event.kind, - naddrAddress, - isAddressable: isAddressableEvent(event), - }); if (naddrAddress) { - console.log( - "ViewPublicationLink: Navigating to publication:", - naddrAddress, - ); - goto(`/publication/naddr/${naddrAddress}`); - } else { - console.log("ViewPublicationLink: No naddr address found for event"); + const url = `/publication/naddr/${naddrAddress}`; + goto(url); } }