From 830108ac9086e9b20240b2b50a028bf4bcb3f0c2 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 14 May 2025 23:06:20 -0500 Subject: [PATCH 01/98] Opens #118#137 --- src/lib/components/Publication.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/components/Publication.svelte b/src/lib/components/Publication.svelte index 3ec008d..b80415b 100644 --- a/src/lib/components/Publication.svelte +++ b/src/lib/components/Publication.svelte @@ -167,7 +167,7 @@ {#if showTocButton && !showToc} - + Show Table of Contents {/if} {#if showTocButton && !showToc} From 5be8bedd4238aca916ea86712aac5d0d624e9916 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 14 May 2025 23:27:30 -0500 Subject: [PATCH 03/98] Remove unused Publication props --- src/lib/components/Publication.svelte | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/components/Publication.svelte b/src/lib/components/Publication.svelte index f7f4df5..7c7f150 100644 --- a/src/lib/components/Publication.svelte +++ b/src/lib/components/Publication.svelte @@ -17,10 +17,8 @@ import PublicationSection from "./PublicationSection.svelte"; import type { PublicationTree } from "$lib/data_structures/publication_tree"; - let { rootAddress, publicationType, indexEvent } = $props<{ + let { rootAddress } = $props<{ rootAddress: string, - publicationType: string, - indexEvent: NDKEvent }>(); const publicationTree = getContext('publicationTree') as PublicationTree; From 2c045dd2c006382134f3db61176da6736b6ee5dc Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 14 May 2025 23:29:51 -0500 Subject: [PATCH 04/98] Init ToC interface --- src/lib/data_structures/table_of_contents.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/lib/data_structures/table_of_contents.ts diff --git a/src/lib/data_structures/table_of_contents.ts b/src/lib/data_structures/table_of_contents.ts new file mode 100644 index 0000000..8bf447f --- /dev/null +++ b/src/lib/data_structures/table_of_contents.ts @@ -0,0 +1,6 @@ +export interface TocEntry { + title: string; + href: string; + expanded: boolean; + children: Array | null; +} From 899ee4661575dabca397a381ce449259f57752ed Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 14 May 2025 23:30:05 -0500 Subject: [PATCH 05/98] Init TableOfContents Svelte component --- src/lib/components/TableOfContents.svelte | 16 +++++++++++++++ src/lib/components/Toc.svelte | 24 ----------------------- 2 files changed, 16 insertions(+), 24 deletions(-) create mode 100644 src/lib/components/TableOfContents.svelte delete mode 100644 src/lib/components/Toc.svelte diff --git a/src/lib/components/TableOfContents.svelte b/src/lib/components/TableOfContents.svelte new file mode 100644 index 0000000..9e606fb --- /dev/null +++ b/src/lib/components/TableOfContents.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/Toc.svelte b/src/lib/components/Toc.svelte deleted file mode 100644 index 9d433b5..0000000 --- a/src/lib/components/Toc.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -
-

Table of contents

- -
- - From d9c9c5b1ba5e20905041e9a2362374ee2bb3a7ad Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Thu, 15 May 2025 09:13:36 -0500 Subject: [PATCH 06/98] Update Deno lockfile --- deno.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deno.lock b/deno.lock index c97022c..96b2728 100644 --- a/deno.lock +++ b/deno.lock @@ -2902,6 +2902,8 @@ "npm:flowbite-svelte@0", "npm:flowbite@2", "npm:he@1.2", + "npm:highlight.js@^11.11.1", + "npm:node-emoji@^2.2.0", "npm:nostr-tools@2.10", "npm:playwright@^1.50.1", "npm:postcss-load-config@6", From 650aa280d45997ca082f3c5234935ccb24ee50f8 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Thu, 15 May 2025 09:14:00 -0500 Subject: [PATCH 07/98] Add comments with implementation plan --- src/lib/components/TableOfContents.svelte | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib/components/TableOfContents.svelte b/src/lib/components/TableOfContents.svelte index 9e606fb..e26cdb9 100644 --- a/src/lib/components/TableOfContents.svelte +++ b/src/lib/components/TableOfContents.svelte @@ -5,12 +5,14 @@ let { rootAddress } = $props<{ rootAddress: string }>(); + // Determine the event kind. + // If index, use the publication tree to build the table of contents. + // If single event, build the table of contents from the rendered HTML. + // Each rendered `` should receive an entry in the ToC. + let toc = $state([]); const publicationTree = getContext('publicationTree') as PublicationTree; - - // TODO: Build the table of contents. - // Base hrefs on d-tags for events within the publication. From 78e4d23f718fe53abb601f555c82cc8514bbcf10 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Fri, 16 May 2025 09:07:24 -0500 Subject: [PATCH 08/98] Add a function to extract ToC entries from HTML --- src/lib/components/TableOfContents.svelte | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/lib/components/TableOfContents.svelte b/src/lib/components/TableOfContents.svelte index e26cdb9..71bcdd0 100644 --- a/src/lib/components/TableOfContents.svelte +++ b/src/lib/components/TableOfContents.svelte @@ -1,4 +1,5 @@ From 42cb538be5b7e906e9df7481532c769b6af8cd6e Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 26 May 2025 08:27:41 -0500 Subject: [PATCH 10/98] Move publication components into their own sub-directory --- deno.lock | 10 ++++++++++ .../components/{ => publications}/Publication.svelte | 0 .../{ => publications}/PublicationFeed.svelte | 0 .../{ => publications}/PublicationHeader.svelte | 2 +- .../{ => publications}/PublicationSection.svelte | 0 src/routes/+page.svelte | 2 +- src/routes/publication/+page.svelte | 2 +- 7 files changed, 13 insertions(+), 3 deletions(-) rename src/lib/components/{ => publications}/Publication.svelte (100%) rename src/lib/components/{ => publications}/PublicationFeed.svelte (100%) rename src/lib/components/{ => publications}/PublicationHeader.svelte (97%) rename src/lib/components/{ => publications}/PublicationSection.svelte (100%) diff --git a/deno.lock b/deno.lock index 9206145..6604928 100644 --- a/deno.lock +++ b/deno.lock @@ -3168,6 +3168,13 @@ "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==" } }, + "redirects": { + "https://esm.sh/bech32": "https://esm.sh/bech32@2.0.0" + }, + "remote": { + "https://esm.sh/bech32@2.0.0": "1b943d1583f3708812c3cfccc8236cf92d8f9b191881984e26e0b9decaed9a16", + "https://esm.sh/bech32@2.0.0/denonext/bech32.mjs": "80859514ec6ba04858364eba363d5ac73ac8d97086754e0baf14fa31d4f1b63a" + }, "workspace": { "dependencies": [ "npm:@nostr-dev-kit/ndk-cache-dexie@2.5", @@ -3201,8 +3208,10 @@ "npm:@types/d3@^7.4.3", "npm:@types/he@1.2", "npm:@types/node@22", + "npm:@types/qrcode@^1.5.5", "npm:asciidoctor@3.0", "npm:autoprefixer@10", + "npm:bech32@2", "npm:d3@^7.9.0", "npm:eslint-plugin-svelte@2", "npm:flowbite-svelte-icons@2.1", @@ -3217,6 +3226,7 @@ "npm:postcss@8", "npm:prettier-plugin-svelte@3", "npm:prettier@3", + "npm:qrcode@^1.5.4", "npm:svelte-check@4", "npm:svelte@5", "npm:tailwind-merge@^3.3.0", diff --git a/src/lib/components/Publication.svelte b/src/lib/components/publications/Publication.svelte similarity index 100% rename from src/lib/components/Publication.svelte rename to src/lib/components/publications/Publication.svelte diff --git a/src/lib/components/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte similarity index 100% rename from src/lib/components/PublicationFeed.svelte rename to src/lib/components/publications/PublicationFeed.svelte diff --git a/src/lib/components/PublicationHeader.svelte b/src/lib/components/publications/PublicationHeader.svelte similarity index 97% rename from src/lib/components/PublicationHeader.svelte rename to src/lib/components/publications/PublicationHeader.svelte index 32a674a..25dfdc4 100644 --- a/src/lib/components/PublicationHeader.svelte +++ b/src/lib/components/publications/PublicationHeader.svelte @@ -2,7 +2,7 @@ import { ndkInstance } from '$lib/ndk'; import { naddrEncode } from '$lib/utils'; import type { NDKEvent } from '@nostr-dev-kit/ndk'; - import { standardRelays } from '../consts'; + import { standardRelays } from '../../consts'; import { Card, Img } from "flowbite-svelte"; import CardActions from "$components/util/CardActions.svelte"; import { userBadge } from "$lib/snippets/UserSnippets.svelte"; diff --git a/src/lib/components/PublicationSection.svelte b/src/lib/components/publications/PublicationSection.svelte similarity index 100% rename from src/lib/components/PublicationSection.svelte rename to src/lib/components/publications/PublicationSection.svelte diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index d3cee5c..dd9ba68 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -3,7 +3,7 @@ import { Alert, Button, Dropdown, Radio, Input } from "flowbite-svelte"; import { ChevronDownOutline, HammerSolid } from "flowbite-svelte-icons"; import { inboxRelays, ndkSignedIn } from '$lib/ndk'; - import PublicationFeed from '$lib/components/PublicationFeed.svelte'; + import PublicationFeed from '$lib/components/publications/PublicationFeed.svelte'; import { feedType } from '$lib/stores'; $effect(() => { diff --git a/src/routes/publication/+page.svelte b/src/routes/publication/+page.svelte index 48b156c..9316069 100644 --- a/src/routes/publication/+page.svelte +++ b/src/routes/publication/+page.svelte @@ -1,5 +1,5 @@ + + diff --git a/src/lib/components/TableOfContents.svelte b/src/lib/components/publications/table_of_contents.svelte.ts similarity index 52% rename from src/lib/components/TableOfContents.svelte rename to src/lib/components/publications/table_of_contents.svelte.ts index 78aa83a..548d4f5 100644 --- a/src/lib/components/TableOfContents.svelte +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -1,35 +1,38 @@ - - - +} diff --git a/src/lib/data_structures/table_of_contents.ts b/src/lib/data_structures/table_of_contents.ts deleted file mode 100644 index 8bf447f..0000000 --- a/src/lib/data_structures/table_of_contents.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface TocEntry { - title: string; - href: string; - expanded: boolean; - children: Array | null; -} From feab392a4478910af71dd0c6ec689ef37cd161ab Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 26 May 2025 09:07:26 -0500 Subject: [PATCH 12/98] Refactor `TableOfContents` class for least privilege --- .../publications/TableOfContents.svelte | 9 +--- .../publications/table_of_contents.svelte.ts | 51 ++++++++++++------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/src/lib/components/publications/TableOfContents.svelte b/src/lib/components/publications/TableOfContents.svelte index 50b383a..15f441e 100644 --- a/src/lib/components/publications/TableOfContents.svelte +++ b/src/lib/components/publications/TableOfContents.svelte @@ -1,15 +1,10 @@ - + + {#each toc as entry} + {entry.title} + {/each} + diff --git a/src/lib/components/publications/table_of_contents.svelte.ts b/src/lib/components/publications/table_of_contents.svelte.ts index 3efddd1..4b6cc7a 100644 --- a/src/lib/components/publications/table_of_contents.svelte.ts +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -132,4 +132,25 @@ export class TableOfContents { } }); } + + /** + * Iterates over all ToC entries in depth-first order. + */ + *[Symbol.iterator](): IterableIterator { + function* traverse(entry: TocEntry | null): IterableIterator { + if (!entry) { + return; + } + + yield entry; + + if (entry.children) { + for (const child of entry.children) { + yield* traverse(child); + } + } + } + + yield* traverse(this.#tocRoot); + } } From f9048c468ca5d8a5d12b4775992d60e30293925e Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 28 May 2025 08:46:52 -0500 Subject: [PATCH 15/98] Add doc comment on ToC constructor --- .../publications/table_of_contents.svelte.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/lib/components/publications/table_of_contents.svelte.ts b/src/lib/components/publications/table_of_contents.svelte.ts index 4b6cc7a..a16f72b 100644 --- a/src/lib/components/publications/table_of_contents.svelte.ts +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -7,12 +7,22 @@ export interface TocEntry { children: Array | null; } +// TODO: Include depth in the `TocEntry` interface, and compute it when adding entries to the ToC. + export class TableOfContents { #tocRoot: TocEntry | null = null; #addresses = $state>(new Map()); #publicationTree: PublicationTree; #pagePathname: string; + /** + * Constructor for the `TableOfContents` class. The constructed ToC initially contains only the + * root entry. Additional entries must be inserted programmatically using class methods. + * + * The `TableOfContents` class should be instantiated as a page-scoped singleton so that + * `pagePathname` is correct wherever the instance is used. The singleton should be made + * made available to the entire component tree under that page. + */ constructor(rootAddress: string, publicationTree: PublicationTree, pagePathname: string) { // TODO: Build out the root entry correctly. this.#tocRoot = { From 4afbd04d5e57bb340a3a6151c4021504dd24338e Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 28 May 2025 08:47:28 -0500 Subject: [PATCH 16/98] Add a TODO --- src/lib/components/publications/TableOfContents.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/components/publications/TableOfContents.svelte b/src/lib/components/publications/TableOfContents.svelte index a49fd1c..891476a 100644 --- a/src/lib/components/publications/TableOfContents.svelte +++ b/src/lib/components/publications/TableOfContents.svelte @@ -7,6 +7,8 @@ let toc = getContext('toc') as TableOfContents; + // TODO: Check root address against ToC root address for correctness. + // Determine the event kind. // If index, use the publication tree to build the table of contents. // If single event, build the table of contents from the rendered HTML. From 640a1263302e483bf4aca46810b7dcad0aeba7ae Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 28 May 2025 08:55:48 -0500 Subject: [PATCH 17/98] Compute depth of ToC entries when adding them --- .../publications/table_of_contents.svelte.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/lib/components/publications/table_of_contents.svelte.ts b/src/lib/components/publications/table_of_contents.svelte.ts index a16f72b..77154aa 100644 --- a/src/lib/components/publications/table_of_contents.svelte.ts +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -1,14 +1,14 @@ import { PublicationTree } from "../../data_structures/publication_tree.ts"; export interface TocEntry { + address: string; title: string; href: string; + depth: number; expanded: boolean; children: Array | null; } -// TODO: Include depth in the `TocEntry` interface, and compute it when adding entries to the ToC. - export class TableOfContents { #tocRoot: TocEntry | null = null; #addresses = $state>(new Map()); @@ -24,14 +24,6 @@ export class TableOfContents { * made available to the entire component tree under that page. */ constructor(rootAddress: string, publicationTree: PublicationTree, pagePathname: string) { - // TODO: Build out the root entry correctly. - this.#tocRoot = { - title: '', - href: '', - expanded: false, - children: null, - }; - this.#publicationTree = publicationTree; this.#pagePathname = pagePathname; @@ -88,8 +80,10 @@ export class TableOfContents { currentParentTocNode!.children ??= []; const childTocEntry: TocEntry = { + address, title: childEvent.getMatchingTags('title')[0][1], href: `${this.#pagePathname}#${this.#normalizeHashPath(childEvent.getMatchingTags('title')[0][1])}`, + depth: i + 1, expanded: false, children: null, }; @@ -130,8 +124,10 @@ export class TableOfContents { const href = `${this.#pagePathname}#${id}`; const tocEntry: TocEntry = { + address: parentEntry.address, title, href, + depth, expanded: false, children: null, }; From 2c3b9adf58178248f7c19736a7a9028301d6f537 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 31 May 2025 20:34:45 -0500 Subject: [PATCH 18/98] Set workspace tab size to 2 spaces --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e06c2f4..6953a21 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,6 @@ }, "files.associations": { "*.svelte": "svelte" - } + }, + "editor.tabSize": 2 } \ No newline at end of file From d3ec3ad3e204768c96058be550b88339b013424d Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 31 May 2025 20:50:38 -0500 Subject: [PATCH 19/98] Add observability to node resolution --- src/lib/data_structures/publication_tree.ts | 32 ++++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index d616740..3184b29 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -1,7 +1,7 @@ -import type NDK from "@nostr-dev-kit/ndk"; -import type { NDKEvent } from "@nostr-dev-kit/ndk"; -import { Lazy } from "./lazy.ts"; -import { findIndexAsync as _findIndexAsync } from '../utils.ts'; +import type NDK from '@nostr-dev-kit/ndk'; +import type { NDKEvent } from '@nostr-dev-kit/ndk'; +import { Lazy } from './lazy.ts'; +import { SvelteSet } from "svelte/reactivity"; enum PublicationTreeNodeType { Branch, @@ -22,6 +22,14 @@ interface PublicationTreeNode { } export class PublicationTree implements AsyncIterable { + // TODO: Abstract this into a `SveltePublicationTree` wrapper class. + /** + * A reactive set of addresses of the events that have been resolved (loaded) into the tree. + * Svelte components can use this set in reactive code blocks to trigger updates when new nodes + * are added to the tree. + */ + resolvedAddresses: SvelteSet = new SvelteSet(); + /** * The root node of the tree. */ @@ -52,6 +60,8 @@ export class PublicationTree implements AsyncIterable { */ #ndk: NDK; + #onNodeResolvedCallbacks: Array<(address: string) => void> = []; + constructor(rootEvent: NDKEvent, ndk: NDK) { const rootAddress = rootEvent.tagAddress(); this.#root = { @@ -185,6 +195,16 @@ export class PublicationTree implements AsyncIterable { this.#cursor.tryMoveTo(address); } + /** + * Registers an observer function that is invoked whenever a new node is resolved. Nodes are + * added lazily. + * + * @param observer The observer function. + */ + onNodeResolved(observer: (address: string) => void) { + this.#onNodeResolvedCallbacks.push(observer); + } + // #region Iteration Cursor #cursor = new class { @@ -504,6 +524,7 @@ export class PublicationTree implements AsyncIterable { } this.#events.set(address, event); + this.resolvedAddresses.add(address); const childAddresses = event.tags.filter(tag => tag[0] === 'a').map(tag => tag[1]); @@ -519,6 +540,9 @@ export class PublicationTree implements AsyncIterable { this.addEventByAddress(address, event); } + // TODO: We may need to move this to `#addNode`, so the observer is notified more eagerly. + this.#onNodeResolvedCallbacks.forEach(observer => observer(address)); + return node; } From c6effb38397e188f9da37b03d0031a5b77fa8fbe Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 2 Jun 2025 08:50:15 -0500 Subject: [PATCH 20/98] Create Svelte-specific wrapper that proxies `PublicationTree` The wrapper keeps the core implementation framework-agnostic, but lets us build Svelte's reactivity into the wrapper. --- .../publications/Publication.svelte | 4 +- .../publications/PublicationSection.svelte | 5 +- .../svelte_publication_tree.svelte.ts | 56 +++++++++++++++++++ src/lib/data_structures/publication_tree.ts | 9 --- src/routes/publication/+page.svelte | 5 +- 5 files changed, 63 insertions(+), 16 deletions(-) create mode 100644 src/lib/components/publications/svelte_publication_tree.svelte.ts diff --git a/src/lib/components/publications/Publication.svelte b/src/lib/components/publications/Publication.svelte index f7e026f..7147de4 100644 --- a/src/lib/components/publications/Publication.svelte +++ b/src/lib/components/publications/Publication.svelte @@ -15,13 +15,13 @@ } from "flowbite-svelte-icons"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; import PublicationSection from "./PublicationSection.svelte"; - import type { PublicationTree } from "$lib/data_structures/publication_tree"; import Details from "$components/util/Details.svelte"; import { publicationColumnVisibility } from "$lib/stores"; import BlogHeader from "$components/cards/BlogHeader.svelte"; import Interactions from "$components/util/Interactions.svelte"; import TocToggle from "$components/util/TocToggle.svelte"; import { pharosInstance } from '$lib/parser'; + import type { SveltePublicationTree } from "./svelte_publication_tree.svelte"; let { rootAddress, publicationType, indexEvent } = $props<{ rootAddress: string; @@ -29,7 +29,7 @@ indexEvent: NDKEvent; }>(); - const publicationTree = getContext("publicationTree") as PublicationTree; + const publicationTree = getContext("publicationTree") as SveltePublicationTree; // #region Loading diff --git a/src/lib/components/publications/PublicationSection.svelte b/src/lib/components/publications/PublicationSection.svelte index 6c2586a..7b24fb3 100644 --- a/src/lib/components/publications/PublicationSection.svelte +++ b/src/lib/components/publications/PublicationSection.svelte @@ -6,7 +6,8 @@ import { getContext } from "svelte"; import type { Asciidoctor, Document } from "asciidoctor"; import { getMatchingTags } from '$lib/utils/nostrUtils'; - + import type { SveltePublicationTree } from "./svelte_publication_tree.svelte"; + let { address, rootAddress, @@ -19,7 +20,7 @@ ref: (ref: HTMLElement) => void, } = $props(); - const publicationTree: PublicationTree = getContext('publicationTree'); + const publicationTree: SveltePublicationTree = getContext('publicationTree'); const asciidoctor: Asciidoctor = getContext('asciidoctor'); let leafEvent: Promise = $derived.by(async () => diff --git a/src/lib/components/publications/svelte_publication_tree.svelte.ts b/src/lib/components/publications/svelte_publication_tree.svelte.ts new file mode 100644 index 0000000..cd96118 --- /dev/null +++ b/src/lib/components/publications/svelte_publication_tree.svelte.ts @@ -0,0 +1,56 @@ +import { SvelteSet } from "svelte/reactivity"; +import { PublicationTree } from "../../data_structures/publication_tree.ts"; +import { NDKEvent } from "../../utils/nostrUtils.ts"; +import NDK from "@nostr-dev-kit/ndk"; + +export class SveltePublicationTree { + resolvedAddresses: SvelteSet = new SvelteSet(); + + #publicationTree: PublicationTree; + + constructor(rootEvent: NDKEvent, ndk: NDK) { + this.#publicationTree = new PublicationTree(rootEvent, ndk); + + this.#publicationTree.onNodeResolved(this.#handleNodeResolved); + } + + // #region Proxied Public Methods + + getEvent(address: string): Promise { + return this.#publicationTree.getEvent(address); + } + + getHierarchy(address: string): Promise { + return this.#publicationTree.getHierarchy(address); + } + + setBookmark(address: string) { + this.#publicationTree.setBookmark(address); + } + + // #endregion + + // #region Proxied Async Iterator Methods + + [Symbol.asyncIterator](): AsyncIterator { + return this; + } + + next(): Promise> { + return this.#publicationTree.next(); + } + + previous(): Promise> { + return this.#publicationTree.previous(); + } + + // #endregion + + // #region Private Methods + + #handleNodeResolved(address: string) { + this.resolvedAddresses.add(address); + } + + // #endregion +} \ No newline at end of file diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 3184b29..7f4c677 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -22,14 +22,6 @@ interface PublicationTreeNode { } export class PublicationTree implements AsyncIterable { - // TODO: Abstract this into a `SveltePublicationTree` wrapper class. - /** - * A reactive set of addresses of the events that have been resolved (loaded) into the tree. - * Svelte components can use this set in reactive code blocks to trigger updates when new nodes - * are added to the tree. - */ - resolvedAddresses: SvelteSet = new SvelteSet(); - /** * The root node of the tree. */ @@ -524,7 +516,6 @@ export class PublicationTree implements AsyncIterable { } this.#events.set(address, event); - this.resolvedAddresses.add(address); const childAddresses = event.tags.filter(tag => tag[0] === 'a').map(tag => tag[1]); diff --git a/src/routes/publication/+page.svelte b/src/routes/publication/+page.svelte index 9316069..7defecf 100644 --- a/src/routes/publication/+page.svelte +++ b/src/routes/publication/+page.svelte @@ -3,13 +3,12 @@ import { TextPlaceholder } from "flowbite-svelte"; import type { PageProps } from "./$types"; import { onDestroy, setContext } from "svelte"; - import { PublicationTree } from "$lib/data_structures/publication_tree"; import Processor from "asciidoctor"; import ArticleNav from "$components/util/ArticleNav.svelte"; - + import { SveltePublicationTree } from "$lib/components/publications/svelte_publication_tree.svelte"; let { data }: PageProps = $props(); - const publicationTree = new PublicationTree(data.indexEvent, data.ndk); + const publicationTree = new SveltePublicationTree(data.indexEvent, data.ndk); setContext("publicationTree", publicationTree); setContext("asciidoctor", Processor()); From 34942c50466627811f60214090d5ff6cafafa878 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 2 Jun 2025 08:51:36 -0500 Subject: [PATCH 21/98] Make adding a node observable --- src/lib/data_structures/publication_tree.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 7f4c677..8c57729 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -52,6 +52,8 @@ export class PublicationTree implements AsyncIterable { */ #ndk: NDK; + #onNodeAddedCallbacks: Array<(address: string) => void> = []; + #onNodeResolvedCallbacks: Array<(address: string) => void> = []; constructor(rootEvent: NDKEvent, ndk: NDK) { @@ -187,6 +189,10 @@ export class PublicationTree implements AsyncIterable { this.#cursor.tryMoveTo(address); } + onNodeAdded(observer: (address: string) => void) { + this.#onNodeAddedCallbacks.push(observer); + } + /** * Registers an observer function that is invoked whenever a new node is resolved. Nodes are * added lazily. @@ -479,6 +485,8 @@ export class PublicationTree implements AsyncIterable { const lazyNode = new Lazy(() => this.#resolveNode(address, parentNode)); parentNode.children!.push(lazyNode); this.#nodes.set(address, lazyNode); + + this.#onNodeAddedCallbacks.forEach(observer => observer(address)); } /** From 1c47ea226ed55cfb07702b1fe8758d3f0fd1fbc0 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 2 Jun 2025 08:53:10 -0500 Subject: [PATCH 22/98] Proxy `getChildAddresses` --- .../components/publications/svelte_publication_tree.svelte.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/components/publications/svelte_publication_tree.svelte.ts b/src/lib/components/publications/svelte_publication_tree.svelte.ts index cd96118..b956fd7 100644 --- a/src/lib/components/publications/svelte_publication_tree.svelte.ts +++ b/src/lib/components/publications/svelte_publication_tree.svelte.ts @@ -16,6 +16,10 @@ export class SveltePublicationTree { // #region Proxied Public Methods + getChildAddresses(address: string): Promise> { + return this.#publicationTree.getChildAddresses(address); + } + getEvent(address: string): Promise { return this.#publicationTree.getEvent(address); } From c30484b9bc5bab6add64b921ea5fe7131803fe80 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 2 Jun 2025 08:53:28 -0500 Subject: [PATCH 23/98] Use `SveltePublicationTree` in ToC class --- src/lib/components/publications/table_of_contents.svelte.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/components/publications/table_of_contents.svelte.ts b/src/lib/components/publications/table_of_contents.svelte.ts index 77154aa..daf190a 100644 --- a/src/lib/components/publications/table_of_contents.svelte.ts +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -1,4 +1,4 @@ -import { PublicationTree } from "../../data_structures/publication_tree.ts"; +import { SveltePublicationTree } from "./svelte_publication_tree.svelte.ts"; export interface TocEntry { address: string; @@ -12,7 +12,7 @@ export interface TocEntry { export class TableOfContents { #tocRoot: TocEntry | null = null; #addresses = $state>(new Map()); - #publicationTree: PublicationTree; + #publicationTree: SveltePublicationTree; #pagePathname: string; /** @@ -23,7 +23,7 @@ export class TableOfContents { * `pagePathname` is correct wherever the instance is used. The singleton should be made * made available to the entire component tree under that page. */ - constructor(rootAddress: string, publicationTree: PublicationTree, pagePathname: string) { + constructor(rootAddress: string, publicationTree: SveltePublicationTree, pagePathname: string) { this.#publicationTree = publicationTree; this.#pagePathname = pagePathname; From f438e68726fcee8405f87335a4d7119e8ec6bf10 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 2 Jun 2025 08:54:19 -0500 Subject: [PATCH 24/98] Remove unused import --- src/lib/data_structures/publication_tree.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 8c57729..2682947 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -1,7 +1,6 @@ import type NDK from '@nostr-dev-kit/ndk'; import type { NDKEvent } from '@nostr-dev-kit/ndk'; import { Lazy } from './lazy.ts'; -import { SvelteSet } from "svelte/reactivity"; enum PublicationTreeNodeType { Branch, From 1e450dd4b752d3c4ec1e9782e022715bc60eb3d2 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 3 Jun 2025 08:53:30 -0500 Subject: [PATCH 25/98] Improve abstraction and controllability of tree-walking methods --- src/lib/data_structures/publication_tree.ts | 118 ++++++++++++++------ 1 file changed, 85 insertions(+), 33 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 2682947..ea57f05 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -12,6 +12,16 @@ enum PublicationTreeNodeStatus { Error, } +export enum TreeTraversalMode { + Leaves, + All, +} + +enum TreeTraversalDirection { + Forward, + Backward, +} + interface PublicationTreeNode { type: PublicationTreeNodeType; status: PublicationTreeNodeStatus; @@ -344,54 +354,80 @@ export class PublicationTree implements AsyncIterable { return this; } - // TODO: Add `previous()` method. - - async next(): Promise> { + /** + * Return the next event in the tree for the given traversal mode. + * + * @param mode The traversal mode. Can be {@link TreeTraversalMode.Leaves} or + * {@link TreeTraversalMode.All}. + * @returns The next event in the tree, or null if the tree is empty. + */ + async next( + mode: TreeTraversalMode = TreeTraversalMode.Leaves + ): Promise> { if (!this.#cursor.target) { if (await this.#cursor.tryMoveTo(this.#bookmark)) { - const event = await this.getEvent(this.#cursor.target!.address); - return { done: false, value: event }; + return this.#yieldEventAtCursor(false); } } - // Based on Raymond Chen's tree traversal algorithm example. - // https://devblogs.microsoft.com/oldnewthing/20200106-00/?p=103300 - do { - if (await this.#cursor.tryMoveToNextSibling()) { - while (await this.#cursor.tryMoveToFirstChild()) { - continue; - } - - if (this.#cursor.target!.status === PublicationTreeNodeStatus.Error) { - return { done: false, value: null }; - } - - const event = await this.getEvent(this.#cursor.target!.address); - return { done: false, value: event }; - } - } while (this.#cursor.tryMoveToParent()); - - if (this.#cursor.target!.status === PublicationTreeNodeStatus.Error) { - return { done: false, value: null }; + if (mode === TreeTraversalMode.Leaves) { + return this.#walkLeaves(TreeTraversalDirection.Forward); } - // If we get to this point, we're at the root node (can't move up any more). - return { done: true, value: null }; + return this.#preorderWalkAll(TreeTraversalDirection.Forward); } - async previous(): Promise> { + /** + * Return the previous event in the tree for the given traversal mode. + * + * @param mode The traversal mode. Can be {@link TreeTraversalMode.Leaves} or + * {@link TreeTraversalMode.All}. + * @returns The previous event in the tree, or null if the tree is empty. + */ + async previous( + mode: TreeTraversalMode = TreeTraversalMode.Leaves + ): Promise> { if (!this.#cursor.target) { if (await this.#cursor.tryMoveTo(this.#bookmark)) { const event = await this.getEvent(this.#cursor.target!.address); return { done: false, value: event }; } } + + if (mode === TreeTraversalMode.Leaves) { + return this.#walkLeaves(TreeTraversalDirection.Backward); + } + + return this.#preorderWalkAll(TreeTraversalDirection.Backward); + } + + async #yieldEventAtCursor(done: boolean): Promise> { + const value = (await this.getEvent(this.#cursor.target!.address)) ?? null; + return { done, value }; + } + + /** + * Walks the tree in the given direction, yielding the event at each leaf. + * + * @param direction The direction to walk the tree. + * @returns The event at the leaf, or null if the tree is empty. + * + * Based on Raymond Chen's tree traversal algorithm example. + * https://devblogs.microsoft.com/oldnewthing/20200106-00/?p=103300 + */ + async #walkLeaves( + direction: TreeTraversalDirection = TreeTraversalDirection.Forward + ): Promise> { + const tryMoveToSibling: () => Promise = direction === TreeTraversalDirection.Forward + ? this.#cursor.tryMoveToNextSibling.bind(this.#cursor) + : this.#cursor.tryMoveToPreviousSibling.bind(this.#cursor); + const tryMoveToChild: () => Promise = direction === TreeTraversalDirection.Forward + ? this.#cursor.tryMoveToFirstChild.bind(this.#cursor) + : this.#cursor.tryMoveToLastChild.bind(this.#cursor); - // Based on Raymond Chen's tree traversal algorithm example. - // https://devblogs.microsoft.com/oldnewthing/20200106-00/?p=103300 do { - if (await this.#cursor.tryMoveToPreviousSibling()) { - while (await this.#cursor.tryMoveToLastChild()) { + if (await tryMoveToSibling()) { + while (await tryMoveToChild()) { continue; } @@ -399,8 +435,7 @@ export class PublicationTree implements AsyncIterable { return { done: false, value: null }; } - const event = await this.getEvent(this.#cursor.target!.address); - return { done: false, value: event }; + return this.#yieldEventAtCursor(false); } } while (this.#cursor.tryMoveToParent()); @@ -408,9 +443,26 @@ export class PublicationTree implements AsyncIterable { return { done: false, value: null }; } + // If we get to this point, we're at the root node (can't move up any more). return { done: true, value: null }; } + /** + * Walks the tree in the given direction, yielding the event at each node. + * + * @param direction The direction to walk the tree. + * @returns The event at the node, or null if the tree is empty. + * + * Based on Raymond Chen's preorder walk algorithm example. + * https://devblogs.microsoft.com/oldnewthing/20200107-00/?p=103304 + */ + async #preorderWalkAll( + direction: TreeTraversalDirection = TreeTraversalDirection.Forward + ): Promise> { + // TODO: Implement this. + return { done: false, value: null }; + } + // #endregion // #region Private Methods From e32354caba3181f875079f44668a3a9df11ee8e3 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 4 Jun 2025 23:35:36 -0500 Subject: [PATCH 26/98] Add option to walk entire publication tree, not just leaves --- src/lib/data_structures/publication_tree.ts | 39 ++++++++++++++++----- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index ea57f05..6776626 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -370,11 +370,12 @@ export class PublicationTree implements AsyncIterable { } } - if (mode === TreeTraversalMode.Leaves) { + switch (mode) { + case TreeTraversalMode.Leaves: return this.#walkLeaves(TreeTraversalDirection.Forward); + case TreeTraversalMode.All: + return this.#preorderWalkAll(TreeTraversalDirection.Forward); } - - return this.#preorderWalkAll(TreeTraversalDirection.Forward); } /** @@ -394,11 +395,12 @@ export class PublicationTree implements AsyncIterable { } } - if (mode === TreeTraversalMode.Leaves) { + switch (mode) { + case TreeTraversalMode.Leaves: return this.#walkLeaves(TreeTraversalDirection.Backward); + case TreeTraversalMode.All: + return this.#preorderWalkAll(TreeTraversalDirection.Backward); } - - return this.#preorderWalkAll(TreeTraversalDirection.Backward); } async #yieldEventAtCursor(done: boolean): Promise> { @@ -459,8 +461,29 @@ export class PublicationTree implements AsyncIterable { async #preorderWalkAll( direction: TreeTraversalDirection = TreeTraversalDirection.Forward ): Promise> { - // TODO: Implement this. - return { done: false, value: null }; + const tryMoveToSibling: () => Promise = direction === TreeTraversalDirection.Forward + ? this.#cursor.tryMoveToNextSibling.bind(this.#cursor) + : this.#cursor.tryMoveToPreviousSibling.bind(this.#cursor); + const tryMoveToChild: () => Promise = direction === TreeTraversalDirection.Forward + ? this.#cursor.tryMoveToFirstChild.bind(this.#cursor) + : this.#cursor.tryMoveToLastChild.bind(this.#cursor); + + if (await tryMoveToChild()) { + return this.#yieldEventAtCursor(false); + } + + do { + if (await tryMoveToSibling()) { + return this.#yieldEventAtCursor(false); + } + } while (this.#cursor.tryMoveToParent()); + + if (this.#cursor.target!.status === PublicationTreeNodeStatus.Error) { + return { done: false, value: null }; + } + + // If we get to this point, we're at the root node (can't move up any more). + return this.#yieldEventAtCursor(true); } // #endregion From 80c75144898ab38c1a80c8a1306d434f6f71336b Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 4 Jun 2025 23:46:53 -0500 Subject: [PATCH 27/98] Allow bookmark observers --- src/lib/data_structures/publication_tree.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 6776626..1590581 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -61,9 +61,11 @@ export class PublicationTree implements AsyncIterable { */ #ndk: NDK; - #onNodeAddedCallbacks: Array<(address: string) => void> = []; + #nodeAddedObservers: Array<(address: string) => void> = []; - #onNodeResolvedCallbacks: Array<(address: string) => void> = []; + #nodeResolvedObservers: Array<(address: string) => void> = []; + + #bookmarkMovedObservers: Array<(address: string) => void> = []; constructor(rootEvent: NDKEvent, ndk: NDK) { const rootAddress = rootEvent.tagAddress(); @@ -195,11 +197,16 @@ export class PublicationTree implements AsyncIterable { */ setBookmark(address: string) { this.#bookmark = address; + this.#bookmarkMovedObservers.forEach(observer => observer(address)); this.#cursor.tryMoveTo(address); } + onBookmarkMoved(observer: (address: string) => void) { + this.#bookmarkMovedObservers.push(observer); + } + onNodeAdded(observer: (address: string) => void) { - this.#onNodeAddedCallbacks.push(observer); + this.#nodeAddedObservers.push(observer); } /** @@ -209,7 +216,7 @@ export class PublicationTree implements AsyncIterable { * @param observer The observer function. */ onNodeResolved(observer: (address: string) => void) { - this.#onNodeResolvedCallbacks.push(observer); + this.#nodeResolvedObservers.push(observer); } // #region Iteration Cursor @@ -560,7 +567,7 @@ export class PublicationTree implements AsyncIterable { parentNode.children!.push(lazyNode); this.#nodes.set(address, lazyNode); - this.#onNodeAddedCallbacks.forEach(observer => observer(address)); + this.#nodeAddedObservers.forEach(observer => observer(address)); } /** @@ -614,7 +621,7 @@ export class PublicationTree implements AsyncIterable { } // TODO: We may need to move this to `#addNode`, so the observer is notified more eagerly. - this.#onNodeResolvedCallbacks.forEach(observer => observer(address)); + this.#nodeResolvedObservers.forEach(observer => observer(address)); return node; } From d3a62c13f6b4716a3a3c971175b5553e2f095b50 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Thu, 5 Jun 2025 08:45:13 -0500 Subject: [PATCH 28/98] Add a `getParent` abstraction to `SveltePublicationTree` --- .../publications/svelte_publication_tree.svelte.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib/components/publications/svelte_publication_tree.svelte.ts b/src/lib/components/publications/svelte_publication_tree.svelte.ts index b956fd7..eb8e65f 100644 --- a/src/lib/components/publications/svelte_publication_tree.svelte.ts +++ b/src/lib/components/publications/svelte_publication_tree.svelte.ts @@ -28,6 +28,14 @@ export class SveltePublicationTree { return this.#publicationTree.getHierarchy(address); } + async getParent(address: string): Promise { + const hierarchy = await this.getHierarchy(address); + + // The last element in the hierarchy is the event with the given address, so the parent is the + // second to last element. + return hierarchy.at(-2) ?? null; + } + setBookmark(address: string) { this.#publicationTree.setBookmark(address); } From 7672a103378059ef55ecc3c2d0f5805073cd0802 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Thu, 5 Jun 2025 09:26:30 -0500 Subject: [PATCH 29/98] Allow node resolution subscription on `SveltePublicationTree` --- .../svelte_publication_tree.svelte.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/lib/components/publications/svelte_publication_tree.svelte.ts b/src/lib/components/publications/svelte_publication_tree.svelte.ts index eb8e65f..aac39f2 100644 --- a/src/lib/components/publications/svelte_publication_tree.svelte.ts +++ b/src/lib/components/publications/svelte_publication_tree.svelte.ts @@ -7,6 +7,7 @@ export class SveltePublicationTree { resolvedAddresses: SvelteSet = new SvelteSet(); #publicationTree: PublicationTree; + #nodeResolvedObservers: Array<(address: string) => void> = []; constructor(rootEvent: NDKEvent, ndk: NDK) { this.#publicationTree = new PublicationTree(rootEvent, ndk); @@ -40,6 +41,14 @@ export class SveltePublicationTree { this.#publicationTree.setBookmark(address); } + /** + * Registers an observer function that is invoked whenever a new node is resolved. + * @param observer The observer function. + */ + onNodeResolved(observer: (address: string) => void) { + this.#nodeResolvedObservers.push(observer); + } + // #endregion // #region Proxied Async Iterator Methods @@ -60,8 +69,19 @@ export class SveltePublicationTree { // #region Private Methods - #handleNodeResolved(address: string) { + /** + * Observer function that is invoked whenever a new node is resolved on the publication tree. + * + * @param address The address of the resolved node. + * + * This member is declared as an arrow function to ensure that the correct `this` context is + * used when the function is invoked in this class's constructor. + */ + #handleNodeResolved = (address: string) => { this.resolvedAddresses.add(address); + for (const observer of this.#nodeResolvedObservers) { + observer(address); + } } // #endregion From 534fc23c8cb99d61521261f86aa3454a6e27e702 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Thu, 5 Jun 2025 09:26:49 -0500 Subject: [PATCH 30/98] Update doc comment on `PublicationTree` --- src/lib/data_structures/publication_tree.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 1590581..515b866 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -154,8 +154,11 @@ export class PublicationTree implements AsyncIterable { /** * Retrieves the addresses of the loaded children, if any, of the node with the given address. + * * @param address The address of the parent node. * @returns An array of addresses of any loaded child nodes. + * + * Note that this method resolves all children of the node. */ async getChildAddresses(address: string): Promise> { const node = await this.#nodes.get(address)?.value(); From ddf8b9006b669a576214cecaf9444137769cd1a7 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Thu, 5 Jun 2025 09:27:41 -0500 Subject: [PATCH 31/98] Update ToC on publication tree node resolution --- .../publications/table_of_contents.svelte.ts | 198 +++++++++++------- 1 file changed, 120 insertions(+), 78 deletions(-) diff --git a/src/lib/components/publications/table_of_contents.svelte.ts b/src/lib/components/publications/table_of_contents.svelte.ts index daf190a..14066e9 100644 --- a/src/lib/components/publications/table_of_contents.svelte.ts +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -1,98 +1,52 @@ -import { SveltePublicationTree } from "./svelte_publication_tree.svelte.ts"; +import { SvelteMap } from 'svelte/reactivity'; +import type { SveltePublicationTree } from './svelte_publication_tree.svelte.ts'; +import type { NDKEvent } from '../../utils/nostrUtils.ts'; export interface TocEntry { address: string; title: string; - href: string; + href?: string; + children: TocEntry[]; + parent?: TocEntry; depth: number; - expanded: boolean; - children: Array | null; } export class TableOfContents { - #tocRoot: TocEntry | null = null; - #addresses = $state>(new Map()); + public addressMap: SvelteMap = new SvelteMap(); + + #root: TocEntry | null = null; #publicationTree: SveltePublicationTree; #pagePathname: string; /** - * Constructor for the `TableOfContents` class. The constructed ToC initially contains only the - * root entry. Additional entries must be inserted programmatically using class methods. - * - * The `TableOfContents` class should be instantiated as a page-scoped singleton so that - * `pagePathname` is correct wherever the instance is used. The singleton should be made - * made available to the entire component tree under that page. + * Constructs a `TableOfContents` from a `SveltePublicationTree`. + * + * @param rootAddress The address of the root event. + * @param publicationTree The SveltePublicationTree instance. + * @param pagePathname The current page pathname for href generation. */ constructor(rootAddress: string, publicationTree: SveltePublicationTree, pagePathname: string) { this.#publicationTree = publicationTree; this.#pagePathname = pagePathname; - - this.insertIntoTocFromPublicationTree(rootAddress); + void this.#initRoot(rootAddress); + this.#publicationTree.onNodeResolved((address: string) => { + void this.#handleNodeResolved(address); + }); } - #normalizeHashPath(title: string): string { - // TODO: Confirm this uses good normalization logic to produce unique hrefs within the page. - return title.toLowerCase().replace(/ /g, '-'); - } + // #region Public Methods - get addresses(): Map { - return this.#addresses; + /** + * Returns the root entry of the ToC. + * + * @returns The root entry of the ToC, or `null` if the ToC has not been initialized. + */ + getRootEntry(): TocEntry | null { + return this.#root; } - async insertIntoTocFromPublicationTree(address: string): Promise { - const targetEvent = await this.#publicationTree.getEvent(address); - if (!targetEvent) { - console.warn(`[ToC] Event ${address} not found.`); - // TODO: Determine how to handle this case in the UI. - return; - } - - const hierarchyEvents = await this.#publicationTree.getHierarchy(address); - if (hierarchyEvents.length === 0) { - // This means we are at root. - return; - } - - // Michael J 05 May 2025 - In this loop, we assume that the parent of the current event has - // already been populated into the ToC. As long as the root is set when the component is - // initialized, this code will work fine. - let currentParentTocNode: TocEntry | null = this.#tocRoot; - for (let i = 0; i < hierarchyEvents.length; i++) { - const currentEvent = hierarchyEvents[i]; - const currentAddress = currentEvent.tagAddress(); - - if (this.#addresses.has(currentAddress)) { - continue; - } - - const currentEventChildAddresses = await this.#publicationTree.getChildAddresses(currentAddress); - for (const address of currentEventChildAddresses) { - if (address === null) { - continue; - } - - const childEvent = await this.#publicationTree.getEvent(address); - if (!childEvent) { - console.warn(`[ToC] Event ${address} not found.`); - continue; - } - - currentParentTocNode!.children ??= []; - - const childTocEntry: TocEntry = { - address, - title: childEvent.getMatchingTags('title')[0][1], - href: `${this.#pagePathname}#${this.#normalizeHashPath(childEvent.getMatchingTags('title')[0][1])}`, - depth: i + 1, - expanded: false, - children: null, - }; - currentParentTocNode!.children.push(childTocEntry); - this.#addresses.set(address, childTocEntry); - } - - currentParentTocNode = this.#addresses.get(currentAddress)!; - } + getEntry(address: string): TocEntry | undefined { + return this.addressMap.get(address); } /** @@ -128,10 +82,8 @@ export class TableOfContents { title, href, depth, - expanded: false, - children: null, + children: [], }; - parentEntry.children ??= []; parentEntry.children.push(tocEntry); this.buildTocFromDocument(header, tocEntry, depth + 1); @@ -139,6 +91,10 @@ export class TableOfContents { }); } + // #endregion + + // #region Iterator Methods + /** * Iterates over all ToC entries in depth-first order. */ @@ -157,6 +113,92 @@ export class TableOfContents { } } - yield* traverse(this.#tocRoot); + yield* traverse(this.#root); } + + // #endregion + + // #region Private Methods + + async #initRoot(rootAddress: string) { + const rootEvent = await this.#publicationTree.getEvent(rootAddress); + if (!rootEvent) { + throw new Error(`[ToC] Root event ${rootAddress} not found.`); + } + + this.#root = { + address: rootAddress, + title: this.#getTitle(rootEvent), + children: [], + depth: 0, + }; + + this.addressMap.set(rootAddress, this.#root); + // Handle any other nodes that have already been resolved. + await this.#handleNodeResolved(rootAddress); + } + + async #handleNodeResolved(address: string) { + if (this.addressMap.has(address)) { + return; + } + const event = await this.#publicationTree.getEvent(address); + if (!event) { + return; + } + + const parentEvent = await this.#publicationTree.getParent(address); + const parentAddress = parentEvent?.tagAddress(); + if (!parentAddress) { + // All non-root nodes must have a parent. + if (!this.#root || address !== this.#root.address) { + throw new Error(`[ToC] Parent not found for address ${address}`); + } + return; + } + + const parentEntry = this.addressMap.get(parentAddress); + if (!parentEntry) { + throw new Error(`[ToC] Parent ToC entry not found for address ${address}`); + } + + const entry: TocEntry = { + address, + title: this.#getTitle(event), + children: [], + parent: parentEntry, + depth: parentEntry.depth + 1, + }; + + // Michael J - 05 June 2025 - The `getChildAddresses` method forces node resolution on the + // publication tree. This is acceptable here, because the tree is always resolved top-down. + // Therefore, by the time we handle a node's resolution, its parent and siblings have already + // been resolved. + const childAddresses = await this.#publicationTree.getChildAddresses(parentAddress); + const filteredChildAddresses = childAddresses.filter((a): a is string => !!a); + const insertIndex = filteredChildAddresses.findIndex(a => a === address); + if (insertIndex === -1 || insertIndex > parentEntry.children.length) { + parentEntry.children.push(entry); + } else { + parentEntry.children.splice(insertIndex, 0, entry); + } + + this.addressMap.set(address, entry); + } + + #getTitle(event: NDKEvent | null): string { + if (!event) { + // TODO: What do we want to return in this case? + return '[untitled]'; + } + const titleTag = event.getMatchingTags?.('title')?.[0]?.[1]; + return titleTag || event.tagAddress() || '[untitled]'; + } + + #normalizeHashPath(title: string): string { + // TODO: Confirm this uses good normalization logic to produce unique hrefs within the page. + return title.toLowerCase().replace(/ /g, '-'); + } + + // #endregion } From f635d08982684cbc3b899b29a42116f63a327928 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Thu, 5 Jun 2025 09:32:35 -0500 Subject: [PATCH 32/98] Add doc comment for ToC --- .../components/publications/table_of_contents.svelte.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/lib/components/publications/table_of_contents.svelte.ts b/src/lib/components/publications/table_of_contents.svelte.ts index 14066e9..c6a26e6 100644 --- a/src/lib/components/publications/table_of_contents.svelte.ts +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -11,6 +11,13 @@ export interface TocEntry { depth: number; } +/** + * Maintains a table of contents (ToC) for a `SveltePublicationTree`. Since publication trees are + * conceptually infinite and lazy-loading, the ToC represents only the portion of the tree that has + * been "discovered". The ToC is updated as new nodes are resolved within the publication tree. + * + * @see SveltePublicationTree + */ export class TableOfContents { public addressMap: SvelteMap = new SvelteMap(); From fa6265b346f9eca03f153f4da16f1a51976697f3 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Fri, 6 Jun 2025 09:37:49 -0500 Subject: [PATCH 33/98] Refactor and support tree node resolution via ToC entry closure --- .../publications/table_of_contents.svelte.ts | 128 ++++++++++-------- 1 file changed, 72 insertions(+), 56 deletions(-) diff --git a/src/lib/components/publications/table_of_contents.svelte.ts b/src/lib/components/publications/table_of_contents.svelte.ts index c6a26e6..8665697 100644 --- a/src/lib/components/publications/table_of_contents.svelte.ts +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -1,5 +1,5 @@ import { SvelteMap } from 'svelte/reactivity'; -import type { SveltePublicationTree } from './svelte_publication_tree.svelte.ts'; +import { SveltePublicationTree } from './svelte_publication_tree.svelte.ts'; import type { NDKEvent } from '../../utils/nostrUtils.ts'; export interface TocEntry { @@ -9,6 +9,9 @@ export interface TocEntry { children: TocEntry[]; parent?: TocEntry; depth: number; + expanded: boolean; + childrenResolved: boolean; + resolveChildren: () => Promise; } /** @@ -35,10 +38,7 @@ export class TableOfContents { constructor(rootAddress: string, publicationTree: SveltePublicationTree, pagePathname: string) { this.#publicationTree = publicationTree; this.#pagePathname = pagePathname; - void this.#initRoot(rootAddress); - this.#publicationTree.onNodeResolved((address: string) => { - void this.#handleNodeResolved(address); - }); + this.#init(rootAddress); } // #region Public Methods @@ -84,12 +84,16 @@ export class TableOfContents { if (id && title) { const href = `${this.#pagePathname}#${id}`; + // TODO: Check this logic. const tocEntry: TocEntry = { address: parentEntry.address, title, href, depth, children: [], + expanded: false, + childrenResolved: true, + resolveChildren: () => Promise.resolve(), }; parentEntry.children.push(tocEntry); @@ -127,84 +131,96 @@ export class TableOfContents { // #region Private Methods - async #initRoot(rootAddress: string) { + async #init(rootAddress: string) { const rootEvent = await this.#publicationTree.getEvent(rootAddress); if (!rootEvent) { throw new Error(`[ToC] Root event ${rootAddress} not found.`); } - this.#root = { - address: rootAddress, - title: this.#getTitle(rootEvent), - children: [], - depth: 0, - }; + this.#root = await this.#buildTocEntry(rootAddress); this.addressMap.set(rootAddress, this.#root); + + // TODO: Parallelize this. // Handle any other nodes that have already been resolved. - await this.#handleNodeResolved(rootAddress); + this.#publicationTree.resolvedAddresses.forEach(async (address) => { + await this.#buildTocEntryFromResolvedNode(address); + }); + + // Set up an observer to handle progressive resolution of the publication tree. + this.#publicationTree.onNodeResolved(async (address: string) => { + await this.#buildTocEntryFromResolvedNode(address); + }); } - async #handleNodeResolved(address: string) { - if (this.addressMap.has(address)) { - return; - } - const event = await this.#publicationTree.getEvent(address); + #getTitle(event: NDKEvent | null): string { if (!event) { - return; + // TODO: What do we want to return in this case? + return '[untitled]'; } + const titleTag = event.getMatchingTags?.('title')?.[0]?.[1]; + return titleTag || event.tagAddress() || '[untitled]'; + } + + #normalizeHashPath(title: string): string { + // TODO: Confirm this uses good normalization logic to produce unique hrefs within the page. + return title.toLowerCase().replace(/ /g, '-'); + } - const parentEvent = await this.#publicationTree.getParent(address); - const parentAddress = parentEvent?.tagAddress(); - if (!parentAddress) { - // All non-root nodes must have a parent. - if (!this.#root || address !== this.#root.address) { - throw new Error(`[ToC] Parent not found for address ${address}`); + async #buildTocEntry(address: string): Promise { + const resolver = async () => { + if (entry.childrenResolved) { + return; } - return; + + const childAddresses = await this.#publicationTree.getChildAddresses(address); + for (const childAddress of childAddresses) { + if (!childAddress) { + continue; + } + + // Michael J - 05 June 2025 - The `getChildAddresses` method forces node resolution on the + // publication tree. This is acceptable here, because the tree is always resolved + // top-down. Therefore, by the time we handle a node's resolution, its parent and + // siblings have already been resolved. + const childEntry = await this.#buildTocEntry(childAddress); + childEntry.parent = entry; + childEntry.depth = entry.depth + 1; + entry.children.push(childEntry); + this.addressMap.set(childAddress, childEntry); + } + + entry.childrenResolved = true; } - const parentEntry = this.addressMap.get(parentAddress); - if (!parentEntry) { - throw new Error(`[ToC] Parent ToC entry not found for address ${address}`); + const event = await this.#publicationTree.getEvent(address); + if (!event) { + throw new Error(`[ToC] Event ${address} not found.`); } + const depth = (await this.#publicationTree.getHierarchy(address)).length; + const entry: TocEntry = { address, title: this.#getTitle(event), + href: `${this.#pagePathname}#${address}`, children: [], - parent: parentEntry, - depth: parentEntry.depth + 1, + depth, + expanded: false, + childrenResolved: false, + resolveChildren: resolver, }; - - // Michael J - 05 June 2025 - The `getChildAddresses` method forces node resolution on the - // publication tree. This is acceptable here, because the tree is always resolved top-down. - // Therefore, by the time we handle a node's resolution, its parent and siblings have already - // been resolved. - const childAddresses = await this.#publicationTree.getChildAddresses(parentAddress); - const filteredChildAddresses = childAddresses.filter((a): a is string => !!a); - const insertIndex = filteredChildAddresses.findIndex(a => a === address); - if (insertIndex === -1 || insertIndex > parentEntry.children.length) { - parentEntry.children.push(entry); - } else { - parentEntry.children.splice(insertIndex, 0, entry); - } - - this.addressMap.set(address, entry); + + return entry; } - #getTitle(event: NDKEvent | null): string { - if (!event) { - // TODO: What do we want to return in this case? - return '[untitled]'; + async #buildTocEntryFromResolvedNode(address: string) { + if (this.addressMap.has(address)) { + return; } - const titleTag = event.getMatchingTags?.('title')?.[0]?.[1]; - return titleTag || event.tagAddress() || '[untitled]'; - } - #normalizeHashPath(title: string): string { - // TODO: Confirm this uses good normalization logic to produce unique hrefs within the page. - return title.toLowerCase().replace(/ /g, '-'); + const entry = await this.#buildTocEntry(address); + this.addressMap.set(address, entry); } // #endregion From 9a6a494f172c1915e96b9d17b9e7ebb68608a3b7 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 9 Jun 2025 22:37:11 -0500 Subject: [PATCH 34/98] Fix `NDKEvent` type imports --- src/lib/components/CommentBox.svelte | 3 +-- .../components/publications/svelte_publication_tree.svelte.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib/components/CommentBox.svelte b/src/lib/components/CommentBox.svelte index c46f902..46b2f4b 100644 --- a/src/lib/components/CommentBox.svelte +++ b/src/lib/components/CommentBox.svelte @@ -2,12 +2,11 @@ import { Button, Textarea, Alert } from 'flowbite-svelte'; import { parseBasicmarkup } from '$lib/utils/markup/basicMarkupParser'; import { nip19 } from 'nostr-tools'; - import { getEventHash, signEvent, getUserMetadata, type NostrProfile } from '$lib/utils/nostrUtils'; + import { getEventHash, signEvent, getUserMetadata, type NostrProfile, type NDKEvent } from '$lib/utils/nostrUtils'; import { standardRelays, fallbackRelays } from '$lib/consts'; import { userRelays } from '$lib/stores/relayStore'; import { get } from 'svelte/store'; import { goto } from '$app/navigation'; - import type { NDKEvent } from '$lib/utils/nostrUtils'; import { onMount } from 'svelte'; const props = $props<{ diff --git a/src/lib/components/publications/svelte_publication_tree.svelte.ts b/src/lib/components/publications/svelte_publication_tree.svelte.ts index aac39f2..9969ed7 100644 --- a/src/lib/components/publications/svelte_publication_tree.svelte.ts +++ b/src/lib/components/publications/svelte_publication_tree.svelte.ts @@ -1,7 +1,6 @@ import { SvelteSet } from "svelte/reactivity"; import { PublicationTree } from "../../data_structures/publication_tree.ts"; -import { NDKEvent } from "../../utils/nostrUtils.ts"; -import NDK from "@nostr-dev-kit/ndk"; +import NDK, { NDKEvent } from "@nostr-dev-kit/ndk"; export class SveltePublicationTree { resolvedAddresses: SvelteSet = new SvelteSet(); From 608ce2b8e4392e53aaeca45451282858f0c64fd2 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 9 Jun 2025 23:20:42 -0500 Subject: [PATCH 35/98] First pass at ToC layout --- .../publications/Publication.svelte | 9 ++- .../publications/TableOfContents.svelte | 65 +++++++++++++++---- src/routes/publication/+page.svelte | 7 ++ 3 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/lib/components/publications/Publication.svelte b/src/lib/components/publications/Publication.svelte index 7147de4..2511de0 100644 --- a/src/lib/components/publications/Publication.svelte +++ b/src/lib/components/publications/Publication.svelte @@ -19,9 +19,9 @@ import { publicationColumnVisibility } from "$lib/stores"; import BlogHeader from "$components/cards/BlogHeader.svelte"; import Interactions from "$components/util/Interactions.svelte"; - import TocToggle from "$components/util/TocToggle.svelte"; import { pharosInstance } from '$lib/parser'; import type { SveltePublicationTree } from "./svelte_publication_tree.svelte"; + import TableOfContents from "./TableOfContents.svelte"; let { rootAddress, publicationType, indexEvent } = $props<{ rootAddress: string; @@ -160,7 +160,12 @@ {#if publicationType !== "blog" || !isLeaf} - + { + publicationTree.setBookmark(address); + }} + /> {/if} diff --git a/src/lib/components/publications/TableOfContents.svelte b/src/lib/components/publications/TableOfContents.svelte index 891476a..aadfded 100644 --- a/src/lib/components/publications/TableOfContents.svelte +++ b/src/lib/components/publications/TableOfContents.svelte @@ -1,22 +1,63 @@ - - {#each toc as entry} - {entry.title} + + {#each entries as entry} + +

{entry.title}

+ {#if entry.children.length > 0} + + {/if} +
{/each} -
+ diff --git a/src/routes/publication/+page.svelte b/src/routes/publication/+page.svelte index 7defecf..457d896 100644 --- a/src/routes/publication/+page.svelte +++ b/src/routes/publication/+page.svelte @@ -6,11 +6,18 @@ import Processor from "asciidoctor"; import ArticleNav from "$components/util/ArticleNav.svelte"; import { SveltePublicationTree } from "$lib/components/publications/svelte_publication_tree.svelte"; + import { TableOfContents } from "$lib/components/publications/table_of_contents.svelte"; let { data }: PageProps = $props(); const publicationTree = new SveltePublicationTree(data.indexEvent, data.ndk); + const toc = new TableOfContents( + data.indexEvent.tagAddress(), + publicationTree, + data.url?.pathname ?? "", + ); setContext("publicationTree", publicationTree); + setContext("toc", toc); setContext("asciidoctor", Processor()); // Get publication metadata for OpenGraph tags From e3848045224f615f7d7366fc9a53bddf7886f6cb Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 10 Jun 2025 08:32:46 -0500 Subject: [PATCH 36/98] Add stronger typing to publication layout store --- src/lib/stores.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/lib/stores.ts b/src/lib/stores.ts index e38f0d4..7962e7b 100644 --- a/src/lib/stores.ts +++ b/src/lib/stores.ts @@ -1,5 +1,5 @@ -import { readable, writable } from "svelte/store"; -import { FeedType } from "./consts"; +import { readable, writable } from 'svelte/store'; +import { FeedType } from './consts.ts'; export let idList = writable([]); @@ -7,8 +7,16 @@ export let alexandriaKinds = readable([30040, 30041, 30818]); export let feedType = writable(FeedType.StandardRelays); +export interface PublicationLayoutVisibility { + toc: boolean; + blog: boolean; + main: boolean; + inner: boolean; + discussion: boolean; + editing: boolean; +} -const defaultVisibility = { +const defaultVisibility: PublicationLayoutVisibility = { toc: false, blog: true, main: true, @@ -18,7 +26,8 @@ const defaultVisibility = { }; function createVisibilityStore() { - const { subscribe, set, update } = writable({ ...defaultVisibility }); + const { subscribe, set, update } + = writable({ ...defaultVisibility }); return { subscribe, From d48ddd465a04be3808d239b3b4b30afc23d82b08 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 10 Jun 2025 08:33:07 -0500 Subject: [PATCH 37/98] Remove unneeded references to Pharos parser --- src/lib/components/publications/Publication.svelte | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/lib/components/publications/Publication.svelte b/src/lib/components/publications/Publication.svelte index 2511de0..5f1a9de 100644 --- a/src/lib/components/publications/Publication.svelte +++ b/src/lib/components/publications/Publication.svelte @@ -19,7 +19,6 @@ import { publicationColumnVisibility } from "$lib/stores"; import BlogHeader from "$components/cards/BlogHeader.svelte"; import Interactions from "$components/util/Interactions.svelte"; - import { pharosInstance } from '$lib/parser'; import type { SveltePublicationTree } from "./svelte_publication_tree.svelte"; import TableOfContents from "./TableOfContents.svelte"; @@ -153,9 +152,6 @@ observer.disconnect(); }; }); - - // Whenever the publication changes, update rootId - let rootId = $derived($pharosInstance.getRootIndexId()); From cd204119f03a6815b3b8d8d8f58115d8bb0cc180 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 10 Jun 2025 08:52:58 -0500 Subject: [PATCH 38/98] Tidy up code formatting in ArticleNav component --- src/lib/components/util/ArticleNav.svelte | 30 ++++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/lib/components/util/ArticleNav.svelte b/src/lib/components/util/ArticleNav.svelte index a5b8631..11f804c 100644 --- a/src/lib/components/util/ArticleNav.svelte +++ b/src/lib/components/util/ArticleNav.svelte @@ -113,34 +113,46 @@
{#if shouldShowBack()} {/if} {#if !isLeaf} {#if publicationType === 'blog'} - {:else if !$publicationColumnVisibility.discussion && !$publicationColumnVisibility.toc} - {/if} {/if}
-

{title} by {@render userBadge(pubkey, author)}

+

{title}by {@render userBadge(pubkey, author)}

{#if $publicationColumnVisibility.inner} {/if} {#if publicationType !== 'blog' && !$publicationColumnVisibility.discussion} {/if}
From db4991b229c561e02ef2970d059e5a6a1a9aa909 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 11 Jun 2025 00:13:49 -0500 Subject: [PATCH 39/98] Update Flowbite Svelte version NB: Upgrading to v1.0.0 or above breaks our current stlying. DO NOT UPGRADE WITHOUT FIXING STYLES. --- deno.lock | 220 ++++++++++++++++++++++++++++++------------------ import_map.json | 2 +- package.json | 2 +- 3 files changed, 140 insertions(+), 84 deletions(-) diff --git a/deno.lock b/deno.lock index 6604928..28f6a0b 100644 --- a/deno.lock +++ b/deno.lock @@ -16,14 +16,15 @@ "npm:@types/d3@^7.4.3": "7.4.3", "npm:@types/he@1.2": "1.2.3", "npm:@types/node@22": "22.13.9", + "npm:@types/qrcode@^1.5.5": "1.5.5", "npm:asciidoctor@3.0": "3.0.4_@asciidoctor+core@3.0.4", "npm:autoprefixer@10": "10.4.20_postcss@8.5.3", + "npm:bech32@2": "2.0.0", "npm:d3@7.9": "7.9.0_d3-selection@3.0.0", "npm:d3@^7.9.0": "7.9.0_d3-selection@3.0.0", "npm:eslint-plugin-svelte@2": "2.46.1_eslint@9.21.0_svelte@5.21.0__acorn@8.14.0_postcss@8.5.3", "npm:flowbite-svelte-icons@2.1": "2.1.1_svelte@5.0.5__acorn@8.14.0_tailwind-merge@3.3.0", - "npm:flowbite-svelte@0": "0.48.4_svelte@5.21.0__acorn@8.14.0", - "npm:flowbite-svelte@0.44": "0.44.24_svelte@4.2.19", + "npm:flowbite-svelte@0.48": "0.48.6_svelte@5.0.5__acorn@8.14.0", "npm:flowbite@2": "2.5.2", "npm:flowbite@2.2": "2.2.1", "npm:he@1.2": "1.2.0", @@ -35,6 +36,7 @@ "npm:postcss@8": "8.5.3", "npm:prettier-plugin-svelte@3": "3.3.3_prettier@3.5.3_svelte@5.21.0__acorn@8.14.0", "npm:prettier@3": "3.5.3", + "npm:qrcode@^1.5.4": "1.5.4", "npm:svelte-check@4": "4.1.4_svelte@5.21.0__acorn@8.14.0_typescript@5.7.3", "npm:svelte@5": "5.21.0_acorn@8.14.0", "npm:svelte@5.0": "5.0.5_acorn@8.14.0", @@ -61,7 +63,7 @@ "integrity": "sha512-x2T9gW42921Zd90juEagtbViPZHNP2MWf0+6rJEkOzW7E9m3TGJtz+Guye9J0gwrpZsTMGCpfYMQy1We3X7osg==", "dependencies": [ "@asciidoctor/core", - "yargs" + "yargs@17.3.1" ] }, "@asciidoctor/core@3.0.4": { @@ -217,14 +219,14 @@ "levn" ] }, - "@floating-ui/core@1.6.9": { - "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "@floating-ui/core@1.7.1": { + "integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==", "dependencies": [ "@floating-ui/utils" ] }, - "@floating-ui/dom@1.6.13": { - "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "@floating-ui/dom@1.7.1": { + "integrity": "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==", "dependencies": [ "@floating-ui/core", "@floating-ui/utils" @@ -789,12 +791,24 @@ "@types/json-schema@7.0.15": { "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, + "@types/node@22.12.0": { + "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", + "dependencies": [ + "undici-types" + ] + }, "@types/node@22.13.9": { "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", "dependencies": [ "undici-types" ] }, + "@types/qrcode@1.5.5": { + "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==", + "dependencies": [ + "@types/node@22.12.0" + ] + }, "@types/resolve@1.20.2": { "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" }, @@ -977,6 +991,9 @@ "balanced-match@1.0.2": { "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "bech32@2.0.0": { + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + }, "binary-extensions@2.3.0": { "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==" }, @@ -1037,6 +1054,9 @@ "camelcase-css@2.0.1": { "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" }, + "camelcase@5.3.1": { + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, "caniuse-lite@1.0.30001702": { "integrity": "sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==" }, @@ -1088,6 +1108,14 @@ "readdirp@4.1.2" ] }, + "cliui@6.0.0": { + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": [ + "string-width@4.2.3", + "strip-ansi@6.0.1", + "wrap-ansi@6.2.0" + ] + }, "cliui@7.0.4": { "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dependencies": [ @@ -1099,16 +1127,6 @@ "clsx@2.1.1": { "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" }, - "code-red@1.0.4": { - "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", - "dependencies": [ - "@jridgewell/sourcemap-codec", - "@types/estree", - "acorn@8.14.0", - "estree-walker@3.0.3", - "periscopic" - ] - }, "color-convert@2.0.1": { "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": [ @@ -1151,13 +1169,6 @@ "which" ] }, - "css-tree@2.3.1": { - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dependencies": [ - "mdn-data", - "source-map-js" - ] - }, "cssesc@3.0.0": { "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" }, @@ -1382,6 +1393,9 @@ "ms@2.1.3" ] }, + "decamelize@1.2.0": { + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + }, "deep-eql@5.0.2": { "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==" }, @@ -1406,6 +1420,9 @@ "didyoumean@1.2.2": { "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, + "dijkstrajs@1.0.3": { + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, "dlv@1.1.3": { "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, @@ -1586,7 +1603,7 @@ "esutils", "fast-deep-equal", "file-entry-cache", - "find-up", + "find-up@5.0.0", "glob-parent@6.0.2", "ignore", "imurmurhash", @@ -1730,10 +1747,17 @@ "to-regex-range" ] }, + "find-up@4.1.0": { + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": [ + "locate-path@5.0.0", + "path-exists" + ] + }, "find-up@5.0.0": { "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dependencies": [ - "locate-path", + "locate-path@6.0.0", "path-exists" ] }, @@ -1768,24 +1792,14 @@ "tailwind-merge@3.3.0" ] }, - "flowbite-svelte@0.44.24_svelte@4.2.19": { - "integrity": "sha512-kXhJZHGpBVq5RFOoYnzRCEM8eFa81DVp4KjUbBsLJptKhizbSSBJuYApWIQb9pBCS8EBhX4PAX+RsgEDZfEqtA==", - "dependencies": [ - "@floating-ui/dom", - "apexcharts", - "flowbite@2.5.2", - "svelte@4.2.19", - "tailwind-merge@2.5.5" - ] - }, - "flowbite-svelte@0.48.4_svelte@5.21.0__acorn@8.14.0": { - "integrity": "sha512-ivlBxNi2u9+D/nFeHs+vLJU6nYjKq/ooAwdXPP3qIlEnUyIl/hVsH87JtVWwVEgF31NwwQcZeKFkWd8K5DWiGw==", + "flowbite-svelte@0.48.6_svelte@5.0.5__acorn@8.14.0": { + "integrity": "sha512-/PmeR3ipHHvda8vVY9MZlymaRoJsk8VddEeoLzIygfYwJV68ey8gHuQPC1dq9J6NDCTE5+xOPtBiYUtVjCfvZw==", "dependencies": [ "@floating-ui/dom", "apexcharts", "flowbite@3.1.2", - "svelte@5.21.0_acorn@8.14.0", - "tailwind-merge@3.0.2" + "svelte@5.0.5_acorn@8.14.0", + "tailwind-merge@3.3.0" ] }, "flowbite@2.2.1": { @@ -2115,10 +2129,16 @@ "locate-character@3.0.0": { "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" }, + "locate-path@5.0.0": { + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": [ + "p-locate@4.1.0" + ] + }, "locate-path@6.0.0": { "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dependencies": [ - "p-locate" + "p-locate@5.0.0" ] }, "lodash.castarray@4.4.0": { @@ -2145,9 +2165,6 @@ "math-intrinsics@1.1.0": { "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" }, - "mdn-data@2.0.30": { - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" - }, "merge2@1.4.1": { "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, @@ -2285,18 +2302,33 @@ "word-wrap" ] }, + "p-limit@2.3.0": { + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": [ + "p-try" + ] + }, "p-limit@3.1.0": { "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dependencies": [ "yocto-queue" ] }, + "p-locate@4.1.0": { + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": [ + "p-limit@2.3.0" + ] + }, "p-locate@5.0.0": { "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dependencies": [ - "p-limit" + "p-limit@3.1.0" ] }, + "p-try@2.2.0": { + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, "package-json-from-dist@1.0.1": { "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" }, @@ -2328,14 +2360,6 @@ "pathval@2.0.0": { "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==" }, - "periscopic@3.1.0": { - "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", - "dependencies": [ - "@types/estree", - "estree-walker@3.0.3", - "is-reference@3.0.3" - ] - }, "picocolors@1.1.1": { "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, @@ -2361,6 +2385,9 @@ "playwright-core" ] }, + "pngjs@5.0.0": { + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" + }, "postcss-import@15.1.0_postcss@8.5.3": { "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dependencies": [ @@ -2554,6 +2581,14 @@ "punycode@2.3.1": { "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" }, + "qrcode@1.5.4": { + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "dependencies": [ + "dijkstrajs", + "pngjs", + "yargs@15.4.1" + ] + }, "queue-microtask@1.2.3": { "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" }, @@ -2575,6 +2610,9 @@ "require-directory@2.1.1": { "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" }, + "require-main-filename@2.0.0": { + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, "resolve-from@4.0.0": { "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" }, @@ -2639,6 +2677,9 @@ "semver@7.7.1": { "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==" }, + "set-blocking@2.0.0": { + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "set-cookie-parser@2.7.1": { "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" }, @@ -2758,25 +2799,6 @@ "svelte@5.21.0_acorn@8.14.0" ] }, - "svelte@4.2.19": { - "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", - "dependencies": [ - "@ampproject/remapping", - "@jridgewell/sourcemap-codec", - "@jridgewell/trace-mapping", - "@types/estree", - "acorn@8.14.0", - "aria-query", - "axobject-query", - "code-red", - "css-tree", - "estree-walker@3.0.3", - "is-reference@3.0.3", - "locate-character", - "magic-string", - "periscopic" - ] - }, "svelte@5.0.5_acorn@8.14.0": { "integrity": "sha512-f4WBlP5g8W6pEoDfx741lewMlemy+LIGpEqjGPWqnHVP92wqlQXl87U5O5Bi2tkSUrO95OxOoqwU8qlqiHmFKA==", "dependencies": [ @@ -2863,9 +2885,6 @@ "tailwind-merge@2.5.5": { "integrity": "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==" }, - "tailwind-merge@3.0.2": { - "integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==" - }, "tailwind-merge@3.3.0": { "integrity": "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==" }, @@ -3026,7 +3045,7 @@ "vite@5.4.14_@types+node@22.13.9": { "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", "dependencies": [ - "@types/node", + "@types/node@22.13.9", "esbuild", "fsevents@2.3.3", "postcss", @@ -3042,7 +3061,7 @@ "vitest@3.1.4_@types+node@22.13.9_vite@5.4.14__@types+node@22.13.9": { "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", "dependencies": [ - "@types/node", + "@types/node@22.13.9", "@vitest/expect", "@vitest/mocker", "@vitest/pretty-format", @@ -3087,6 +3106,9 @@ "yaeti" ] }, + "which-module@2.0.1": { + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, "which@2.0.2": { "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dependencies": [ @@ -3115,6 +3137,14 @@ "wordwrap@1.0.0": { "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" }, + "wrap-ansi@6.2.0": { + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": [ + "ansi-styles@4.3.0", + "string-width@4.2.3", + "strip-ansi@6.0.1" + ] + }, "wrap-ansi@7.0.0": { "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dependencies": [ @@ -3134,6 +3164,9 @@ "wrappy@1.0.2": { "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "y18n@4.0.3": { + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, "y18n@5.0.8": { "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, @@ -3146,19 +3179,42 @@ "yaml@2.7.0": { "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==" }, + "yargs-parser@18.1.3": { + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": [ + "camelcase", + "decamelize" + ] + }, "yargs-parser@21.1.1": { "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" }, + "yargs@15.4.1": { + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": [ + "cliui@6.0.0", + "decamelize", + "find-up@4.1.0", + "get-caller-file", + "require-directory", + "require-main-filename", + "set-blocking", + "string-width@4.2.3", + "which-module", + "y18n@4.0.3", + "yargs-parser@18.1.3" + ] + }, "yargs@17.3.1": { "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==", "dependencies": [ - "cliui", + "cliui@7.0.4", "escalade", "get-caller-file", "require-directory", "string-width@4.2.3", - "y18n", - "yargs-parser" + "y18n@5.0.8", + "yargs-parser@21.1.1" ] }, "yocto-queue@0.1.0": { @@ -3185,7 +3241,7 @@ "npm:asciidoctor@3.0", "npm:d3@7.9", "npm:flowbite-svelte-icons@2.1", - "npm:flowbite-svelte@0.44", + "npm:flowbite-svelte@0.48", "npm:flowbite@2.2", "npm:he@1.2", "npm:nostr-tools@2.10", @@ -3215,7 +3271,7 @@ "npm:d3@^7.9.0", "npm:eslint-plugin-svelte@2", "npm:flowbite-svelte-icons@2.1", - "npm:flowbite-svelte@0", + "npm:flowbite-svelte@0.48", "npm:flowbite@2", "npm:he@1.2", "npm:highlight.js@^11.11.1", diff --git a/import_map.json b/import_map.json index 4c3af16..af4012c 100644 --- a/import_map.json +++ b/import_map.json @@ -12,7 +12,7 @@ "tailwind-merge": "npm:tailwind-merge@2.5.x", "svelte": "npm:svelte@5.0.x", "flowbite": "npm:flowbite@2.2.x", - "flowbite-svelte": "npm:flowbite-svelte@0.44.x", + "flowbite-svelte": "npm:flowbite-svelte@0.48.x", "flowbite-svelte-icons": "npm:flowbite-svelte-icons@2.1.x", "child_process": "node:child_process" } diff --git a/package.json b/package.json index 787d2e7..3df9454 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "autoprefixer": "10.x", "eslint-plugin-svelte": "2.x", "flowbite": "2.x", - "flowbite-svelte": "0.x", + "flowbite-svelte": "0.48.x", "flowbite-svelte-icons": "2.1.x", "playwright": "^1.50.1", "postcss": "8.x", From 29c06551cf7a51585e0cc68b1dde2d6c6dfd74d7 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 11 Jun 2025 00:14:34 -0500 Subject: [PATCH 40/98] Remove ToC from page context --- src/routes/publication/+page.svelte | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/routes/publication/+page.svelte b/src/routes/publication/+page.svelte index 457d896..a73d37e 100644 --- a/src/routes/publication/+page.svelte +++ b/src/routes/publication/+page.svelte @@ -10,14 +10,8 @@ let { data }: PageProps = $props(); const publicationTree = new SveltePublicationTree(data.indexEvent, data.ndk); - const toc = new TableOfContents( - data.indexEvent.tagAddress(), - publicationTree, - data.url?.pathname ?? "", - ); setContext("publicationTree", publicationTree); - setContext("toc", toc); setContext("asciidoctor", Processor()); // Get publication metadata for OpenGraph tags From b07914f9635d64e077fee3156f56ecb27d50fc0c Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 11 Jun 2025 00:16:03 -0500 Subject: [PATCH 41/98] Allow two ToC component variants - A sidebar variant is meant for integration within a sidebar. This is used in the current Publication component. - An accordion variant is intended for standalone use. --- .../publications/Publication.svelte | 30 ++++++--- .../publications/TableOfContents.svelte | 65 ++++++++++++++----- 2 files changed, 69 insertions(+), 26 deletions(-) diff --git a/src/lib/components/publications/Publication.svelte b/src/lib/components/publications/Publication.svelte index 5f1a9de..ec4ec10 100644 --- a/src/lib/components/publications/Publication.svelte +++ b/src/lib/components/publications/Publication.svelte @@ -7,6 +7,7 @@ SidebarGroup, SidebarWrapper, Heading, + CloseButton, } from "flowbite-svelte"; import { getContext, onDestroy, onMount } from "svelte"; import { @@ -90,6 +91,10 @@ return currentBlog !== null && $publicationColumnVisibility.inner; } + function closeToc() { + publicationColumnVisibility.update((v) => ({ ...v, toc: false })); + } + function closeDiscussion() { publicationColumnVisibility.update((v) => ({ ...v, discussion: false })); } @@ -155,13 +160,20 @@ -{#if publicationType !== "blog" || !isLeaf} - { - publicationTree.setBookmark(address); - }} - /> +{#if publicationType !== 'blog' || !isLeaf} + {#if $publicationColumnVisibility.toc} + + + { + publicationTree.setBookmark(address); + }} + /> + + {/if} {/if} @@ -205,9 +217,7 @@ {#if $publicationColumnVisibility.blog}
- import type { TableOfContents, TocEntry } from '$lib/components/publications/table_of_contents.svelte'; + import { TableOfContents, type TocEntry } from '$lib/components/publications/table_of_contents.svelte'; import { getContext } from 'svelte'; - import { Accordion, AccordionItem, Card } from 'flowbite-svelte'; + import { Accordion, AccordionItem, Card, SidebarDropdownWrapper, SidebarGroup, SidebarItem } from 'flowbite-svelte'; import Self from './TableOfContents.svelte'; + import type { SveltePublicationTree } from './svelte_publication_tree.svelte'; + import { page } from '$app/state'; - let { + export type TocDisplayMode = 'accordion' | 'sidebar'; + + let { + displayMode = 'accordion', + rootAddress, depth, onSectionFocused, } = $props<{ + displayMode?: TocDisplayMode; + rootAddress: string; depth: number; onSectionFocused?: (address: string) => void; }>(); - let toc = getContext('toc') as TableOfContents; + let publicationTree = getContext('publicationTree') as SveltePublicationTree; + let toc = new TableOfContents(rootAddress, publicationTree, page.url.pathname ?? ""); - let entries = $derived( - Array + let entries = $derived.by(() => { + console.debug("[ToC] Filtering entries for depth", depth); + const entries = Array .from(toc.addressMap.values()) - .filter((entry) => entry.depth === depth) - ); + .filter((entry) => entry.depth === depth); + console.debug("[ToC] Filtered entries", entries.map((e) => e.title)); + return entries; + }); // Track the currently visible section for highlighting let currentSection = $state(null); @@ -51,13 +63,34 @@ } - - {#each entries as entry} - -

{entry.title}

+{#if displayMode === 'accordion'} + + {#each entries as entry} + + {#snippet header()} + {entry.title} + {/snippet} + {#if entry.children.length > 0} + + {/if} + + {/each} + +{:else} + + {#each entries as entry} {#if entry.children.length > 0} - + + + + {:else} + {/if} -
- {/each} -
+ {/each} + +{/if} \ No newline at end of file From 052392d7f0d09b3da76e9a22f8b3cb7f20f186d4 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 11 Jun 2025 00:36:17 -0500 Subject: [PATCH 42/98] Add action bindings on ToC component elements --- .../publications/TableOfContents.svelte | 74 +++++++++++-------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/src/lib/components/publications/TableOfContents.svelte b/src/lib/components/publications/TableOfContents.svelte index 8356509..9a1f252 100644 --- a/src/lib/components/publications/TableOfContents.svelte +++ b/src/lib/components/publications/TableOfContents.svelte @@ -23,50 +23,49 @@ let publicationTree = getContext('publicationTree') as SveltePublicationTree; let toc = new TableOfContents(rootAddress, publicationTree, page.url.pathname ?? ""); - let entries = $derived.by(() => { - console.debug("[ToC] Filtering entries for depth", depth); - const entries = Array + let entries = $derived( + Array .from(toc.addressMap.values()) - .filter((entry) => entry.depth === depth); - console.debug("[ToC] Filtered entries", entries.map((e) => e.title)); - return entries; - }); + .filter((entry) => entry.depth === depth) + ); - // Track the currently visible section for highlighting - let currentSection = $state(null); + function getEntryExpanded(address: string) { + return toc.getEntry(address)?.expanded; + } - // Handle section visibility changes from the IntersectionObserver - function handleSectionVisibility(address: string, isVisible: boolean) { - if (isVisible) { - currentSection = address; + function setEntryExpanded(address: string, expanded: boolean = false) { + const entry = toc.getEntry(address); + if (!entry) { + return; } - } - // Toggle expansion of a ToC entry - async function toggleExpansion(entry: TocEntry) { - // Update the current section in the ToC - const tocEntry = toc.getEntry(entry.address); - if (tocEntry) { - // Ensure the parent sections are expanded - let parent = tocEntry.parent; - while (parent) { - parent.expanded = true; - parent = parent.parent; - } + entry.expanded = expanded; + if (entry.childrenResolved) { + return; } - entry.expanded = !entry.expanded; - if (entry.expanded && !entry.childrenResolved) { - onSectionFocused?.(entry.address); - await entry.resolveChildren(); + if (expanded) { + entry.resolveChildren(); } } + + function handleEntryClick(address: string, expanded: boolean = false) { + setEntryExpanded(address, expanded); + onSectionFocused?.(address); + } + {#if displayMode === 'accordion'} {#each entries as entry} - + {@const address = entry.address} + getEntryExpanded(address), + (open) => setEntryExpanded(address, open) + } + > {#snippet header()} {entry.title} {/snippet} @@ -79,8 +78,15 @@ {:else} {#each entries as entry} + {@const address = entry.address} {#if entry.children.length > 0} - + getEntryExpanded(address), + (open) => setEntryExpanded(address, open) + } + > {:else} - + + handleEntryClick(address, !getEntryExpanded(address))} + /> {/if} {/each} From 7219e68ca3bb5b829f3a5b5b48967ed04045be57 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 11 Jun 2025 07:53:43 -0500 Subject: [PATCH 43/98] Make accordion non-flush --- src/lib/components/publications/TableOfContents.svelte | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib/components/publications/TableOfContents.svelte b/src/lib/components/publications/TableOfContents.svelte index 9a1f252..9813d57 100644 --- a/src/lib/components/publications/TableOfContents.svelte +++ b/src/lib/components/publications/TableOfContents.svelte @@ -57,7 +57,7 @@ {#if displayMode === 'accordion'} - + {#each entries as entry} {@const address = entry.address} {entry.title} {/snippet} {#if entry.children.length > 0} - + {/if} {/each} From 2d6d38b3b38e2046d9a11f1e83a2a876edb455a8 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 11 Jun 2025 23:07:12 -0500 Subject: [PATCH 44/98] Fix data flow between publication tree and ToC --- src/lib/components/publications/TableOfContents.svelte | 8 +------- .../components/publications/table_of_contents.svelte.ts | 4 ++-- src/lib/data_structures/publication_tree.ts | 1 - 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/lib/components/publications/TableOfContents.svelte b/src/lib/components/publications/TableOfContents.svelte index 9813d57..50296d2 100644 --- a/src/lib/components/publications/TableOfContents.svelte +++ b/src/lib/components/publications/TableOfContents.svelte @@ -40,13 +40,7 @@ } entry.expanded = expanded; - if (entry.childrenResolved) { - return; - } - - if (expanded) { - entry.resolveChildren(); - } + entry.resolveChildren(); } function handleEntryClick(address: string, expanded: boolean = false) { diff --git a/src/lib/components/publications/table_of_contents.svelte.ts b/src/lib/components/publications/table_of_contents.svelte.ts index 8665697..63d3b9c 100644 --- a/src/lib/components/publications/table_of_contents.svelte.ts +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -148,8 +148,8 @@ export class TableOfContents { }); // Set up an observer to handle progressive resolution of the publication tree. - this.#publicationTree.onNodeResolved(async (address: string) => { - await this.#buildTocEntryFromResolvedNode(address); + this.#publicationTree.onNodeResolved((address: string) => { + this.#buildTocEntryFromResolvedNode(address); }); } diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 515b866..a7aece6 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -623,7 +623,6 @@ export class PublicationTree implements AsyncIterable { this.addEventByAddress(address, event); } - // TODO: We may need to move this to `#addNode`, so the observer is notified more eagerly. this.#nodeResolvedObservers.forEach(observer => observer(address)); return node; From c235646ffeedfddbe75ffb4b4204d420ec6f8399 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 14 Jun 2025 14:10:42 -0500 Subject: [PATCH 45/98] Ensure ToC addresses update correctly Closing and reopening the ToC component shows updates, but it is not responsive to clicks. This will be fixed in a future commit. --- .../publications/TableOfContents.svelte | 32 ++++++++++++------- .../publications/table_of_contents.svelte.ts | 17 +++++++--- src/lib/data_structures/publication_tree.ts | 2 +- src/routes/publication/+page.svelte | 3 ++ 4 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/lib/components/publications/TableOfContents.svelte b/src/lib/components/publications/TableOfContents.svelte index 50296d2..0f66e4e 100644 --- a/src/lib/components/publications/TableOfContents.svelte +++ b/src/lib/components/publications/TableOfContents.svelte @@ -1,16 +1,13 @@ + {#if displayMode === 'accordion'} {#each entries as entry} @@ -79,7 +75,14 @@ {#each entries as entry} {@const address = entry.address} {@const expanded = toc.expandedMap.get(address) ?? false} - {#if entry.children.length > 0} + {@const isLeaf = toc.leaves.has(address)} + {#if isLeaf} + + onSectionFocused?.(address)} + /> + {:else} {@const childDepth = depth + 1} - {:else} - - handleEntryClick(address, !expanded)} - /> {/if} {/each} diff --git a/src/lib/components/publications/table_of_contents.svelte.ts b/src/lib/components/publications/table_of_contents.svelte.ts index a566f52..95fa0e2 100644 --- a/src/lib/components/publications/table_of_contents.svelte.ts +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -1,4 +1,4 @@ -import { SvelteMap } from 'svelte/reactivity'; +import { SvelteMap, SvelteSet } from 'svelte/reactivity'; import { SveltePublicationTree } from './svelte_publication_tree.svelte.ts'; import type { NDKEvent } from '../../utils/nostrUtils.ts'; import { indexKind } from '../../consts.ts'; @@ -24,6 +24,7 @@ export interface TocEntry { export class TableOfContents { public addressMap: SvelteMap = new SvelteMap(); public expandedMap: SvelteMap = new SvelteMap(); + public leaves: SvelteSet = new SvelteSet(); #root: TocEntry | null = null; #publicationTree: SveltePublicationTree; @@ -186,6 +187,13 @@ export class TableOfContents { continue; } + // Michael J - 16 June 2025 - This duplicates logic in the outer function, but is necessary + // here so that we can determine whether to render an entry as a leaf before it is fully + // resolved. + if (childAddress.split(':')[0] !== indexKind.toString()) { + this.leaves.add(childAddress); + } + // Michael J - 05 June 2025 - The `getChildAddresses` method forces node resolution on the // publication tree. This is acceptable here, because the tree is always resolved // top-down. Therefore, by the time we handle a node's resolution, its parent and @@ -217,6 +225,14 @@ export class TableOfContents { resolveChildren: resolver, }; this.expandedMap.set(address, false); + + // Michael J - 16 June 2025 - We determine whether to add a leaf both here and in the inner + // resolver function. The resolver function is called when entries are resolved by expanding + // a ToC entry, and we'll reach the block below when entries are resolved by the publication + // tree. + if (event.kind !== indexKind) { + this.leaves.add(address); + } return entry; } From 7b441dc0a580ac16c703ec8eda14cc699d762006 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Fri, 27 Jun 2025 15:55:32 -0500 Subject: [PATCH 52/98] Adjust some spacing --- src/routes/publication/+page.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/publication/+page.svelte b/src/routes/publication/+page.svelte index 34ec929..4348871 100644 --- a/src/routes/publication/+page.svelte +++ b/src/routes/publication/+page.svelte @@ -20,8 +20,8 @@ // Get publication metadata for OpenGraph tags let title = $derived( data.indexEvent?.getMatchingTags("title")[0]?.[1] || - data.parser?.getIndexTitle(data.parser?.getRootIndexId()) || - "Alexandria Publication", + data.parser?.getIndexTitle(data.parser?.getRootIndexId()) || + "Alexandria Publication", ); let currentUrl = data.url?.href ?? ""; @@ -33,7 +33,7 @@ ); let summary = $derived( data.indexEvent?.getMatchingTags("summary")[0]?.[1] || - "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages.", + "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages.", ); onDestroy(() => data.parser.reset()); From 166eb237c172cbc0eb959619a28a4f43249d45c8 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 28 Jun 2025 17:42:11 -0500 Subject: [PATCH 53/98] Update SvelteKit --- deno.lock | 517 ++++++++++++++++++++++++++++++++++++++++----------- package.json | 2 +- 2 files changed, 406 insertions(+), 113 deletions(-) diff --git a/deno.lock b/deno.lock index 28f6a0b..0a222bd 100644 --- a/deno.lock +++ b/deno.lock @@ -1,5 +1,5 @@ { - "version": "4", + "version": "5", "specifiers": { "npm:@nostr-dev-kit/ndk-cache-dexie@2.5": "2.5.13_typescript@5.7.3", "npm:@nostr-dev-kit/ndk@2.11": "2.11.2_typescript@5.7.3", @@ -10,6 +10,7 @@ "npm:@sveltejs/adapter-static@3": "3.0.8_@sveltejs+kit@2.17.3__@sveltejs+vite-plugin-svelte@4.0.4___svelte@5.21.0____acorn@8.14.0___vite@5.4.14____@types+node@22.13.9___@types+node@22.13.9__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_@sveltejs+vite-plugin-svelte@4.0.4__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9", "npm:@sveltejs/kit@2": "2.17.3_@sveltejs+vite-plugin-svelte@4.0.4__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9", "npm:@sveltejs/kit@^2.16.0": "2.17.3_@sveltejs+vite-plugin-svelte@4.0.4__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9", + "npm:@sveltejs/kit@^2.22.2": "2.22.2_@sveltejs+vite-plugin-svelte@4.0.4__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_svelte@5.0.5__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_acorn@8.15.0_@types+node@22.13.9", "npm:@sveltejs/vite-plugin-svelte@4": "4.0.4_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9", "npm:@tailwindcss/forms@0.5": "0.5.10_tailwindcss@3.4.17__postcss@8.5.3", "npm:@tailwindcss/typography@0.5": "0.5.16_tailwindcss@3.4.17__postcss@8.5.3", @@ -22,7 +23,7 @@ "npm:bech32@2": "2.0.0", "npm:d3@7.9": "7.9.0_d3-selection@3.0.0", "npm:d3@^7.9.0": "7.9.0_d3-selection@3.0.0", - "npm:eslint-plugin-svelte@2": "2.46.1_eslint@9.21.0_svelte@5.21.0__acorn@8.14.0_postcss@8.5.3", + "npm:eslint-plugin-svelte@2": "2.46.1_eslint@9.21.0_svelte@5.21.0__acorn@8.14.0_postcss@8.5.3_svelte@5.0.5__acorn@8.14.0", "npm:flowbite-svelte-icons@2.1": "2.1.1_svelte@5.0.5__acorn@8.14.0_tailwind-merge@3.3.0", "npm:flowbite-svelte@0.48": "0.48.6_svelte@5.0.5__acorn@8.14.0", "npm:flowbite@2": "2.5.2", @@ -64,7 +65,8 @@ "dependencies": [ "@asciidoctor/core", "yargs@17.3.1" - ] + ], + "bin": true }, "@asciidoctor/core@3.0.4": { "integrity": "sha512-41SDMi7iRRBViPe0L6VWFTe55bv6HEOJeRqMj5+E5wB1YPdUPuTucL4UAESPZM6OWmn4t/5qM5LusXomFUVwVQ==", @@ -90,7 +92,8 @@ "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", "dependencies": [ "@babel/types" - ] + ], + "bin": true }, "@babel/types@7.26.9": { "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", @@ -100,73 +103,119 @@ ] }, "@esbuild/aix-ppc64@0.21.5": { - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==" + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "os": ["aix"], + "cpu": ["ppc64"] }, "@esbuild/android-arm64@0.21.5": { - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==" + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "os": ["android"], + "cpu": ["arm64"] }, "@esbuild/android-arm@0.21.5": { - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==" + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "os": ["android"], + "cpu": ["arm"] }, "@esbuild/android-x64@0.21.5": { - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==" + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "os": ["android"], + "cpu": ["x64"] }, "@esbuild/darwin-arm64@0.21.5": { - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==" + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "os": ["darwin"], + "cpu": ["arm64"] }, "@esbuild/darwin-x64@0.21.5": { - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==" + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "os": ["darwin"], + "cpu": ["x64"] }, "@esbuild/freebsd-arm64@0.21.5": { - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==" + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "os": ["freebsd"], + "cpu": ["arm64"] }, "@esbuild/freebsd-x64@0.21.5": { - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==" + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "os": ["freebsd"], + "cpu": ["x64"] }, "@esbuild/linux-arm64@0.21.5": { - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==" + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "os": ["linux"], + "cpu": ["arm64"] }, "@esbuild/linux-arm@0.21.5": { - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==" + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "os": ["linux"], + "cpu": ["arm"] }, "@esbuild/linux-ia32@0.21.5": { - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==" + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "os": ["linux"], + "cpu": ["ia32"] }, "@esbuild/linux-loong64@0.21.5": { - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==" + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "os": ["linux"], + "cpu": ["loong64"] }, "@esbuild/linux-mips64el@0.21.5": { - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==" + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "os": ["linux"], + "cpu": ["mips64el"] }, "@esbuild/linux-ppc64@0.21.5": { - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==" + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "os": ["linux"], + "cpu": ["ppc64"] }, "@esbuild/linux-riscv64@0.21.5": { - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==" + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "os": ["linux"], + "cpu": ["riscv64"] }, "@esbuild/linux-s390x@0.21.5": { - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==" + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "os": ["linux"], + "cpu": ["s390x"] }, "@esbuild/linux-x64@0.21.5": { - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==" + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "os": ["linux"], + "cpu": ["x64"] }, "@esbuild/netbsd-x64@0.21.5": { - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==" + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "os": ["netbsd"], + "cpu": ["x64"] }, "@esbuild/openbsd-x64@0.21.5": { - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==" + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "os": ["openbsd"], + "cpu": ["x64"] }, "@esbuild/sunos-x64@0.21.5": { - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==" + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "os": ["sunos"], + "cpu": ["x64"] }, "@esbuild/win32-arm64@0.21.5": { - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==" + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "os": ["win32"], + "cpu": ["arm64"] }, "@esbuild/win32-ia32@0.21.5": { - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==" + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "os": ["win32"], + "cpu": ["ia32"] }, "@esbuild/win32-x64@0.21.5": { - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==" + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "os": ["win32"], + "cpu": ["x64"] }, "@eslint-community/eslint-utils@4.4.1_eslint@9.21.0": { "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", @@ -388,7 +437,8 @@ "integrity": "sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==", "dependencies": [ "playwright" - ] + ], + "bin": true }, "@polka/url@1.0.0-next.28": { "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==" @@ -407,6 +457,9 @@ "magic-string", "picomatch@4.0.2", "rollup" + ], + "optionalPeers": [ + "rollup" ] }, "@rollup/plugin-json@6.1.0_rollup@4.34.9": { @@ -414,12 +467,15 @@ "dependencies": [ "@rollup/pluginutils@5.1.4_rollup@4.34.9", "rollup" + ], + "optionalPeers": [ + "rollup" ] }, "@rollup/plugin-node-resolve@15.3.1": { "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", "dependencies": [ - "@rollup/pluginutils@5.1.4", + "@rollup/pluginutils@5.1.4_rollup@4.34.9", "@types/resolve", "deepmerge", "is-module", @@ -435,6 +491,9 @@ "is-module", "resolve", "rollup" + ], + "optionalPeers": [ + "rollup" ] }, "@rollup/pluginutils@5.1.4": { @@ -443,6 +502,9 @@ "@types/estree", "estree-walker@2.0.2", "picomatch@4.0.2" + ], + "optionalPeers": [ + "rollup" ] }, "@rollup/pluginutils@5.1.4_rollup@4.34.9": { @@ -452,64 +514,105 @@ "estree-walker@2.0.2", "picomatch@4.0.2", "rollup" + ], + "optionalPeers": [ + "rollup" ] }, "@rollup/rollup-android-arm-eabi@4.34.9": { - "integrity": "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==" + "integrity": "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==", + "os": ["android"], + "cpu": ["arm"] }, "@rollup/rollup-android-arm64@4.34.9": { - "integrity": "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==" + "integrity": "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==", + "os": ["android"], + "cpu": ["arm64"] }, "@rollup/rollup-darwin-arm64@4.34.9": { - "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==" + "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==", + "os": ["darwin"], + "cpu": ["arm64"] }, "@rollup/rollup-darwin-x64@4.34.9": { - "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==" + "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==", + "os": ["darwin"], + "cpu": ["x64"] }, "@rollup/rollup-freebsd-arm64@4.34.9": { - "integrity": "sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==" + "integrity": "sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==", + "os": ["freebsd"], + "cpu": ["arm64"] }, "@rollup/rollup-freebsd-x64@4.34.9": { - "integrity": "sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==" + "integrity": "sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==", + "os": ["freebsd"], + "cpu": ["x64"] }, "@rollup/rollup-linux-arm-gnueabihf@4.34.9": { - "integrity": "sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==" + "integrity": "sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==", + "os": ["linux"], + "cpu": ["arm"] }, "@rollup/rollup-linux-arm-musleabihf@4.34.9": { - "integrity": "sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==" + "integrity": "sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==", + "os": ["linux"], + "cpu": ["arm"] }, "@rollup/rollup-linux-arm64-gnu@4.34.9": { - "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==" + "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==", + "os": ["linux"], + "cpu": ["arm64"] }, "@rollup/rollup-linux-arm64-musl@4.34.9": { - "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==" + "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==", + "os": ["linux"], + "cpu": ["arm64"] }, "@rollup/rollup-linux-loongarch64-gnu@4.34.9": { - "integrity": "sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==" + "integrity": "sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==", + "os": ["linux"], + "cpu": ["loong64"] }, "@rollup/rollup-linux-powerpc64le-gnu@4.34.9": { - "integrity": "sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==" + "integrity": "sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==", + "os": ["linux"], + "cpu": ["ppc64"] }, "@rollup/rollup-linux-riscv64-gnu@4.34.9": { - "integrity": "sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==" + "integrity": "sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==", + "os": ["linux"], + "cpu": ["riscv64"] }, "@rollup/rollup-linux-s390x-gnu@4.34.9": { - "integrity": "sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==" + "integrity": "sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==", + "os": ["linux"], + "cpu": ["s390x"] }, "@rollup/rollup-linux-x64-gnu@4.34.9": { - "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==" + "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==", + "os": ["linux"], + "cpu": ["x64"] }, "@rollup/rollup-linux-x64-musl@4.34.9": { - "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==" + "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==", + "os": ["linux"], + "cpu": ["x64"] }, "@rollup/rollup-win32-arm64-msvc@4.34.9": { - "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==" + "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==", + "os": ["win32"], + "cpu": ["arm64"] }, "@rollup/rollup-win32-ia32-msvc@4.34.9": { - "integrity": "sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==" + "integrity": "sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==", + "os": ["win32"], + "cpu": ["ia32"] }, "@rollup/rollup-win32-x64-msvc@4.34.9": { - "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==" + "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==", + "os": ["win32"], + "cpu": ["x64"] }, "@scure/base@1.1.1": { "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==" @@ -535,10 +638,16 @@ "@sindresorhus/is@4.6.0": { "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==" }, + "@sveltejs/acorn-typescript@1.0.5_acorn@8.15.0": { + "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "dependencies": [ + "acorn@8.15.0" + ] + }, "@sveltejs/adapter-auto@3.3.1_@sveltejs+kit@2.17.3__@sveltejs+vite-plugin-svelte@4.0.4___svelte@5.21.0____acorn@8.14.0___vite@5.4.14____@types+node@22.13.9___@types+node@22.13.9__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_@sveltejs+vite-plugin-svelte@4.0.4__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9": { "integrity": "sha512-5Sc7WAxYdL6q9j/+D0jJKjGREGlfIevDyHSQ2eNETHcB1TKlQWHcAo8AS8H1QdjNvSXpvOwNjykDUHPEAyGgdQ==", "dependencies": [ - "@sveltejs/kit", + "@sveltejs/kit@2.17.3_@sveltejs+vite-plugin-svelte@4.0.4__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9", "import-meta-resolve" ] }, @@ -548,20 +657,20 @@ "@rollup/plugin-commonjs", "@rollup/plugin-json", "@rollup/plugin-node-resolve@16.0.0_rollup@4.34.9", - "@sveltejs/kit", + "@sveltejs/kit@2.17.3_@sveltejs+vite-plugin-svelte@4.0.4__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9", "rollup" ] }, "@sveltejs/adapter-static@3.0.8_@sveltejs+kit@2.17.3__@sveltejs+vite-plugin-svelte@4.0.4___svelte@5.21.0____acorn@8.14.0___vite@5.4.14____@types+node@22.13.9___@types+node@22.13.9__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_@sveltejs+vite-plugin-svelte@4.0.4__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9": { "integrity": "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==", "dependencies": [ - "@sveltejs/kit" + "@sveltejs/kit@2.17.3_@sveltejs+vite-plugin-svelte@4.0.4__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9" ] }, "@sveltejs/kit@2.17.3_@sveltejs+vite-plugin-svelte@4.0.4__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9": { "integrity": "sha512-GcNaPDr0ti4O/TonPewkML2DG7UVXkSxPN3nPMlpmx0Rs4b2kVP4gymz98WEHlfzPXdd4uOOT1Js26DtieTNBQ==", "dependencies": [ - "@sveltejs/vite-plugin-svelte", + "@sveltejs/vite-plugin-svelte@4.0.4_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9", "@types/cookie", "cookie", "devalue", @@ -575,21 +684,53 @@ "sirv", "svelte@5.21.0_acorn@8.14.0", "vite" - ] + ], + "bin": true + }, + "@sveltejs/kit@2.22.2_@sveltejs+vite-plugin-svelte@4.0.4__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_svelte@5.0.5__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_acorn@8.15.0_@types+node@22.13.9": { + "integrity": "sha512-2MvEpSYabUrsJAoq5qCOBGAlkICjfjunrnLcx3YAk2XV7TvAIhomlKsAgR4H/4uns5rAfYmj7Wet5KRtc8dPIg==", + "dependencies": [ + "@sveltejs/acorn-typescript", + "@sveltejs/vite-plugin-svelte@4.0.4_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9_svelte@5.0.5__acorn@8.14.0", + "@types/cookie", + "acorn@8.15.0", + "cookie", + "devalue", + "esm-env", + "kleur", + "magic-string", + "mrmime", + "sade", + "set-cookie-parser", + "sirv", + "svelte@5.0.5_acorn@8.14.0", + "vite", + "vitefu" + ], + "bin": true }, "@sveltejs/vite-plugin-svelte-inspector@3.0.1_@sveltejs+vite-plugin-svelte@4.0.4__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9": { "integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==", "dependencies": [ - "@sveltejs/vite-plugin-svelte", + "@sveltejs/vite-plugin-svelte@4.0.4_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9_svelte@5.0.5__acorn@8.14.0", "debug@4.4.0", "svelte@5.21.0_acorn@8.14.0", "vite" ] }, + "@sveltejs/vite-plugin-svelte-inspector@3.0.1_@sveltejs+vite-plugin-svelte@4.0.4__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9_svelte@5.0.5__acorn@8.14.0": { + "integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==", + "dependencies": [ + "@sveltejs/vite-plugin-svelte@4.0.4_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9_svelte@5.0.5__acorn@8.14.0", + "debug@4.4.0", + "svelte@5.0.5_acorn@8.14.0", + "vite" + ] + }, "@sveltejs/vite-plugin-svelte@4.0.4_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9": { "integrity": "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==", "dependencies": [ - "@sveltejs/vite-plugin-svelte-inspector", + "@sveltejs/vite-plugin-svelte-inspector@3.0.1_@sveltejs+vite-plugin-svelte@4.0.4__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9", "debug@4.4.0", "deepmerge", "kleur", @@ -599,6 +740,19 @@ "vitefu" ] }, + "@sveltejs/vite-plugin-svelte@4.0.4_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9_svelte@5.0.5__acorn@8.14.0": { + "integrity": "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==", + "dependencies": [ + "@sveltejs/vite-plugin-svelte-inspector@3.0.1_@sveltejs+vite-plugin-svelte@4.0.4__svelte@5.21.0___acorn@8.14.0__vite@5.4.14___@types+node@22.13.9__@types+node@22.13.9_svelte@5.21.0__acorn@8.14.0_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9_svelte@5.0.5__acorn@8.14.0", + "debug@4.4.0", + "deepmerge", + "kleur", + "magic-string", + "svelte@5.0.5_acorn@8.14.0", + "vite", + "vitefu" + ] + }, "@tailwindcss/forms@0.5.10_tailwindcss@3.4.17__postcss@8.5.3": { "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", "dependencies": [ @@ -828,6 +982,9 @@ "estree-walker@3.0.3", "magic-string", "vite" + ], + "optionalPeers": [ + "vite" ] }, "@vitest/pretty-format@3.1.4": { @@ -884,10 +1041,16 @@ ] }, "acorn@7.4.1": { - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "bin": true }, "acorn@8.14.0": { - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==" + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "bin": true + }, + "acorn@8.15.0": { + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "bin": true }, "ajv@6.12.6": { "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", @@ -956,7 +1119,8 @@ "handlebars", "nunjucks", "pug" - ] + ], + "bin": true }, "assert-never@1.4.0": { "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==" @@ -977,7 +1141,8 @@ "picocolors", "postcss", "postcss-value-parser" - ] + ], + "bin": true }, "axobject-query@4.1.0": { "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==" @@ -1023,13 +1188,15 @@ "electron-to-chromium", "node-releases", "update-browserslist-db" - ] + ], + "bin": true }, "bufferutil@4.0.9": { "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", "dependencies": [ "node-gyp-build" - ] + ], + "scripts": true }, "cac@6.7.14": { "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==" @@ -1094,12 +1261,14 @@ "dependencies": [ "anymatch", "braces", - "fsevents@2.3.3", "glob-parent@5.1.2", "is-binary-path", "is-glob", "normalize-path", "readdirp@3.6.0" + ], + "optionalDependencies": [ + "fsevents@2.3.3" ] }, "chokidar@4.0.3": { @@ -1170,7 +1339,8 @@ ] }, "cssesc@3.0.0": { - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": true }, "d3-array@3.2.4": { "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", @@ -1228,7 +1398,8 @@ "commander@7.2.0", "iconv-lite", "rw" - ] + ], + "bin": true }, "d3-ease@3.0.1": { "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" @@ -1444,7 +1615,8 @@ "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dependencies": [ "jake" - ] + ], + "bin": true }, "electron-to-chromium@1.5.111": { "integrity": "sha512-vJyJlO95wQRAw6K2ZGF/8nol7AcbCOnp8S6H91mwOOBbXoS9seDBYxCTPYAFsvXLxl3lc0jLXXe9GLxC4nXVog==" @@ -1480,7 +1652,8 @@ "es6-symbol", "esniff", "next-tick" - ] + ], + "scripts": true }, "es6-iterator@2.0.3": { "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", @@ -1499,7 +1672,7 @@ }, "esbuild@0.21.5": { "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dependencies": [ + "optionalDependencies": [ "@esbuild/aix-ppc64", "@esbuild/android-arm", "@esbuild/android-arm64", @@ -1523,7 +1696,9 @@ "@esbuild/win32-arm64", "@esbuild/win32-ia32", "@esbuild/win32-x64" - ] + ], + "scripts": true, + "bin": true }, "escalade@3.2.0": { "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" @@ -1553,7 +1728,31 @@ "postcss-selector-parser@6.1.2", "semver", "svelte@5.21.0_acorn@8.14.0", - "svelte-eslint-parser" + "svelte-eslint-parser@0.43.0_svelte@5.21.0__acorn@8.14.0_postcss@8.5.3" + ], + "optionalPeers": [ + "svelte@^3.37.0 || ^4.0.0 || ^5.0.0" + ] + }, + "eslint-plugin-svelte@2.46.1_eslint@9.21.0_svelte@5.21.0__acorn@8.14.0_postcss@8.5.3_svelte@5.0.5__acorn@8.14.0": { + "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", + "dependencies": [ + "@eslint-community/eslint-utils", + "@jridgewell/sourcemap-codec", + "eslint", + "eslint-compat-utils", + "esutils", + "known-css-properties", + "postcss", + "postcss-load-config@3.1.4_postcss@8.5.3", + "postcss-safe-parser", + "postcss-selector-parser@6.1.2", + "semver", + "svelte@5.0.5_acorn@8.14.0", + "svelte-eslint-parser@0.43.0_svelte@5.21.0__acorn@8.14.0_postcss@8.5.3_svelte@5.0.5__acorn@8.14.0" + ], + "optionalPeers": [ + "svelte@5.0.5_acorn@8.14.0" ] }, "eslint-scope@7.2.2": { @@ -1613,7 +1812,8 @@ "minimatch@3.1.2", "natural-compare", "optionator" - ] + ], + "bin": true }, "esm-env@1.2.2": { "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" @@ -1721,12 +1921,18 @@ "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", "dependencies": [ "picomatch@4.0.2" + ], + "optionalPeers": [ + "picomatch@4.0.2" ] }, "fdir@6.4.4_picomatch@4.0.2": { "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "dependencies": [ "picomatch@4.0.2" + ], + "optionalPeers": [ + "picomatch@4.0.2" ] }, "file-entry-cache@8.0.0": { @@ -1840,10 +2046,14 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "fsevents@2.3.2": { - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==" + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "os": ["darwin"], + "scripts": true }, "fsevents@2.3.3": { - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==" + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "os": ["darwin"], + "scripts": true }, "function-bind@1.1.2": { "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" @@ -1894,7 +2104,8 @@ "minipass", "package-json-from-dist", "path-scurry" - ] + ], + "bin": true }, "glob@8.1.0": { "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", @@ -1904,7 +2115,8 @@ "inherits", "minimatch@5.1.6", "once" - ] + ], + "deprecated": true }, "globals@14.0.0": { "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==" @@ -1918,9 +2130,12 @@ "minimist", "neo-async", "source-map", - "uglify-js", "wordwrap" - ] + ], + "optionalDependencies": [ + "uglify-js" + ], + "bin": true }, "has-flag@4.0.0": { "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" @@ -1941,7 +2156,8 @@ ] }, "he@1.2.0": { - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": true }, "highlight.js@11.11.1": { "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==" @@ -1973,7 +2189,8 @@ "dependencies": [ "once", "wrappy" - ] + ], + "deprecated": true }, "inherits@2.0.4": { "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" @@ -2051,7 +2268,9 @@ "jackspeak@3.4.3": { "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dependencies": [ - "@isaacs/cliui", + "@isaacs/cliui" + ], + "optionalDependencies": [ "@pkgjs/parseargs" ] }, @@ -2062,10 +2281,12 @@ "chalk", "filelist", "minimatch@3.1.2" - ] + ], + "bin": true }, "jiti@1.21.7": { - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==" + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "bin": true }, "js-stringify@1.0.2": { "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==" @@ -2074,7 +2295,8 @@ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dependencies": [ "argparse" - ] + ], + "bin": true }, "json-buffer@3.0.1": { "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" @@ -2176,7 +2398,8 @@ ] }, "mini-svg-data-uri@1.4.4": { - "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==" + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "bin": true }, "minimatch@3.1.2": { "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", @@ -2223,7 +2446,8 @@ ] }, "nanoid@3.3.8": { - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==" + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "bin": true }, "natural-compare@1.4.0": { "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" @@ -2244,7 +2468,8 @@ ] }, "node-gyp-build@4.8.4": { - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==" + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "bin": true }, "node-releases@2.0.19": { "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" @@ -2264,7 +2489,12 @@ "@scure/base@1.1.1", "@scure/bip32", "@scure/bip39", - "nostr-wasm", + "typescript" + ], + "optionalDependencies": [ + "nostr-wasm" + ], + "optionalPeers": [ "typescript" ] }, @@ -2277,7 +2507,8 @@ "a-sync-waterfall", "asap", "commander@5.1.0" - ] + ], + "bin": true }, "object-assign@4.1.1": { "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" @@ -2376,14 +2607,18 @@ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==" }, "playwright-core@1.50.1": { - "integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==" + "integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==", + "bin": true }, "playwright@1.50.1": { "integrity": "sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==", "dependencies": [ - "fsevents@2.3.2", "playwright-core" - ] + ], + "optionalDependencies": [ + "fsevents@2.3.2" + ], + "bin": true }, "pngjs@5.0.0": { "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" @@ -2410,6 +2645,9 @@ "lilconfig@2.1.0", "postcss", "yaml@1.10.2" + ], + "optionalPeers": [ + "postcss" ] }, "postcss-load-config@4.0.2_postcss@8.5.3": { @@ -2418,6 +2656,9 @@ "lilconfig@3.1.3", "postcss", "yaml@2.7.0" + ], + "optionalPeers": [ + "postcss" ] }, "postcss-load-config@6.0.1_postcss@8.5.3": { @@ -2425,6 +2666,9 @@ "dependencies": [ "lilconfig@3.1.3", "postcss" + ], + "optionalPeers": [ + "postcss" ] }, "postcss-nested@6.2.0_postcss@8.5.3": { @@ -2482,7 +2726,8 @@ ] }, "prettier@3.5.3": { - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==" + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "bin": true }, "promise@7.3.1": { "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", @@ -2587,7 +2832,8 @@ "dijkstrajs", "pngjs", "yargs@15.4.1" - ] + ], + "bin": true }, "queue-microtask@1.2.3": { "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" @@ -2622,7 +2868,8 @@ "is-core-module", "path-parse", "supports-preserve-symlinks-flag" - ] + ], + "bin": true }, "reusify@1.1.0": { "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==" @@ -2633,6 +2880,9 @@ "rollup@4.34.9": { "integrity": "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==", "dependencies": [ + "@types/estree" + ], + "optionalDependencies": [ "@rollup/rollup-android-arm-eabi", "@rollup/rollup-android-arm64", "@rollup/rollup-darwin-arm64", @@ -2652,9 +2902,9 @@ "@rollup/rollup-win32-arm64-msvc", "@rollup/rollup-win32-ia32-msvc", "@rollup/rollup-win32-x64-msvc", - "@types/estree", "fsevents@2.3.3" - ] + ], + "bin": true }, "run-parallel@1.2.0": { "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", @@ -2675,7 +2925,8 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver@7.7.1": { - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==" + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": true }, "set-blocking@2.0.0": { "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" @@ -2765,7 +3016,8 @@ "mz", "pirates", "ts-interface-checker" - ] + ], + "bin": true }, "supports-color@7.2.0": { "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", @@ -2786,7 +3038,8 @@ "sade", "svelte@5.21.0_acorn@8.14.0", "typescript" - ] + ], + "bin": true }, "svelte-eslint-parser@0.43.0_svelte@5.21.0__acorn@8.14.0_postcss@8.5.3": { "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", @@ -2797,6 +3050,23 @@ "postcss", "postcss-scss", "svelte@5.21.0_acorn@8.14.0" + ], + "optionalPeers": [ + "svelte@^3.37.0 || ^4.0.0 || ^5.0.0" + ] + }, + "svelte-eslint-parser@0.43.0_svelte@5.21.0__acorn@8.14.0_postcss@8.5.3_svelte@5.0.5__acorn@8.14.0": { + "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", + "dependencies": [ + "eslint-scope@7.2.2", + "eslint-visitor-keys@3.4.3", + "espree@9.6.1_acorn@8.14.0", + "postcss", + "postcss-scss", + "svelte@5.0.5_acorn@8.14.0" + ], + "optionalPeers": [ + "svelte@5.0.5_acorn@8.14.0" ] }, "svelte@5.0.5_acorn@8.14.0": { @@ -2913,7 +3183,8 @@ "postcss-selector-parser@6.1.2", "resolve", "sucrase" - ] + ], + "bin": true }, "thenify-all@1.6.0": { "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", @@ -2992,10 +3263,12 @@ "integrity": "sha512-Jp57Qyy8wXeMkdNuZiglE6v2Cypg13eDA1chHwDG6kq51X7gk4K7P7HaDdzZKCxkegXkVHNcPD0n5aW6OZH3aA==" }, "typescript@5.7.3": { - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==" + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "bin": true }, "uglify-js@3.19.3": { - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==" + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "bin": true }, "undici-types@6.20.0": { "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" @@ -3012,7 +3285,8 @@ "browserslist", "escalade", "picocolors" - ] + ], + "bin": true }, "uri-js@4.4.1": { "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", @@ -3024,7 +3298,8 @@ "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", "dependencies": [ "node-gyp-build" - ] + ], + "scripts": true }, "utf8-buffer@1.0.0": { "integrity": "sha512-ueuhzvWnp5JU5CiGSY4WdKbiN/PO2AZ/lpeLiz2l38qwdLy/cW40XobgyuIWucNyum0B33bVB0owjFCeGBSLqg==" @@ -3040,22 +3315,32 @@ "es-module-lexer", "pathe", "vite" - ] + ], + "bin": true }, "vite@5.4.14_@types+node@22.13.9": { "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", "dependencies": [ "@types/node@22.13.9", "esbuild", - "fsevents@2.3.3", "postcss", "rollup" - ] + ], + "optionalDependencies": [ + "fsevents@2.3.3" + ], + "optionalPeers": [ + "@types/node@22.13.9" + ], + "bin": true }, "vitefu@1.0.6_vite@5.4.14__@types+node@22.13.9_@types+node@22.13.9": { "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", "dependencies": [ "vite" + ], + "optionalPeers": [ + "vite" ] }, "vitest@3.1.4_@types+node@22.13.9_vite@5.4.14__@types+node@22.13.9": { @@ -3083,7 +3368,11 @@ "vite", "vite-node", "why-is-node-running" - ] + ], + "optionalPeers": [ + "@types/node@22.13.9" + ], + "bin": true }, "void-elements@3.1.0": { "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" @@ -3113,14 +3402,16 @@ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dependencies": [ "isexe" - ] + ], + "bin": true }, "why-is-node-running@2.3.0": { "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dependencies": [ "siginfo", "stackback" - ] + ], + "bin": true }, "with@7.0.2": { "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", @@ -3171,13 +3462,15 @@ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, "yaeti@0.0.6": { - "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==" + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", + "deprecated": true }, "yaml@1.10.2": { "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" }, "yaml@2.7.0": { - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==" + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "bin": true }, "yargs-parser@18.1.3": { "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", @@ -3257,7 +3550,7 @@ "npm:@sveltejs/adapter-auto@3", "npm:@sveltejs/adapter-node@^5.2.12", "npm:@sveltejs/adapter-static@3", - "npm:@sveltejs/kit@^2.16.0", + "npm:@sveltejs/kit@^2.22.2", "npm:@sveltejs/vite-plugin-svelte@4", "npm:@tailwindcss/forms@0.5", "npm:@tailwindcss/typography@0.5", diff --git a/package.json b/package.json index 3df9454..a200816 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@sveltejs/adapter-auto": "3.x", "@sveltejs/adapter-node": "^5.2.12", "@sveltejs/adapter-static": "3.x", - "@sveltejs/kit": "^2.16.0", + "@sveltejs/kit": "^2.22.2", "@sveltejs/vite-plugin-svelte": "4.x", "@types/d3": "^7.4.3", "@types/he": "1.2.x", From d299017b9eabedee6c5165cb1e5169d1382bdab3 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 28 Jun 2025 17:42:47 -0500 Subject: [PATCH 54/98] Jump to element via ToC --- .../components/publications/Publication.svelte | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/lib/components/publications/Publication.svelte b/src/lib/components/publications/Publication.svelte index a32bdb9..c5efd96 100644 --- a/src/lib/components/publications/Publication.svelte +++ b/src/lib/components/publications/Publication.svelte @@ -22,6 +22,7 @@ import Interactions from "$components/util/Interactions.svelte"; import type { SveltePublicationTree } from "./svelte_publication_tree.svelte"; import TableOfContents from "./TableOfContents.svelte"; + import { goto } from "$app/navigation"; let { rootAddress, publicationType, indexEvent } = $props<{ rootAddress: string; @@ -33,8 +34,6 @@ // #region Loading - // TODO: Test load handling. - let leaves = $state>([]); let isLoading = $state(false); let isDone = $state(false); @@ -82,7 +81,8 @@ // #endregion - // region Columns visibility + // #region Columns visibility + let currentBlog: null | string = $state(null); let currentBlogEvent: null | NDKEvent = $state(null); const isLeaf = $derived(indexEvent.kind === 30041); @@ -123,6 +123,10 @@ return currentBlog && currentBlogEvent && window.innerWidth < 1140; } + // #endregion + + // #region Lifecycle hooks + onDestroy(() => { // reset visibility publicationColumnVisibility.reset(); @@ -157,6 +161,8 @@ observer.disconnect(); }; }); + + // #endregion @@ -170,6 +176,9 @@ depth={2} onSectionFocused={(address: string) => { publicationTree.setBookmark(address); + goto(`#${address}`, { + replaceState: true, + }); }} /> From 5fb32e35b23906aa531fa54118a5c7f73b402a26 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 28 Jun 2025 17:44:08 -0500 Subject: [PATCH 55/98] Formatting and SvelteKit 2/Svelte 5 upgrades --- src/lib/components/publications/TableOfContents.svelte | 7 +++++-- src/routes/+layout.svelte | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/lib/components/publications/TableOfContents.svelte b/src/lib/components/publications/TableOfContents.svelte index 9079cd6..09280b8 100644 --- a/src/lib/components/publications/TableOfContents.svelte +++ b/src/lib/components/publications/TableOfContents.svelte @@ -1,5 +1,8 @@ From 1af98ec9d607a64f8c8e3a5f26b2b0efccdc77c5 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 28 Jun 2025 18:44:54 -0500 Subject: [PATCH 58/98] Remove unused component --- src/lib/components/util/TocToggle.svelte | 143 ----------------------- 1 file changed, 143 deletions(-) delete mode 100644 src/lib/components/util/TocToggle.svelte diff --git a/src/lib/components/util/TocToggle.svelte b/src/lib/components/util/TocToggle.svelte deleted file mode 100644 index 8fe9626..0000000 --- a/src/lib/components/util/TocToggle.svelte +++ /dev/null @@ -1,143 +0,0 @@ - - - -{#if $publicationColumnVisibility.toc} - - - - Table of contents -

(This ToC is only for demo purposes, and is not fully-functional.)

- {#each tocItems as item} - - {/each} -
-
-
-{/if} \ No newline at end of file From 9afcd37aafae4374495410825632e6f585edd08a Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 28 Jun 2025 18:45:16 -0500 Subject: [PATCH 59/98] Remove unneeded load actions for publication page --- src/routes/publication/+page.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/routes/publication/+page.ts b/src/routes/publication/+page.ts index b100f70..d154cd8 100644 --- a/src/routes/publication/+page.ts +++ b/src/routes/publication/+page.ts @@ -83,10 +83,11 @@ async function fetchEventByDTag(ndk: any, dTag: string): Promise { } } +// TODO: Use path params instead of query params. export const load: Load = async ({ url, parent }: { url: URL; parent: () => Promise }) => { const id = url.searchParams.get('id'); const dTag = url.searchParams.get('d'); - const { ndk, parser } = await parent(); + const { ndk } = await parent(); if (!id && !dTag) { throw error(400, 'No publication root event ID or d tag provided.'); @@ -98,12 +99,9 @@ export const load: Load = async ({ url, parent }: { url: URL; parent: () => Prom : await fetchEventByDTag(ndk, dTag!); const publicationType = getMatchingTags(indexEvent, 'type')[0]?.[1]; - const fetchPromise = parser.fetch(indexEvent); return { - waitable: fetchPromise, publicationType, indexEvent, - url, }; }; From 8b2ac0e48f30b5e57a73d1c7e5443ae30a5e11c8 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 28 Jun 2025 18:45:41 -0500 Subject: [PATCH 60/98] Simplify `getMatchingTags` invocation --- src/lib/components/util/ArticleNav.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/util/ArticleNav.svelte b/src/lib/components/util/ArticleNav.svelte index 11f804c..916a298 100644 --- a/src/lib/components/util/ArticleNav.svelte +++ b/src/lib/components/util/ArticleNav.svelte @@ -16,7 +16,7 @@ }>(); let title: string = $derived(indexEvent.getMatchingTags('title')[0]?.[1]); - let author: string = $derived(indexEvent.getMatchingTags(event, 'author')[0]?.[1] ?? 'unknown'); + let author: string = $derived(indexEvent.getMatchingTags('author')[0]?.[1] ?? 'unknown'); let pubkey: string = $derived(indexEvent.getMatchingTags('p')[0]?.[1] ?? null); let isLeaf: boolean = $derived(indexEvent.kind === 30041); From 700dec88752c2772eca383ea5f60373f4f62eaac Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 28 Jun 2025 18:46:03 -0500 Subject: [PATCH 61/98] Proxy bookmark observers through `SveltePublicationTree` --- .../svelte_publication_tree.svelte.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/lib/components/publications/svelte_publication_tree.svelte.ts b/src/lib/components/publications/svelte_publication_tree.svelte.ts index 9969ed7..0c1eefc 100644 --- a/src/lib/components/publications/svelte_publication_tree.svelte.ts +++ b/src/lib/components/publications/svelte_publication_tree.svelte.ts @@ -7,11 +7,13 @@ export class SveltePublicationTree { #publicationTree: PublicationTree; #nodeResolvedObservers: Array<(address: string) => void> = []; + #bookmarkMovedObservers: Array<(address: string) => void> = []; constructor(rootEvent: NDKEvent, ndk: NDK) { this.#publicationTree = new PublicationTree(rootEvent, ndk); this.#publicationTree.onNodeResolved(this.#handleNodeResolved); + this.#publicationTree.onBookmarkMoved(this.#handleBookmarkMoved); } // #region Proxied Public Methods @@ -48,6 +50,14 @@ export class SveltePublicationTree { this.#nodeResolvedObservers.push(observer); } + /** + * Registers an observer function that is invoked whenever the bookmark is moved. + * @param observer The observer function. + */ + onBookmarkMoved(observer: (address: string) => void) { + this.#bookmarkMovedObservers.push(observer); + } + // #endregion // #region Proxied Async Iterator Methods @@ -83,5 +93,19 @@ export class SveltePublicationTree { } } + /** + * Observer function that is invoked whenever the bookmark is moved on the publication tree. + * + * @param address The address of the new bookmark. + * + * This member is declared as an arrow function to ensure that the correct `this` context is + * used when the function is invoked in this class's constructor. + */ + #handleBookmarkMoved = (address: string) => { + for (const observer of this.#bookmarkMovedObservers) { + observer(address); + } + } + // #endregion } \ No newline at end of file From b8b9deb727afbb802323bf63ef4590c913ebc121 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 28 Jun 2025 18:46:34 -0500 Subject: [PATCH 62/98] Don't invoke `goto` in `onSectionFocused` --- src/lib/components/publications/Publication.svelte | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/lib/components/publications/Publication.svelte b/src/lib/components/publications/Publication.svelte index c5efd96..a9daf52 100644 --- a/src/lib/components/publications/Publication.svelte +++ b/src/lib/components/publications/Publication.svelte @@ -22,7 +22,6 @@ import Interactions from "$components/util/Interactions.svelte"; import type { SveltePublicationTree } from "./svelte_publication_tree.svelte"; import TableOfContents from "./TableOfContents.svelte"; - import { goto } from "$app/navigation"; let { rootAddress, publicationType, indexEvent } = $props<{ rootAddress: string; @@ -174,12 +173,7 @@ displayMode='sidebar' rootAddress={rootAddress} depth={2} - onSectionFocused={(address: string) => { - publicationTree.setBookmark(address); - goto(`#${address}`, { - replaceState: true, - }); - }} + onSectionFocused={(address: string) => publicationTree.setBookmark(address)} /> {/if} From ddb56d07ecc893c97f8622edc6455e69eb648042 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 28 Jun 2025 18:46:57 -0500 Subject: [PATCH 63/98] Clean up key and await in publication page --- src/routes/publication/+page.svelte | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/routes/publication/+page.svelte b/src/routes/publication/+page.svelte index 4348871..077e866 100644 --- a/src/routes/publication/+page.svelte +++ b/src/routes/publication/+page.svelte @@ -59,22 +59,16 @@
-{#key data} - + +
+ -{/key} - -
- {#await data.waitable} - - {:then} - - {/await}
From 458004bd9f744b9bedff1ec3e39497e67350ce93 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 28 Jun 2025 18:48:18 -0500 Subject: [PATCH 64/98] First pass at use of IndexedDB for publication bookmarks Scrolling to the bookmark doesn't yet work, so the IndexedDB usage is moot until that is fixed. IndexedDB access should be moved into a service layer before merging. --- src/routes/publication/+page.svelte | 54 +++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/routes/publication/+page.svelte b/src/routes/publication/+page.svelte index 077e866..5e92cbd 100644 --- a/src/routes/publication/+page.svelte +++ b/src/routes/publication/+page.svelte @@ -2,12 +2,14 @@ import Publication from "$lib/components/publications/Publication.svelte"; import { TextPlaceholder } from "flowbite-svelte"; import type { PageProps } from "./$types"; - import { onDestroy, setContext } from "svelte"; + import { onDestroy, onMount, setContext } from "svelte"; import Processor from "asciidoctor"; import ArticleNav from "$components/util/ArticleNav.svelte"; import { SveltePublicationTree } from "$lib/components/publications/svelte_publication_tree.svelte"; import { TableOfContents } from "$lib/components/publications/table_of_contents.svelte"; import { page } from "$app/state"; + import { goto } from "$app/navigation"; + let { data }: PageProps = $props(); const publicationTree = new SveltePublicationTree(data.indexEvent, data.ndk); @@ -23,7 +25,7 @@ data.parser?.getIndexTitle(data.parser?.getRootIndexId()) || "Alexandria Publication", ); - let currentUrl = data.url?.href ?? ""; + let currentUrl = $derived(`${page.url.origin}${page.url.pathname}${page.url.search}`); // Get image and summary from the event tags if available // If image unavailable, use the Alexandria default pic. @@ -36,6 +38,54 @@ "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages.", ); + publicationTree.onBookmarkMoved(address => { + goto(`#${address}`, { + replaceState: true, + }); + + // TODO: Extract IndexedDB interaction to a service layer. + // Store bookmark in IndexedDB + const db = indexedDB.open('alexandria', 1); + db.onupgradeneeded = () => { + const objectStore = db.result.createObjectStore('bookmarks', { keyPath: 'key' }); + }; + + db.onsuccess = () => { + const transaction = db.result.transaction(['bookmarks'], 'readwrite'); + const store = transaction.objectStore('bookmarks'); + const bookmarkKey = `${data.indexEvent.tagAddress()}`; + store.put({ key: bookmarkKey, address }); + }; + }); + + onMount(() => { + // TODO: Extract IndexedDB interaction to a service layer. + // Read bookmark from IndexedDB + const db = indexedDB.open('alexandria', 1); + db.onupgradeneeded = () => { + const objectStore = db.result.createObjectStore('bookmarks', { keyPath: 'key' }); + }; + + db.onsuccess = () => { + const transaction = db.result.transaction(['bookmarks'], 'readonly'); + const store = transaction.objectStore('bookmarks'); + const bookmarkKey = `${data.indexEvent.tagAddress()}`; + const request = store.get(bookmarkKey); + + request.onsuccess = () => { + if (request.result?.address) { + // Set the bookmark in the publication tree + publicationTree.setBookmark(request.result.address); + + // Jump to the bookmarked element + goto(`#${request.result.address}`, { + replaceState: true, + }); + } + }; + }; + }); + onDestroy(() => data.parser.reset()); From 0fc90e5cebd0759219d294a096b4ff2287711b1e Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 30 Jun 2025 09:23:39 -0500 Subject: [PATCH 65/98] Use ToC hrefs for proper navigation --- src/lib/components/publications/Publication.svelte | 5 +++-- src/lib/components/publications/TableOfContents.svelte | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/components/publications/Publication.svelte b/src/lib/components/publications/Publication.svelte index a9daf52..f183806 100644 --- a/src/lib/components/publications/Publication.svelte +++ b/src/lib/components/publications/Publication.svelte @@ -167,8 +167,9 @@ {#if publicationType !== 'blog' || !isLeaf} {#if $publicationColumnVisibility.toc} - - + - {#if displayMode === 'accordion'} @@ -80,9 +79,9 @@ {@const expanded = toc.expandedMap.get(address) ?? false} {@const isLeaf = toc.leaves.has(address)} {#if isLeaf} - onSectionFocused?.(address)} /> {:else} From b492a33cc7b92608a66c6457350c096dd6e0db71 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 30 Jun 2025 09:23:49 -0500 Subject: [PATCH 66/98] Fit ToC styling to theme --- src/app.css | 14 -------------- src/lib/components/publications/Publication.svelte | 5 +++++ .../components/publications/TableOfContents.svelte | 4 ++++ src/lib/components/util/ArticleNav.svelte | 9 +++++++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/app.css b/src/app.css index 21e1a48..572628b 100644 --- a/src/app.css +++ b/src/app.css @@ -154,20 +154,6 @@ @apply text-gray-800 hover:text-primary-400 dark:text-gray-300 dark:hover:text-primary-500; } - /* Sidebar */ - aside.sidebar-leather { - @apply fixed md:sticky top-[130px] sm:top-[146px] h-[calc(100vh-130px)] sm:h-[calc(100vh-146px)] z-10; - @apply bg-primary-0 dark:bg-primary-1000 px-5 w-full sm:w-auto sm:max-w-xl; - } - - aside.sidebar-leather > div { - @apply bg-primary-50 dark:bg-gray-800 h-full px-5 py-0; - } - - a.sidebar-item-leather { - @apply hover:bg-primary-100 dark:hover:bg-gray-800; - } - div.skeleton-leather div { @apply bg-primary-100 dark:bg-primary-800; } diff --git a/src/lib/components/publications/Publication.svelte b/src/lib/components/publications/Publication.svelte index f183806..eff97b0 100644 --- a/src/lib/components/publications/Publication.svelte +++ b/src/lib/components/publications/Publication.svelte @@ -37,6 +37,7 @@ let isLoading = $state(false); let isDone = $state(false); let lastElementRef = $state(null); + let activeAddress = $state(null); let observer: IntersectionObserver; @@ -169,7 +170,11 @@ {#if $publicationColumnVisibility.toc} + {:else} + + {#each entries as entry} {@const address = entry.address} @@ -82,12 +84,14 @@ onSectionFocused?.(address)} /> {:else} {@const childDepth = depth + 1} expanded, (open) => setEntryExpanded(address, open) diff --git a/src/lib/components/util/ArticleNav.svelte b/src/lib/components/util/ArticleNav.svelte index 916a298..24d2137 100644 --- a/src/lib/components/util/ArticleNav.svelte +++ b/src/lib/components/util/ArticleNav.svelte @@ -139,8 +139,13 @@ {/if} {/if}
-
-

{title}by {@render userBadge(pubkey, author)}

+
+

+ {title} +

+

+ by {@render userBadge(pubkey, author)} +

{#if $publicationColumnVisibility.inner} From 6a0a35f091f2e1618b80f9a10d8d10a7b09ca309 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 1 Jul 2025 08:06:29 -0500 Subject: [PATCH 67/98] Add border styling to ToC sidebar --- src/lib/components/publications/Publication.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/publications/Publication.svelte b/src/lib/components/publications/Publication.svelte index eff97b0..3bfdace 100644 --- a/src/lib/components/publications/Publication.svelte +++ b/src/lib/components/publications/Publication.svelte @@ -170,7 +170,7 @@ {#if $publicationColumnVisibility.toc} From eae495fa7776bd74a8f5b221078fcd34dbe65f73 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 1 Jul 2025 08:41:27 -0500 Subject: [PATCH 68/98] Ensure correct sorting of children of tree nodes --- .../publications/table_of_contents.svelte.ts | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/lib/components/publications/table_of_contents.svelte.ts b/src/lib/components/publications/table_of_contents.svelte.ts index 95fa0e2..afe8920 100644 --- a/src/lib/components/publications/table_of_contents.svelte.ts +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -181,7 +181,7 @@ export class TableOfContents { return; } - const childAddresses = await this.#publicationTree.getChildAddresses(address); + const childAddresses = await this.#publicationTree.getChildAddresses(entry.address); for (const childAddress of childAddresses) { if (!childAddress) { continue; @@ -205,6 +205,8 @@ export class TableOfContents { this.addressMap.set(childAddress, childEntry); } + await this.#matchChildrenToTagOrder(entry); + entry.childrenResolved = true; } @@ -237,6 +239,31 @@ export class TableOfContents { return entry; } + /** + * Reorders the children of a ToC entry to match the order of 'a' tags in the corresponding + * Nostr index event. + * + * @param entry The ToC entry to reorder. + */ + async #matchChildrenToTagOrder(entry: TocEntry) { + const parentEvent = await this.#publicationTree.getEvent(entry.address); + if (parentEvent?.kind === indexKind) { + const tagOrder = parentEvent.getMatchingTags('a').map(tag => tag[1]); + const addressToOrdinal = new Map(); + + // Build map of addresses to their ordinals from tag order + tagOrder.forEach((address, index) => { + addressToOrdinal.set(address, index); + }); + + entry.children.sort((a, b) => { + const aOrdinal = addressToOrdinal.get(a.address) ?? Number.MAX_SAFE_INTEGER; + const bOrdinal = addressToOrdinal.get(b.address) ?? Number.MAX_SAFE_INTEGER; + return aOrdinal - bOrdinal; + }); + } + } + #buildTocEntryFromResolvedNode(address: string) { if (this.addressMap.has(address)) { return; From f0d35e0f29184c1c1b7f9e6f4d76a5e7367b558c Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 7 Jul 2025 09:04:16 -0500 Subject: [PATCH 69/98] Add time complexity notes on child sort function --- src/lib/components/publications/table_of_contents.svelte.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/components/publications/table_of_contents.svelte.ts b/src/lib/components/publications/table_of_contents.svelte.ts index afe8920..118a4ec 100644 --- a/src/lib/components/publications/table_of_contents.svelte.ts +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -244,6 +244,9 @@ export class TableOfContents { * Nostr index event. * * @param entry The ToC entry to reorder. + * + * This function has a time complexity of `O(n log n)`, where `n` is the number of children the + * parent event has. Average size of `n` is small enough to be negligible. */ async #matchChildrenToTagOrder(entry: TocEntry) { const parentEvent = await this.#publicationTree.getEvent(entry.address); From ff8cfd39add1186bcc6d657952b6fda36af981bd Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 7 Jul 2025 09:18:39 -0500 Subject: [PATCH 70/98] Remove unused function --- src/lib/components/publications/table_of_contents.svelte.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/lib/components/publications/table_of_contents.svelte.ts b/src/lib/components/publications/table_of_contents.svelte.ts index 118a4ec..532c0a7 100644 --- a/src/lib/components/publications/table_of_contents.svelte.ts +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -164,11 +164,6 @@ export class TableOfContents { return titleTag || event.tagAddress() || '[untitled]'; } - #normalizeHashPath(title: string): string { - // TODO: Confirm this uses good normalization logic to produce unique hrefs within the page. - return title.toLowerCase().replace(/ /g, '-'); - } - async #buildTocEntry(address: string): Promise { const resolver = async () => { if (entry.childrenResolved) { From d62e2fef5fb86cdb4fc41ec26077dd603ff153fe Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 7 Jul 2025 09:39:00 -0500 Subject: [PATCH 71/98] Add contextual and todo comments --- .../publications/table_of_contents.svelte.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/lib/components/publications/table_of_contents.svelte.ts b/src/lib/components/publications/table_of_contents.svelte.ts index 532c0a7..aaf3ac3 100644 --- a/src/lib/components/publications/table_of_contents.svelte.ts +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -133,6 +133,18 @@ export class TableOfContents { // #region Private Methods + /** + * Initializes the ToC from the associated publication tree. + * + * @param rootAddress The address of the publication's root event. + * + * Michael J - 07 July 2025 - NOTE: Since the publication tree is conceptually infinite and + * lazy-loading, the ToC is not guaranteed to contain all the nodes at any layer until the + * publication has been fully resolved. + * + * Michael J - 07 July 2025 - TODO: If the relay provides event metadata, use the metadata to + * initialize the ToC with all of its first-level children. + */ async #init(rootAddress: string) { const rootEvent = await this.#publicationTree.getEvent(rootAddress); if (!rootEvent) { @@ -165,6 +177,9 @@ export class TableOfContents { } async #buildTocEntry(address: string): Promise { + // Michael J - 07 July 2025 - NOTE: This arrow function is nested so as to use its containing + // scope in its operation. Do not move it to the top level without ensuring it still has access + // to the necessary variables. const resolver = async () => { if (entry.childrenResolved) { return; From 70e7674a774a9384935c6da5e38d04e4221faee9 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 8 Jul 2025 09:18:11 -0500 Subject: [PATCH 72/98] Parallelize ToC initialization step --- .../publications/table_of_contents.svelte.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/lib/components/publications/table_of_contents.svelte.ts b/src/lib/components/publications/table_of_contents.svelte.ts index aaf3ac3..6c07f66 100644 --- a/src/lib/components/publications/table_of_contents.svelte.ts +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -155,11 +155,12 @@ export class TableOfContents { this.addressMap.set(rootAddress, this.#root); - // TODO: Parallelize this. - // Handle any other nodes that have already been resolved. - this.#publicationTree.resolvedAddresses.forEach((address) => { - this.#buildTocEntryFromResolvedNode(address); - }); + // Handle any other nodes that have already been resolved in parallel. + await Promise.all( + Array.from(this.#publicationTree.resolvedAddresses).map((address) => + this.#buildTocEntryFromResolvedNode(address) + ) + ); // Set up an observer to handle progressive resolution of the publication tree. this.#publicationTree.onNodeResolved((address: string) => { From 43d77d0f0477af975438120f3f84778d49cc8490 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 8 Jul 2025 09:18:32 -0500 Subject: [PATCH 73/98] Remove accordion display mode from ToC component --- .../publications/TableOfContents.svelte | 93 ++++++------------- 1 file changed, 30 insertions(+), 63 deletions(-) diff --git a/src/lib/components/publications/TableOfContents.svelte b/src/lib/components/publications/TableOfContents.svelte index 3c0b8f1..08097ed 100644 --- a/src/lib/components/publications/TableOfContents.svelte +++ b/src/lib/components/publications/TableOfContents.svelte @@ -4,17 +4,13 @@ type TocEntry } from '$lib/components/publications/table_of_contents.svelte'; import { getContext } from 'svelte'; - import { Accordion, AccordionItem, SidebarDropdownWrapper, SidebarGroup, SidebarItem } from 'flowbite-svelte'; + import { SidebarDropdownWrapper, SidebarGroup, SidebarItem } from 'flowbite-svelte'; import Self from './TableOfContents.svelte'; - export type TocDisplayMode = 'accordion' | 'sidebar'; - let { - displayMode = 'accordion', depth, onSectionFocused, - } = $props<{ - displayMode?: TocDisplayMode; + } = $props<{ rootAddress: string; depth: number; onSectionFocused?: (address: string) => void; @@ -46,65 +42,36 @@ } - -{#if displayMode === 'accordion'} - - {#each entries as entry} - {@const address = entry.address} - {@const expanded = toc.expandedMap.get(address) ?? false} - + + + {#each entries as entry} + {@const address = entry.address} + {@const expanded = toc.expandedMap.get(address) ?? false} + {@const isLeaf = toc.leaves.has(address)} + {#if isLeaf} + onSectionFocused?.(address)} + /> + {:else} + {@const childDepth = depth + 1} + expanded, (open) => setEntryExpanded(address, open) } > - {#snippet header()} - {entry.title} - {/snippet} - {#if entry.children.length > 0} - - {/if} - - {/each} - -{:else} - - - - {#each entries as entry} - {@const address = entry.address} - {@const expanded = toc.expandedMap.get(address) ?? false} - {@const isLeaf = toc.leaves.has(address)} - {#if isLeaf} - onSectionFocused?.(address)} + - {:else} - {@const childDepth = depth + 1} - expanded, - (open) => setEntryExpanded(address, open) - } - > - - - {/if} - {/each} - -{/if} + + {/if} + {/each} + From e714ac26317cbd6c5d597b33ddf8ff8ab228bcb1 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 8 Jul 2025 09:23:05 -0500 Subject: [PATCH 74/98] Pass publication section DOM into ToC class --- .../publications/Publication.svelte | 30 +++++++++++++++++-- .../publications/table_of_contents.svelte.ts | 8 ++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/lib/components/publications/Publication.svelte b/src/lib/components/publications/Publication.svelte index 3bfdace..9a0dd70 100644 --- a/src/lib/components/publications/Publication.svelte +++ b/src/lib/components/publications/Publication.svelte @@ -22,6 +22,7 @@ import Interactions from "$components/util/Interactions.svelte"; import type { SveltePublicationTree } from "./svelte_publication_tree.svelte"; import TableOfContents from "./TableOfContents.svelte"; + import type { TableOfContents as TocType } from "./table_of_contents.svelte"; let { rootAddress, publicationType, indexEvent } = $props<{ rootAddress: string; @@ -30,6 +31,7 @@ }>(); const publicationTree = getContext("publicationTree") as SveltePublicationTree; + const toc = getContext("toc") as TocType; // #region Loading @@ -125,6 +127,29 @@ // #endregion + /** + * Performs actions on the DOM element for a publication tree leaf when it is mounted. + * + * @param el The DOM element that was mounted. + * @param address The address of the event that was mounted. + */ + function onPublicationSectionMounted(el: HTMLElement, address: string) { + // Update last element ref for the intersection observer. + setLastElementRef(el, leaves.length); + + // Michael J - 08 July 2025 - NOTE: Updating the ToC from here somewhat breaks separation of + // concerns, since the TableOfContents component is primarily responsible for working with the + // ToC data structure. However, the Publication component has direct access to the needed DOM + // element already, and I want to avoid complicated callbacks between the two components. + // Update the ToC from the contents of the leaf section. + const entry = toc.getEntry(address); + if (!entry) { + console.warn(`[Publication] No parent found for ${address}`); + return; + } + toc.buildTocFromDocument(el, entry); + } + // #region Lifecycle hooks onDestroy(() => { @@ -201,11 +226,12 @@ Error loading content. One or more events could not be loaded. {:else} + {@const address = leaf.tagAddress()} setLastElementRef(el, i)} + {address} + ref={(el) => onPublicationSectionMounted(el, address)} /> {/if} {/each} diff --git a/src/lib/components/publications/table_of_contents.svelte.ts b/src/lib/components/publications/table_of_contents.svelte.ts index 6c07f66..0e4e310 100644 --- a/src/lib/components/publications/table_of_contents.svelte.ts +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -74,11 +74,11 @@ export class TableOfContents { buildTocFromDocument( parentElement: HTMLElement, parentEntry: TocEntry, - depth: number = 1 ) { parentElement - .querySelectorAll(`h${depth}`) + .querySelectorAll(`h${parentEntry.depth}`) .forEach((header) => { + // TODO: Correctly update ToC state from DOM. const title = header.textContent?.trim(); const id = header.id; @@ -91,7 +91,7 @@ export class TableOfContents { address: parentEntry.address, title, href, - depth, + depth: parentEntry.depth + 1, children: [], childrenResolved: true, resolveChildren: () => Promise.resolve(), @@ -99,7 +99,7 @@ export class TableOfContents { parentEntry.children.push(tocEntry); this.expandedMap.set(tocEntry.address, false); - this.buildTocFromDocument(header, tocEntry, depth + 1); + this.buildTocFromDocument(header, tocEntry); } }); } From c8891f31091843de8643fbda5a942ee6212ca19e Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 13 Jul 2025 00:37:14 +0200 Subject: [PATCH 75/98] Amber and npub-only login implemented --- package-lock.json | 17 +- package.json | 2 +- src/lib/components/Login.svelte | 338 ++++++++++++++--- src/lib/components/LoginMenu.svelte | 480 +++++++++++++++++++++++++ src/lib/components/Navigation.svelte | 4 +- src/lib/components/util/Profile.svelte | 8 - src/lib/utils/nostrUtils.ts | 2 +- 7 files changed, 772 insertions(+), 79 deletions(-) create mode 100644 src/lib/components/LoginMenu.svelte diff --git a/package-lock.json b/package-lock.json index b631ab9..095e9ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "alexandria", "version": "0.0.6", "dependencies": { - "@nostr-dev-kit/ndk": "2.11.x", + "@nostr-dev-kit/ndk": "^2.14.32", "@nostr-dev-kit/ndk-cache-dexie": "2.5.x", "@popperjs/core": "2.11.x", "@tailwindcss/forms": "0.5.x", @@ -568,8 +568,9 @@ } }, "node_modules/@nostr-dev-kit/ndk": { - "version": "2.11.2", - "license": "MIT", + "version": "2.14.32", + "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.14.32.tgz", + "integrity": "sha512-LUBO35RCB9/emBYsXNDece7m/WO2rGYR8j4SD0Crb3z8GcKTJq6P8OjpZ6+Kr+sLNo8N0uL07XxtAvEBnp2OqQ==", "dependencies": { "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", @@ -577,14 +578,14 @@ "@scure/base": "^1.1.9", "debug": "^4.3.6", "light-bolt11-decoder": "^3.2.0", - "nostr-tools": "^2.7.1", - "tseep": "^1.2.2", - "typescript-lru-cache": "^2.0.0", - "utf8-buffer": "^1.0.0", - "websocket-polyfill": "^0.0.3" + "tseep": "^1.3.1", + "typescript-lru-cache": "^2" }, "engines": { "node": ">=16" + }, + "peerDependencies": { + "nostr-tools": "^2" } }, "node_modules/@nostr-dev-kit/ndk-cache-dexie": { diff --git a/package.json b/package.json index 787d2e7..cb2a5d9 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "test": "vitest" }, "dependencies": { - "@nostr-dev-kit/ndk": "2.11.x", + "@nostr-dev-kit/ndk": "^2.14.32", "@nostr-dev-kit/ndk-cache-dexie": "2.5.x", "@popperjs/core": "2.11.x", "@tailwindcss/forms": "0.5.x", diff --git a/src/lib/components/Login.svelte b/src/lib/components/Login.svelte index e0d1171..8ea7dda 100644 --- a/src/lib/components/Login.svelte +++ b/src/lib/components/Login.svelte @@ -1,76 +1,296 @@ -
- {#if $ndkSignedIn} - - {:else} - - -
- - {#if signInFailed} -
- {errorMessage} + {#if isLoading} + 🔄 Generating QR code... + {:else} + 🔗 Connect with Amber + {/if} + + +
+

Click to generate a QR code for your mobile Amber app

+
+ {:else} +
+
+

Scan with Amber

+

Open Amber on your phone and scan this QR code

+
+ + + {#if qrCodeDataUrl} +
+ Nostr Connect QR Code +
+ {/if} + + +
+ +
+ + +
+
+ +
+

1. Open Amber on your phone

+

2. Scan the QR code above

+

3. Approve the connection in Amber

- {/if} - + ✍️ Test Sign Event + + + +
+ {/if} + + {#if result} +
+ {result}
- - {/if} + {/if} +
diff --git a/src/lib/components/LoginMenu.svelte b/src/lib/components/LoginMenu.svelte new file mode 100644 index 0000000..b7b8c74 --- /dev/null +++ b/src/lib/components/LoginMenu.svelte @@ -0,0 +1,480 @@ + + +
+ {#if !npub} + +
+ + +
+

Login with...

+ + + +
+
+ {#if result} +
+ {result} + +
+ {/if} +
+ {:else} + +
+ + +
+
+

{profileHandle || shortenNpub(npub)}

+
    +
  • + +
  • +
  • + +
  • +
+
+
+
+
+ {/if} +
+ +{#if showQrCode && qrCodeDataUrl} + +
+
+
+

Scan with Amber

+

Open Amber on your phone and scan this QR code

+ +
+ Nostr Connect QR Code +
+ +
+ +
+ + +
+
+ +
+

1. Open Amber on your phone

+

2. Scan the QR code above

+

3. Approve the connection in Amber

+
+ + +
+
+
+{/if} \ No newline at end of file diff --git a/src/lib/components/Navigation.svelte b/src/lib/components/Navigation.svelte index 4fefd1a..e634c55 100644 --- a/src/lib/components/Navigation.svelte +++ b/src/lib/components/Navigation.svelte @@ -7,7 +7,7 @@ NavHamburger, NavBrand, } from "flowbite-svelte"; - import Login from "./Login.svelte"; + import LoginMenu from "./LoginMenu.svelte"; let { class: className = "" } = $props(); @@ -19,7 +19,7 @@
- +
diff --git a/src/lib/components/util/Profile.svelte b/src/lib/components/util/Profile.svelte index 9d75bd5..402eb3b 100644 --- a/src/lib/components/util/Profile.svelte +++ b/src/lib/components/util/Profile.svelte @@ -80,14 +80,6 @@ function shortenNpub(long: string|undefined) { Sign out - {:else} - {/if}
diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index 9d80b1c..b85dbb9 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -87,7 +87,7 @@ export async function getUserMetadata(identifier: string): Promise name: profile?.name || fallback.name, displayName: profile?.displayName, nip05: profile?.nip05, - picture: profile?.image, + picture: profile?.picture || profile?.image, about: profile?.about, banner: profile?.banner, website: profile?.website, From 97036fe9302f5fc5e677e5ff6d353f42f8b631f7 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 13 Jul 2025 01:52:22 +0200 Subject: [PATCH 76/98] fixed all logins/logouts universal user store --- src/lib/components/CommentBox.svelte | 32 ++- src/lib/components/Login.svelte | 296 ------------------- src/lib/components/LoginMenu.svelte | 271 +++--------------- src/lib/components/LoginModal.svelte | 12 +- src/lib/components/PublicationHeader.svelte | 2 - src/lib/components/cards/BlogHeader.svelte | 1 + src/lib/components/util/CardActions.svelte | 12 +- src/lib/components/util/Profile.svelte | 15 +- src/lib/ndk.ts | 73 +---- src/lib/stores/relayStore.ts | 4 - src/lib/stores/userStore.ts | 298 ++++++++++++++++++++ src/routes/+layout.ts | 35 ++- src/routes/+page.svelte | 8 +- src/routes/contact/+page.svelte | 11 +- src/routes/events/+page.svelte | 13 +- 15 files changed, 430 insertions(+), 653 deletions(-) delete mode 100644 src/lib/components/Login.svelte delete mode 100644 src/lib/stores/relayStore.ts create mode 100644 src/lib/stores/userStore.ts diff --git a/src/lib/components/CommentBox.svelte b/src/lib/components/CommentBox.svelte index c46f902..552ad5f 100644 --- a/src/lib/components/CommentBox.svelte +++ b/src/lib/components/CommentBox.svelte @@ -4,16 +4,13 @@ import { nip19 } from 'nostr-tools'; import { getEventHash, signEvent, getUserMetadata, type NostrProfile } from '$lib/utils/nostrUtils'; import { standardRelays, fallbackRelays } from '$lib/consts'; - import { userRelays } from '$lib/stores/relayStore'; - import { get } from 'svelte/store'; + import { userStore } from '$lib/stores/userStore'; import { goto } from '$app/navigation'; import type { NDKEvent } from '$lib/utils/nostrUtils'; import { onMount } from 'svelte'; const props = $props<{ event: NDKEvent; - userPubkey: string; - userRelayPreference: boolean; }>(); let content = $state(''); @@ -24,11 +21,13 @@ let showOtherRelays = $state(false); let showFallbackRelays = $state(false); let userProfile = $state(null); + let user = $state($userStore); + userStore.subscribe(val => user = val); // Fetch user profile on mount onMount(async () => { - if (props.userPubkey) { - const npub = nip19.npubEncode(props.userPubkey); + if (user.signedIn && user.pubkey) { + const npub = nip19.npubEncode(user.pubkey); userProfile = await getUserMetadata(npub); } }); @@ -92,6 +91,11 @@ } async function handleSubmit(useOtherRelays = false, useFallbackRelays = false) { + if (!user.signedIn || !user.pubkey) { + error = 'You must be signed in to comment'; + return; + } + isSubmitting = true; error = null; success = null; @@ -135,7 +139,7 @@ created_at: Math.floor(Date.now() / 1000), tags, content, - pubkey: props.userPubkey + pubkey: user.pubkey }; const id = getEventHash(eventToSign); @@ -147,10 +151,10 @@ sig }; - // Determine which relays to use - let relays = props.userRelayPreference ? get(userRelays) : standardRelays; + // Determine which relays to use based on user's relay preference + let relays = user.relays.inbox.length > 0 ? user.relays.inbox : standardRelays; if (useOtherRelays) { - relays = props.userRelayPreference ? standardRelays : get(userRelays); + relays = user.relays.inbox.length > 0 ? standardRelays : user.relays.inbox; } if (useFallbackRelays) { relays = fallbackRelays; @@ -282,16 +286,16 @@ /> {/if} - {userProfile.displayName || userProfile.name || nip19.npubEncode(props.userPubkey).slice(0, 8) + '...'} + {userProfile.displayName || userProfile.name || (user.pubkey ? nip19.npubEncode(user.pubkey).slice(0, 8) + '...' : 'Unknown')}
{/if}
- {#if !props.userPubkey} + {#if !user.signedIn} Please sign in to post comments. Your comments will be signed with your current account. diff --git a/src/lib/components/Login.svelte b/src/lib/components/Login.svelte deleted file mode 100644 index 8ea7dda..0000000 --- a/src/lib/components/Login.svelte +++ /dev/null @@ -1,296 +0,0 @@ - - -
-
- {#if !npub} -
-

Welcome to Alexandria

-

Connect with Amber to start reading and publishing

-
- - {#if !showQrCode} - - -
-

Click to generate a QR code for your mobile Amber app

-
- {:else} -
-
-

Scan with Amber

-

Open Amber on your phone and scan this QR code

-
- - - {#if qrCodeDataUrl} -
- Nostr Connect QR Code -
- {/if} - - -
- -
- - -
-
- -
-

1. Open Amber on your phone

-

2. Scan the QR code above

-

3. Approve the connection in Amber

-
-
- {/if} - {:else} -
-
✅ Connected to Amber
-
{npub}
-
- -
- - - -
- {/if} - - {#if result} -
- {result} -
- {/if} -
-
diff --git a/src/lib/components/LoginMenu.svelte b/src/lib/components/LoginMenu.svelte index b7b8c74..36c7e68 100644 --- a/src/lib/components/LoginMenu.svelte +++ b/src/lib/components/LoginMenu.svelte @@ -1,55 +1,24 @@
- {#if !npub} + {#if !user.signedIn}
-

{profileHandle || shortenNpub(npub)}

+

{user.profile?.displayName || user.profile?.name || (user.npub ? shortenNpub(user.npub) : 'Unknown')}

  • @@ -431,24 +244,22 @@

    Scan with Amber

    Open Amber on your phone and scan this QR code

    -
    Nostr Connect QR Code
    -
    -

    1. Open Amber on your phone

    2. Scan the QR code above

    3. Approve the connection in Amber

    -
+{/if} + +{#if showAmberReconnect} +
+
+
+

Reconnect Amber Wallet

+

+ Your Amber wallet session could not be restored automatically.
+ Please reconnect your Amber wallet to continue. +

+ + +
+
+
{/if} \ No newline at end of file diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index b45c6e8..4744b85 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -6,6 +6,7 @@ import { loginMethodStorageKey } from '$lib/stores/userStore'; import Pharos, { pharosInstance } from '$lib/parser'; import { feedType } from '$lib/stores'; import type { LayoutLoad } from './$types'; +import { get } from 'svelte/store'; export const ssr = false; @@ -31,9 +32,33 @@ export const load: LayoutLoad = () => { console.log('Restoring extension login...'); loginWithExtension(); } else if (loginMethod === 'amber') { - // Amber login restoration would require more context (e.g., session, signer), so skip for now - alert('Amber login cannot be restored automatically. Please reconnect your Amber wallet.'); - console.warn('Amber login cannot be restored automatically. Please reconnect your Amber wallet.'); + // Attempt to restore Amber (NIP-46) session from localStorage + const relay = 'wss://relay.nsec.app'; + const localNsec = localStorage.getItem('amber/nsec'); + if (localNsec) { + import('@nostr-dev-kit/ndk').then(async ({ NDKNip46Signer, default: NDK }) => { + const ndk = get(ndkInstance); + try { + const amberSigner = NDKNip46Signer.nostrconnect(ndk, relay, localNsec, { + name: 'Alexandria', + perms: 'sign_event:1;sign_event:4', + }); + // Try to reconnect (blockUntilReady will resolve if Amber is running and session is valid) + await amberSigner.blockUntilReady(); + const user = await amberSigner.user(); + await loginWithAmber(amberSigner, user); + console.log('Amber session restored.'); + } catch (err) { + // If reconnection fails, show a non-blocking prompt (handled in LoginMenu UI) + console.warn('Amber session could not be restored. Prompting user to reconnect.'); + // Optionally, set a flag in localStorage or a Svelte store to show a reconnect banner/modal + localStorage.setItem('alexandria/amber/reconnect', '1'); + } + }); + } else { + // No session data, prompt user to reconnect (handled in LoginMenu UI) + localStorage.setItem('alexandria/amber/reconnect', '1'); + } } else if (loginMethod === 'npub') { console.log('Restoring npub login...'); loginWithNpub(pubkey); From c30bd1d5199a386b1f8a580d8884989fd85d625e Mon Sep 17 00:00:00 2001 From: silberengel Date: Mon, 14 Jul 2025 21:30:38 +0200 Subject: [PATCH 78/98] Fix the page redirection to sustain the Amber sessions --- src/lib/components/CommentBox.svelte | 10 ++-- src/lib/components/EventDetails.svelte | 45 +++++++++++++--- src/lib/components/LoginMenu.svelte | 3 +- src/lib/components/Preview.svelte | 1 + src/lib/components/PublicationHeader.svelte | 32 +++++++++--- src/lib/components/util/Details.svelte | 25 ++++++--- src/lib/snippets/UserSnippets.svelte | 58 +++++++++++++++++---- src/lib/stores/userStore.ts | 2 +- src/lib/utils/nostrUtils.ts | 11 ++-- src/routes/about/+page.svelte | 5 +- src/routes/events/+page.svelte | 9 ++-- src/routes/start/+page.svelte | 21 ++++---- 12 files changed, 164 insertions(+), 58 deletions(-) diff --git a/src/lib/components/CommentBox.svelte b/src/lib/components/CommentBox.svelte index 552ad5f..ab37d19 100644 --- a/src/lib/components/CommentBox.svelte +++ b/src/lib/components/CommentBox.svelte @@ -263,11 +263,15 @@ {/if} {#if success} + {@const s = success} - Comment published successfully to {success.relay}! - + Comment published successfully to {s.relay}! + {/if} diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index 17c3d0c..1672820 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -8,6 +8,8 @@ import type { NDKEvent } from '$lib/utils/nostrUtils'; import { getMatchingTags } from '$lib/utils/nostrUtils'; import ProfileHeader from "$components/cards/ProfileHeader.svelte"; + import { goto } from '$app/navigation'; + import { onMount } from 'svelte'; const { event, profile = null, searchValue = null } = $props<{ event: NDKEvent; @@ -27,6 +29,12 @@ let showFullContent = $state(false); let parsedContent = $state(''); let contentPreview = $state(''); + let authorTag: string = $derived(getMatchingTags(event, 'author')[0]?.[1] ?? ''); + let pTag: string = $derived(getMatchingTags(event, 'p')[0]?.[1] ?? ''); + + function isValidNostrPubkey(str: string): boolean { + return /^[a-f0-9]{64}$/i.test(str) || (str.startsWith('npub1') && str.length >= 59 && str.length <= 63); + } function getEventTitle(event: NDKEvent): string { return getMatchingTags(event, 'title')[0]?.[1] || 'Untitled'; @@ -48,9 +56,9 @@ function renderTag(tag: string[]): string { if (tag[0] === 'a' && tag.length > 1) { const [kind, pubkey, d] = tag[1].split(':'); - return `a:${tag[1]}`; + return ``; } else if (tag[0] === 'e' && tag.length > 1) { - return `e:${tag[1]}`; + return ``; } else { return `${tag[0]}:${tag[1]}`; } @@ -100,6 +108,21 @@ const norm = (s: string) => s.replace(/^nostr:/, '').toLowerCase(); return norm(value) === norm(searchValue); } + + onMount(() => { + function handleInternalLinkClick(event: MouseEvent) { + const target = event.target as HTMLElement; + if (target.tagName === 'A') { + const href = (target as HTMLAnchorElement).getAttribute('href'); + if (href && href.startsWith('/')) { + event.preventDefault(); + goto(href); + } + } + } + document.addEventListener('click', handleInternalLinkClick); + return () => document.removeEventListener('click', handleInternalLinkClick); + });
@@ -108,11 +131,19 @@ {/if}
- {#if toNpub(event.pubkey)} - Author: {@render userBadge(toNpub(event.pubkey) as string, profile?.display_name || event.pubkey)} - {:else} - Author: {profile?.display_name || event.pubkey} - {/if} + Author: + {#if authorTag && pTag && isValidNostrPubkey(pTag)} + {authorTag} {@render userBadge(pTag, '')} + {:else if authorTag} + {authorTag} + {:else if pTag && isValidNostrPubkey(pTag)} + {@render userBadge(pTag, '')} + {:else if authorTag} + {authorTag} + {:else} + unknown + {/if} +
diff --git a/src/lib/components/LoginMenu.svelte b/src/lib/components/LoginMenu.svelte index 921d5f6..c5a037a 100644 --- a/src/lib/components/LoginMenu.svelte +++ b/src/lib/components/LoginMenu.svelte @@ -5,6 +5,7 @@ import { get } from 'svelte/store'; import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; import { onMount } from 'svelte'; + import { goto } from '$app/navigation'; // UI state let isLoadingExtension: boolean = $state(false); @@ -230,7 +231,7 @@
  • diff --git a/src/lib/components/util/Details.svelte b/src/lib/components/util/Details.svelte index e776e9d..f2fe837 100644 --- a/src/lib/components/util/Details.svelte +++ b/src/lib/components/util/Details.svelte @@ -11,10 +11,8 @@ let { event, isModal = false } = $props(); let title: string = $derived(getMatchingTags(event, 'title')[0]?.[1]); - let author: string = $derived(getMatchingTags(event, 'author')[0]?.[1] ?? 'unknown'); let version: string = $derived(getMatchingTags(event, 'version')[0]?.[1] ?? '1'); let image: string = $derived(getMatchingTags(event, 'image')[0]?.[1] ?? null); - let originalAuthor: string = $derived(getMatchingTags(event, 'p')[0]?.[1] ?? null); let summary: string = $derived(getMatchingTags(event, 'summary')[0]?.[1] ?? null); let type: string = $derived(getMatchingTags(event, 'type')[0]?.[1] ?? null); let language: string = $derived(getMatchingTags(event, 'l')[0]?.[1] ?? null); @@ -25,6 +23,12 @@ let rootId: string = $derived(getMatchingTags(event, 'd')[0]?.[1] ?? null); let kind = $derived(event.kind); + let authorTag: string = $derived(getMatchingTags(event, 'author')[0]?.[1] ?? ''); + let pTag: string = $derived(getMatchingTags(event, 'p')[0]?.[1] ?? ''); + + function isValidNostrPubkey(str: string): boolean { + return /^[a-f0-9]{64}$/i.test(str) || (str.startsWith('npub1') && str.length >= 59 && str.length <= 63); + } @@ -32,7 +36,8 @@
    {#if !isModal}
    -

    {@render userBadge(event.pubkey, author)}

    + +

    {@render userBadge(event.pubkey, '')}

    {/if} @@ -46,10 +51,16 @@

    {title}

    by - {#if originalAuthor !== null} - {@render userBadge(originalAuthor, author)} + {#if authorTag && pTag && isValidNostrPubkey(pTag)} + {authorTag} {@render userBadge(pTag, '')} + {:else if authorTag} + {authorTag} + {:else if pTag && isValidNostrPubkey(pTag)} + {@render userBadge(pTag, '')} + {:else if authorTag} + {authorTag} {:else} - {author} + unknown {/if}

    {#if version !== '1' } @@ -81,7 +92,7 @@ {:else} Author: {/if} - {@render userBadge(event.pubkey, author)} + {@render userBadge(event.pubkey, '')}
    diff --git a/src/lib/snippets/UserSnippets.svelte b/src/lib/snippets/UserSnippets.svelte index d8c960e..7fc5632 100644 --- a/src/lib/snippets/UserSnippets.svelte +++ b/src/lib/snippets/UserSnippets.svelte @@ -1,18 +1,58 @@ {#snippet userBadge(identifier: string, displayText: string | undefined)} - {#if toNpub(identifier)} - {#await createProfileLinkWithVerification(toNpub(identifier) as string, displayText)} - {@html createProfileLink(toNpub(identifier) as string, displayText)} - {:then html} - {@html html} - {:catch} - {@html createProfileLink(toNpub(identifier) as string, displayText)} - {/await} + {@const npub = toNpub(identifier)} + {#if npub} + {#if !displayText || displayText.trim().toLowerCase() === 'unknown'} + {#await getUserMetadata(npub) then profile} + {@const p = profile as NostrProfileWithLegacy} + + + + {:catch} + + + + {/await} + {:else} + {#await createProfileLinkWithVerification(npub as string, displayText)} + + + + {:then html} + + + {@html html.replace(/([\s\S]*<\/a>)/, '').trim()} + + {:catch} + + + + {/await} + {/if} {:else} {displayText ?? ''} {/if} diff --git a/src/lib/stores/userStore.ts b/src/lib/stores/userStore.ts index 5c95ff8..3e3d77f 100644 --- a/src/lib/stores/userStore.ts +++ b/src/lib/stores/userStore.ts @@ -168,7 +168,7 @@ export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser) { if (!ndk) throw new Error('NDK not initialized'); // Only clear previous login state after successful login const npub = user.npub; - const profile = await getUserMetadata(npub); + const profile = await getUserMetadata(npub, true); // Force fresh fetch const [persistedInboxes, persistedOutboxes] = getPersistedRelays(user); for (const relay of persistedInboxes) { ndk.addExplicitRelay(relay); diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index b85dbb9..faa001b 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -46,11 +46,11 @@ function escapeHtml(text: string): string { /** * Get user metadata for a nostr identifier (npub or nprofile) */ -export async function getUserMetadata(identifier: string): Promise { +export async function getUserMetadata(identifier: string, force = false): Promise { // Remove nostr: prefix if present const cleanId = identifier.replace(/^nostr:/, ''); - if (npubCache.has(cleanId)) { + if (!force && npubCache.has(cleanId)) { return npubCache.get(cleanId)!; } @@ -111,7 +111,8 @@ export function createProfileLink(identifier: string, displayText: string | unde const defaultText = `${cleanId.slice(0, 8)}...${cleanId.slice(-4)}`; const escapedText = escapeHtml(displayText || defaultText); - return `@${escapedText}`; + // Remove target="_blank" for internal navigation + return `@${escapedText}`; } /** @@ -167,9 +168,9 @@ export async function createProfileLinkWithVerification(identifier: string, disp const type = nip05.endsWith('edu') ? 'edu' : 'standard'; switch (type) { case 'edu': - return `@${displayIdentifier}${graduationCapSvg}`; + return `@${displayIdentifier}${graduationCapSvg}`; case 'standard': - return `@${displayIdentifier}${badgeCheckSvg}`; + return `@${displayIdentifier}${badgeCheckSvg}`; } } /** diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte index a6badf3..b5b7c09 100644 --- a/src/routes/about/+page.svelte +++ b/src/routes/about/+page.svelte @@ -1,6 +1,7 @@ diff --git a/src/routes/start/+page.svelte b/src/routes/start/+page.svelte index 05d0776..04177de 100644 --- a/src/routes/start/+page.svelte +++ b/src/routes/start/+page.svelte @@ -1,5 +1,6 @@ + +
    +
    + +
    + +
    + +
    +
    +
    - +
    - + {#if dTagError} -
    {dTagError}
    +
    {dTagError}
    {/if}
    -
    - +
    +
    {#if loading} - Publishing... + Publishing... {/if} {#if error} -
    {error}
    +
    {error}
    {/if} {#if success} -
    {success}
    -
    Relays: {publishedRelays.join(', ')}
    +
    {success}
    +
    + Relays: {publishedRelays.join(", ")} +
    {#if lastPublishedEventId} -
    - Event ID: {lastPublishedEventId} -
    {/if} {/if} -
    \ No newline at end of file +
    diff --git a/src/lib/components/EventSearch.svelte b/src/lib/components/EventSearch.svelte index e9dfed2..c32f72b 100644 --- a/src/lib/components/EventSearch.svelte +++ b/src/lib/components/EventSearch.svelte @@ -4,7 +4,11 @@ import { goto } from "$app/navigation"; import type { NDKEvent } from "$lib/utils/nostrUtils"; import RelayDisplay from "./RelayDisplay.svelte"; - import { searchEvent, searchBySubscription, searchNip05 } from "$lib/utils/search_utility"; + import { + searchEvent, + searchBySubscription, + searchNip05, + } from "$lib/utils/search_utility"; import { neventEncode, naddrEncode, nprofileEncode } from "$lib/utils"; import { standardRelays } from "$lib/consts"; import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils"; @@ -33,7 +37,7 @@ eventIds: Set, addresses: Set, searchType?: string, - searchTerm?: string + searchTerm?: string, ) => void; event: NDKEvent | null; onClear?: () => void; @@ -43,7 +47,9 @@ // Component state let searchQuery = $state(""); let localError = $state(null); - let relayStatuses = $state>({}); + let relayStatuses = $state>( + {}, + ); let foundEvent = $state(null); let searching = $state(false); let searchCompleted = $state(false); @@ -56,7 +62,11 @@ let currentAbortController: AbortController | null = null; // Derived values - let hasActiveSearch = $derived(searching || (Object.values(relayStatuses).some(s => s === "pending") && !foundEvent)); + let hasActiveSearch = $derived( + searching || + (Object.values(relayStatuses).some((s) => s === "pending") && + !foundEvent), + ); let showError = $derived(localError || error); let showSuccess = $derived(searchCompleted && searchResultCount !== null); @@ -75,18 +85,39 @@ const foundEvent = await searchNip05(query); if (foundEvent) { handleFoundEvent(foundEvent); - updateSearchState(false, true, 1, 'nip05'); + updateSearchState(false, true, 1, "nip05"); } else { relayStatuses = {}; - if (activeSub) { try { activeSub.stop(); } catch (e) { console.warn('Error stopping subscription:', e); } activeSub = null; } - if (currentAbortController) { currentAbortController.abort(); currentAbortController = null; } - updateSearchState(false, true, 0, 'nip05'); + if (activeSub) { + try { + activeSub.stop(); + } catch (e) { + console.warn("Error stopping subscription:", e); + } + activeSub = null; + } + if (currentAbortController) { + currentAbortController.abort(); + currentAbortController = null; + } + updateSearchState(false, true, 0, "nip05"); } } catch (error) { - localError = error instanceof Error ? error.message : 'NIP-05 lookup failed'; + localError = + error instanceof Error ? error.message : "NIP-05 lookup failed"; relayStatuses = {}; - if (activeSub) { try { activeSub.stop(); } catch (e) { console.warn('Error stopping subscription:', e); } activeSub = null; } - if (currentAbortController) { currentAbortController.abort(); currentAbortController = null; } + if (activeSub) { + try { + activeSub.stop(); + } catch (e) { + console.warn("Error stopping subscription:", e); + } + activeSub = null; + } + if (currentAbortController) { + currentAbortController.abort(); + currentAbortController = null; + } updateSearchState(false, false, null, null); isProcessingSearch = false; currentProcessingSearchValue = null; @@ -102,26 +133,49 @@ console.warn("[Events] Event not found for query:", query); localError = "Event not found"; relayStatuses = {}; - if (activeSub) { try { activeSub.stop(); } catch (e) { console.warn('Error stopping subscription:', e); } activeSub = null; } - if (currentAbortController) { currentAbortController.abort(); currentAbortController = null; } + if (activeSub) { + try { + activeSub.stop(); + } catch (e) { + console.warn("Error stopping subscription:", e); + } + activeSub = null; + } + if (currentAbortController) { + currentAbortController.abort(); + currentAbortController = null; + } updateSearchState(false, false, null, null); } else { console.log("[Events] Event found:", foundEvent); handleFoundEvent(foundEvent); - updateSearchState(false, true, 1, 'event'); + updateSearchState(false, true, 1, "event"); } } catch (err) { console.error("[Events] Error fetching event:", err, "Query:", query); localError = "Error fetching event. Please check the ID and try again."; relayStatuses = {}; - if (activeSub) { try { activeSub.stop(); } catch (e) { console.warn('Error stopping subscription:', e); } activeSub = null; } - if (currentAbortController) { currentAbortController.abort(); currentAbortController = null; } + if (activeSub) { + try { + activeSub.stop(); + } catch (e) { + console.warn("Error stopping subscription:", e); + } + activeSub = null; + } + if (currentAbortController) { + currentAbortController.abort(); + currentAbortController = null; + } updateSearchState(false, false, null, null); isProcessingSearch = false; } } - async function handleSearchEvent(clearInput: boolean = true, queryOverride?: string) { + async function handleSearchEvent( + clearInput: boolean = true, + queryOverride?: string, + ) { if (searching) { console.log("EventSearch: Already searching, skipping"); return; @@ -131,7 +185,9 @@ updateSearchState(true); isResetting = false; isUserEditing = false; // Reset user editing flag when search starts - const query = (queryOverride !== undefined ? queryOverride : searchQuery).trim(); + const query = ( + queryOverride !== undefined ? queryOverride : searchQuery + ).trim(); if (!query) { updateSearchState(false, false, null, null); return; @@ -140,7 +196,7 @@ const dTag = query.slice(2).trim().toLowerCase(); if (dTag) { console.log("EventSearch: Processing d-tag search:", dTag); - navigateToSearch(dTag, 'd'); + navigateToSearch(dTag, "d"); updateSearchState(false, false, null, null); return; } @@ -148,23 +204,23 @@ if (query.toLowerCase().startsWith("t:")) { const searchTerm = query.slice(2).trim(); if (searchTerm) { - await handleSearchBySubscription('t', searchTerm); + await handleSearchBySubscription("t", searchTerm); return; } } if (query.toLowerCase().startsWith("n:")) { const searchTerm = query.slice(2).trim(); if (searchTerm) { - await handleSearchBySubscription('n', searchTerm); + await handleSearchBySubscription("n", searchTerm); return; } } - if (query.includes('@')) { + if (query.includes("@")) { await handleNip05Search(query); return; } if (clearInput) { - navigateToSearch(query, 'id'); + navigateToSearch(query, "id"); // Don't clear searchQuery here - let the effect handle it } await handleEventSearch(query); @@ -176,7 +232,7 @@ if (searching || isResetting || isUserEditing) { return; } - + if (dTagValue) { // If dTagValue is set, show it as "d:tag" in the search bar searchQuery = `d:${dTagValue}`; @@ -191,7 +247,13 @@ // Debounced effect to handle searchValue changes $effect(() => { - if (!searchValue || searching || isResetting || isProcessingSearch || isWaitingForSearchResult) { + if ( + !searchValue || + searching || + isResetting || + isProcessingSearch || + isWaitingForSearchResult + ) { return; } @@ -205,7 +267,7 @@ currentNevent = neventEncode(foundEvent, standardRelays); } catch {} try { - currentNaddr = getMatchingTags(foundEvent, 'd')[0]?.[1] + currentNaddr = getMatchingTags(foundEvent, "d")[0]?.[1] ? naddrEncode(foundEvent, standardRelays) : null; } catch {} @@ -214,11 +276,30 @@ } catch {} // Debug log for comparison - console.log('[EventSearch effect] searchValue:', searchValue, 'foundEvent.id:', currentEventId, 'foundEvent.pubkey:', foundEvent.pubkey, 'toNpub(pubkey):', currentNpub, 'foundEvent.kind:', foundEvent.kind, 'currentNaddr:', currentNaddr, 'currentNevent:', currentNevent); + console.log( + "[EventSearch effect] searchValue:", + searchValue, + "foundEvent.id:", + currentEventId, + "foundEvent.pubkey:", + foundEvent.pubkey, + "toNpub(pubkey):", + currentNpub, + "foundEvent.kind:", + foundEvent.kind, + "currentNaddr:", + currentNaddr, + "currentNevent:", + currentNevent, + ); // Also check if searchValue is an nprofile and matches the current event's pubkey let currentNprofile = null; - if (searchValue && searchValue.startsWith('nprofile1') && foundEvent.kind === 0) { + if ( + searchValue && + searchValue.startsWith("nprofile1") && + foundEvent.kind === 0 + ) { try { currentNprofile = nprofileEncode(foundEvent.pubkey, standardRelays); } catch {} @@ -261,10 +342,15 @@ // Simple effect to handle dTagValue changes $effect(() => { - if (dTagValue && !searching && !isResetting && dTagValue !== lastProcessedDTagValue) { + if ( + dTagValue && + !searching && + !isResetting && + dTagValue !== lastProcessedDTagValue + ) { console.log("EventSearch: Processing dTagValue:", dTagValue); lastProcessedDTagValue = dTagValue; - handleSearchBySubscription('d', dTagValue); + handleSearchBySubscription("d", dTagValue); } }); @@ -276,7 +362,12 @@ }); // Search utility functions - function updateSearchState(isSearching: boolean, completed: boolean = false, count: number | null = null, type: string | null = null) { + function updateSearchState( + isSearching: boolean, + completed: boolean = false, + count: number | null = null, + type: string | null = null, + ) { searching = isSearching; searchCompleted = completed; searchResultCount = count; @@ -297,32 +388,32 @@ currentProcessingSearchValue = null; lastSearchValue = null; updateSearchState(false, false, null, null); - + // Cancel ongoing search if (currentAbortController) { currentAbortController.abort(); currentAbortController = null; } - + // Clean up subscription if (activeSub) { try { activeSub.stop(); } catch (e) { - console.warn('Error stopping subscription:', e); + console.warn("Error stopping subscription:", e); } activeSub = null; } - + // Clear search results onSearchResults([], [], [], new Set(), new Set()); - + // Clear any pending timeout if (searchTimeout) { clearTimeout(searchTimeout); searchTimeout = null; } - + // Reset the flag after a short delay to allow effects to settle setTimeout(() => { isResetting = false; @@ -332,40 +423,40 @@ function handleFoundEvent(event: NDKEvent) { foundEvent = event; relayStatuses = {}; // Clear relay statuses when event is found - + // Stop any ongoing subscription if (activeSub) { try { activeSub.stop(); } catch (e) { - console.warn('Error stopping subscription:', e); + console.warn("Error stopping subscription:", e); } activeSub = null; } - + // Abort any ongoing fetch if (currentAbortController) { currentAbortController.abort(); currentAbortController = null; } - + // Clear search state searching = false; searchCompleted = true; searchResultCount = 1; - searchResultType = 'event'; - + searchResultType = "event"; + // Update last processed search value to prevent re-processing if (searchValue) { lastProcessedSearchValue = searchValue; lastSearchValue = searchValue; } - + // Reset processing flag isProcessingSearch = false; currentProcessingSearchValue = null; isWaitingForSearchResult = false; - + onEventFound(event); } @@ -379,8 +470,14 @@ } // Search handlers - async function handleSearchBySubscription(searchType: 'd' | 't' | 'n', searchTerm: string) { - console.log("EventSearch: Starting subscription search:", { searchType, searchTerm }); + async function handleSearchBySubscription( + searchType: "d" | "t" | "n", + searchTerm: string, + ) { + console.log("EventSearch: Starting subscription search:", { + searchType, + searchTerm, + }); isResetting = false; // Allow effects to run for new searches localError = null; updateSearchState(true); @@ -403,7 +500,7 @@ updatedResult.eventIds, updatedResult.addresses, updatedResult.searchType, - updatedResult.searchTerm + updatedResult.searchTerm, ); }, onSubscriptionCreated: (sub) => { @@ -412,9 +509,9 @@ activeSub.stop(); } activeSub = sub; - } + }, }, - currentAbortController.signal + currentAbortController.signal, ); console.log("EventSearch: Search completed:", result); onSearchResults( @@ -424,16 +521,19 @@ result.eventIds, result.addresses, result.searchType, - result.searchTerm + result.searchTerm, ); - const totalCount = result.events.length + result.secondOrder.length + result.tTagEvents.length; + const totalCount = + result.events.length + + result.secondOrder.length + + result.tTagEvents.length; relayStatuses = {}; // Clear relay statuses when search completes // Stop any ongoing subscription if (activeSub) { try { activeSub.stop(); } catch (e) { - console.warn('Error stopping subscription:', e); + console.warn("Error stopping subscription:", e); } activeSub = null; } @@ -447,20 +547,25 @@ currentProcessingSearchValue = null; isWaitingForSearchResult = false; } catch (error) { - if (error instanceof Error && error.message === 'Search cancelled') { + if (error instanceof Error && error.message === "Search cancelled") { isProcessingSearch = false; currentProcessingSearchValue = null; isWaitingForSearchResult = false; return; } console.error("EventSearch: Search failed:", error); - localError = error instanceof Error ? error.message : 'Search failed'; + localError = error instanceof Error ? error.message : "Search failed"; // Provide more specific error messages for different failure types if (error instanceof Error) { - if (error.message.includes('timeout') || error.message.includes('connection')) { - localError = 'Search timed out. The relays may be temporarily unavailable. Please try again.'; - } else if (error.message.includes('NDK not initialized')) { - localError = 'Nostr client not initialized. Please refresh the page and try again.'; + if ( + error.message.includes("timeout") || + error.message.includes("connection") + ) { + localError = + "Search timed out. The relays may be temporarily unavailable. Please try again."; + } else if (error.message.includes("NDK not initialized")) { + localError = + "Nostr client not initialized. Please refresh the page and try again."; } else { localError = `Search failed: ${error.message}`; } @@ -471,35 +576,35 @@ try { activeSub.stop(); } catch (e) { - console.warn('Error stopping subscription:', e); + console.warn("Error stopping subscription:", e); + } + activeSub = null; } - activeSub = null; - } - // Abort any ongoing fetch - if (currentAbortController) { - currentAbortController.abort(); - currentAbortController = null; + // Abort any ongoing fetch + if (currentAbortController) { + currentAbortController.abort(); + currentAbortController = null; + } + updateSearchState(false, false, null, null); + isProcessingSearch = false; + currentProcessingSearchValue = null; + isWaitingForSearchResult = false; } - updateSearchState(false, false, null, null); - isProcessingSearch = false; - currentProcessingSearchValue = null; - isWaitingForSearchResult = false; - } } function handleClear() { isResetting = true; - searchQuery = ''; + searchQuery = ""; isUserEditing = false; // Reset user editing flag resetSearchState(); - + // Clear URL parameters to reset the page - goto('', { + goto("", { replaceState: true, keepFocus: true, noScroll: true, }); - + // Ensure all search state is cleared searching = false; searchCompleted = false; @@ -512,17 +617,17 @@ currentProcessingSearchValue = null; lastSearchValue = null; isWaitingForSearchResult = false; - + // Clear any pending timeout if (searchTimeout) { clearTimeout(searchTimeout); searchTimeout = null; } - + if (onClear) { onClear(); } - + // Reset the flag after a short delay to allow effects to settle setTimeout(() => { isResetting = false; @@ -533,12 +638,16 @@ if (searchResultCount === 0) { return "Search completed. No results found."; } - - const typeLabel = searchResultType === 'n' ? 'profile' : - searchResultType === 'nip05' ? 'NIP-05 address' : 'event'; - const countLabel = searchResultType === 'n' ? 'profiles' : 'events'; - - return searchResultCount === 1 + + const typeLabel = + searchResultType === "n" + ? "profile" + : searchResultType === "nip05" + ? "NIP-05 address" + : "event"; + const countLabel = searchResultType === "n" ? "profiles" : "events"; + + return searchResultCount === 1 ? `Search completed. Found 1 ${typeLabel}.` : `Search completed. Found ${searchResultCount} ${countLabel}.`; } @@ -551,9 +660,10 @@ bind:value={searchQuery} placeholder="Enter event ID, nevent, naddr, d:tag-name, t:topic, or n:username..." class="flex-grow" - onkeydown={(e: KeyboardEvent) => e.key === "Enter" && handleSearchEvent(true)} - oninput={() => isUserEditing = true} - onblur={() => isUserEditing = false} + onkeydown={(e: KeyboardEvent) => + e.key === "Enter" && handleSearchEvent(true)} + oninput={() => (isUserEditing = true)} + onblur={() => (isUserEditing = false)} /> -
    {/if} -
    \ No newline at end of file +
    diff --git a/src/lib/components/publications/PublicationHeader.svelte b/src/lib/components/publications/PublicationHeader.svelte index 25dfdc4..36e76eb 100644 --- a/src/lib/components/publications/PublicationHeader.svelte +++ b/src/lib/components/publications/PublicationHeader.svelte @@ -1,8 +1,8 @@ {#if title != null && href != null} - + {#if image} -
    - -
    +
    + +
    {/if} -
    + diff --git a/src/lib/components/publications/PublicationSection.svelte b/src/lib/components/publications/PublicationSection.svelte index 7b24fb3..d5b5ecb 100644 --- a/src/lib/components/publications/PublicationSection.svelte +++ b/src/lib/components/publications/PublicationSection.svelte @@ -1,45 +1,54 @@ - -
    - {#await Promise.all([leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches])} - +
    + {#await Promise.all( [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches], )} + {:then [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches]} {#each divergingBranches as [branch, depth]} - {@render sectionHeading(getMatchingTags(branch, 'title')[0]?.[1] ?? '', depth)} + {@render sectionHeading( + getMatchingTags(branch, "title")[0]?.[1] ?? "", + depth, + )} {/each} {#if leafTitle} {@const leafDepth = leafHierarchy.length - 1} {@render sectionHeading(leafTitle, leafDepth)} {/if} - {@render contentParagraph(leafContent.toString(), publicationType ?? 'article', false)} + {@render contentParagraph( + leafContent.toString(), + publicationType ?? "article", + false, + )} {/await}
    diff --git a/src/lib/components/publications/TableOfContents.svelte b/src/lib/components/publications/TableOfContents.svelte index 08097ed..e50bce0 100644 --- a/src/lib/components/publications/TableOfContents.svelte +++ b/src/lib/components/publications/TableOfContents.svelte @@ -1,22 +1,23 @@ - - diff --git a/src/lib/components/util/CardActions.svelte b/src/lib/components/util/CardActions.svelte index ef6b45e..28cab68 100644 --- a/src/lib/components/util/CardActions.svelte +++ b/src/lib/components/util/CardActions.svelte @@ -20,20 +20,42 @@ // Subscribe to userStore let user = $state($userStore); - userStore.subscribe(val => user = val); + userStore.subscribe((val) => (user = val)); // Derive metadata from event - let title = $derived(event.tags.find((t: string[]) => t[0] === 'title')?.[1] ?? ''); - let summary = $derived(event.tags.find((t: string[]) => t[0] === 'summary')?.[1] ?? ''); - let image = $derived(event.tags.find((t: string[]) => t[0] === 'image')?.[1] ?? null); - let author = $derived(event.tags.find((t: string[]) => t[0] === 'author')?.[1] ?? ''); - let originalAuthor = $derived(event.tags.find((t: string[]) => t[0] === 'original_author')?.[1] ?? null); - let version = $derived(event.tags.find((t: string[]) => t[0] === 'version')?.[1] ?? ''); - let source = $derived(event.tags.find((t: string[]) => t[0] === 'source')?.[1] ?? null); - let type = $derived(event.tags.find((t: string[]) => t[0] === 'type')?.[1] ?? null); - let language = $derived(event.tags.find((t: string[]) => t[0] === 'language')?.[1] ?? null); - let publisher = $derived(event.tags.find((t: string[]) => t[0] === 'publisher')?.[1] ?? null); - let identifier = $derived(event.tags.find((t: string[]) => t[0] === 'identifier')?.[1] ?? null); + let title = $derived( + event.tags.find((t: string[]) => t[0] === "title")?.[1] ?? "", + ); + let summary = $derived( + event.tags.find((t: string[]) => t[0] === "summary")?.[1] ?? "", + ); + let image = $derived( + event.tags.find((t: string[]) => t[0] === "image")?.[1] ?? null, + ); + let author = $derived( + event.tags.find((t: string[]) => t[0] === "author")?.[1] ?? "", + ); + let originalAuthor = $derived( + event.tags.find((t: string[]) => t[0] === "original_author")?.[1] ?? null, + ); + let version = $derived( + event.tags.find((t: string[]) => t[0] === "version")?.[1] ?? "", + ); + let source = $derived( + event.tags.find((t: string[]) => t[0] === "source")?.[1] ?? null, + ); + let type = $derived( + event.tags.find((t: string[]) => t[0] === "type")?.[1] ?? null, + ); + let language = $derived( + event.tags.find((t: string[]) => t[0] === "language")?.[1] ?? null, + ); + let publisher = $derived( + event.tags.find((t: string[]) => t[0] === "publisher")?.[1] ?? null, + ); + let identifier = $derived( + event.tags.find((t: string[]) => t[0] === "identifier")?.[1] ?? null, + ); // UI state let detailsModalOpen: boolean = $state(false); @@ -48,7 +70,7 @@ (() => { const isUserFeed = user.signedIn && $feedType === FeedType.UserRelays; const relays = isUserFeed ? user.relays.inbox : standardRelays; - + console.debug("[CardActions] Selected relays:", { eventId: event.id, isSignedIn: user.signedIn, @@ -100,7 +122,7 @@ * Navigates to the event details page */ function viewEventDetails() { - const nevent = getIdentifier('nevent'); + const nevent = getIdentifier("nevent"); goto(`/events?id=${encodeURIComponent(nevent)}`); } diff --git a/src/lib/components/util/ContainingIndexes.svelte b/src/lib/components/util/ContainingIndexes.svelte index 1c7da12..992115d 100644 --- a/src/lib/components/util/ContainingIndexes.svelte +++ b/src/lib/components/util/ContainingIndexes.svelte @@ -17,13 +17,19 @@ let lastEventId = $state(null); async function loadContainingIndexes() { - console.log("[ContainingIndexes] Loading containing indexes for event:", event.id); + console.log( + "[ContainingIndexes] Loading containing indexes for event:", + event.id, + ); loading = true; error = null; try { containingIndexes = await findContainingIndexEvents(event); - console.log("[ContainingIndexes] Found containing indexes:", containingIndexes.length); + console.log( + "[ContainingIndexes] Found containing indexes:", + containingIndexes.length, + ); } catch (err) { error = err instanceof Error diff --git a/src/lib/components/util/Details.svelte b/src/lib/components/util/Details.svelte index 9c6d312..ff9643a 100644 --- a/src/lib/components/util/Details.svelte +++ b/src/lib/components/util/Details.svelte @@ -4,37 +4,54 @@ import Interactions from "$components/util/Interactions.svelte"; import { P } from "flowbite-svelte"; import { getMatchingTags } from "$lib/utils/nostrUtils"; - import { goto } from '$app/navigation'; + import { goto } from "$app/navigation"; // isModal // - don't show interactions in modal view // - don't show all the details when _not_ in modal view let { event, isModal = false } = $props(); - let title: string = $derived(getMatchingTags(event, 'title')[0]?.[1]); + let title: string = $derived(getMatchingTags(event, "title")[0]?.[1]); let author: string = $derived( getMatchingTags(event, "author")[0]?.[1] ?? "unknown", ); - let version: string = $derived(getMatchingTags(event, 'version')[0]?.[1] ?? '1'); - let image: string = $derived(getMatchingTags(event, 'image')[0]?.[1] ?? null); - let summary: string = $derived(getMatchingTags(event, 'summary')[0]?.[1] ?? null); - let type: string = $derived(getMatchingTags(event, 'type')[0]?.[1] ?? null); - let language: string = $derived(getMatchingTags(event, 'l')[0]?.[1] ?? null); - let source: string = $derived(getMatchingTags(event, 'source')[0]?.[1] ?? null); - let publisher: string = $derived(getMatchingTags(event, 'published_by')[0]?.[1] ?? null); - let identifier: string = $derived(getMatchingTags(event, 'i')[0]?.[1] ?? null); - let hashtags: string[] = $derived(getMatchingTags(event, 't').map(tag => tag[1])); - let rootId: string = $derived(getMatchingTags(event, 'd')[0]?.[1] ?? null); + let version: string = $derived( + getMatchingTags(event, "version")[0]?.[1] ?? "1", + ); + let image: string = $derived(getMatchingTags(event, "image")[0]?.[1] ?? null); + let summary: string = $derived( + getMatchingTags(event, "summary")[0]?.[1] ?? null, + ); + let type: string = $derived(getMatchingTags(event, "type")[0]?.[1] ?? null); + let language: string = $derived(getMatchingTags(event, "l")[0]?.[1] ?? null); + let source: string = $derived( + getMatchingTags(event, "source")[0]?.[1] ?? null, + ); + let publisher: string = $derived( + getMatchingTags(event, "published_by")[0]?.[1] ?? null, + ); + let identifier: string = $derived( + getMatchingTags(event, "i")[0]?.[1] ?? null, + ); + let hashtags: string[] = $derived( + getMatchingTags(event, "t").map((tag) => tag[1]), + ); + let rootId: string = $derived(getMatchingTags(event, "d")[0]?.[1] ?? null); let kind = $derived(event.kind); - let authorTag: string = $derived(getMatchingTags(event, 'author')[0]?.[1] ?? ''); - let pTag: string = $derived(getMatchingTags(event, 'p')[0]?.[1] ?? ''); + let authorTag: string = $derived( + getMatchingTags(event, "author")[0]?.[1] ?? "", + ); + let pTag: string = $derived(getMatchingTags(event, "p")[0]?.[1] ?? ""); let originalAuthor: string = $derived( getMatchingTags(event, "p")[0]?.[1] ?? null, ); function isValidNostrPubkey(str: string): boolean { - return /^[a-f0-9]{64}$/i.test(str) || (str.startsWith('npub1') && str.length >= 59 && str.length <= 63); + return ( + /^[a-f0-9]{64}$/i.test(str) || + (str.startsWith("npub1") && str.length >= 59 && str.length <= 63) + ); } @@ -42,7 +59,9 @@ {#if !isModal}
    -

    {@render userBadge(event.pubkey, author)}

    +

    {@render userBadge(event.pubkey, author)}

    {/if} @@ -63,11 +82,11 @@

    by {#if authorTag && pTag && isValidNostrPubkey(pTag)} - {authorTag} {@render userBadge(pTag, '')} + {authorTag} {@render userBadge(pTag, "")} {:else if authorTag} {authorTag} {:else if pTag && isValidNostrPubkey(pTag)} - {@render userBadge(pTag, '')} + {@render userBadge(pTag, "")} {:else if originalAuthor !== null} {@render userBadge(originalAuthor, author)} {:else} @@ -111,7 +130,7 @@ {:else} Author: {/if} - {@render userBadge(event.pubkey, '')} + {@render userBadge(event.pubkey, "")}

    diff --git a/src/lib/components/util/Profile.svelte b/src/lib/components/util/Profile.svelte index 88ebe4c..7ba8f81 100644 --- a/src/lib/components/util/Profile.svelte +++ b/src/lib/components/util/Profile.svelte @@ -1,11 +1,15 @@ -
    -
    +
    +
    - +
    diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte index caabdcb..72b4697 100644 --- a/src/routes/about/+page.svelte +++ b/src/routes/about/+page.svelte @@ -1,7 +1,7 @@
    @@ -700,18 +750,4 @@ {getResultMessage()}
    {/if} - - -
    -
    - {#each Object.entries(relayStatuses) as [relay, status]} - - {/each} -
    - {#if !foundEvent && hasActiveSearch} -
    - Searching relays... -
    - {/if} -
    diff --git a/src/lib/components/Login.svelte b/src/lib/components/Login.svelte deleted file mode 100644 index e24490d..0000000 --- a/src/lib/components/Login.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - -
    - {#if $ndkSignedIn} - - {:else} - - -
    - - {#if signInFailed} -
    - {errorMessage} -
    - {/if} - -
    -
    - {/if} -
    diff --git a/src/lib/components/LoginMenu.svelte b/src/lib/components/LoginMenu.svelte index 8814333..4ac4222 100644 --- a/src/lib/components/LoginMenu.svelte +++ b/src/lib/components/LoginMenu.svelte @@ -11,10 +11,11 @@ loginWithNpub, logoutUser, } from "$lib/stores/userStore"; - import { get } from "svelte/store"; + import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import { onMount } from "svelte"; import { goto } from "$app/navigation"; + import NetworkStatus from "./NetworkStatus.svelte"; // UI state let isLoadingExtension: boolean = $state(false); @@ -36,13 +37,16 @@ } }); - // Subscribe to userStore - let user = $state(get(userStore)); - userStore.subscribe((val) => { - user = val; + // Use reactive user state from store + let user = $derived($userStore); + + // Handle user state changes with effects + $effect(() => { + const currentUser = user; + // Check for fallback flag when user state changes to signed in if ( - val.signedIn && + currentUser.signedIn && localStorage.getItem("alexandria/amber/fallback") === "1" && !showAmberFallback ) { @@ -53,7 +57,7 @@ } // Set up periodic check when user is signed in - if (val.signedIn && !fallbackCheckInterval) { + if (currentUser.signedIn && !fallbackCheckInterval) { fallbackCheckInterval = setInterval(() => { if ( localStorage.getItem("alexandria/amber/fallback") === "1" && @@ -65,7 +69,7 @@ showAmberFallback = true; } }, 500); // Check every 500ms - } else if (!val.signedIn && fallbackCheckInterval) { + } else if (!currentUser.signedIn && fallbackCheckInterval) { clearInterval(fallbackCheckInterval); fallbackCheckInterval = null; } @@ -249,6 +253,10 @@ > 📖 npub (read only) +
    +
    Network Status:
    + +
    {#if result} @@ -313,6 +321,10 @@ Unknown login method {/if} +
  • +
    Network Status:
    + +
  • -
  • - {#if isNav} -
  • - -
  • - {:else} - - {/if} - -
    -
    - - {/key} -
    - {/if} + + +
    +
    + {#if username} +

    {username}

    + {#if isNav}

    @{tag}

    {/if} + {:else} +

    Loading...

    + {/if} +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • + {#if isNav} +
    • + +
    • + {:else} + + {/if} +
    +
    +
    +
    + diff --git a/src/lib/components/util/ViewPublicationLink.svelte b/src/lib/components/util/ViewPublicationLink.svelte index fbbc550..fd7538d 100644 --- a/src/lib/components/util/ViewPublicationLink.svelte +++ b/src/lib/components/util/ViewPublicationLink.svelte @@ -3,7 +3,8 @@ import { getMatchingTags } from "$lib/utils/nostrUtils"; import { naddrEncode } from "$lib/utils"; import { getEventType } from "$lib/utils/mime"; - import { standardRelays } from "$lib/consts"; + import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; + import { communityRelays } from "$lib/consts"; import { goto } from "$app/navigation"; let { event, className = "" } = $props<{ @@ -25,7 +26,7 @@ return null; } try { - return naddrEncode(event, standardRelays); + return naddrEncode(event, $activeInboxRelays); } catch { return null; } diff --git a/src/lib/consts.ts b/src/lib/consts.ts index 19955c5..4241d1d 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -1,45 +1,48 @@ export const wikiKind = 30818; export const indexKind = 30040; export const zettelKinds = [30041, 30818]; -export const communityRelay = "wss://theforest.nostr1.com"; -export const profileRelays = [ + +export const communityRelays = [ + "wss://theforest.nostr1.com", + //"wss://theforest.gitcitadel.eu" +]; + +export const searchRelays = [ "wss://profiles.nostr1.com", "wss://aggr.nostr.land", "wss://relay.noswhere.com", -]; -export const standardRelays = [ - "wss://thecitadel.nostr1.com", - "wss://theforest.nostr1.com", - "wss://profiles.nostr1.com", - // Removed gitcitadel.nostr1.com as it's causing connection issues - //'wss://thecitadel.gitcitadel.eu', - //'wss://theforest.gitcitadel.eu', + "wss://nostr.wine", ]; -// Non-auth relays for anonymous users -export const anonymousRelays = [ - "wss://thecitadel.nostr1.com", +export const secondaryRelays = [ "wss://theforest.nostr1.com", - "wss://profiles.nostr1.com", - "wss://freelay.sovbit.host", -]; -export const fallbackRelays = [ - "wss://purplepag.es", - "wss://indexer.coracle.social", - "wss://relay.noswhere.com", - "wss://aggr.nostr.land", + //"wss://theforest.gitcitadel.eu" + "wss://thecitadel.nostr1.com", + //"wss://thecitadel.gitcitadel.eu", "wss://nostr.land", "wss://nostr.wine", "wss://nostr.sovbit.host", - "wss://freelay.sovbit.host", "wss://nostr21.com", - "wss://greensoul.space", - "wss://relay.damus.io", - "wss://relay.nostr.band", +]; + +export const anonymousRelays = [ + "wss://freelay.sovbit.host", + "wss://thecitadel.nostr1.com" +]; + +export const lowbandwidthRelays = [ + "wss://theforest.nostr1.com", + "wss://thecitadel.nostr1.com", + "wss://aggr.nostr.land" +]; + +export const localRelays = [ + "wss://localhost:8080", + "wss://localhost:4869" ]; export enum FeedType { - StandardRelays = "standard", + CommunityRelays = "standard", UserRelays = "user", } diff --git a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts index 83537c3..d62f189 100644 --- a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts +++ b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts @@ -8,8 +8,9 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NetworkNode, NetworkLink, GraphData, GraphState } from "../types"; import { nip19 } from "nostr-tools"; -import { standardRelays } from "$lib/consts"; +import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; import { getMatchingTags } from "$lib/utils/nostrUtils"; +import { get } from "svelte/store"; // Configuration const DEBUG = false; // Set to true to enable debug logging @@ -71,13 +72,13 @@ export function createNetworkNode( pubkey: event.pubkey, identifier: dTag, kind: event.kind, - relays: standardRelays, + relays: [...get(activeInboxRelays), ...get(activeOutboxRelays)], }); // Create nevent (NIP-19 event reference) for the event node.nevent = nip19.neventEncode({ id: event.id, - relays: standardRelays, + relays: [...get(activeInboxRelays), ...get(activeOutboxRelays)], kind: event.kind, }); } catch (error) { diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index 4e319b8..cd13df2 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -8,15 +8,29 @@ import NDK, { } from "@nostr-dev-kit/ndk"; import { get, writable, type Writable } from "svelte/store"; import { - fallbackRelays, + secondaryRelays, FeedType, loginStorageKey, - standardRelays, + communityRelays, anonymousRelays, + searchRelays, } from "./consts"; -import { feedType } from "./stores"; +import { + buildCompleteRelaySet, + testRelayConnection, + discoverLocalRelays, + getUserLocalRelays, + getUserBlockedRelays, + getUserOutboxRelays, + deduplicateRelayUrls, +} from "./utils/relay_management"; + +// Re-export testRelayConnection for components that need it +export { testRelayConnection }; +import { startNetworkMonitoring, NetworkCondition } from "./utils/network_detection"; import { userStore } from "./stores/userStore"; import { userPubkey } from "$lib/stores/authStore.Svelte"; +import { startNetworkStatusMonitoring, stopNetworkStatusMonitoring } from "./stores/networkStore"; export const ndkInstance: Writable = writable(); export const ndkSignedIn = writable(false); @@ -24,6 +38,10 @@ export const activePubkey = writable(null); export const inboxRelays = writable([]); export const outboxRelays = writable([]); +// New relay management stores +export const activeInboxRelays = writable([]); +export const activeOutboxRelays = writable([]); + /** * Custom authentication policy that handles NIP-42 authentication manually * when the default NDK authentication fails @@ -207,83 +225,7 @@ export function checkWebSocketSupport(): void { } } -/** - * Tests connection to a relay and returns connection status - * @param relayUrl The relay URL to test - * @param ndk The NDK instance - * @returns Promise that resolves to connection status - */ -export async function testRelayConnection( - relayUrl: string, - ndk: NDK, -): Promise<{ - connected: boolean; - requiresAuth: boolean; - error?: string; - actualUrl?: string; -}> { - return new Promise((resolve) => { - console.debug(`[NDK.ts] Testing connection to: ${relayUrl}`); - - // Ensure the URL is using wss:// protocol - const secureUrl = ensureSecureWebSocket(relayUrl); - - const relay = new NDKRelay(secureUrl, undefined, new NDK()); - let authRequired = false; - let connected = false; - let error: string | undefined; - let actualUrl: string | undefined; - - const timeout = setTimeout(() => { - relay.disconnect(); - resolve({ - connected: false, - requiresAuth: authRequired, - error: "Connection timeout", - actualUrl, - }); - }, 5000); - relay.on("connect", () => { - console.debug(`[NDK.ts] Connected to ${secureUrl}`); - connected = true; - actualUrl = secureUrl; - clearTimeout(timeout); - relay.disconnect(); - resolve({ - connected: true, - requiresAuth: authRequired, - error, - actualUrl, - }); - }); - - relay.on("notice", (message: string) => { - if (message.includes("auth-required")) { - authRequired = true; - console.debug(`[NDK.ts] ${secureUrl} requires authentication`); - } - }); - - relay.on("disconnect", () => { - if (!connected) { - error = "Connection failed"; - console.error(`[NDK.ts] Failed to connect to ${secureUrl}`); - clearTimeout(timeout); - resolve({ - connected: false, - requiresAuth: authRequired, - error, - actualUrl, - }); - } - }); - - // Log the actual WebSocket URL being used - console.debug(`[NDK.ts] Attempting connection to: ${secureUrl}`); - relay.connect(); - }); -} /** * Gets the user's pubkey from local storage, if it exists. @@ -433,98 +375,180 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { return relay; } -export function getActiveRelays(ndk: NDK): NDKRelaySet { + + + + +/** + * Gets the active relay set for the current user + * @param ndk NDK instance + * @returns Promise that resolves to object with inbox and outbox relay arrays + */ +export async function getActiveRelaySet(ndk: NDK): Promise<{ inboxRelays: string[]; outboxRelays: string[] }> { const user = get(userStore); + + if (user.signedIn && user.ndkUser) { + return await buildCompleteRelaySet(ndk, user.ndkUser); + } else { + return await buildCompleteRelaySet(ndk, null); + } +} - // Filter out problematic relays that are known to cause connection issues - const filterProblematicRelays = (relays: string[]) => { - return relays.filter((relay) => { - // Filter out gitcitadel.nostr1.com which is causing connection issues - if (relay.includes("gitcitadel.nostr1.com")) { - console.warn(`[NDK.ts] Filtering out problematic relay: ${relay}`); - return false; +/** + * Updates the active relay stores and NDK pool with new relay URLs + * @param ndk NDK instance + */ +export async function updateActiveRelayStores(ndk: NDK): Promise { + try { + // Get the active relay set from the relay management system + const relaySet = await getActiveRelaySet(ndk); + + // Update the stores with the new relay configuration + activeInboxRelays.set(relaySet.inboxRelays); + activeOutboxRelays.set(relaySet.outboxRelays); + + // Add relays to NDK pool (deduplicated) + const allRelayUrls = deduplicateRelayUrls([...relaySet.inboxRelays, ...relaySet.outboxRelays]); + for (const url of allRelayUrls) { + try { + const relay = createRelayWithAuth(url, ndk); + ndk.pool?.addRelay(relay); + } catch (error) { + // Silently ignore relay addition failures } - return true; - }); - }; + } + } catch (error) { + // Silently ignore relay store update errors + } +} - return get(feedType) === FeedType.UserRelays && user.signedIn - ? new NDKRelaySet( - new Set( - filterProblematicRelays(user.relays.inbox).map( - (relay) => - new NDKRelay(relay, NDKRelayAuthPolicies.signIn({ ndk }), ndk), - ), - ), - ndk, - ) - : new NDKRelaySet( - new Set( - filterProblematicRelays(standardRelays).map( - (relay) => - new NDKRelay(relay, NDKRelayAuthPolicies.signIn({ ndk }), ndk), - ), - ), - ndk, - ); +/** + * Logs the current relay configuration to console + */ +export function logCurrentRelayConfiguration(): void { + const inboxRelays = get(activeInboxRelays); + const outboxRelays = get(activeOutboxRelays); + + console.log('🔌 Current Relay Configuration:'); + console.log('📥 Inbox Relays:', inboxRelays); + console.log('📤 Outbox Relays:', outboxRelays); + console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`); } /** - * Initializes an instance of NDK, and connects it to the logged-in user's preferred relay set - * (if available), or to Alexandria's standard relay set. - * @returns The initialized NDK instance. + * Updates relay stores when user state changes + * @param ndk NDK instance */ -export function initNdk(): NDK { - const startingPubkey = getPersistedLogin(); - const [startingInboxes, _] = - startingPubkey != null - ? getPersistedRelays(new NDKUser({ pubkey: startingPubkey })) - : [null, null]; +export async function refreshRelayStores(ndk: NDK): Promise { + console.debug('[NDK.ts] Refreshing relay stores due to user state change'); + await updateActiveRelayStores(ndk); +} + +/** + * Updates relay stores when network condition changes + * @param ndk NDK instance + */ +export async function refreshRelayStoresOnNetworkChange(ndk: NDK): Promise { + console.debug('[NDK.ts] Refreshing relay stores due to network condition change'); + await updateActiveRelayStores(ndk); +} + +/** + * Starts network monitoring for relay optimization + * @param ndk NDK instance + */ +export function startNetworkMonitoringForRelays(ndk: NDK): void { + // Use centralized network monitoring instead of separate monitoring + startNetworkStatusMonitoring(); +} - // Ensure all relay URLs use secure WebSocket protocol - const secureRelayUrls = ( - startingInboxes != null - ? Array.from(startingInboxes.values()) - : anonymousRelays - ).map(ensureSecureWebSocket); +/** + * Creates NDKRelaySet from relay URLs with proper authentication + * @param relayUrls Array of relay URLs + * @param ndk NDK instance + * @returns NDKRelaySet + */ +function createRelaySetFromUrls(relayUrls: string[], ndk: NDK): NDKRelaySet { + const relays = relayUrls.map(url => + new NDKRelay(url, NDKRelayAuthPolicies.signIn({ ndk }), ndk) + ); + + return new NDKRelaySet(new Set(relays), ndk); +} - console.debug("[NDK.ts] Initializing NDK with relay URLs:", secureRelayUrls); +/** + * Gets the active relay set as NDKRelaySet for use in queries + * @param ndk NDK instance + * @param useInbox Whether to use inbox relays (true) or outbox relays (false) + * @returns Promise that resolves to NDKRelaySet + */ +export async function getActiveRelaySetAsNDKRelaySet( + ndk: NDK, + useInbox: boolean = true +): Promise { + const relaySet = await getActiveRelaySet(ndk); + const urls = useInbox ? relaySet.inboxRelays : relaySet.outboxRelays; + + return createRelaySetFromUrls(urls, ndk); +} + +/** + * Initializes an instance of NDK with the new relay management system + * @returns The initialized NDK instance + */ +export function initNdk(): NDK { + console.debug("[NDK.ts] Initializing NDK with new relay management system"); const ndk = new NDK({ - autoConnectUserRelays: true, + autoConnectUserRelays: false, // We'll manage relays manually enableOutboxModel: true, - explicitRelayUrls: secureRelayUrls, }); // Set up custom authentication policy ndk.relayAuthDefaultPolicy = NDKRelayAuthPolicies.signIn({ ndk }); - // Connect with better error handling - ndk - .connect() - .then(() => { + // Connect with better error handling and reduced retry attempts + let retryCount = 0; + const maxRetries = 2; + + const attemptConnection = async () => { + try { + await ndk.connect(); console.debug("[NDK.ts] NDK connected successfully"); - }) - .catch((error) => { - console.error("[NDK.ts] Failed to connect NDK:", error); - // Try to reconnect after a delay - setTimeout(() => { - console.debug("[NDK.ts] Attempting to reconnect..."); - ndk.connect().catch((retryError) => { - console.error("[NDK.ts] Reconnection failed:", retryError); - }); - }, 5000); - }); + // Update relay stores after connection + await updateActiveRelayStores(ndk); + // Start network monitoring for relay optimization + startNetworkMonitoringForRelays(ndk); + } catch (error) { + console.warn("[NDK.ts] Failed to connect NDK:", error); + + // Only retry a limited number of times + if (retryCount < maxRetries) { + retryCount++; + console.debug(`[NDK.ts] Attempting to reconnect (${retryCount}/${maxRetries})...`); + setTimeout(attemptConnection, 3000); + } else { + console.warn("[NDK.ts] Max retries reached, continuing with limited functionality"); + // Still try to update relay stores even if connection failed + try { + await updateActiveRelayStores(ndk); + startNetworkMonitoringForRelays(ndk); + } catch (storeError) { + console.warn("[NDK.ts] Failed to update relay stores:", storeError); + } + } + } + }; + + attemptConnection(); return ndk; } /** - * Signs in with a NIP-07 browser extension, and determines the user's preferred inbox and outbox - * relays. - * @returns The user's profile, if it is available. - * @throws If sign-in fails. This may because there is no accessible NIP-07 extension, or because - * NDK is unable to fetch the user's profile or relay lists. + * Signs in with a NIP-07 browser extension using the new relay management system + * @returns The user's profile, if it is available + * @throws If sign-in fails */ export async function loginWithExtension( pubkey?: string, @@ -542,23 +566,10 @@ export async function loginWithExtension( activePubkey.set(signerUser.pubkey); userPubkey.set(signerUser.pubkey); - const [persistedInboxes, persistedOutboxes] = - getPersistedRelays(signerUser); - for (const relay of persistedInboxes) { - ndk.addExplicitRelay(relay); - } - const user = ndk.getUser({ pubkey: signerUser.pubkey }); - const [inboxes, outboxes] = await getUserPreferredRelays(ndk, user); - - inboxRelays.set( - Array.from(inboxes ?? persistedInboxes).map((relay) => relay.url), - ); - outboxRelays.set( - Array.from(outboxes ?? persistedOutboxes).map((relay) => relay.url), - ); - - persistRelays(signerUser, inboxes, outboxes); + + // Update relay stores with the new system + await updateActiveRelayStores(ndk); ndk.signer = signer; ndk.activeUser = user; @@ -582,73 +593,17 @@ export function logout(user: NDKUser): void { activePubkey.set(null); userPubkey.set(null); ndkSignedIn.set(false); - ndkInstance.set(initNdk()); // Re-initialize with anonymous instance + + // Clear relay stores + activeInboxRelays.set([]); + activeOutboxRelays.set([]); + + // Stop network monitoring + stopNetworkStatusMonitoring(); + + // Re-initialize with anonymous instance + const newNdk = initNdk(); + ndkInstance.set(newNdk); } -/** - * Fetches the user's NIP-65 relay list, if one can be found, and returns the inbox and outbox - * relay sets. - * @returns A tuple of relay sets of the form `[inboxRelays, outboxRelays]`. - */ -export async function getUserPreferredRelays( - ndk: NDK, - user: NDKUser, - fallbacks: readonly string[] = fallbackRelays, -): Promise<[Set, Set]> { - const relayList = await ndk.fetchEvent( - { - kinds: [10002], - authors: [user.pubkey], - }, - { - groupable: false, - skipVerification: false, - skipValidation: false, - }, - NDKRelaySet.fromRelayUrls(fallbacks, ndk), - ); - - const inboxRelays = new Set(); - const outboxRelays = new Set(); - // Filter out problematic relays - const filterProblematicRelay = (url: string): boolean => { - if (url.includes("gitcitadel.nostr1.com")) { - console.warn( - `[NDK.ts] Filtering out problematic relay from user preferences: ${url}`, - ); - return false; - } - return true; - }; - - if (relayList == null) { - const relayMap = await window.nostr?.getRelays?.(); - Object.entries(relayMap ?? {}).forEach(([url, relayType]) => { - if (filterProblematicRelay(url)) { - const relay = createRelayWithAuth(url, ndk); - if (relayType.read) inboxRelays.add(relay); - if (relayType.write) outboxRelays.add(relay); - } - }); - } else { - relayList.tags.forEach((tag) => { - if (filterProblematicRelay(tag[1])) { - switch (tag[0]) { - case "r": - inboxRelays.add(createRelayWithAuth(tag[1], ndk)); - break; - case "w": - outboxRelays.add(createRelayWithAuth(tag[1], ndk)); - break; - default: - inboxRelays.add(createRelayWithAuth(tag[1], ndk)); - outboxRelays.add(createRelayWithAuth(tag[1], ndk)); - break; - } - } - }); - } - - return [inboxRelays, outboxRelays]; -} diff --git a/src/lib/stores.ts b/src/lib/stores.ts index ae38008..3315022 100644 --- a/src/lib/stores.ts +++ b/src/lib/stores.ts @@ -1,11 +1,11 @@ -import { readable, writable } from "svelte/store"; -import { FeedType } from "./consts.ts"; +import { writable } from "svelte/store"; -export let idList = writable([]); +// The old feedType store is no longer needed since we use the new relay management system +// All relay selection is now handled by the activeInboxRelays and activeOutboxRelays stores in ndk.ts -export let alexandriaKinds = readable([30040, 30041, 30818]); +export let idList = writable([]); -export let feedType = writable(FeedType.StandardRelays); +export let alexandriaKinds = writable([30040, 30041, 30818]); export interface PublicationLayoutVisibility { toc: boolean; diff --git a/src/lib/stores/networkStore.ts b/src/lib/stores/networkStore.ts new file mode 100644 index 0000000..981b0c9 --- /dev/null +++ b/src/lib/stores/networkStore.ts @@ -0,0 +1,55 @@ +import { writable, type Writable } from 'svelte/store'; +import { detectNetworkCondition, NetworkCondition, startNetworkMonitoring } from '$lib/utils/network_detection'; + +// Network status store +export const networkCondition = writable(NetworkCondition.ONLINE); +export const isNetworkChecking = writable(false); + +// Network monitoring state +let stopNetworkMonitoring: (() => void) | null = null; + +/** + * Starts network monitoring if not already running + */ +export function startNetworkStatusMonitoring(): void { + if (stopNetworkMonitoring) { + return; // Already monitoring + } + + console.debug('[networkStore.ts] Starting network status monitoring'); + + stopNetworkMonitoring = startNetworkMonitoring( + (condition: NetworkCondition) => { + console.debug(`[networkStore.ts] Network condition changed to: ${condition}`); + networkCondition.set(condition); + }, + 60000 // Check every 60 seconds to reduce spam + ); +} + +/** + * Stops network monitoring + */ +export function stopNetworkStatusMonitoring(): void { + if (stopNetworkMonitoring) { + console.debug('[networkStore.ts] Stopping network status monitoring'); + stopNetworkMonitoring(); + stopNetworkMonitoring = null; + } +} + +/** + * Manually check network status (for immediate updates) + */ +export async function checkNetworkStatus(): Promise { + try { + isNetworkChecking.set(true); + const condition = await detectNetworkCondition(); + networkCondition.set(condition); + } catch (error) { + console.warn('[networkStore.ts] Failed to check network status:', error); + networkCondition.set(NetworkCondition.OFFLINE); + } finally { + isNetworkChecking.set(false); + } +} \ No newline at end of file diff --git a/src/lib/stores/relayStore.ts b/src/lib/stores/relayStore.ts deleted file mode 100644 index 2c038c7..0000000 --- a/src/lib/stores/relayStore.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { writable } from "svelte/store"; - -// Initialize with empty array, will be populated from user preferences -export const userRelays = writable([]); diff --git a/src/lib/stores/userStore.ts b/src/lib/stores/userStore.ts index e5dbe8b..3275e23 100644 --- a/src/lib/stores/userStore.ts +++ b/src/lib/stores/userStore.ts @@ -8,8 +8,8 @@ import { NDKRelay, } from "@nostr-dev-kit/ndk"; import { getUserMetadata } from "$lib/utils/nostrUtils"; -import { ndkInstance } from "$lib/ndk"; -import { loginStorageKey, fallbackRelays } from "$lib/consts"; +import { ndkInstance, activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; +import { loginStorageKey } from "$lib/consts"; import { nip19 } from "nostr-tools"; export interface UserState { @@ -70,7 +70,7 @@ function getPersistedRelays(user: NDKUser): [Set, Set] { async function getUserPreferredRelays( ndk: any, user: NDKUser, - fallbacks: readonly string[] = fallbackRelays, + fallbacks: readonly string[] = [...get(activeInboxRelays), ...get(activeOutboxRelays)], ): Promise<[Set, Set]> { const relayList = await ndk.fetchEvent( { diff --git a/src/lib/utils/ZettelParser.ts b/src/lib/utils/ZettelParser.ts index 24b0891..42d1b77 100644 --- a/src/lib/utils/ZettelParser.ts +++ b/src/lib/utils/ZettelParser.ts @@ -1,7 +1,7 @@ import { ndkInstance } from "$lib/ndk"; import { signEvent, getEventHash } from "$lib/utils/nostrUtils"; import { getMimeTags } from "$lib/utils/mime"; -import { standardRelays } from "$lib/consts"; +import { communityRelays } from "$lib/consts"; import { nip19 } from "nostr-tools"; export interface ZettelSection { diff --git a/src/lib/utils/community_checker.ts b/src/lib/utils/community_checker.ts index 56c9030..7e6ea11 100644 --- a/src/lib/utils/community_checker.ts +++ b/src/lib/utils/community_checker.ts @@ -1,4 +1,4 @@ -import { communityRelay } from "$lib/consts"; +import { communityRelays } from "$lib/consts"; import { RELAY_CONSTANTS, SEARCH_LIMITS } from "./search_constants"; // Cache for pubkeys with kind 1 events on communityRelay @@ -13,40 +13,54 @@ export async function checkCommunity(pubkey: string): Promise { } try { - const relayUrl = communityRelay; - const ws = new WebSocket(relayUrl); - return await new Promise((resolve) => { - ws.onopen = () => { - ws.send( - JSON.stringify([ - "REQ", - RELAY_CONSTANTS.COMMUNITY_REQUEST_ID, - { - kinds: RELAY_CONSTANTS.COMMUNITY_REQUEST_KINDS, - authors: [pubkey], - limit: SEARCH_LIMITS.COMMUNITY_CHECK, - }, - ]), - ); - }; - ws.onmessage = (event) => { - const data = JSON.parse(event.data); - if (data[0] === "EVENT" && data[2]?.kind === 1) { - communityCache.set(pubkey, true); - ws.close(); - resolve(true); - } else if (data[0] === "EOSE") { - communityCache.set(pubkey, false); - ws.close(); - resolve(false); + // Try each community relay until we find one that works + for (const relayUrl of communityRelays) { + try { + const ws = new WebSocket(relayUrl); + const result = await new Promise((resolve) => { + ws.onopen = () => { + ws.send( + JSON.stringify([ + "REQ", + RELAY_CONSTANTS.COMMUNITY_REQUEST_ID, + { + kinds: RELAY_CONSTANTS.COMMUNITY_REQUEST_KINDS, + authors: [pubkey], + limit: SEARCH_LIMITS.COMMUNITY_CHECK, + }, + ]), + ); + }; + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data[0] === "EVENT" && data[2]?.kind === 1) { + communityCache.set(pubkey, true); + ws.close(); + resolve(true); + } else if (data[0] === "EOSE") { + communityCache.set(pubkey, false); + ws.close(); + resolve(false); + } + }; + ws.onerror = () => { + ws.close(); + resolve(false); + }; + }); + + if (result) { + return true; } - }; - ws.onerror = () => { - communityCache.set(pubkey, false); - ws.close(); - resolve(false); - }; - }); + } catch { + // Continue to next relay if this one fails + continue; + } + } + + // If we get here, no relay found the user + communityCache.set(pubkey, false); + return false; } catch { communityCache.set(pubkey, false); return false; diff --git a/src/lib/utils/network_detection.ts b/src/lib/utils/network_detection.ts new file mode 100644 index 0000000..1e812db --- /dev/null +++ b/src/lib/utils/network_detection.ts @@ -0,0 +1,189 @@ +import { deduplicateRelayUrls } from './relay_management'; + +/** + * Network conditions for relay selection + */ +export enum NetworkCondition { + ONLINE = 'online', + SLOW = 'slow', + OFFLINE = 'offline' +} + +/** + * Network connectivity test endpoints + */ +const NETWORK_ENDPOINTS = [ + 'https://www.google.com/favicon.ico', + 'https://httpbin.org/status/200', + 'https://api.github.com/zen' +]; + +/** + * Detects if the network is online using more reliable endpoints + * @returns Promise that resolves to true if online, false otherwise + */ +export async function isNetworkOnline(): Promise { + for (const endpoint of NETWORK_ENDPOINTS) { + try { + // Use a simple fetch without HEAD method to avoid CORS issues + const response = await fetch(endpoint, { + method: 'GET', + cache: 'no-cache', + signal: AbortSignal.timeout(3000), + mode: 'no-cors' // Use no-cors mode to avoid CORS issues + }); + // With no-cors mode, we can't check response.ok, so we assume success if no error + return true; + } catch (error) { + console.debug(`[network_detection.ts] Failed to reach ${endpoint}:`, error); + continue; + } + } + + console.debug('[network_detection.ts] All network endpoints failed'); + return false; +} + +/** + * Tests network speed by measuring response time + * @returns Promise that resolves to network speed in milliseconds + */ +export async function testNetworkSpeed(): Promise { + const startTime = performance.now(); + + for (const endpoint of NETWORK_ENDPOINTS) { + try { + await fetch(endpoint, { + method: 'GET', + cache: 'no-cache', + signal: AbortSignal.timeout(5000), + mode: 'no-cors' // Use no-cors mode to avoid CORS issues + }); + + const endTime = performance.now(); + return endTime - startTime; + } catch (error) { + console.debug(`[network_detection.ts] Speed test failed for ${endpoint}:`, error); + continue; + } + } + + console.debug('[network_detection.ts] Network speed test failed for all endpoints'); + return Infinity; // Very slow if it fails +} + +/** + * Determines network condition based on connectivity and speed + * @returns Promise that resolves to NetworkCondition + */ +export async function detectNetworkCondition(): Promise { + const isOnline = await isNetworkOnline(); + + if (!isOnline) { + console.debug('[network_detection.ts] Network condition: OFFLINE'); + return NetworkCondition.OFFLINE; + } + + const speed = await testNetworkSpeed(); + + // Consider network slow if response time > 2000ms + if (speed > 2000) { + console.debug(`[network_detection.ts] Network condition: SLOW (${speed.toFixed(0)}ms)`); + return NetworkCondition.SLOW; + } + + console.debug(`[network_detection.ts] Network condition: ONLINE (${speed.toFixed(0)}ms)`); + return NetworkCondition.ONLINE; +} + +/** + * Gets the appropriate relay sets based on network condition + * @param networkCondition The detected network condition + * @param discoveredLocalRelays Array of discovered local relay URLs + * @param lowbandwidthRelays Array of low bandwidth relay URLs + * @param fullRelaySet The complete relay set for normal conditions + * @returns Object with inbox and outbox relay arrays + */ +export function getRelaySetForNetworkCondition( + networkCondition: NetworkCondition, + discoveredLocalRelays: string[], + lowbandwidthRelays: string[], + fullRelaySet: { inboxRelays: string[]; outboxRelays: string[] } +): { inboxRelays: string[]; outboxRelays: string[] } { + switch (networkCondition) { + case NetworkCondition.OFFLINE: + // When offline, use local relays if available, otherwise rely on cache + // This will be improved when IndexedDB local relay is implemented + if (discoveredLocalRelays.length > 0) { + console.debug('[network_detection.ts] Using local relays (offline)'); + return { + inboxRelays: discoveredLocalRelays, + outboxRelays: discoveredLocalRelays + }; + } else { + console.debug('[network_detection.ts] No local relays available, will rely on cache (offline)'); + return { + inboxRelays: [], + outboxRelays: [] + }; + } + + case NetworkCondition.SLOW: + // Local relays + low bandwidth relays when slow (deduplicated) + console.debug('[network_detection.ts] Using local + low bandwidth relays (slow network)'); + const slowInboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...lowbandwidthRelays]); + const slowOutboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...lowbandwidthRelays]); + return { + inboxRelays: slowInboxRelays, + outboxRelays: slowOutboxRelays + }; + + case NetworkCondition.ONLINE: + default: + // Full relay set when online + console.debug('[network_detection.ts] Using full relay set (online)'); + return fullRelaySet; + } +} + +/** + * Starts periodic network monitoring with reduced frequency to avoid spam + * @param onNetworkChange Callback function called when network condition changes + * @param checkInterval Interval in milliseconds between network checks (default: 60 seconds) + * @returns Function to stop the monitoring + */ +export function startNetworkMonitoring( + onNetworkChange: (condition: NetworkCondition) => void, + checkInterval: number = 60000 // Increased to 60 seconds to reduce spam +): () => void { + let lastCondition: NetworkCondition | null = null; + let intervalId: number | null = null; + + const checkNetwork = async () => { + try { + const currentCondition = await detectNetworkCondition(); + + if (currentCondition !== lastCondition) { + console.debug(`[network_detection.ts] Network condition changed: ${lastCondition} -> ${currentCondition}`); + lastCondition = currentCondition; + onNetworkChange(currentCondition); + } + } catch (error) { + console.warn('[network_detection.ts] Network monitoring error:', error); + } + }; + + // Initial check + checkNetwork(); + + // Set up periodic monitoring + intervalId = window.setInterval(checkNetwork, checkInterval); + + // Return function to stop monitoring + return () => { + if (intervalId !== null) { + clearInterval(intervalId); + intervalId = null; + } + }; +} \ No newline at end of file diff --git a/src/lib/utils/nostrEventService.ts b/src/lib/utils/nostrEventService.ts index 5efb37b..e7d437f 100644 --- a/src/lib/utils/nostrEventService.ts +++ b/src/lib/utils/nostrEventService.ts @@ -1,11 +1,13 @@ import { nip19 } from "nostr-tools"; import { getEventHash, signEvent, prefixNostrAddresses } from "./nostrUtils"; -import { standardRelays, fallbackRelays } from "$lib/consts"; -import { userRelays } from "$lib/stores/relayStore"; +import { communityRelays, secondaryRelays } from "$lib/consts"; import { get } from "svelte/store"; import { goto } from "$app/navigation"; import type { NDKEvent } from "./nostrUtils"; import { EVENT_KINDS, TIME_CONSTANTS, TIMEOUTS } from "./search_constants"; +import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; +import { ndkInstance } from "$lib/ndk"; +import { NDKRelaySet } from "@nostr-dev-kit/ndk"; export interface RootEventInfo { rootId: string; @@ -358,82 +360,44 @@ export async function createSignedEvent( } /** - * Publish event to a single relay - */ -async function publishToRelay( - relayUrl: string, - signedEvent: any, -): Promise { - const ws = new WebSocket(relayUrl); - - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - ws.close(); - reject(new Error("Timeout")); - }, TIMEOUTS.GENERAL); - - ws.onopen = () => { - ws.send(JSON.stringify(["EVENT", signedEvent])); - }; - - ws.onmessage = (e) => { - const [type, id, ok, message] = JSON.parse(e.data); - if (type === "OK" && id === signedEvent.id) { - clearTimeout(timeout); - if (ok) { - ws.close(); - resolve(); - } else { - ws.close(); - reject(new Error(message)); - } - } - }; - - ws.onerror = () => { - clearTimeout(timeout); - ws.close(); - reject(new Error("WebSocket error")); - }; - }); -} - -/** - * Publish event to relays + * Publishes an event to relays using the new relay management system + * @param event The event to publish + * @param relayUrls Array of relay URLs to publish to + * @returns Promise that resolves to array of successful relay URLs */ export async function publishEvent( - signedEvent: any, - useOtherRelays = false, - useFallbackRelays = false, - userRelayPreference = false, -): Promise { - // Determine which relays to use - let relays = userRelayPreference ? get(userRelays) : standardRelays; - if (useOtherRelays) { - relays = userRelayPreference ? standardRelays : get(userRelays); - } - if (useFallbackRelays) { - relays = fallbackRelays; + event: NDKEvent, + relayUrls: string[], +): Promise { + const successfulRelays: string[] = []; + const ndk = get(ndkInstance); + + if (!ndk) { + throw new Error("NDK instance not available"); } - // Try to publish to relays - for (const relayUrl of relays) { - try { - await publishToRelay(relayUrl, signedEvent); - return { - success: true, - relay: relayUrl, - eventId: signedEvent.id, - }; - } catch (e) { - console.error(`Failed to publish to ${relayUrl}:`, e); - } + // Create relay set from URLs + const relaySet = NDKRelaySet.fromRelayUrls(relayUrls, ndk); + + try { + // Publish with timeout + await event.publish(relaySet).withTimeout(10000); + + // For now, assume all relays were successful + // In a more sophisticated implementation, you'd track individual relay responses + successfulRelays.push(...relayUrls); + + console.debug("[nostrEventService] Published event successfully:", { + eventId: event.id, + relayCount: relayUrls.length, + successfulRelays + }); + } catch (error) { + console.error("[nostrEventService] Failed to publish event:", error); + throw new Error(`Failed to publish event: ${error}`); } - return { - success: false, - error: "Failed to publish to any relays", - }; + return successfulRelays; } /** diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index 23ed7b9..a7aef5e 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -4,7 +4,8 @@ import { ndkInstance } from "$lib/ndk"; import { npubCache } from "./npubCache"; import NDK, { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; import type { NDKFilter, NDKKind } from "@nostr-dev-kit/ndk"; -import { standardRelays, fallbackRelays, anonymousRelays } from "$lib/consts"; +import { communityRelays, secondaryRelays, anonymousRelays } from "$lib/consts"; +import { activeInboxRelays } from "$lib/ndk"; import { NDKRelaySet as NDKRelaySetFromNDK } from "@nostr-dev-kit/ndk"; import { sha256 } from "@noble/hashes/sha256"; import { schnorr } from "@noble/curves/secp256k1"; @@ -174,9 +175,9 @@ export async function createProfileLinkWithVerification( }; const allRelays = [ - ...standardRelays, + ...communityRelays, ...userRelays, - ...fallbackRelays, + ...secondaryRelays, ].filter((url, idx, arr) => arr.indexOf(url) === idx); const filteredRelays = filterProblematicRelays(allRelays); @@ -422,91 +423,43 @@ export async function fetchEventWithFallback( filterOrId: string | NDKFilter, timeoutMs: number = 3000, ): Promise { - // Get user relays if logged in - const userRelays = ndk.activeUser - ? Array.from(ndk.pool?.relays.values() || []) - .filter((r) => r.status === 1) // Only use connected relays - .map((r) => r.url) - .filter((url) => !url.includes("gitcitadel.nostr1.com")) // Filter out problematic relay - : []; - - // Determine which relays to use based on user authentication status - const isSignedIn = ndk.signer && ndk.activeUser; - const primaryRelays = isSignedIn ? standardRelays : anonymousRelays; - - // Create three relay sets in priority order - const relaySets = [ - NDKRelaySetFromNDK.fromRelayUrls(primaryRelays, ndk), // 1. Primary relays (auth or anonymous) - NDKRelaySetFromNDK.fromRelayUrls(userRelays, ndk), // 2. User relays (if logged in) - NDKRelaySetFromNDK.fromRelayUrls(fallbackRelays, ndk), // 3. fallback relays (last resort) - ]; + // Use the active inbox relays from the relay management system + const inboxRelays = get(activeInboxRelays); + + // Create relay set from active inbox relays + const relaySet = NDKRelaySetFromNDK.fromRelayUrls(inboxRelays, ndk); try { - let found: NDKEvent | null = null; - const triedRelaySets: string[] = []; - - // Helper function to try fetching from a relay set - async function tryFetchFromRelaySet( - relaySet: NDKRelaySetFromNDK, - setName: string, - ): Promise { - if (relaySet.relays.size === 0) return null; - triedRelaySets.push(setName); - - if ( - typeof filterOrId === "string" && - new RegExp(`^[0-9a-f]{${VALIDATION.HEX_LENGTH}}$`, "i").test(filterOrId) - ) { - return await ndk - .fetchEvent({ ids: [filterOrId] }, undefined, relaySet) - .withTimeout(timeoutMs); - } else { - const filter = - typeof filterOrId === "string" ? { ids: [filterOrId] } : filterOrId; - const results = await ndk - .fetchEvents(filter, undefined, relaySet) - .withTimeout(timeoutMs); - return results instanceof Set - ? (Array.from(results)[0] as NDKEvent) - : null; - } + if (relaySet.relays.size === 0) { + console.warn("No inbox relays available for event fetch"); + return null; } - // Try each relay set in order - for (const [index, relaySet] of relaySets.entries()) { - const setName = - index === 0 - ? isSignedIn - ? "standard relays" - : "anonymous relays" - : index === 1 - ? "user relays" - : "fallback relays"; - - found = await tryFetchFromRelaySet(relaySet, setName); - if (found) break; + let found: NDKEvent | null = null; + + if ( + typeof filterOrId === "string" && + new RegExp(`^[0-9a-f]{${VALIDATION.HEX_LENGTH}}$`, "i").test(filterOrId) + ) { + found = await ndk + .fetchEvent({ ids: [filterOrId] }, undefined, relaySet) + .withTimeout(timeoutMs); + } else { + const filter = + typeof filterOrId === "string" ? { ids: [filterOrId] } : filterOrId; + const results = await ndk + .fetchEvents(filter, undefined, relaySet) + .withTimeout(timeoutMs); + found = results instanceof Set + ? (Array.from(results)[0] as NDKEvent) + : null; } if (!found) { const timeoutSeconds = timeoutMs / 1000; - const relayUrls = relaySets - .map((set, i) => { - const setName = - i === 0 - ? isSignedIn - ? "standard relays" - : "anonymous relays" - : i === 1 - ? "user relays" - : "fallback relays"; - const urls = Array.from(set.relays).map((r) => r.url); - return urls.length > 0 ? `${setName} (${urls.join(", ")})` : null; - }) - .filter(Boolean) - .join(", then "); - + const relayUrls = Array.from(relaySet.relays).map((r: any) => r.url).join(", "); console.warn( - `Event not found after ${timeoutSeconds}s timeout. Tried ${relayUrls}. Some relays may be offline or slow.`, + `Event not found after ${timeoutSeconds}s timeout. Tried inbox relays: ${relayUrls}. Some relays may be offline or slow.`, ); return null; } diff --git a/src/lib/utils/profile_search.ts b/src/lib/utils/profile_search.ts index 20b3db0..1fde323 100644 --- a/src/lib/utils/profile_search.ts +++ b/src/lib/utils/profile_search.ts @@ -2,7 +2,7 @@ import { ndkInstance } from "$lib/ndk"; import { getUserMetadata, getNpubFromNip05 } from "$lib/utils/nostrUtils"; import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk"; import { searchCache } from "$lib/utils/searchCache"; -import { standardRelays, fallbackRelays } from "$lib/consts"; +import { communityRelays, secondaryRelays } from "$lib/consts"; import { get } from "svelte/store"; import type { NostrProfile, ProfileSearchResult } from "./search_types"; import { @@ -270,7 +270,7 @@ async function quickRelaySearch( console.log("Normalized search term for relay search:", normalizedSearchTerm); // Use all profile relays for better coverage - const quickRelayUrls = [...standardRelays, ...fallbackRelays]; // Use all available relays + const quickRelayUrls = [...communityRelays, ...secondaryRelays]; // Use all available relays console.log("Using all relays for search:", quickRelayUrls); // Create relay sets for parallel search diff --git a/src/lib/utils/relayDiagnostics.ts b/src/lib/utils/relayDiagnostics.ts index dee9bc4..9a0db7c 100644 --- a/src/lib/utils/relayDiagnostics.ts +++ b/src/lib/utils/relayDiagnostics.ts @@ -1,6 +1,7 @@ -import { standardRelays, anonymousRelays, fallbackRelays } from "$lib/consts"; +import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; import NDK from "@nostr-dev-kit/ndk"; import { TIMEOUTS } from "./search_constants"; +import { get } from "svelte/store"; export interface RelayDiagnostic { url: string; @@ -85,9 +86,8 @@ export async function testRelay(url: string): Promise { * Tests all relays and returns diagnostic information */ export async function testAllRelays(): Promise { - const allRelays = [ - ...new Set([...standardRelays, ...anonymousRelays, ...fallbackRelays]), - ]; + // Use the new relay management system + const allRelays = [...get(activeInboxRelays), ...get(activeOutboxRelays)]; console.log("[RelayDiagnostics] Testing", allRelays.length, "relays..."); diff --git a/src/lib/utils/relay_management.ts b/src/lib/utils/relay_management.ts new file mode 100644 index 0000000..dbaa08a --- /dev/null +++ b/src/lib/utils/relay_management.ts @@ -0,0 +1,424 @@ +import NDK, { NDKRelay, NDKUser } from "@nostr-dev-kit/ndk"; +import { communityRelays, searchRelays, secondaryRelays, anonymousRelays, lowbandwidthRelays, localRelays } from "../consts"; +import { getRelaySetForNetworkCondition, NetworkCondition } from "./network_detection"; +import { networkCondition } from "../stores/networkStore"; +import { get } from "svelte/store"; + +/** + * Normalizes a relay URL to a standard format + * @param url The relay URL to normalize + * @returns The normalized relay URL + */ +export function normalizeRelayUrl(url: string): string { + let normalized = url.toLowerCase().trim(); + + // Ensure protocol is present + if (!normalized.startsWith('ws://') && !normalized.startsWith('wss://')) { + normalized = 'wss://' + normalized; + } + + // Remove trailing slash + normalized = normalized.replace(/\/$/, ''); + + return normalized; +} + +/** + * Normalizes an array of relay URLs + * @param urls Array of relay URLs to normalize + * @returns Array of normalized relay URLs + */ +export function normalizeRelayUrls(urls: string[]): string[] { + return urls.map(normalizeRelayUrl); +} + +/** + * Removes duplicates from an array of relay URLs + * @param urls Array of relay URLs + * @returns Array of unique relay URLs + */ +export function deduplicateRelayUrls(urls: string[]): string[] { + const normalized = normalizeRelayUrls(urls); + return [...new Set(normalized)]; +} + +/** + * Tests connection to a relay and returns connection status + * @param relayUrl The relay URL to test + * @param ndk The NDK instance + * @returns Promise that resolves to connection status + */ +export async function testRelayConnection( + relayUrl: string, + ndk: NDK, +): Promise<{ + connected: boolean; + requiresAuth: boolean; + error?: string; + actualUrl?: string; +}> { + return new Promise((resolve) => { + // Ensure the URL is using wss:// protocol + const secureUrl = ensureSecureWebSocket(relayUrl); + + // Use the existing NDK instance instead of creating a new one + const relay = new NDKRelay(secureUrl, undefined, ndk); + let authRequired = false; + let connected = false; + let error: string | undefined; + let actualUrl: string | undefined; + + const timeout = setTimeout(() => { + relay.disconnect(); + resolve({ + connected: false, + requiresAuth: authRequired, + error: "Connection timeout", + actualUrl, + }); + }, 3000); // Increased timeout to 3 seconds to give relays more time + + relay.on("connect", () => { + connected = true; + actualUrl = secureUrl; + clearTimeout(timeout); + relay.disconnect(); + resolve({ + connected: true, + requiresAuth: authRequired, + error, + actualUrl, + }); + }); + + relay.on("notice", (message: string) => { + if (message.includes("auth-required")) { + authRequired = true; + } + }); + + relay.on("disconnect", () => { + if (!connected) { + error = "Connection failed"; + clearTimeout(timeout); + resolve({ + connected: false, + requiresAuth: authRequired, + error, + actualUrl, + }); + } + }); + + relay.connect(); + }); +} + +/** + * Ensures a relay URL uses secure WebSocket protocol for remote relays + * @param url The relay URL to secure + * @returns The URL with wss:// protocol (except for localhost) + */ +function ensureSecureWebSocket(url: string): string { + // For localhost, always use ws:// (never wss://) + if (url.includes('localhost') || url.includes('127.0.0.1')) { + // Convert any wss://localhost to ws://localhost + return url.replace(/^wss:\/\//, "ws://"); + } + + // Replace ws:// with wss:// for remote relays + const secureUrl = url.replace(/^ws:\/\//, "wss://"); + + if (secureUrl !== url) { + console.warn( + `[relay_management.ts] Protocol upgrade for rem ote relay: ${url} -> ${secureUrl}`, + ); + } + + return secureUrl; +} + +/** + * Tests connection to local relays + * @param localRelayUrls Array of local relay URLs to test + * @param ndk NDK instance + * @returns Promise that resolves to array of working local relay URLs + */ +async function testLocalRelays(localRelayUrls: string[], ndk: NDK): Promise { + const workingRelays: string[] = []; + + await Promise.all( + localRelayUrls.map(async (url) => { + try { + const result = await testRelayConnection(url, ndk); + if (result.connected) { + workingRelays.push(url); + } + } catch (error) { + // Silently ignore local relay failures + } + }) + ); + + return workingRelays; +} + +/** + * Discovers local relays by testing common localhost URLs + * @param ndk NDK instance + * @returns Promise that resolves to array of working local relay URLs + */ +export async function discoverLocalRelays(ndk: NDK): Promise { + try { + // Convert wss:// URLs from consts to ws:// for local testing + const localRelayUrls = localRelays.map(url => + url.replace(/^wss:\/\//, 'ws://') + ); + + const workingRelays = await testLocalRelays(localRelayUrls, ndk); + + // If no local relays are working, return empty array + // The network detection logic will provide fallback relays + return workingRelays; + } catch (error) { + // Silently fail and return empty array + return []; + } +} + +/** + * Fetches user's local relays from kind 10432 event + * @param ndk NDK instance + * @param user User to fetch local relays for + * @returns Promise that resolves to array of local relay URLs + */ +export async function getUserLocalRelays(ndk: NDK, user: NDKUser): Promise { + try { + const localRelayEvent = await ndk.fetchEvent( + { + kinds: [10432 as any], + authors: [user.pubkey], + }, + { + groupable: false, + skipVerification: false, + skipValidation: false, + } + ); + + if (!localRelayEvent) { + return []; + } + + const localRelays: string[] = []; + localRelayEvent.tags.forEach((tag) => { + if (tag[0] === 'r' && tag[1]) { + localRelays.push(tag[1]); + } + }); + + return localRelays; + } catch (error) { + console.info('[relay_management.ts] Error fetching user local relays:', error); + return []; + } +} + +/** + * Fetches user's blocked relays from kind 10006 event + * @param ndk NDK instance + * @param user User to fetch blocked relays for + * @returns Promise that resolves to array of blocked relay URLs + */ +export async function getUserBlockedRelays(ndk: NDK, user: NDKUser): Promise { + try { + const blockedRelayEvent = await ndk.fetchEvent( + { + kinds: [10006], + authors: [user.pubkey], + }, + { + groupable: false, + skipVerification: false, + skipValidation: false, + } + ); + + if (!blockedRelayEvent) { + return []; + } + + const blockedRelays: string[] = []; + blockedRelayEvent.tags.forEach((tag) => { + if (tag[0] === 'r' && tag[1]) { + blockedRelays.push(tag[1]); + } + }); + + return blockedRelays; + } catch (error) { + console.info('[relay_management.ts] Error fetching user blocked relays:', error); + return []; + } +} + +/** + * Fetches user's outbox relays from NIP-65 relay list + * @param ndk NDK instance + * @param user User to fetch outbox relays for + * @returns Promise that resolves to array of outbox relay URLs + */ +export async function getUserOutboxRelays(ndk: NDK, user: NDKUser): Promise { + try { + const relayList = await ndk.fetchEvent( + { + kinds: [10002], + authors: [user.pubkey], + }, + { + groupable: false, + skipVerification: false, + skipValidation: false, + } + ); + + if (!relayList) { + return []; + } + + const outboxRelays: string[] = []; + relayList.tags.forEach((tag) => { + if (tag[0] === 'w' && tag[1]) { + outboxRelays.push(tag[1]); + } + }); + + return outboxRelays; + } catch (error) { + console.info('[relay_management.ts] Error fetching user outbox relays:', error); + return []; + } +} + +/** + * Tests a set of relays in batches to avoid overwhelming them + * @param relayUrls Array of relay URLs to test + * @param ndk NDK instance + * @returns Promise that resolves to array of working relay URLs + */ +async function testRelaySet(relayUrls: string[], ndk: NDK): Promise { + const workingRelays: string[] = []; + const maxConcurrent = 3; // Test 3 relays at a time to avoid overwhelming them + + for (let i = 0; i < relayUrls.length; i += maxConcurrent) { + const batch = relayUrls.slice(i, i + maxConcurrent); + + const batchPromises = batch.map(async (url) => { + try { + const result = await testRelayConnection(url, ndk); + return result.connected ? url : null; + } catch (error) { + return null; + } + }); + + const batchResults = await Promise.all(batchPromises); + const batchWorkingRelays = batchResults.filter((url): url is string => url !== null); + workingRelays.push(...batchWorkingRelays); + } + + return workingRelays; +} + +/** + * Builds a complete relay set for a user, including local, user-specific, and fallback relays + * @param ndk NDK instance + * @param user NDKUser or null for anonymous access + * @returns Promise that resolves to inbox and outbox relay arrays + */ +export async function buildCompleteRelaySet( + ndk: NDK, + user: NDKUser | null +): Promise<{ inboxRelays: string[]; outboxRelays: string[] }> { + // Discover local relays first + const discoveredLocalRelays = await discoverLocalRelays(ndk); + + // Get user-specific relays if available + let userOutboxRelays: string[] = []; + let userLocalRelays: string[] = []; + let blockedRelays: string[] = []; + + if (user) { + try { + userOutboxRelays = await getUserOutboxRelays(ndk, user); + } catch (error) { + // Silently ignore user relay fetch errors + } + + try { + userLocalRelays = await getUserLocalRelays(ndk, user); + } catch (error) { + // Silently ignore user local relay fetch errors + } + + try { + blockedRelays = await getUserBlockedRelays(ndk, user); + } catch (error) { + // Silently ignore blocked relay fetch errors + } + } + + // Build initial relay sets and deduplicate + const finalInboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...userLocalRelays]); + const finalOutboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...userOutboxRelays]); + + // Test relays and filter out non-working ones + let testedInboxRelays: string[] = []; + let testedOutboxRelays: string[] = []; + + if (finalInboxRelays.length > 0) { + testedInboxRelays = await testRelaySet(finalInboxRelays, ndk); + } + + if (finalOutboxRelays.length > 0) { + testedOutboxRelays = await testRelaySet(finalOutboxRelays, ndk); + } + + // If no relays passed testing, use remote relays without testing + if (testedInboxRelays.length === 0 && testedOutboxRelays.length === 0) { + const remoteRelays = deduplicateRelayUrls([...secondaryRelays, ...searchRelays]); + return { + inboxRelays: remoteRelays, + outboxRelays: remoteRelays + }; + } + + // Use tested relays and deduplicate + const inboxRelays = testedInboxRelays.length > 0 ? deduplicateRelayUrls(testedInboxRelays) : deduplicateRelayUrls(secondaryRelays); + const outboxRelays = testedOutboxRelays.length > 0 ? deduplicateRelayUrls(testedOutboxRelays) : deduplicateRelayUrls(secondaryRelays); + + // Apply network condition optimization + const currentNetworkCondition = get(networkCondition); + const networkOptimizedRelaySet = getRelaySetForNetworkCondition( + currentNetworkCondition, + discoveredLocalRelays, + lowbandwidthRelays, + { inboxRelays, outboxRelays } + ); + + // Filter out blocked relays and deduplicate final sets + const finalRelaySet = { + inboxRelays: deduplicateRelayUrls(networkOptimizedRelaySet.inboxRelays.filter(r => !blockedRelays.includes(r))), + outboxRelays: deduplicateRelayUrls(networkOptimizedRelaySet.outboxRelays.filter(r => !blockedRelays.includes(r))) + }; + + // If no relays are working, use anonymous relays as fallback + if (finalRelaySet.inboxRelays.length === 0 && finalRelaySet.outboxRelays.length === 0) { + return { + inboxRelays: deduplicateRelayUrls(anonymousRelays), + outboxRelays: deduplicateRelayUrls(anonymousRelays) + }; + } + + return finalRelaySet; +} \ No newline at end of file diff --git a/src/lib/utils/subscription_search.ts b/src/lib/utils/subscription_search.ts index 97ef19d..22cefb4 100644 --- a/src/lib/utils/subscription_search.ts +++ b/src/lib/utils/subscription_search.ts @@ -3,7 +3,7 @@ import { getMatchingTags, getNpubFromNip05 } from "$lib/utils/nostrUtils"; import { nip19 } from "$lib/utils/nostrUtils"; import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk"; import { searchCache } from "$lib/utils/searchCache"; -import { communityRelay, profileRelays } from "$lib/consts"; +import { communityRelays, searchRelays } from "$lib/consts"; import { get } from "svelte/store"; import type { SearchResult, @@ -20,6 +20,12 @@ import { isEmojiReaction, } from "./search_utils"; import { TIMEOUTS, SEARCH_LIMITS } from "./search_constants"; +import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; + +// Helper function to normalize URLs for comparison +const normalizeUrl = (url: string): string => { + return url.replace(/\/$/, ''); // Remove trailing slash +}; /** * Search for events by subscription type (d, t, n) @@ -292,23 +298,40 @@ function createPrimaryRelaySet( searchType: SearchSubscriptionType, ndk: any, ): NDKRelaySet { + // Use the new relay management system + const searchRelays = [...get(activeInboxRelays), ...get(activeOutboxRelays)]; + console.debug('subscription_search: Active relay stores:', { + inboxRelays: get(activeInboxRelays), + outboxRelays: get(activeOutboxRelays), + searchRelays + }); + + // Debug: Log all relays in NDK pool + const poolRelays = Array.from(ndk.pool.relays.values()); + console.debug('subscription_search: NDK pool relays:', poolRelays.map((r: any) => r.url)); + if (searchType === "n") { - // For profile searches, use profile relays first - const profileRelaySet = Array.from(ndk.pool.relays.values()).filter( + // For profile searches, use search relays first + const profileRelaySet = poolRelays.filter( (relay: any) => - profileRelays.some( - (profileRelay) => - relay.url === profileRelay || relay.url === profileRelay + "/", + searchRelays.some( + (searchRelay: string) => + normalizeUrl(relay.url) === normalizeUrl(searchRelay), ), ); + console.debug('subscription_search: Profile relay set:', profileRelaySet.map((r: any) => r.url)); return new NDKRelaySet(new Set(profileRelaySet) as any, ndk); } else { - // For other searches, use community relay first - const communityRelaySet = Array.from(ndk.pool.relays.values()).filter( + // For other searches, use active relays first + const activeRelaySet = poolRelays.filter( (relay: any) => - relay.url === communityRelay || relay.url === communityRelay + "/", + searchRelays.some( + (searchRelay: string) => + normalizeUrl(relay.url) === normalizeUrl(searchRelay), + ), ); - return new NDKRelaySet(new Set(communityRelaySet) as any, ndk); + console.debug('subscription_search: Active relay set:', activeRelaySet.map((r: any) => r.url)); + return new NDKRelaySet(new Set(activeRelaySet) as any, ndk); } } @@ -511,15 +534,16 @@ async function searchOtherRelaysInBackground( new Set( Array.from(ndk.pool.relays.values()).filter((relay: any) => { if (searchType === "n") { - // For profile searches, exclude profile relays from fallback search - return !profileRelays.some( - (profileRelay) => - relay.url === profileRelay || relay.url === profileRelay + "/", + // For profile searches, exclude search relays from fallback search + return !searchRelays.some( + (searchRelay: string) => + normalizeUrl(relay.url) === normalizeUrl(searchRelay), ); } else { - // For other searches, exclude community relay from fallback search - return ( - relay.url !== communityRelay && relay.url !== communityRelay + "/" + // For other searches, exclude community relays from fallback search + return !communityRelays.some( + (communityRelay: string) => + normalizeUrl(relay.url) === normalizeUrl(communityRelay), ); } }), diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 9cb3197..47be24c 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -5,6 +5,7 @@ import { page } from "$app/stores"; import { Alert } from "flowbite-svelte"; import { HammerSolid } from "flowbite-svelte-icons"; + import { logCurrentRelayConfiguration } from "$lib/ndk"; // Get standard metadata for OpenGraph tags let title = "Library of Alexandria"; @@ -18,6 +19,9 @@ onMount(() => { const rect = document.body.getBoundingClientRect(); // document.body.style.height = `${rect.height}px`; + + // Log relay configuration when layout mounts + logCurrentRelayConfiguration(); }); diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index eb40d1c..dd255ca 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -1,5 +1,3 @@ -import { feedTypeStorageKey } from "$lib/consts"; -import { FeedType } from "$lib/consts"; import { getPersistedLogin, initNdk, ndkInstance } from "$lib/ndk"; import { loginWithExtension, @@ -8,18 +6,13 @@ import { } from "$lib/stores/userStore"; import { loginMethodStorageKey } from "$lib/stores/userStore"; import Pharos, { pharosInstance } from "$lib/parser"; -import { feedType } from "$lib/stores"; import type { LayoutLoad } from "./$types"; import { get } from "svelte/store"; export const ssr = false; export const load: LayoutLoad = () => { - const initialFeedType = - (localStorage.getItem(feedTypeStorageKey) as FeedType) ?? - FeedType.StandardRelays; - feedType.set(initialFeedType); - + // Initialize NDK with new relay management system const ndk = initNdk(); ndkInstance.set(ndk); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 594d3cc..8461bd4 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,14 +1,17 @@ + + {#if eventCount.total > 0} +
    + Showing {eventCount.displayed} of {eventCount.total} events. +
    + {/if} + diff --git a/src/routes/contact/+page.svelte b/src/routes/contact/+page.svelte index fbe29c0..4137220 100644 --- a/src/routes/contact/+page.svelte +++ b/src/routes/contact/+page.svelte @@ -9,9 +9,9 @@ Input, Modal, } from "flowbite-svelte"; - import { ndkInstance, ndkSignedIn } from "$lib/ndk"; + import { ndkInstance, ndkSignedIn, activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; import { userStore } from "$lib/stores/userStore"; - import { standardRelays } from "$lib/consts"; + import { communityRelays } from "$lib/consts"; import type NDK from "@nostr-dev-kit/ndk"; import { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk"; // @ts-ignore - Workaround for Svelte component import issue @@ -62,12 +62,13 @@ const repoAddress = "naddr1qvzqqqrhnypzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqy88wumn8ghj7mn0wvhxcmmv9uqq5stvv4uxzmnywf5kz2elajr"; - // Hard-coded relays to ensure we have working relays + // Use the new relay management system instead of hardcoded relays const allRelays = [ "wss://relay.damus.io", "wss://relay.nostr.band", "wss://nos.lol", - ...standardRelays, + ...$activeInboxRelays, + ...$activeOutboxRelays, ]; // Hard-coded repository owner pubkey and ID from the task @@ -451,7 +452,7 @@ Also renders nostr identifiers: npubs, nprofiles, nevents, notes, and naddrs. Wi size="xs" class="absolute bottom-2 right-2 z-10 opacity-60 hover:opacity-100" color="light" - on:click={toggleSize} + onclick={toggleSize} > {isExpanded ? "⌃" : "⌄"} @@ -459,7 +460,7 @@ Also renders nostr identifiers: npubs, nprofiles, nevents, notes, and naddrs. Wi
    - - + +
    diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index c0932b5..0b8c20d 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -13,13 +13,9 @@ import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils"; import EventInput from "$lib/components/EventInput.svelte"; import { userPubkey, isLoggedIn } from "$lib/stores/authStore.Svelte"; - import { - testAllRelays, - logRelayDiagnostics, - } from "$lib/utils/relayDiagnostics"; import CopyToClipboard from "$lib/components/util/CopyToClipboard.svelte"; import { neventEncode, naddrEncode } from "$lib/utils"; - import { standardRelays } from "$lib/consts"; + import { activeInboxRelays, activeOutboxRelays, logCurrentRelayConfiguration } from "$lib/ndk"; import { getEventType } from "$lib/utils/mime"; import ViewPublicationLink from "$lib/components/util/ViewPublicationLink.svelte"; import { checkCommunity } from "$lib/utils/search_utility"; @@ -246,8 +242,15 @@ return "Reference"; } - function getNeventAddress(event: NDKEvent): string { - return neventEncode(event, standardRelays); + function getNeventUrl(event: NDKEvent): string { + if (event.kind === 0) { + return neventEncode(event, $activeInboxRelays); + } + return neventEncode(event, $activeInboxRelays); + } + + function getNaddrUrl(event: NDKEvent): string { + return naddrEncode(event, $activeInboxRelays); } function isAddressableEvent(event: NDKEvent): boolean { @@ -259,7 +262,7 @@ return null; } try { - return naddrEncode(event, standardRelays); + return naddrEncode(event, $activeInboxRelays); } catch { return null; } @@ -498,12 +501,11 @@ handleUrlChange(); }); + // Log relay configuration when page mounts onMount(() => { - userRelayPreference = localStorage.getItem("useUserRelays") === "true"; - - // Run relay diagnostics to help identify connection issues - testAllRelays().then(logRelayDiagnostics).catch(console.error); + logCurrentRelayConfiguration(); }); +
    @@ -942,8 +944,8 @@ {#if event.kind !== 0}
    {#if isAddressableEvent(event)} {@const naddrAddress = getViewPublicationNaddr(event)} diff --git a/src/routes/publication/+page.ts b/src/routes/publication/+page.ts index b870262..6a06156 100644 --- a/src/routes/publication/+page.ts +++ b/src/routes/publication/+page.ts @@ -2,7 +2,7 @@ import { error } from "@sveltejs/kit"; import type { Load } from "@sveltejs/kit"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { nip19 } from "nostr-tools"; -import { getActiveRelays } from "$lib/ndk"; +import { getActiveRelaySetAsNDKRelaySet } from "$lib/ndk"; import { getMatchingTags } from "$lib/utils/nostrUtils"; /** @@ -68,10 +68,11 @@ async function fetchEventById(ndk: any, id: string): Promise { */ async function fetchEventByDTag(ndk: any, dTag: string): Promise { try { + const relaySet = await getActiveRelaySetAsNDKRelaySet(ndk, true); // true for inbox relays const event = await ndk.fetchEvent( { "#d": [dTag] }, { closeOnEose: false }, - getActiveRelays(ndk), + relaySet, ); if (!event) { From ba1f0164ba8131090419dfb33ad423661056bd3c Mon Sep 17 00:00:00 2001 From: silberengel Date: Fri, 18 Jul 2025 20:02:14 +0200 Subject: [PATCH 87/98] fixed feed display --- src/app.css | 6 +- src/lib/components/LoginMenu.svelte | 440 --------------- src/lib/components/Navigation.svelte | 7 +- src/lib/components/RelayActions.svelte | 2 +- src/lib/components/cards/BlogHeader.svelte | 20 +- .../publications/PublicationFeed.svelte | 64 ++- .../publications/PublicationHeader.svelte | 49 +- src/lib/components/util/CardActions.svelte | 3 +- src/lib/components/util/Profile.svelte | 523 +++++++++++++++--- src/lib/utils/nostrEventService.ts | 3 +- src/routes/+page.svelte | 12 - 11 files changed, 544 insertions(+), 585 deletions(-) delete mode 100644 src/lib/components/LoginMenu.svelte diff --git a/src/app.css b/src/app.css index 1f07464..fbaca62 100644 --- a/src/app.css +++ b/src/app.css @@ -159,6 +159,10 @@ @apply bg-primary-100 dark:bg-primary-800; } + div.skeleton-leather { + @apply h-48; + } + div.textarea-leather { @apply bg-primary-0 dark:bg-primary-1000; } @@ -246,7 +250,7 @@ } .ArticleBox.grid.active .ArticleBoxImage { - @apply max-h-72; + @apply max-h-40; } .tags span { diff --git a/src/lib/components/LoginMenu.svelte b/src/lib/components/LoginMenu.svelte deleted file mode 100644 index 4ac4222..0000000 --- a/src/lib/components/LoginMenu.svelte +++ /dev/null @@ -1,440 +0,0 @@ - - -
    - {#if !user.signedIn} - -
    - - -
    -

    Login with...

    - - - -
    -
    Network Status:
    - -
    -
    -
    - {#if result} -
    - {result} - -
    - {/if} -
    - {:else} - -
    - - -
    -
    -

    - {user.profile?.displayName || - user.profile?.name || - (user.npub ? shortenNpub(user.npub) : "Unknown")} -

    -
      -
    • - -
    • -
    • - {#if user.loginMethod === "extension"} - Logged in with extension - {:else if user.loginMethod === "amber"} - Logged in with Amber - {:else if user.loginMethod === "npub"} - Logged in with npub - {:else} - Unknown login method - {/if} -
    • -
    • -
      Network Status:
      - -
    • -
    • - -
    • -
    -
    -
    -
    -
    - {/if} -
    - -{#if showQrCode && qrCodeDataUrl} - -
    -
    -
    -

    - Scan with Amber -

    -

    - Open Amber on your phone and scan this QR code -

    -
    - Nostr Connect QR Code -
    -
    - -
    - - -
    -
    -
    -

    1. Open Amber on your phone

    -

    2. Scan the QR code above

    -

    3. Approve the connection in Amber

    -
    - -
    -
    -
    -{/if} - -{#if showAmberFallback} -
    -
    -
    -

    - Amber Session Restored -

    -

    - Your Amber wallet session could not be restored automatically, so - you've been switched to read-only mode.
    - You can still browse and read content, but you'll need to reconnect Amber - to publish or comment. -

    - - -
    -
    -
    -{/if} diff --git a/src/lib/components/Navigation.svelte b/src/lib/components/Navigation.svelte index aa6b7e2..fdcfe32 100644 --- a/src/lib/components/Navigation.svelte +++ b/src/lib/components/Navigation.svelte @@ -7,9 +7,12 @@ NavHamburger, NavBrand, } from "flowbite-svelte"; - import LoginMenu from "./LoginMenu.svelte"; + import Profile from "./util/Profile.svelte"; + import { userStore } from "$lib/stores/userStore"; let { class: className = "" } = $props(); + + let userState = $derived($userStore); @@ -19,7 +22,7 @@
    - +
    diff --git a/src/lib/components/RelayActions.svelte b/src/lib/components/RelayActions.svelte index 041f4a4..e7f289c 100644 --- a/src/lib/components/RelayActions.svelte +++ b/src/lib/components/RelayActions.svelte @@ -55,7 +55,7 @@ const relaySet = createRelaySetFromUrls([relay], ndk); const found = await ndk .fetchEvent({ ids: [event?.id || ""] }, undefined, relaySet) - .withTimeout(3000); + .withTimeout(2000); relaySearchResults = { ...relaySearchResults, [relay]: found ? "found" : "notfound", diff --git a/src/lib/components/cards/BlogHeader.svelte b/src/lib/components/cards/BlogHeader.svelte index adaa6bf..0696d89 100644 --- a/src/lib/components/cards/BlogHeader.svelte +++ b/src/lib/components/cards/BlogHeader.svelte @@ -54,24 +54,28 @@ ? 'active' : ''}" > -
    +
    {@render userBadge(authorPubkey, author)} {publishedAt()}
    -
    {#if image && active}
    - + {title
    {/if} -
    + +
    @@ -83,9 +87,15 @@
    {/if}
    + {#if active} {/if} + + +
    + +
    {/if} diff --git a/src/lib/components/publications/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte index 14bd331..2236dce 100644 --- a/src/lib/components/publications/PublicationFeed.svelte +++ b/src/lib/components/publications/PublicationFeed.svelte @@ -137,9 +137,10 @@ allRelays.map((r: string) => [r, "pending"]), ); let allEvents: NDKEvent[] = []; + const eventMap = new Map(); // Helper to fetch from a single relay with timeout - async function fetchFromRelay(relay: string): Promise { + async function fetchFromRelay(relay: string): Promise { try { console.debug(`[PublicationFeed] Fetching from relay: ${relay}`); const relaySet = NDKRelaySetFromNDK.fromRelayUrls([relay], ndk); @@ -156,57 +157,60 @@ }, relaySet, ) - .withTimeout(10000); // Increased timeout to 10 seconds + .withTimeout(5000); // Reduced timeout to 5 seconds for faster response console.debug(`[PublicationFeed] Raw events from ${relay}:`, eventSet.size); eventSet = filterValidIndexEvents(eventSet); console.debug(`[PublicationFeed] Valid events from ${relay}:`, eventSet.size); relayStatuses = { ...relayStatuses, [relay]: "found" }; - return Array.from(eventSet); + + // Add new events to the map and update the view immediately + const newEvents: NDKEvent[] = []; + for (const event of eventSet) { + const tagAddress = event.tagAddress(); + if (!eventMap.has(tagAddress)) { + eventMap.set(tagAddress, event); + newEvents.push(event); + } + } + + if (newEvents.length > 0) { + // Update allIndexEvents with new events + allIndexEvents = Array.from(eventMap.values()); + // Sort by created_at descending + allIndexEvents.sort((a, b) => b.created_at! - a.created_at!); + + // Update the view immediately with new events + eventsInView = allIndexEvents.slice(0, 30); + endOfFeed = allIndexEvents.length <= 30; + + console.debug(`[PublicationFeed] Updated view with ${newEvents.length} new events from ${relay}, total: ${allIndexEvents.length}`); + } } catch (err) { console.error(`[PublicationFeed] Error fetching from relay ${relay}:`, err); relayStatuses = { ...relayStatuses, [relay]: "notfound" }; - return []; } } - // Fetch from all relays in parallel, do not block on any single relay + // Fetch from all relays in parallel, return events as they arrive console.debug(`[PublicationFeed] Starting fetch from ${allRelays.length} relays`); - const results = await Promise.allSettled(allRelays.map(fetchFromRelay)); - for (const result of results) { - if (result.status === "fulfilled") { - allEvents = allEvents.concat(result.value); - } - } + // Start all relay fetches in parallel + const fetchPromises = allRelays.map(fetchFromRelay); - console.debug(`[PublicationFeed] Total events fetched:`, allEvents.length); + // Wait for all to complete (but events are shown as they arrive) + await Promise.allSettled(fetchPromises); - // Deduplicate by tagAddress - const eventMap = new Map( - allEvents.map((event) => [event.tagAddress(), event]), - ); - allIndexEvents = Array.from(eventMap.values()); - console.debug(`[PublicationFeed] Events after deduplication:`, allIndexEvents.length); + console.debug(`[PublicationFeed] All relays completed, final event count:`, allIndexEvents.length); - // Sort by created_at descending - allIndexEvents.sort((a, b) => b.created_at! - a.created_at!); - // Cache the fetched events indexEventCache.set(allRelays, allIndexEvents); - // Initially show first page + // Final update to ensure we have the latest view eventsInView = allIndexEvents.slice(0, 30); endOfFeed = allIndexEvents.length <= 30; loading = false; - - console.debug(`[PublicationFeed] Final state:`, { - totalEvents: allIndexEvents.length, - eventsInView: eventsInView.length, - endOfFeed, - loading - }); } // Function to filter events based on search query @@ -332,7 +336,7 @@ } function getSkeletonIds(): string[] { - const skeletonHeight = 124; // The height of the skeleton component in pixels. + const skeletonHeight = 192; // The height of the card component in pixels (h-48 = 12rem = 192px). const skeletonCount = Math.floor(window.innerHeight / skeletonHeight) - 2; const skeletonIds = []; for (let i = 0; i < skeletonCount; i++) { diff --git a/src/lib/components/publications/PublicationHeader.svelte b/src/lib/components/publications/PublicationHeader.svelte index 465c423..2a0ea56 100644 --- a/src/lib/components/publications/PublicationHeader.svelte +++ b/src/lib/components/publications/PublicationHeader.svelte @@ -1,9 +1,7 @@ {#if title != null && href != null} - + {#if image}
    - + {title
    {/if} -
    + + + + +
    +
    {/if} diff --git a/src/lib/components/util/CardActions.svelte b/src/lib/components/util/CardActions.svelte index 2083d4e..d5d49c5 100644 --- a/src/lib/components/util/CardActions.svelte +++ b/src/lib/components/util/CardActions.svelte @@ -8,8 +8,7 @@ import CopyToClipboard from "$components/util/CopyToClipboard.svelte"; import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import { neventEncode, naddrEncode } from "$lib/utils"; - import { communityRelays, secondaryRelays, FeedType } from "$lib/consts"; - import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; + import { activeInboxRelays } from "$lib/ndk"; import { userStore } from "$lib/stores/userStore"; import { goto } from "$app/navigation"; import type { NDKEvent } from "$lib/utils/nostrUtils"; diff --git a/src/lib/components/util/Profile.svelte b/src/lib/components/util/Profile.svelte index 19fb0bb..399539a 100644 --- a/src/lib/components/util/Profile.svelte +++ b/src/lib/components/util/Profile.svelte @@ -1,7 +1,13 @@
    -
    - - -
    -
    - {#if username} -

    {username}

    - {#if isNav}

    @{tag}

    {/if} - {:else} -

    Loading...

    - {/if} -
      -
    • - -
    • -
    • - -
    • -
    • - -
    • - {#if isNav} + {#if !userState.signedIn} + +
      + + +
      +

      Login with...

      + + + +
      +
      Network Status:
      + +
      +
      +
      + {#if result} +
      + {result} + +
      + {/if} +
      + {:else} + +
      + + +
      +
      + {#if username} +

      {username}

      + {#if isNav}

      @{tag}

      {/if} + {:else if isProfileLoading} +

      Loading profile...

      + {:else} +

      Loading...

      + {/if} +
        +
      • + +
      • - {:else} - - {/if} -
      +
    • + {#if userState.loginMethod === "extension"} + Logged in with extension + {:else if userState.loginMethod === "amber"} + Logged in with Amber + {:else if userState.loginMethod === "npub"} + Logged in with npub + {:else} + Unknown login method + {/if} +
    • +
    • + +
    • + {#if isNav} +
    • + +
    • + {:else} + + {/if} +
    +
    +
    +
    +
    + {/if} +
    + +{#if showQrCode && qrCodeDataUrl} + +
    +
    +
    +

    + Scan with Amber +

    +

    + Open Amber on your phone and scan this QR code +

    +
    + Nostr Connect QR Code +
    +
    + +
    + + +
    +
    +

    1. Open Amber on your phone

    +

    2. Scan the QR code above

    +

    3. Approve the connection in Amber

    +
    +
    - +
    -
    +{/if} + +{#if showAmberFallback} +
    +
    +
    +

    + Amber Session Restored +

    +

    + Your Amber wallet session could not be restored automatically, so + you've been switched to read-only mode.
    + You can still browse and read content, but you'll need to reconnect Amber + to publish or comment. +

    + + +
    +
    +
    +{/if} diff --git a/src/lib/utils/nostrEventService.ts b/src/lib/utils/nostrEventService.ts index e7d437f..1a233a8 100644 --- a/src/lib/utils/nostrEventService.ts +++ b/src/lib/utils/nostrEventService.ts @@ -1,6 +1,5 @@ import { nip19 } from "nostr-tools"; import { getEventHash, signEvent, prefixNostrAddresses } from "./nostrUtils"; -import { communityRelays, secondaryRelays } from "$lib/consts"; import { get } from "svelte/store"; import { goto } from "$app/navigation"; import type { NDKEvent } from "./nostrUtils"; @@ -381,7 +380,7 @@ export async function publishEvent( try { // Publish with timeout - await event.publish(relaySet).withTimeout(10000); + await event.publish(relaySet).withTimeout(5000); // For now, assume all relays were successful // In a more sophisticated implementation, you'd track individual relay responses diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 8461bd4..03a9c14 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -14,18 +14,6 @@ } - - - - Pardon our dust! The publication view is currently using an experimental - loader, and may be unstable. - - -
    Date: Fri, 18 Jul 2025 20:19:01 +0200 Subject: [PATCH 88/98] added pastel placeholders --- src/lib/components/cards/BlogHeader.svelte | 29 +++--- src/lib/components/cards/ProfileHeader.svelte | 26 +++--- .../publications/PublicationHeader.svelte | 27 ++++-- src/lib/components/util/Details.svelte | 23 +++-- src/lib/components/util/LazyImage.svelte | 90 +++++++++++++++++++ src/lib/consts.ts | 6 +- src/lib/stores/userStore.ts | 15 +++- src/lib/utils/image_utils.ts | 31 +++++++ src/lib/utils/nostrUtils.ts | 18 +++- src/lib/utils/relay_management.ts | 19 +++- 10 files changed, 241 insertions(+), 43 deletions(-) create mode 100644 src/lib/components/util/LazyImage.svelte create mode 100644 src/lib/utils/image_utils.ts diff --git a/src/lib/components/cards/BlogHeader.svelte b/src/lib/components/cards/BlogHeader.svelte index 0696d89..0c3a2bc 100644 --- a/src/lib/components/cards/BlogHeader.svelte +++ b/src/lib/components/cards/BlogHeader.svelte @@ -1,12 +1,14 @@ + +
    + +
    +
    + + + { + setTimeout(() => { + imageLoaded = true; + }, 100); + }} + onerror={() => { + imageError = true; + }} + /> + + + {#if imageError} +
    +
    + Failed to load +
    +
    + {/if} +
    \ No newline at end of file diff --git a/src/lib/consts.ts b/src/lib/consts.ts index 4241d1d..b6df380 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -36,9 +36,9 @@ export const lowbandwidthRelays = [ "wss://aggr.nostr.land" ]; -export const localRelays = [ - "wss://localhost:8080", - "wss://localhost:4869" +export const localRelays: string[] = [ + // "wss://localhost:8080", + // "wss://localhost:4869" ]; export enum FeedType { diff --git a/src/lib/stores/userStore.ts b/src/lib/stores/userStore.ts index 3275e23..6e8ef07 100644 --- a/src/lib/stores/userStore.ts +++ b/src/lib/stores/userStore.ts @@ -162,7 +162,20 @@ export async function loginWithExtension() { const signer = new NDKNip07Signer(); const user = await signer.user(); const npub = user.npub; - const profile = await getUserMetadata(npub); + + // Try to fetch user metadata, but don't fail if it times out + let profile: NostrProfile | null = null; + try { + profile = await getUserMetadata(npub); + } catch (error) { + console.warn("Failed to fetch user metadata during login:", error); + // Continue with login even if metadata fetch fails + profile = { + name: npub.slice(0, 8) + "..." + npub.slice(-4), + displayName: npub.slice(0, 8) + "..." + npub.slice(-4), + }; + } + // Fetch user's preferred relays const [persistedInboxes, persistedOutboxes] = getPersistedRelays(user); for (const relay of persistedInboxes) { diff --git a/src/lib/utils/image_utils.ts b/src/lib/utils/image_utils.ts new file mode 100644 index 0000000..4922995 --- /dev/null +++ b/src/lib/utils/image_utils.ts @@ -0,0 +1,31 @@ +/** + * Generate a dark-pastel color based on a string (like an event ID) + * @param seed - The string to generate a color from + * @returns A dark-pastel hex color + */ +export function generateDarkPastelColor(seed: string): string { + // Create a simple hash from the seed string + let hash = 0; + for (let i = 0; i < seed.length; i++) { + const char = seed.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32-bit integer + } + + // Use the hash to generate lighter pastel colors + // Keep values in the 120-200 range for better pastel effect + const r = Math.abs(hash) % 80 + 120; // 120-200 range + const g = Math.abs(hash >> 8) % 80 + 120; // 120-200 range + const b = Math.abs(hash >> 16) % 80 + 120; // 120-200 range + + return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; +} + +/** + * Test function to verify color generation + * @param eventId - The event ID to test + * @returns The generated color + */ +export function testColorGeneration(eventId: string): string { + return generateDarkPastelColor(eventId); +} \ No newline at end of file diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index a7aef5e..85e9e8d 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -426,12 +426,18 @@ export async function fetchEventWithFallback( // Use the active inbox relays from the relay management system const inboxRelays = get(activeInboxRelays); + // Check if we have any relays available + if (inboxRelays.length === 0) { + console.warn("No inbox relays available for event fetch"); + return null; + } + // Create relay set from active inbox relays const relaySet = NDKRelaySetFromNDK.fromRelayUrls(inboxRelays, ndk); try { if (relaySet.relays.size === 0) { - console.warn("No inbox relays available for event fetch"); + console.warn("No relays in relay set for event fetch"); return null; } @@ -467,7 +473,15 @@ export async function fetchEventWithFallback( // Always wrap as NDKEvent return found instanceof NDKEvent ? found : new NDKEvent(ndk, found); } catch (err) { - console.error("Error in fetchEventWithFallback:", err); + if (err instanceof Error && err.message === 'Timeout') { + const timeoutSeconds = timeoutMs / 1000; + const relayUrls = Array.from(relaySet.relays).map((r: any) => r.url).join(", "); + console.warn( + `Event fetch timed out after ${timeoutSeconds}s. Tried inbox relays: ${relayUrls}. Some relays may be offline or slow.`, + ); + } else { + console.error("Error in fetchEventWithFallback:", err); + } return null; } } diff --git a/src/lib/utils/relay_management.ts b/src/lib/utils/relay_management.ts index dbaa08a..e02c7f8 100644 --- a/src/lib/utils/relay_management.ts +++ b/src/lib/utils/relay_management.ts @@ -147,19 +147,30 @@ function ensureSecureWebSocket(url: string): string { async function testLocalRelays(localRelayUrls: string[], ndk: NDK): Promise { const workingRelays: string[] = []; + if (localRelayUrls.length === 0) { + return workingRelays; + } + + console.debug(`[relay_management.ts] Testing ${localRelayUrls.length} local relays...`); + await Promise.all( localRelayUrls.map(async (url) => { try { const result = await testRelayConnection(url, ndk); if (result.connected) { workingRelays.push(url); + console.debug(`[relay_management.ts] Local relay connected: ${url}`); + } else { + console.debug(`[relay_management.ts] Local relay failed: ${url} - ${result.error}`); } } catch (error) { - // Silently ignore local relay failures + // Silently ignore local relay failures - they're optional + console.debug(`[relay_management.ts] Local relay error (ignored): ${url}`); } }) ); + console.debug(`[relay_management.ts] Found ${workingRelays.length} working local relays`); return workingRelays; } @@ -170,6 +181,12 @@ async function testLocalRelays(localRelayUrls: string[], ndk: NDK): Promise { try { + // If no local relays are configured, return empty array + if (localRelays.length === 0) { + console.debug('[relay_management.ts] No local relays configured'); + return []; + } + // Convert wss:// URLs from consts to ws:// for local testing const localRelayUrls = localRelays.map(url => url.replace(/^wss:\/\//, 'ws://') From 97d54e64ac51945db5d0216eb7e2a7663cc0c4fa Mon Sep 17 00:00:00 2001 From: silberengel Date: Fri, 18 Jul 2025 20:51:30 +0200 Subject: [PATCH 89/98] highlight current section and pad header --- src/app.css | 27 +++++ .../publications/TableOfContents.svelte | 105 +++++++++++++++++- src/lib/consts.ts | 2 + src/lib/data_structures/publication_tree.ts | 3 +- src/lib/ndk.ts | 6 +- src/lib/utils/relay_management.ts | 14 ++- 6 files changed, 146 insertions(+), 11 deletions(-) diff --git a/src/app.css b/src/app.css index fbaca62..da20412 100644 --- a/src/app.css +++ b/src/app.css @@ -288,6 +288,8 @@ /* Rendered publication content */ .publication-leather { @apply flex flex-col space-y-4; + scroll-margin-top: 150px; + scroll-behavior: smooth; h1, h2, @@ -441,6 +443,21 @@ scrollbar-color: rgba(156, 163, 175, 0.5) transparent !important; } + /* Section scroll behavior */ + section[id] { + scroll-margin-top: 150px; + } + + /* Ensure section headers maintain their padding */ + section[id] h1, + section[id] h2, + section[id] h3, + section[id] h4, + section[id] h5, + section[id] h6 { + @apply pt-4; + } + .description-textarea { min-height: 100% !important; } @@ -486,4 +503,14 @@ @apply bg-primary-0 dark:bg-primary-1000 text-gray-900 dark:text-gray-100 border-s-4 border-primary-200 rounded shadow-none px-4 py-2; @apply focus:border-primary-600 dark:focus:border-primary-400; } + + /* Table of Contents highlighting */ + .toc-highlight { + @apply bg-primary-100 dark:bg-primary-800 border-l-4 border-primary-600 dark:border-primary-400; + transition: all 0.2s ease-in-out; + } + + .toc-highlight:hover { + @apply bg-primary-200 dark:bg-primary-700; + } } diff --git a/src/lib/components/publications/TableOfContents.svelte b/src/lib/components/publications/TableOfContents.svelte index e50bce0..0a16108 100644 --- a/src/lib/components/publications/TableOfContents.svelte +++ b/src/lib/components/publications/TableOfContents.svelte @@ -10,6 +10,7 @@ SidebarItem, } from "flowbite-svelte"; import Self from "./TableOfContents.svelte"; + import { onMount, onDestroy } from "svelte"; let { depth, onSectionFocused } = $props<{ rootAddress: string; @@ -32,6 +33,10 @@ return newEntries; }); + // Track the currently visible section + let currentVisibleSection = $state(null); + let observer: IntersectionObserver; + function setEntryExpanded(address: string, expanded: boolean = false) { const entry = toc.getEntry(address); if (!entry) { @@ -41,6 +46,100 @@ toc.expandedMap.set(address, expanded); entry.resolveChildren(); } + + function handleSectionClick(address: string) { + // Smooth scroll to the section + const element = document.getElementById(address); + if (element) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + } + + onSectionFocused?.(address); + } + + // Check if an entry is currently visible + function isEntryVisible(address: string): boolean { + return currentVisibleSection === address; + } + + // Set up intersection observer to track visible sections + onMount(() => { + observer = new IntersectionObserver( + (entries) => { + // Find the section that is most visible in the viewport + let maxIntersectionRatio = 0; + let mostVisibleSection: string | null = null; + + entries.forEach((entry) => { + if (entry.isIntersecting && entry.intersectionRatio > maxIntersectionRatio) { + maxIntersectionRatio = entry.intersectionRatio; + mostVisibleSection = entry.target.id; + } + }); + + if (mostVisibleSection && mostVisibleSection !== currentVisibleSection) { + currentVisibleSection = mostVisibleSection; + } + }, + { + threshold: [0, 0.25, 0.5, 0.75, 1], + rootMargin: "-20% 0px -20% 0px", // Consider section visible when it's in the middle 60% of the viewport + } + ); + + // Function to observe all section elements + function observeSections() { + const sections = document.querySelectorAll('section[id]'); + sections.forEach((section) => { + observer.observe(section); + }); + } + + // Initial observation + observeSections(); + + // Set up a mutation observer to watch for new sections being added + const mutationObserver = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as Element; + // Check if the added node is a section with an id + if (element.tagName === 'SECTION' && element.id) { + observer.observe(element); + } + // Check if the added node contains sections + const sections = element.querySelectorAll?.('section[id]'); + if (sections) { + sections.forEach((section) => { + observer.observe(section); + }); + } + } + }); + }); + }); + + // Start observing the document body for changes + mutationObserver.observe(document.body, { + childList: true, + subtree: true, + }); + + return () => { + observer.disconnect(); + mutationObserver.disconnect(); + }; + }); + + onDestroy(() => { + if (observer) { + observer.disconnect(); + } + }); @@ -50,18 +149,20 @@ {@const address = entry.address} {@const expanded = toc.expandedMap.get(address) ?? false} {@const isLeaf = toc.leaves.has(address)} + {@const isVisible = isEntryVisible(address)} {#if isLeaf} onSectionFocused?.(address)} + class={isVisible ? "toc-highlight" : ""} + onclick={() => handleSectionClick(address)} /> {:else} {@const childDepth = depth + 1} expanded, (open) => setEntryExpanded(address, open)} > diff --git a/src/lib/consts.ts b/src/lib/consts.ts index b6df380..998cbec 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -1,3 +1,5 @@ +// AI SHOULD NEVER CHANGE THIS FILE + export const wikiKind = 30818; export const indexKind = 30040; export const zettelKinds = [30041, 30818]; diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index dbb20cb..2fefad7 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -558,9 +558,10 @@ export class PublicationTree implements AsyncIterable { currentEvent = this.#events.get(currentAddress!); if (!currentEvent) { - throw new Error( + console.warn( `[PublicationTree] Event with address ${currentAddress} not found.`, ); + return null; } // Stop immediately if the target of the search is found. diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index cd13df2..6e1120e 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -349,7 +349,7 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { const connectionTimeout = setTimeout(() => { console.warn(`[NDK.ts] Connection timeout for ${secureUrl}`); relay.disconnect(); - }, 10000); // 10 second timeout + }, 5000); // 5 second timeout // Set up custom authentication handling only if user is signed in if (ndk.signer && ndk.activeUser) { @@ -509,7 +509,7 @@ export function initNdk(): NDK { // Connect with better error handling and reduced retry attempts let retryCount = 0; - const maxRetries = 2; + const maxRetries = 1; // Reduce to 1 retry const attemptConnection = async () => { try { @@ -526,7 +526,7 @@ export function initNdk(): NDK { if (retryCount < maxRetries) { retryCount++; console.debug(`[NDK.ts] Attempting to reconnect (${retryCount}/${maxRetries})...`); - setTimeout(attemptConnection, 3000); + setTimeout(attemptConnection, 2000); // Reduce timeout to 2 seconds } else { console.warn("[NDK.ts] Max retries reached, continuing with limited functionality"); // Still try to update relay stores even if connection failed diff --git a/src/lib/utils/relay_management.ts b/src/lib/utils/relay_management.ts index e02c7f8..09aa5ac 100644 --- a/src/lib/utils/relay_management.ts +++ b/src/lib/utils/relay_management.ts @@ -325,7 +325,7 @@ export async function getUserOutboxRelays(ndk: NDK, user: NDKUser): Promise { const workingRelays: string[] = []; - const maxConcurrent = 3; // Test 3 relays at a time to avoid overwhelming them + const maxConcurrent = 2; // Reduce to 2 relays at a time to avoid overwhelming them for (let i = 0; i < relayUrls.length; i += maxConcurrent) { const batch = relayUrls.slice(i, i + maxConcurrent); @@ -335,12 +335,16 @@ async function testRelaySet(relayUrls: string[], ndk: NDK): Promise { const result = await testRelayConnection(url, ndk); return result.connected ? url : null; } catch (error) { + console.debug(`[relay_management.ts] Failed to test relay ${url}:`, error); return null; } }); - const batchResults = await Promise.all(batchPromises); - const batchWorkingRelays = batchResults.filter((url): url is string => url !== null); + const batchResults = await Promise.allSettled(batchPromises); + const batchWorkingRelays = batchResults + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map(result => result.value) + .filter((url): url is string => url !== null); workingRelays.push(...batchWorkingRelays); } @@ -369,13 +373,13 @@ export async function buildCompleteRelaySet( try { userOutboxRelays = await getUserOutboxRelays(ndk, user); } catch (error) { - // Silently ignore user relay fetch errors + console.debug('[relay_management.ts] Error fetching user outbox relays:', error); } try { userLocalRelays = await getUserLocalRelays(ndk, user); } catch (error) { - // Silently ignore user local relay fetch errors + console.debug('[relay_management.ts] Error fetching user local relays:', error); } try { From c7ce63621021322a2013c1f5b92f9c5182d365f2 Mon Sep 17 00:00:00 2001 From: silberengel Date: Fri, 18 Jul 2025 21:39:51 +0200 Subject: [PATCH 90/98] ToC corrections --- src/app.css | 4 ++-- src/lib/data_structures/publication_tree.ts | 19 ++++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/app.css b/src/app.css index da20412..7a55d9d 100644 --- a/src/app.css +++ b/src/app.css @@ -506,11 +506,11 @@ /* Table of Contents highlighting */ .toc-highlight { - @apply bg-primary-100 dark:bg-primary-800 border-l-4 border-primary-600 dark:border-primary-400; + @apply bg-primary-200 dark:bg-primary-700 border-l-4 border-primary-600 dark:border-primary-400 font-medium; transition: all 0.2s ease-in-out; } .toc-highlight:hover { - @apply bg-primary-200 dark:bg-primary-700; + @apply bg-primary-300 dark:bg-primary-600; } } diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 2fefad7..f87b1c3 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -246,8 +246,11 @@ export class PublicationTree implements AsyncIterable { async tryMoveTo(address?: string) { if (!address) { const startEvent = await this.#tree.#depthFirstRetrieve(); + if (!startEvent) { + return false; + } this.target = await this.#tree.#nodes - .get(startEvent!.tagAddress()) + .get(startEvent.tagAddress()) ?.value(); } else { this.target = await this.#tree.#nodes.get(address)?.value(); @@ -424,8 +427,7 @@ export class PublicationTree implements AsyncIterable { ): Promise> { if (!this.#cursor.target) { if (await this.#cursor.tryMoveTo(this.#bookmark)) { - const event = await this.getEvent(this.#cursor.target!.address); - return { done: false, value: event }; + return this.#yieldEventAtCursor(false); } } @@ -440,7 +442,10 @@ export class PublicationTree implements AsyncIterable { async #yieldEventAtCursor( done: boolean, ): Promise> { - const value = (await this.getEvent(this.#cursor.target!.address)) ?? null; + if (!this.#cursor.target) { + return { done, value: null }; + } + const value = (await this.getEvent(this.#cursor.target.address)) ?? null; return { done, value }; } @@ -471,7 +476,7 @@ export class PublicationTree implements AsyncIterable { continue; } - if (this.#cursor.target!.status === PublicationTreeNodeStatus.Error) { + if (this.#cursor.target && this.#cursor.target.status === PublicationTreeNodeStatus.Error) { return { done: false, value: null }; } @@ -479,7 +484,7 @@ export class PublicationTree implements AsyncIterable { } } while (this.#cursor.tryMoveToParent()); - if (this.#cursor.target!.status === PublicationTreeNodeStatus.Error) { + if (this.#cursor.target && this.#cursor.target.status === PublicationTreeNodeStatus.Error) { return { done: false, value: null }; } @@ -518,7 +523,7 @@ export class PublicationTree implements AsyncIterable { } } while (this.#cursor.tryMoveToParent()); - if (this.#cursor.target!.status === PublicationTreeNodeStatus.Error) { + if (this.#cursor.target && this.#cursor.target.status === PublicationTreeNodeStatus.Error) { return { done: false, value: null }; } From bd6d96ed6274840913f508abd757a13f585d3ff7 Mon Sep 17 00:00:00 2001 From: silberengel Date: Fri, 18 Jul 2025 21:51:28 +0200 Subject: [PATCH 91/98] Made ToC auto-expanding --- .../publications/Publication.svelte | 7 ++- .../publications/TableOfContents.svelte | 20 ++++++-- src/lib/data_structures/publication_tree.ts | 47 ++++++++++++------- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/src/lib/components/publications/Publication.svelte b/src/lib/components/publications/Publication.svelte index c751f5d..52489e5 100644 --- a/src/lib/components/publications/Publication.svelte +++ b/src/lib/components/publications/Publication.svelte @@ -182,7 +182,7 @@ }, { threshold: 0.5 }, ); - loadMore(8); + loadMore(12); return () => { observer.disconnect(); @@ -210,6 +210,11 @@ depth={2} onSectionFocused={(address: string) => publicationTree.setBookmark(address)} + onLoadMore={() => { + if (!isLoading && !isDone) { + loadMore(4); + } + }} /> {/if} diff --git a/src/lib/components/publications/TableOfContents.svelte b/src/lib/components/publications/TableOfContents.svelte index 0a16108..a2fc748 100644 --- a/src/lib/components/publications/TableOfContents.svelte +++ b/src/lib/components/publications/TableOfContents.svelte @@ -12,10 +12,11 @@ import Self from "./TableOfContents.svelte"; import { onMount, onDestroy } from "svelte"; - let { depth, onSectionFocused } = $props<{ + let { depth, onSectionFocused, onLoadMore } = $props<{ rootAddress: string; depth: number; onSectionFocused?: (address: string) => void; + onLoadMore?: () => void; }>(); let toc = getContext("toc") as TableOfContents; @@ -58,6 +59,14 @@ } onSectionFocused?.(address); + + // Check if this is the last entry and trigger loading more events + const currentEntries = entries; + const lastEntry = currentEntries[currentEntries.length - 1]; + if (lastEntry && lastEntry.address === address) { + console.debug('[TableOfContents] Last entry clicked, triggering load more'); + onLoadMore?.(); + } } // Check if an entry is currently visible @@ -145,27 +154,28 @@ - {#each entries as entry} + {#each entries as entry, index} {@const address = entry.address} {@const expanded = toc.expandedMap.get(address) ?? false} {@const isLeaf = toc.leaves.has(address)} {@const isVisible = isEntryVisible(address)} + {@const isLastEntry = index === entries.length - 1} {#if isLeaf} handleSectionClick(address)} /> {:else} {@const childDepth = depth + 1} expanded, (open) => setEntryExpanded(address, open)} > - + {/if} {/each} diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index f87b1c3..8b3a8f3 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -45,6 +45,11 @@ export class PublicationTree implements AsyncIterable { * A map of addresses in the tree to their corresponding events. */ #events: Map; + + /** + * Simple cache for fetched events to avoid re-fetching. + */ + #eventCache: Map = new Map(); /** * An ordered list of the addresses of the leaves of the tree. @@ -589,13 +594,11 @@ export class PublicationTree implements AsyncIterable { } // Augment the tree with the children of the current event. - for (const childAddress of currentChildAddresses) { - if (this.#nodes.has(childAddress)) { - continue; - } - - await this.#addNode(childAddress, currentNode!); - } + const childPromises = currentChildAddresses + .filter(childAddress => !this.#nodes.has(childAddress)) + .map(childAddress => this.#addNode(childAddress, currentNode!)); + + await Promise.all(childPromises); // Push the popped address's children onto the stack for the next iteration. while (currentChildAddresses.length > 0) { @@ -630,12 +633,23 @@ export class PublicationTree implements AsyncIterable { address: string, parentNode: PublicationTreeNode, ): Promise { - const [kind, pubkey, dTag] = address.split(":"); - const event = await this.#ndk.fetchEvent({ - kinds: [parseInt(kind)], - authors: [pubkey], - "#d": [dTag], - }); + // Check cache first + let event = this.#eventCache.get(address); + + if (!event) { + const [kind, pubkey, dTag] = address.split(":"); + const fetchedEvent = await this.#ndk.fetchEvent({ + kinds: [parseInt(kind)], + authors: [pubkey], + "#d": [dTag], + }); + + // Cache the event if found + if (fetchedEvent) { + this.#eventCache.set(address, fetchedEvent); + event = fetchedEvent; + } + } if (!event) { console.debug( @@ -665,9 +679,10 @@ export class PublicationTree implements AsyncIterable { children: [], }; - for (const address of childAddresses) { - this.addEventByAddress(address, event); - } + const childPromises = childAddresses.map(address => + this.addEventByAddress(address, event) + ); + await Promise.all(childPromises); this.#nodeResolvedObservers.forEach((observer) => observer(address)); From 3f511dfcb1cfdd96e07c195ebdf8c5cb204dc909 Mon Sep 17 00:00:00 2001 From: silberengel Date: Fri, 18 Jul 2025 22:02:14 +0200 Subject: [PATCH 92/98] fixed event search --- src/lib/components/EventSearch.svelte | 17 ++ src/routes/events/+page.svelte | 219 ++++---------------------- 2 files changed, 49 insertions(+), 187 deletions(-) diff --git a/src/lib/components/EventSearch.svelte b/src/lib/components/EventSearch.svelte index 6e4a49f..cb71ade 100644 --- a/src/lib/components/EventSearch.svelte +++ b/src/lib/components/EventSearch.svelte @@ -251,6 +251,11 @@ return; } + // Check if we've already processed this searchValue + if (searchValue === lastProcessedSearchValue) { + return; + } + // If we already have the event for this searchValue, do nothing if (foundEvent) { const currentEventId = foundEvent.id; @@ -307,6 +312,7 @@ (currentNprofile && searchValue === currentNprofile) ) { // Already displaying the event for this searchValue + lastProcessedSearchValue = searchValue; return; } } @@ -318,6 +324,7 @@ searchTimeout = setTimeout(() => { isProcessingSearch = true; isWaitingForSearchResult = true; + lastProcessedSearchValue = searchValue; if (searchValue) { handleSearchEvent(false, searchValue); } @@ -585,6 +592,11 @@ isProcessingSearch = false; currentProcessingSearchValue = null; isWaitingForSearchResult = false; + + // Update last processed search value to prevent re-processing + if (searchValue) { + lastProcessedSearchValue = searchValue; + } } catch (error) { if (error instanceof Error && error.message === "Search cancelled") { isProcessingSearch = false; @@ -628,6 +640,11 @@ isProcessingSearch = false; currentProcessingSearchValue = null; isWaitingForSearchResult = false; + + // Update last processed search value to prevent re-processing even on error + if (searchValue) { + lastProcessedSearchValue = searchValue; + } } } diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index 0b8c20d..ae93f34 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -79,7 +79,19 @@ // Use Svelte 5 idiomatic effect to update searchValue when $page.url.searchParams.get('id') changes $effect(() => { const url = $page.url.searchParams; - searchValue = url.get("id") ?? url.get("d"); + const idParam = url.get("id"); + const dParam = url.get("d"); + + if (idParam) { + searchValue = idParam; + dTagValue = null; + } else if (dParam) { + searchValue = null; + dTagValue = dParam.toLowerCase(); + } else { + searchValue = null; + dTagValue = null; + } }); // Add support for t and n parameters @@ -92,12 +104,28 @@ // Decode the t parameter and set it as searchValue with t: prefix const decodedT = decodeURIComponent(tParam); searchValue = `t:${decodedT}`; - } - - if (nParam) { + dTagValue = null; + } else if (nParam) { // Decode the n parameter and set it as searchValue with n: prefix const decodedN = decodeURIComponent(nParam); searchValue = `n:${decodedN}`; + dTagValue = null; + } + }); + + // Handle side panel visibility based on search type + $effect(() => { + const url = $page.url.searchParams; + const hasIdParam = url.get("id"); + const hasDParam = url.get("d"); + const hasTParam = url.get("t"); + const hasNParam = url.get("n"); + + // Close side panel for searches that return multiple results + if (hasDParam || hasTParam || hasNParam) { + showSidePanel = false; + event = null; + profile = null; } }); @@ -316,190 +344,7 @@ communityStatus = { ...communityStatus, ...newCommunityStatus }; } - function updateSearchFromURL() { - const id = $page.url.searchParams.get("id"); - const dTag = $page.url.searchParams.get("d"); - const tParam = $page.url.searchParams.get("t"); - const nParam = $page.url.searchParams.get("n"); - - console.log("Events page URL update:", { - id, - dTag, - tParam, - nParam, - searchValue, - }); - - if (id !== searchValue) { - console.log("ID changed, updating searchValue:", { - old: searchValue, - new: id, - }); - searchValue = id; - dTagValue = null; - // Only close side panel if we're clearing the search - if (!id) { - showSidePanel = false; - event = null; - profile = null; - } - } - - if (dTag !== dTagValue) { - console.log("DTag changed, updating dTagValue:", { - old: dTagValue, - new: dTag, - }); - // Normalize d-tag to lowercase for consistent searching - dTagValue = dTag ? dTag.toLowerCase() : null; - searchValue = null; - // For d-tag searches (which return multiple results), close side panel - showSidePanel = false; - event = null; - profile = null; - } - - // Handle t parameter - if (tParam) { - const decodedT = decodeURIComponent(tParam); - const tSearchValue = `t:${decodedT}`; - if (tSearchValue !== searchValue) { - console.log("T parameter changed, updating searchValue:", { - old: searchValue, - new: tSearchValue, - }); - searchValue = tSearchValue; - dTagValue = null; - // For t-tag searches (which return multiple results), close side panel - showSidePanel = false; - event = null; - profile = null; - } - } - - // Handle n parameter - if (nParam) { - const decodedN = decodeURIComponent(nParam); - const nSearchValue = `n:${decodedN}`; - if (nSearchValue !== searchValue) { - console.log("N parameter changed, updating searchValue:", { - old: searchValue, - new: nSearchValue, - }); - searchValue = nSearchValue; - dTagValue = null; - // For n-tag searches (which return multiple results), close side panel - showSidePanel = false; - event = null; - profile = null; - } - } - - // Reset state if all parameters are absent - if (!id && !dTag && !tParam && !nParam) { - event = null; - searchResults = []; - profile = null; - searchType = null; - searchTerm = null; - showSidePanel = false; - searchInProgress = false; - secondOrderSearchMessage = null; - } - } - - // Force search when URL changes - function handleUrlChange() { - const id = $page.url.searchParams.get("id"); - const dTag = $page.url.searchParams.get("d"); - const tParam = $page.url.searchParams.get("t"); - const nParam = $page.url.searchParams.get("n"); - - console.log("Events page URL change:", { - id, - dTag, - tParam, - nParam, - currentSearchValue: searchValue, - currentDTagValue: dTagValue, - }); - - // Handle ID parameter changes - if (id !== searchValue) { - console.log("ID parameter changed:", { old: searchValue, new: id }); - searchValue = id; - dTagValue = null; - if (!id) { - showSidePanel = false; - event = null; - profile = null; - } - } - - // Handle d-tag parameter changes - if (dTag !== dTagValue) { - console.log("d-tag parameter changed:", { old: dTagValue, new: dTag }); - dTagValue = dTag ? dTag.toLowerCase() : null; - searchValue = null; - showSidePanel = false; - event = null; - profile = null; - } - - // Handle t parameter changes - if (tParam) { - const decodedT = decodeURIComponent(tParam); - const tSearchValue = `t:${decodedT}`; - if (tSearchValue !== searchValue) { - console.log("t parameter changed:", { - old: searchValue, - new: tSearchValue, - }); - searchValue = tSearchValue; - dTagValue = null; - showSidePanel = false; - event = null; - profile = null; - } - } - - // Handle n parameter changes - if (nParam) { - const decodedN = decodeURIComponent(nParam); - const nSearchValue = `n:${decodedN}`; - if (nSearchValue !== searchValue) { - console.log("n parameter changed:", { - old: searchValue, - new: nSearchValue, - }); - searchValue = nSearchValue; - dTagValue = null; - showSidePanel = false; - event = null; - profile = null; - } - } - - // Reset state if all parameters are absent - if (!id && !dTag && !tParam && !nParam) { - console.log("All parameters absent, resetting state"); - event = null; - searchResults = []; - profile = null; - searchType = null; - searchTerm = null; - showSidePanel = false; - searchInProgress = false; - secondOrderSearchMessage = null; - searchValue = null; - dTagValue = null; - } - } - // Listen for URL changes - $effect(() => { - handleUrlChange(); - }); // Log relay configuration when page mounts onMount(() => { From fd3b26741da7404e14912f58b8da40ec809b3c5c Mon Sep 17 00:00:00 2001 From: silberengel Date: Fri, 18 Jul 2025 22:21:19 +0200 Subject: [PATCH 93/98] login working --- src/lib/components/util/Profile.svelte | 172 +++++++++++++++++++------ src/lib/stores/userStore.ts | 73 +++++++++-- src/lib/utils/nostrUtils.ts | 35 ++++- 3 files changed, 224 insertions(+), 56 deletions(-) diff --git a/src/lib/components/util/Profile.svelte b/src/lib/components/util/Profile.svelte index 399539a..ad52084 100644 --- a/src/lib/components/util/Profile.svelte +++ b/src/lib/components/util/Profile.svelte @@ -20,6 +20,8 @@ import { goto } from "$app/navigation"; import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import { onMount } from "svelte"; + import { getUserMetadata } from "$lib/utils/nostrUtils"; + import { activeInboxRelays } from "$lib/ndk"; let { pubkey, isNav = false } = $props<{ pubkey?: string, isNav?: boolean }>(); @@ -35,7 +37,7 @@ let profileAvatarId = "profile-avatar-btn"; let showAmberFallback = $state(false); let fallbackCheckInterval: ReturnType | null = null; - let isProfileLoading = $state(false); + let isRefreshingProfile = $state(false); onMount(() => { if (localStorage.getItem("alexandria/amber/fallback") === "1") { @@ -44,7 +46,7 @@ } }); - // Use profile data from userStore instead of fetching separately + // Use profile data from userStore let userState = $derived($userStore); let profile = $derived(userState.profile); let pfp = $derived(profile?.picture); @@ -52,6 +54,14 @@ let tag = $derived(profile?.name); let npub = $derived(userState.npub); + // Debug logging + $effect(() => { + console.log("Profile component - userState:", userState); + console.log("Profile component - profile:", profile); + console.log("Profile component - pfp:", pfp); + console.log("Profile component - username:", username); + }); + // Handle user state changes with effects $effect(() => { const currentUser = userState; @@ -87,51 +97,131 @@ } }); - // Fetch profile when user signs in or when pubkey changes + // Auto-refresh profile when user signs in $effect(() => { const currentUser = userState; - // If user is signed in but profile is not available, fetch it - if (currentUser.signedIn && !profile && currentUser.npub) { - const ndk = get(ndkInstance); - if (!ndk) return; + // If user is signed in and we have an npub but no profile data, refresh it + if (currentUser.signedIn && currentUser.npub && !profile?.name && !isRefreshingProfile) { + console.log("Profile: User signed in but no profile data, refreshing..."); + refreshProfile(); + } + }); - isProfileLoading = true; + // Debug activeInboxRelays + $effect(() => { + const inboxRelays = get(activeInboxRelays); + console.log("Profile component - activeInboxRelays:", inboxRelays); + }); + + // Manual trigger to refresh profile when user signs in + $effect(() => { + const currentUser = userState; + + if (currentUser.signedIn && currentUser.npub && !isRefreshingProfile) { + console.log("Profile: User signed in, triggering profile refresh..."); + // Add a small delay to ensure relays are ready + setTimeout(() => { + refreshProfile(); + }, 1000); + } + }); + + // Refresh profile when login method changes (e.g., Amber to read-only) + $effect(() => { + const currentUser = userState; + + if (currentUser.signedIn && currentUser.npub && currentUser.loginMethod) { + console.log("Profile: Login method detected:", currentUser.loginMethod); - // Use the current user's npub to fetch profile - const user = ndk.getUser({ npub: currentUser.npub }); + // If switching to read-only mode (npub), refresh profile + if (currentUser.loginMethod === "npub" && !isRefreshingProfile) { + console.log("Profile: Switching to read-only mode, refreshing profile..."); + setTimeout(() => { + refreshProfile(); + }, 500); + } + } + }); + + // Track login method changes and refresh profile when switching from Amber to npub + let previousLoginMethod = $state(null); + + $effect(() => { + const currentUser = userState; + + if (currentUser.signedIn && currentUser.loginMethod !== previousLoginMethod) { + console.log("Profile: Login method changed from", previousLoginMethod, "to", currentUser.loginMethod); - user.fetchProfile().then((userProfile: NDKUserProfile | null) => { - if (userProfile && !profile) { - // Only update if we don't already have profile data - profile = userProfile; - } - isProfileLoading = false; - }).catch(() => { - isProfileLoading = false; - }); + // If switching from Amber to npub (read-only), refresh profile + if (previousLoginMethod === "amber" && currentUser.loginMethod === "npub") { + console.log("Profile: Switching from Amber to read-only mode, refreshing profile..."); + setTimeout(() => { + refreshProfile(); + }, 1000); + } + + previousLoginMethod = currentUser.loginMethod; } + }); + + // Function to refresh profile data + async function refreshProfile() { + if (!userState.signedIn || !userState.npub) return; - // Fallback to fetching profile if not available in userStore and pubkey prop is provided - if (!profile && pubkey) { + isRefreshingProfile = true; + try { + console.log("Refreshing profile for npub:", userState.npub); + + // Try using NDK's built-in profile fetching first const ndk = get(ndkInstance); - if (!ndk) return; - - isProfileLoading = true; + if (ndk && userState.ndkUser) { + console.log("Using NDK's built-in profile fetching"); + const userProfile = await userState.ndkUser.fetchProfile(); + console.log("NDK profile fetch result:", userProfile); + + if (userProfile) { + const profileData = { + name: userProfile.name, + displayName: userProfile.displayName, + nip05: userProfile.nip05, + picture: userProfile.image, + about: userProfile.bio, + banner: userProfile.banner, + website: userProfile.website, + lud16: userProfile.lud16, + }; + + console.log("Converted profile data:", profileData); + + // Update the userStore with fresh profile data + userStore.update(currentState => ({ + ...currentState, + profile: profileData + })); + + return; + } + } - const user = ndk.getUser({ pubkey: pubkey ?? undefined }); + // Fallback to getUserMetadata + console.log("Falling back to getUserMetadata"); + const freshProfile = await getUserMetadata(userState.npub, true); // Force fresh fetch + console.log("Fresh profile data from getUserMetadata:", freshProfile); - user.fetchProfile().then((userProfile: NDKUserProfile | null) => { - if (userProfile && !profile) { - // Only update if we don't already have profile data - profile = userProfile; - } - isProfileLoading = false; - }).catch(() => { - isProfileLoading = false; - }); + // Update the userStore with fresh profile data + userStore.update(currentState => ({ + ...currentState, + profile: freshProfile + })); + } catch (error) { + console.error("Failed to refresh profile:", error); + } finally { + isRefreshingProfile = false; } - }); + } + + // Generate QR code const generateQrCode = async (text: string): Promise => { @@ -237,7 +327,6 @@ localStorage.removeItem("amber/nsec"); localStorage.removeItem("alexandria/amber/fallback"); logoutUser(); - profile = null; } function handleViewProfile() { @@ -255,6 +344,12 @@ function handleAmberFallbackDismiss() { showAmberFallback = false; localStorage.removeItem("alexandria/amber/fallback"); + + // Refresh profile when switching to read-only mode + setTimeout(() => { + console.log("Profile: Amber fallback dismissed, refreshing profile for read-only mode..."); + refreshProfile(); + }, 500); } function shortenNpub(long: string | null | undefined) { @@ -337,7 +432,7 @@ type="button" aria-label="Open profile menu" > - {#if isProfileLoading && !pfp} + {#if !pfp}
    {:else} {username} {#if isNav}

    @{tag}

    {/if} - {:else if isProfileLoading} + {:else if !pfp}

    Loading profile...

    {:else}

    Loading...

    @@ -381,6 +476,7 @@ />View profile +
  • {#if userState.loginMethod === "extension"} Logged in with extension diff --git a/src/lib/stores/userStore.ts b/src/lib/stores/userStore.ts index 6e8ef07..4e2b067 100644 --- a/src/lib/stores/userStore.ts +++ b/src/lib/stores/userStore.ts @@ -163,10 +163,14 @@ export async function loginWithExtension() { const user = await signer.user(); const npub = user.npub; + console.log("Login with extension - fetching profile for npub:", npub); + // Try to fetch user metadata, but don't fail if it times out let profile: NostrProfile | null = null; try { - profile = await getUserMetadata(npub); + console.log("Login with extension - attempting to fetch profile..."); + profile = await getUserMetadata(npub, true); // Force fresh fetch + console.log("Login with extension - fetched profile:", profile); } catch (error) { console.warn("Failed to fetch user metadata during login:", error); // Continue with login even if metadata fetch fails @@ -174,6 +178,7 @@ export async function loginWithExtension() { name: npub.slice(0, 8) + "..." + npub.slice(-4), displayName: npub.slice(0, 8) + "..." + npub.slice(-4), }; + console.log("Login with extension - using fallback profile:", profile); } // Fetch user's preferred relays @@ -185,7 +190,8 @@ export async function loginWithExtension() { persistRelays(user, inboxes, outboxes); ndk.signer = signer; ndk.activeUser = user; - userStore.set({ + + const userState = { pubkey: user.pubkey, npub, profile, @@ -195,11 +201,14 @@ export async function loginWithExtension() { (relay) => relay.url, ), }, - loginMethod: "extension", + loginMethod: "extension" as const, ndkUser: user, signer, signedIn: true, - }); + }; + + console.log("Login with extension - setting userStore with:", userState); + userStore.set(userState); clearLogin(); localStorage.removeItem("alexandria/logout/flag"); persistLogin(user, "extension"); @@ -213,7 +222,23 @@ export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser) { if (!ndk) throw new Error("NDK not initialized"); // Only clear previous login state after successful login const npub = user.npub; - const profile = await getUserMetadata(npub, true); // Force fresh fetch + + console.log("Login with Amber - fetching profile for npub:", npub); + + let profile: NostrProfile | null = null; + try { + profile = await getUserMetadata(npub, true); // Force fresh fetch + console.log("Login with Amber - fetched profile:", profile); + } catch (error) { + console.warn("Failed to fetch user metadata during Amber login:", error); + // Continue with login even if metadata fetch fails + profile = { + name: npub.slice(0, 8) + "..." + npub.slice(-4), + displayName: npub.slice(0, 8) + "..." + npub.slice(-4), + }; + console.log("Login with Amber - using fallback profile:", profile); + } + const [persistedInboxes, persistedOutboxes] = getPersistedRelays(user); for (const relay of persistedInboxes) { ndk.addExplicitRelay(relay); @@ -222,7 +247,8 @@ export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser) { persistRelays(user, inboxes, outboxes); ndk.signer = amberSigner; ndk.activeUser = user; - userStore.set({ + + const userState = { pubkey: user.pubkey, npub, profile, @@ -232,11 +258,14 @@ export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser) { (relay) => relay.url, ), }, - loginMethod: "amber", + loginMethod: "amber" as const, ndkUser: user, signer: amberSigner, signedIn: true, - }); + }; + + console.log("Login with Amber - setting userStore with:", userState); + userStore.set(userState); clearLogin(); localStorage.removeItem("alexandria/logout/flag"); persistLogin(user, "amber"); @@ -267,20 +296,40 @@ export async function loginWithNpub(pubkeyOrNpub: string) { console.error("Failed to encode npub from hex pubkey:", hexPubkey, e); throw e; } + + console.log("Login with npub - fetching profile for npub:", npub); + const user = ndk.getUser({ npub }); - const profile = await getUserMetadata(npub); + let profile: NostrProfile | null = null; + try { + profile = await getUserMetadata(npub, true); // Force fresh fetch + console.log("Login with npub - fetched profile:", profile); + } catch (error) { + console.warn("Failed to fetch user metadata during npub login:", error); + // Continue with login even if metadata fetch fails + profile = { + name: npub.slice(0, 8) + "..." + npub.slice(-4), + displayName: npub.slice(0, 8) + "..." + npub.slice(-4), + }; + console.log("Login with npub - using fallback profile:", profile); + } + ndk.signer = undefined; ndk.activeUser = user; - userStore.set({ + + const userState = { pubkey: user.pubkey, npub, profile, relays: { inbox: [], outbox: [] }, - loginMethod: "npub", + loginMethod: "npub" as const, ndkUser: user, signer: null, signedIn: true, - }); + }; + + console.log("Login with npub - setting userStore with:", userState); + userStore.set(userState); clearLogin(); localStorage.removeItem("alexandria/logout/flag"); persistLogin(user, "npub"); diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index 85e9e8d..f70d139 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -60,8 +60,12 @@ export async function getUserMetadata( // Remove nostr: prefix if present const cleanId = identifier.replace(/^nostr:/, ""); + console.log("getUserMetadata called with identifier:", identifier, "force:", force); + if (!force && npubCache.has(cleanId)) { - return npubCache.get(cleanId)!; + const cached = npubCache.get(cleanId)!; + console.log("getUserMetadata returning cached profile:", cached); + return cached; } const fallback = { name: `${cleanId.slice(0, 8)}...${cleanId.slice(-4)}` }; @@ -69,12 +73,14 @@ export async function getUserMetadata( try { const ndk = get(ndkInstance); if (!ndk) { + console.warn("getUserMetadata: No NDK instance available"); npubCache.set(cleanId, fallback); return fallback; } const decoded = nip19.decode(cleanId); if (!decoded) { + console.warn("getUserMetadata: Failed to decode identifier:", cleanId); npubCache.set(cleanId, fallback); return fallback; } @@ -86,19 +92,27 @@ export async function getUserMetadata( } else if (decoded.type === "nprofile") { pubkey = decoded.data.pubkey; } else { + console.warn("getUserMetadata: Unsupported identifier type:", decoded.type); npubCache.set(cleanId, fallback); return fallback; } + console.log("getUserMetadata: Fetching profile for pubkey:", pubkey); + const profileEvent = await fetchEventWithFallback(ndk, { kinds: [0], authors: [pubkey], }); + + console.log("getUserMetadata: Profile event found:", profileEvent); + const profile = profileEvent && profileEvent.content ? JSON.parse(profileEvent.content) : null; + console.log("getUserMetadata: Parsed profile:", profile); + const metadata: NostrProfile = { name: profile?.name || fallback.name, displayName: profile?.displayName || profile?.display_name, @@ -110,9 +124,11 @@ export async function getUserMetadata( lud16: profile?.lud16, }; + console.log("getUserMetadata: Final metadata:", metadata); npubCache.set(cleanId, metadata); return metadata; } catch (e) { + console.error("getUserMetadata: Error fetching profile:", e); npubCache.set(cleanId, fallback); return fallback; } @@ -426,9 +442,11 @@ export async function fetchEventWithFallback( // Use the active inbox relays from the relay management system const inboxRelays = get(activeInboxRelays); + console.log("fetchEventWithFallback: Using inbox relays:", inboxRelays); + // Check if we have any relays available if (inboxRelays.length === 0) { - console.warn("No inbox relays available for event fetch"); + console.warn("fetchEventWithFallback: No inbox relays available for event fetch"); return null; } @@ -437,10 +455,14 @@ export async function fetchEventWithFallback( try { if (relaySet.relays.size === 0) { - console.warn("No relays in relay set for event fetch"); + console.warn("fetchEventWithFallback: No relays in relay set for event fetch"); return null; } + console.log("fetchEventWithFallback: Relay set size:", relaySet.relays.size); + console.log("fetchEventWithFallback: Filter:", filterOrId); + console.log("fetchEventWithFallback: Relay URLs:", Array.from(relaySet.relays).map((r: any) => r.url)); + let found: NDKEvent | null = null; if ( @@ -465,11 +487,12 @@ export async function fetchEventWithFallback( const timeoutSeconds = timeoutMs / 1000; const relayUrls = Array.from(relaySet.relays).map((r: any) => r.url).join(", "); console.warn( - `Event not found after ${timeoutSeconds}s timeout. Tried inbox relays: ${relayUrls}. Some relays may be offline or slow.`, + `fetchEventWithFallback: Event not found after ${timeoutSeconds}s timeout. Tried inbox relays: ${relayUrls}. Some relays may be offline or slow.`, ); return null; } + console.log("fetchEventWithFallback: Found event:", found.id); // Always wrap as NDKEvent return found instanceof NDKEvent ? found : new NDKEvent(ndk, found); } catch (err) { @@ -477,10 +500,10 @@ export async function fetchEventWithFallback( const timeoutSeconds = timeoutMs / 1000; const relayUrls = Array.from(relaySet.relays).map((r: any) => r.url).join(", "); console.warn( - `Event fetch timed out after ${timeoutSeconds}s. Tried inbox relays: ${relayUrls}. Some relays may be offline or slow.`, + `fetchEventWithFallback: Event fetch timed out after ${timeoutSeconds}s. Tried inbox relays: ${relayUrls}. Some relays may be offline or slow.`, ); } else { - console.error("Error in fetchEventWithFallback:", err); + console.error("fetchEventWithFallback: Error in fetchEventWithFallback:", err); } return null; } From fab4a00992e568d0bfeaf888e824d4f6a05678b8 Mon Sep 17 00:00:00 2001 From: silberengel Date: Fri, 18 Jul 2025 23:06:49 +0200 Subject: [PATCH 94/98] publishing from events works --- src/lib/components/EventInput.svelte | 25 +++++--- src/lib/components/EventSearch.svelte | 17 ++++- src/lib/components/util/Profile.svelte | 26 ++++++-- src/lib/ndk.ts | 15 ++++- src/lib/stores/userStore.ts | 33 +++++++++- src/lib/utils/nostrUtils.ts | 15 +++-- src/lib/utils/relay_management.ts | 88 +++++++++++++++++++++++++- 7 files changed, 194 insertions(+), 25 deletions(-) diff --git a/src/lib/components/EventInput.svelte b/src/lib/components/EventInput.svelte index 1b9cb6b..cc2e2a3 100644 --- a/src/lib/components/EventInput.svelte +++ b/src/lib/components/EventInput.svelte @@ -16,6 +16,7 @@ import { get } from "svelte/store"; import { ndkInstance } from "$lib/ndk"; import { userPubkey } from "$lib/stores/authStore.Svelte"; + import { userStore } from "$lib/stores/userStore"; import { NDKEvent as NDKEventClass } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "$lib/utils/nostrUtils"; import { prefixNostrAddresses } from "$lib/utils/nostrUtils"; @@ -99,8 +100,12 @@ function validate(): { valid: boolean; reason?: string } { const currentUserPubkey = get(userPubkey as any); - if (!currentUserPubkey) return { valid: false, reason: "Not logged in." }; - const pubkey = String(currentUserPubkey); + const userState = get(userStore); + + // Try userPubkey first, then fallback to userStore + const pubkey = currentUserPubkey || userState.pubkey; + if (!pubkey) return { valid: false, reason: "Not logged in." }; + if (!content.trim()) return { valid: false, reason: "Content required." }; if (kind === 30023) { const v = validateNotAsciidoc(content); @@ -137,14 +142,18 @@ try { const ndk = get(ndkInstance); const currentUserPubkey = get(userPubkey as any); - if (!ndk || !currentUserPubkey) { + const userState = get(userStore); + + // Try userPubkey first, then fallback to userStore + const pubkey = currentUserPubkey || userState.pubkey; + if (!ndk || !pubkey) { error = "NDK or pubkey missing."; loading = false; return; } - const pubkey = String(currentUserPubkey); + const pubkeyString = String(pubkey); - if (!/^[a-fA-F0-9]{64}$/.test(pubkey)) { + if (!/^[a-fA-F0-9]{64}$/.test(pubkeyString)) { error = "Invalid public key: must be a 64-character hex string."; loading = false; return; @@ -158,7 +167,7 @@ return; } - const baseEvent = { pubkey, created_at: createdAt }; + const baseEvent = { pubkey: pubkeyString, created_at: createdAt }; let events: NDKEvent[] = []; console.log("Publishing event with kind:", kind); @@ -235,7 +244,7 @@ kind, content: prefixedContent, tags: eventTags, - pubkey, + pubkey: pubkeyString, created_at: createdAt, }; @@ -520,7 +529,7 @@ Event ID: {lastPublishedEventId} diff --git a/src/lib/components/EventSearch.svelte b/src/lib/components/EventSearch.svelte index cb71ade..10f888b 100644 --- a/src/lib/components/EventSearch.svelte +++ b/src/lib/components/EventSearch.svelte @@ -13,6 +13,8 @@ import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils"; import type { SearchResult } from '$lib/utils/search_types'; + import { userStore } from "$lib/stores/userStore"; + import { get } from "svelte/store"; // Props definition let { @@ -492,7 +494,7 @@ // Wait for relays to be available (with timeout) let retryCount = 0; - const maxRetries = 10; // Wait up to 5 seconds (10 * 500ms) + const maxRetries = 20; // Wait up to 10 seconds (20 * 500ms) for user login to complete while ($activeInboxRelays.length === 0 && $activeOutboxRelays.length === 0 && retryCount < maxRetries) { console.debug(`EventSearch: Waiting for relays... (attempt ${retryCount + 1}/${maxRetries})`); @@ -500,6 +502,19 @@ retryCount++; } + // Additional wait for user-specific relays if user is logged in + const currentUser = get(userStore); + if (currentUser.signedIn && currentUser.pubkey) { + console.debug(`EventSearch: User is logged in (${currentUser.pubkey}), waiting for user-specific relays...`); + retryCount = 0; + while ($activeOutboxRelays.length <= 9 && retryCount < maxRetries) { + // If we still have the default relay count (9), wait for user-specific relays + console.debug(`EventSearch: Waiting for user-specific relays... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, 500)); + retryCount++; + } + } + // Check if we have any relays available if ($activeInboxRelays.length === 0 && $activeOutboxRelays.length === 0) { console.warn("EventSearch: No relays available after waiting, failing search"); diff --git a/src/lib/components/util/Profile.svelte b/src/lib/components/util/Profile.svelte index ad52084..cc5ff4a 100644 --- a/src/lib/components/util/Profile.svelte +++ b/src/lib/components/util/Profile.svelte @@ -114,12 +114,24 @@ console.log("Profile component - activeInboxRelays:", inboxRelays); }); - // Manual trigger to refresh profile when user signs in + // Track if we've already refreshed the profile for this session + let hasRefreshedProfile = $state(false); + + // Reset the refresh flag when user logs out + $effect(() => { + const currentUser = userState; + if (!currentUser.signedIn) { + hasRefreshedProfile = false; + } + }); + + // Manual trigger to refresh profile when user signs in (only once) $effect(() => { const currentUser = userState; - if (currentUser.signedIn && currentUser.npub && !isRefreshingProfile) { + if (currentUser.signedIn && currentUser.npub && !isRefreshingProfile && !hasRefreshedProfile) { console.log("Profile: User signed in, triggering profile refresh..."); + hasRefreshedProfile = true; // Add a small delay to ensure relays are ready setTimeout(() => { refreshProfile(); @@ -131,12 +143,13 @@ $effect(() => { const currentUser = userState; - if (currentUser.signedIn && currentUser.npub && currentUser.loginMethod) { + if (currentUser.signedIn && currentUser.npub && currentUser.loginMethod && !isRefreshingProfile) { console.log("Profile: Login method detected:", currentUser.loginMethod); // If switching to read-only mode (npub), refresh profile - if (currentUser.loginMethod === "npub" && !isRefreshingProfile) { + if (currentUser.loginMethod === "npub" && !hasRefreshedProfile) { console.log("Profile: Switching to read-only mode, refreshing profile..."); + hasRefreshedProfile = true; setTimeout(() => { refreshProfile(); }, 500); @@ -150,12 +163,13 @@ $effect(() => { const currentUser = userState; - if (currentUser.signedIn && currentUser.loginMethod !== previousLoginMethod) { + if (currentUser.signedIn && currentUser.loginMethod !== previousLoginMethod && !isRefreshingProfile) { console.log("Profile: Login method changed from", previousLoginMethod, "to", currentUser.loginMethod); // If switching from Amber to npub (read-only), refresh profile - if (previousLoginMethod === "amber" && currentUser.loginMethod === "npub") { + if (previousLoginMethod === "amber" && currentUser.loginMethod === "npub" && !hasRefreshedProfile) { console.log("Profile: Switching from Amber to read-only mode, refreshing profile..."); + hasRefreshedProfile = true; setTimeout(() => { refreshProfile(); }, 1000); diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index 6e1120e..7b9745e 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -386,10 +386,13 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { */ export async function getActiveRelaySet(ndk: NDK): Promise<{ inboxRelays: string[]; outboxRelays: string[] }> { const user = get(userStore); + console.debug('[NDK.ts] getActiveRelaySet: User state:', { signedIn: user.signedIn, hasNdkUser: !!user.ndkUser, pubkey: user.pubkey }); if (user.signedIn && user.ndkUser) { + console.debug('[NDK.ts] getActiveRelaySet: Building relay set for authenticated user:', user.ndkUser.pubkey); return await buildCompleteRelaySet(ndk, user.ndkUser); } else { + console.debug('[NDK.ts] getActiveRelaySet: Building relay set for anonymous user'); return await buildCompleteRelaySet(ndk, null); } } @@ -400,25 +403,33 @@ export async function getActiveRelaySet(ndk: NDK): Promise<{ inboxRelays: string */ export async function updateActiveRelayStores(ndk: NDK): Promise { try { + console.debug('[NDK.ts] updateActiveRelayStores: Starting relay store update'); + // Get the active relay set from the relay management system const relaySet = await getActiveRelaySet(ndk); + console.debug('[NDK.ts] updateActiveRelayStores: Got relay set:', relaySet); // Update the stores with the new relay configuration activeInboxRelays.set(relaySet.inboxRelays); activeOutboxRelays.set(relaySet.outboxRelays); + console.debug('[NDK.ts] updateActiveRelayStores: Updated stores with inbox:', relaySet.inboxRelays.length, 'outbox:', relaySet.outboxRelays.length); // Add relays to NDK pool (deduplicated) const allRelayUrls = deduplicateRelayUrls([...relaySet.inboxRelays, ...relaySet.outboxRelays]); + console.debug('[NDK.ts] updateActiveRelayStores: Adding', allRelayUrls.length, 'relays to NDK pool'); + for (const url of allRelayUrls) { try { const relay = createRelayWithAuth(url, ndk); ndk.pool?.addRelay(relay); } catch (error) { - // Silently ignore relay addition failures + console.debug('[NDK.ts] updateActiveRelayStores: Failed to add relay', url, ':', error); } } + + console.debug('[NDK.ts] updateActiveRelayStores: Relay store update completed'); } catch (error) { - // Silently ignore relay store update errors + console.warn('[NDK.ts] updateActiveRelayStores: Error updating relay stores:', error); } } diff --git a/src/lib/stores/userStore.ts b/src/lib/stores/userStore.ts index 4e2b067..376367c 100644 --- a/src/lib/stores/userStore.ts +++ b/src/lib/stores/userStore.ts @@ -8,9 +8,10 @@ import { NDKRelay, } from "@nostr-dev-kit/ndk"; import { getUserMetadata } from "$lib/utils/nostrUtils"; -import { ndkInstance, activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; +import { ndkInstance, activeInboxRelays, activeOutboxRelays, updateActiveRelayStores } from "$lib/ndk"; import { loginStorageKey } from "$lib/consts"; import { nip19 } from "nostr-tools"; +import { userPubkey } from "$lib/stores/authStore.Svelte"; export interface UserState { pubkey: string | null; @@ -209,6 +210,15 @@ export async function loginWithExtension() { console.log("Login with extension - setting userStore with:", userState); userStore.set(userState); + + // Update relay stores with the new user's relays + try { + console.debug('[userStore.ts] loginWithExtension: Updating relay stores for authenticated user'); + await updateActiveRelayStores(ndk); + } catch (error) { + console.warn('[userStore.ts] loginWithExtension: Failed to update relay stores:', error); + } + clearLogin(); localStorage.removeItem("alexandria/logout/flag"); persistLogin(user, "extension"); @@ -266,6 +276,16 @@ export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser) { console.log("Login with Amber - setting userStore with:", userState); userStore.set(userState); + userPubkey.set(user.pubkey); + + // Update relay stores with the new user's relays + try { + console.debug('[userStore.ts] loginWithAmber: Updating relay stores for authenticated user'); + await updateActiveRelayStores(ndk); + } catch (error) { + console.warn('[userStore.ts] loginWithAmber: Failed to update relay stores:', error); + } + clearLogin(); localStorage.removeItem("alexandria/logout/flag"); persistLogin(user, "amber"); @@ -330,6 +350,16 @@ export async function loginWithNpub(pubkeyOrNpub: string) { console.log("Login with npub - setting userStore with:", userState); userStore.set(userState); + userPubkey.set(user.pubkey); + + // Update relay stores with the new user's relays + try { + console.debug('[userStore.ts] loginWithNpub: Updating relay stores for authenticated user'); + await updateActiveRelayStores(ndk); + } catch (error) { + console.warn('[userStore.ts] loginWithNpub: Failed to update relay stores:', error); + } + clearLogin(); localStorage.removeItem("alexandria/logout/flag"); persistLogin(user, "npub"); @@ -393,6 +423,7 @@ export function logoutUser() { signer: null, signedIn: false, }); + userPubkey.set(null); const ndk = get(ndkInstance); if (ndk) { diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index f70d139..da8bb1b 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -5,7 +5,7 @@ import { npubCache } from "./npubCache"; import NDK, { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; import type { NDKFilter, NDKKind } from "@nostr-dev-kit/ndk"; import { communityRelays, secondaryRelays, anonymousRelays } from "$lib/consts"; -import { activeInboxRelays } from "$lib/ndk"; +import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; import { NDKRelaySet as NDKRelaySetFromNDK } from "@nostr-dev-kit/ndk"; import { sha256 } from "@noble/hashes/sha256"; import { schnorr } from "@noble/curves/secp256k1"; @@ -439,19 +439,22 @@ export async function fetchEventWithFallback( filterOrId: string | NDKFilter, timeoutMs: number = 3000, ): Promise { - // Use the active inbox relays from the relay management system + // Use both inbox and outbox relays for better event discovery const inboxRelays = get(activeInboxRelays); + const outboxRelays = get(activeOutboxRelays); + const allRelays = [...(inboxRelays || []), ...(outboxRelays || [])]; console.log("fetchEventWithFallback: Using inbox relays:", inboxRelays); + console.log("fetchEventWithFallback: Using outbox relays:", outboxRelays); // Check if we have any relays available - if (inboxRelays.length === 0) { - console.warn("fetchEventWithFallback: No inbox relays available for event fetch"); + if (allRelays.length === 0) { + console.warn("fetchEventWithFallback: No relays available for event fetch"); return null; } - // Create relay set from active inbox relays - const relaySet = NDKRelaySetFromNDK.fromRelayUrls(inboxRelays, ndk); + // Create relay set from all available relays + const relaySet = NDKRelaySetFromNDK.fromRelayUrls(allRelays, ndk); try { if (relaySet.relays.size === 0) { diff --git a/src/lib/utils/relay_management.ts b/src/lib/utils/relay_management.ts index 09aa5ac..2787938 100644 --- a/src/lib/utils/relay_management.ts +++ b/src/lib/utils/relay_management.ts @@ -287,6 +287,7 @@ export async function getUserBlockedRelays(ndk: NDK, user: NDKUser): Promise { try { + console.debug('[relay_management.ts] Fetching outbox relays for user:', user.pubkey); const relayList = await ndk.fetchEvent( { kinds: [10002], @@ -300,16 +301,29 @@ export async function getUserOutboxRelays(ndk: NDK, user: NDKUser): Promise { + console.debug('[relay_management.ts] Processing tag:', tag); if (tag[0] === 'w' && tag[1]) { outboxRelays.push(tag[1]); + console.debug('[relay_management.ts] Added outbox relay:', tag[1]); + } else if (tag[0] === 'r' && tag[1]) { + // Some relay lists use 'r' for both inbox and outbox + outboxRelays.push(tag[1]); + console.debug('[relay_management.ts] Added relay (r tag):', tag[1]); + } else { + console.debug('[relay_management.ts] Skipping tag:', tag[0], 'value:', tag[1]); } }); + console.debug('[relay_management.ts] Final outbox relays:', outboxRelays); return outboxRelays; } catch (error) { console.info('[relay_management.ts] Error fetching user outbox relays:', error); @@ -317,6 +331,56 @@ export async function getUserOutboxRelays(ndk: NDK, user: NDKUser): Promise { + try { + // Check if we're in a browser environment with extension support + if (typeof window === 'undefined' || !window.nostr) { + console.debug('[relay_management.ts] No window.nostr available'); + return []; + } + + console.debug('[relay_management.ts] Extension available, checking for getRelays()'); + const extensionRelays: string[] = []; + + // Try to get relays from the extension's API + // Different extensions may expose their relay config differently + if (window.nostr.getRelays) { + console.debug('[relay_management.ts] getRelays() method found, calling it...'); + try { + const relays = await window.nostr.getRelays(); + console.debug('[relay_management.ts] getRelays() returned:', relays); + if (relays && typeof relays === 'object') { + // Convert relay object to array of URLs + const relayUrls = Object.keys(relays); + extensionRelays.push(...relayUrls); + console.debug('[relay_management.ts] Got relays from extension:', relayUrls); + } + } catch (error) { + console.debug('[relay_management.ts] Extension getRelays() failed:', error); + } + } else { + console.debug('[relay_management.ts] getRelays() method not found on window.nostr'); + } + + // If getRelays() didn't work, try alternative methods + if (extensionRelays.length === 0) { + // Some extensions might expose relays through other methods + // This is a fallback for extensions that don't expose getRelays() + console.debug('[relay_management.ts] Extension does not expose relay configuration'); + } + + console.debug('[relay_management.ts] Final extension relays:', extensionRelays); + return extensionRelays; + } catch (error) { + console.debug('[relay_management.ts] Error getting extension relays:', error); + return []; + } +} + /** * Tests a set of relays in batches to avoid overwhelming them * @param relayUrls Array of relay URLs to test @@ -361,37 +425,55 @@ export async function buildCompleteRelaySet( ndk: NDK, user: NDKUser | null ): Promise<{ inboxRelays: string[]; outboxRelays: string[] }> { + console.debug('[relay_management.ts] buildCompleteRelaySet: Starting with user:', user?.pubkey || 'null'); + // Discover local relays first const discoveredLocalRelays = await discoverLocalRelays(ndk); + console.debug('[relay_management.ts] buildCompleteRelaySet: Discovered local relays:', discoveredLocalRelays); // Get user-specific relays if available let userOutboxRelays: string[] = []; let userLocalRelays: string[] = []; let blockedRelays: string[] = []; + let extensionRelays: string[] = []; if (user) { + console.debug('[relay_management.ts] buildCompleteRelaySet: Fetching user-specific relays for:', user.pubkey); + try { userOutboxRelays = await getUserOutboxRelays(ndk, user); + console.debug('[relay_management.ts] buildCompleteRelaySet: User outbox relays:', userOutboxRelays); } catch (error) { console.debug('[relay_management.ts] Error fetching user outbox relays:', error); } try { userLocalRelays = await getUserLocalRelays(ndk, user); + console.debug('[relay_management.ts] buildCompleteRelaySet: User local relays:', userLocalRelays); } catch (error) { console.debug('[relay_management.ts] Error fetching user local relays:', error); } try { blockedRelays = await getUserBlockedRelays(ndk, user); + console.debug('[relay_management.ts] buildCompleteRelaySet: User blocked relays:', blockedRelays); } catch (error) { // Silently ignore blocked relay fetch errors } + + try { + extensionRelays = await getExtensionRelays(); + console.debug('[relay_management.ts] Extension relays gathered:', extensionRelays); + } catch (error) { + console.debug('[relay_management.ts] Error fetching extension relays:', error); + } + } else { + console.debug('[relay_management.ts] buildCompleteRelaySet: No user provided, skipping user-specific relays'); } // Build initial relay sets and deduplicate const finalInboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...userLocalRelays]); - const finalOutboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...userOutboxRelays]); + const finalOutboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...userOutboxRelays, ...extensionRelays]); // Test relays and filter out non-working ones let testedInboxRelays: string[] = []; @@ -441,5 +523,9 @@ export async function buildCompleteRelaySet( }; } + console.debug('[relay_management.ts] buildCompleteRelaySet: Final relay sets - inbox:', finalRelaySet.inboxRelays.length, 'outbox:', finalRelaySet.outboxRelays.length); + console.debug('[relay_management.ts] buildCompleteRelaySet: Final inbox relays:', finalRelaySet.inboxRelays); + console.debug('[relay_management.ts] buildCompleteRelaySet: Final outbox relays:', finalRelaySet.outboxRelays); + return finalRelaySet; } \ No newline at end of file From c1d60069a886749d3ae46742ac77cb734659abb0 Mon Sep 17 00:00:00 2001 From: silberengel Date: Fri, 18 Jul 2025 23:12:17 +0200 Subject: [PATCH 95/98] commenting works --- src/lib/stores/userStore.ts | 1 + src/lib/utils/nostrEventService.ts | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/lib/stores/userStore.ts b/src/lib/stores/userStore.ts index 376367c..4fdcf48 100644 --- a/src/lib/stores/userStore.ts +++ b/src/lib/stores/userStore.ts @@ -210,6 +210,7 @@ export async function loginWithExtension() { console.log("Login with extension - setting userStore with:", userState); userStore.set(userState); + userPubkey.set(user.pubkey); // Update relay stores with the new user's relays try { diff --git a/src/lib/utils/nostrEventService.ts b/src/lib/utils/nostrEventService.ts index 1a233a8..4ad05f8 100644 --- a/src/lib/utils/nostrEventService.ts +++ b/src/lib/utils/nostrEventService.ts @@ -2,11 +2,10 @@ import { nip19 } from "nostr-tools"; import { getEventHash, signEvent, prefixNostrAddresses } from "./nostrUtils"; import { get } from "svelte/store"; import { goto } from "$app/navigation"; -import type { NDKEvent } from "./nostrUtils"; import { EVENT_KINDS, TIME_CONSTANTS, TIMEOUTS } from "./search_constants"; import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; import { ndkInstance } from "$lib/ndk"; -import { NDKRelaySet } from "@nostr-dev-kit/ndk"; +import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk"; export interface RootEventInfo { rootId: string; @@ -360,12 +359,12 @@ export async function createSignedEvent( /** * Publishes an event to relays using the new relay management system - * @param event The event to publish + * @param event The event to publish (can be NDKEvent or plain event object) * @param relayUrls Array of relay URLs to publish to * @returns Promise that resolves to array of successful relay URLs */ export async function publishEvent( - event: NDKEvent, + event: NDKEvent | any, relayUrls: string[], ): Promise { const successfulRelays: string[] = []; @@ -379,15 +378,25 @@ export async function publishEvent( const relaySet = NDKRelaySet.fromRelayUrls(relayUrls, ndk); try { + // If event is a plain object, create an NDKEvent from it + let ndkEvent: NDKEvent; + if (event.publish && typeof event.publish === 'function') { + // It's already an NDKEvent + ndkEvent = event; + } else { + // It's a plain event object, create NDKEvent + ndkEvent = new NDKEvent(ndk, event); + } + // Publish with timeout - await event.publish(relaySet).withTimeout(5000); + await ndkEvent.publish(relaySet).withTimeout(5000); // For now, assume all relays were successful // In a more sophisticated implementation, you'd track individual relay responses successfulRelays.push(...relayUrls); console.debug("[nostrEventService] Published event successfully:", { - eventId: event.id, + eventId: ndkEvent.id, relayCount: relayUrls.length, successfulRelays }); From 5822c087a632b193f17d4aebf6753fdce44d2a6a Mon Sep 17 00:00:00 2001 From: silberengel Date: Fri, 18 Jul 2025 23:30:13 +0200 Subject: [PATCH 96/98] blog updated --- src/lib/components/cards/BlogHeader.svelte | 2 +- src/lib/parser.ts | 37 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/lib/components/cards/BlogHeader.svelte b/src/lib/components/cards/BlogHeader.svelte index 0c3a2bc..f6f10f5 100644 --- a/src/lib/components/cards/BlogHeader.svelte +++ b/src/lib/components/cards/BlogHeader.svelte @@ -68,7 +68,7 @@ class="ArticleBoxImage flex justify-center items-center p-2 h-40 -mt-2" in:scale={{ start: 0.8, duration: 500, delay: 100, easing: quintOut }} > - {#if image && active} + {#if image} Date: Fri, 18 Jul 2025 23:32:36 +0200 Subject: [PATCH 97/98] turned on local relays --- src/lib/consts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/consts.ts b/src/lib/consts.ts index 998cbec..ef41e0d 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -39,8 +39,8 @@ export const lowbandwidthRelays = [ ]; export const localRelays: string[] = [ - // "wss://localhost:8080", - // "wss://localhost:4869" + "wss://localhost:8080", + "wss://localhost:4869" ]; export enum FeedType { From 0eb8509e2fa26dc3bb7bcb09abb5003860c2d913 Mon Sep 17 00:00:00 2001 From: silberengel Date: Fri, 18 Jul 2025 23:43:50 +0200 Subject: [PATCH 98/98] v0.0.2 packages --- package-lock.json | 182 +++++++++++++++++++++++----------------------- 1 file changed, 91 insertions(+), 91 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0d82dc9..f256933 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1577,9 +1577,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.25.0.tgz", - "integrity": "sha512-Yc/WUMqYjYIZp2JsFUajw+cx7hIIqL1Z4uuhVl/yess65bGITbmG1aRIVOrlHg4oxmZqMluUJaVTLMLZZ9sNlg==", + "version": "2.25.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.25.1.tgz", + "integrity": "sha512-8H+fxDEp7Xq6tLFdrGdS5fLu6ONDQQ9DgyjboXpChubuFdfH9QoFX09ypssBpyNkJNZFt9eW3yLmXIc9CesPCA==", "dev": true, "dependencies": { "@sveltejs/acorn-typescript": "^1.0.5", @@ -2559,37 +2559,18 @@ } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" } }, "node_modules/cliui": { @@ -3450,15 +3431,6 @@ "node": ">=4" } }, - "node_modules/eslint-plugin-svelte/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -5516,25 +5488,16 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, "engines": { - "node": ">=8.6" + "node": ">= 14.18.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/require-directory": { @@ -5956,9 +5919,9 @@ } }, "node_modules/svelte": { - "version": "5.36.7", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.36.7.tgz", - "integrity": "sha512-QsaFAxL1PZvo9hwaN+x7Sq2U8oJARmsEuM8TEZVy98nx5D5IKzRi8FKkPvmOx9NXScSYnItDGLErBBn/ieIn2A==", + "version": "5.36.8", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.36.8.tgz", + "integrity": "sha512-8JbZWQu96hMjH/oYQPxXW6taeC6Awl6muGHeZzJTxQx7NGRQ/J9wN1hkzRKLOlSDlbS2igiFg7p5xyTp5uXG3A==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -6003,34 +5966,6 @@ "typescript": ">=5.0.0" } }, - "node_modules/svelte-check/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/svelte-check/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/svelte-eslint-parser": { "version": "0.43.0", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", @@ -6249,6 +6184,51 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tailwindcss/node_modules/postcss-load-config": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", @@ -6295,6 +6275,28 @@ "node": ">=4" } }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/tailwindcss/node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6835,14 +6837,12 @@ } }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", - "bin": { - "yaml": "bin.mjs" - }, + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, "engines": { - "node": ">= 14.6" + "node": ">= 6" } }, "node_modules/yargs": {