From 2458d5b232a104f5b224a144932f04dd26955f5a Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 30 Jul 2025 14:07:44 -0500 Subject: [PATCH 01/98] Add design documentation on relay selector --- .../docs/relay_selector_design.md | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/lib/data_structures/docs/relay_selector_design.md diff --git a/src/lib/data_structures/docs/relay_selector_design.md b/src/lib/data_structures/docs/relay_selector_design.md new file mode 100644 index 0000000..f8acb4b --- /dev/null +++ b/src/lib/data_structures/docs/relay_selector_design.md @@ -0,0 +1,144 @@ +# Relay Selector Class Design + +The relay selector will be a singleton that tracks, rates, and ranks Nostr relays to help the application determine which relay should be used to handle each request. It will weight relays based on observed characteristics, then use these weights to implement a weighted round robin algorithm for selecting relays, with some additional modifications to account for domain-specific features of Nostr. + +## Relay Weights + +### Categories + +Relays are broadly divided into three categories: + +1. **Public**: no authorization is required +2. **Private Write**: authorization is required to write to this relay, but not to read +3. **Private Read and Write**: authorization is required to use any features of this relay + +The broadest level of relay selection is based on these categories. + +- For users that are not logged in, public relays are used exclusively. +- For logged-in users, public and private read relays are initially rated equally for read operations. +- For logged-in users, private write relays are preferred above public relays for write operations. + +### Weighted Metrics + +Several weighted metrics are used to compute a relay's score. The score is used to rank relays to determine which to prefer when fetching events. + +#### Response Time + +The response time weight of each relay is computed according to the logarithmic function $`r(t) = -log(t) + 1`$, where $`t`$ is the median response time in seconds. This function has a few features which make it useful: + +- $`r(1) = 1`$, making a response time of 1s the netural point. This causes the algorithm to prefer relays that respond in under 1s. +- $`r(0.3) \approx 1.5`$ and $`r(3) \approx 0.5`$. This clusters the 0.5 to 1.5 weight range in the 300ms to 3s response time range, which is a sufficiently rapid response time to keep user's from switching context. +- The function has a long tail, so it doesn't discount slower response times too heavily, too quickly. + +#### Success Rate + +The success rate $`s(x)`$ is computed as the fraction of total requests sent to the relay that returned at least one event in response. The optimal score is 1, meaning the relay successfully responds to 100% of requests. + +#### Trust Level + +Certain relays may be assigned a constant "trust level" score $`T`$. This modifier is a number in the range $`[-0.5, 0.5]`$ that indicates how much a relay is trusted by the GitCitadel organization. + +A few factors contribute to a higher trust rating: + +- Effective filtering of spam and abusive content. +- Good data transparency, including such policies as honoring deletion requests. +- Event aggregation policies that aim at synchronization with the broader relay network. + +#### Preferred Vendors + +Certain relays may be assigned a constant "preferred vendor" score $`V`$. This modifier is a number in the range $`[0, 0.5]`$. It is used to increase the priority of GitCitadel's preferred relay vendors. + +### Overall Weight + +The overall weight of a relay is calculated as $`w(t, x) = r(t) \times s(x) + T + V`$. The `RelaySelector` class maintains a list of relays sorted by their overall weights. The weights may be updated at runtime when $`t`$ or $`x`$ change. On update, the relay list is re-sorted to account for the new weights. + +## Algorithm + +The relay weights contribute to a weighted round robin (WRR) algorithm for relay selection. Pseudocode for the algorithm is given below: + +```pseudocode +Constants and Variables: + const N // Number of relays + const CW // Connection weight + wInit // Map of relay URLs to initial weights + conn // Map of relay URLs to the number of active connections to that relay + wCurr // Current relay weights + rSorted // List of relay URLs sorted in ascending order + +Function getRelay: + r = rSorted[N - 1] // Get the highest-ranked relay + conn[r]++ // Increment the number of connections + wCurr[r] = wInit[r] + conn[r] * CW // Adjust current weights based on new connection weight + sort rSorted by wCurr // Re-sort based on updated weights + return r +``` + +## Class Methods + +The `RelaySelector` class should expose the following methods to support updates to relay weights. Pseudocode for each method is given below. + +### Add Response Time Datum + +This function updates the class state by side effect. Locking should be used in concurrent use cases. + +```pseudocode +Constants and Variables: + const CW // Connection weight + rT // A map of relay URLs to their Trust Level scores + rV // A map of relay URLs to their Preferred Vendor scores + rTimes // A map of relay URLs to a list or recorded response times + rReqs // A map of relay URLs to the number of recorded requests + rSucc // A map of relay URLs to the number of successful requests + rTimes // A map of relay URLs to recorded response times + wInit // Map of relay URLs to initial weights + conn // Map of relay URLs to the number of active connections to that relay + wCurr // Current relay weights + rSorted // List of relay URLs sorted in ascending order + +Parameters: + r // A relay URL + rt // A response time datum recorded for the given relay + +Function addResponseTimeDatum: + append rt to rTimes[r] + sort rTimes[r] + rtMed = median of rTimes[r] + rtWeight = -1 * log(rtMed) + 1 + succRate = rSucc[r] / rReqs[r] + wInit[r] = rtWeight * succRate + rT[r] + rV[r] + wCurr[r] = wInit[r] + conn[r] * CW + sort rSorted by wCurr +``` + +### Add Success Rate Datum + +This function updates the class state by side effect. Locking should be used in concurrent use cases. + +```pseudocode +Constants and Variables: + const CW // Connection weight + rT // A map of relay URLs to their Trust Level scores + rV // A map of relay URLs to their Preferred Vendor scores + rReqs // A map of relay URLs to the number of recorded requests + rSucc // A map of relay URLs to the number of successful requests + rTimes // A map of relay URLs to recorded response times + wInit // Map of relay URLs to initial weights + conn // Map of relay URLs to the number of active connections to that relay + wCurr // Current relay weights + rSorted // List of relay URLs sorted in ascending order + +Parameters: + r // A relay URL + s // A boolean value indicating whether the latest request to relay r succeeded + +Function addSuccessRateDatum: + rReqs[r]++ + if s is true: + rSucc[r]++ + rtMed = median of rTimes[r] + rtWeight = -1 * log(rtMed) + 1 + succRate = rSuccReqs[r] / rReqs[r] + wInit[r] = rtWeight * succRate + rT[r] + rV[r] + wCurr[r] = wInit[r] + conn[r] * CW + sort rSorted by wCurr +``` From 1475004a29b5efd2d15a600e05993948afedafdd Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Fri, 1 Aug 2025 09:01:56 -0500 Subject: [PATCH 02/98] Add details to relay selector design doc --- .../docs/relay_selector_design.md | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/lib/data_structures/docs/relay_selector_design.md b/src/lib/data_structures/docs/relay_selector_design.md index f8acb4b..0fb1616 100644 --- a/src/lib/data_structures/docs/relay_selector_design.md +++ b/src/lib/data_structures/docs/relay_selector_design.md @@ -18,6 +18,17 @@ The broadest level of relay selection is based on these categories. - For logged-in users, public and private read relays are initially rated equally for read operations. - For logged-in users, private write relays are preferred above public relays for write operations. +### User Preferences + +The relay selector will respect user relay preferences while still attempting to optimize for responsiveness and success rate. + +- User inbox relays will be stored in a separate list from general-purpose relays, and weighted and sorted separately using the same algorithm as the general-purpose relay list. +- Local relays (beginning with `wss://localhost` or `ws://localhost`) will be stored _unranked_ in a separate list, and used when the relay selector is operating on a web browser (as opposed to a server). +- When a caller requests relays from the relay selector, the selector will return: + - The highest-ranked general-purpose relay + - The highest-ranked user inbox relay + - (If on browser) any local relays + ### Weighted Metrics Several weighted metrics are used to compute a relay's score. The score is used to rank relays to determine which to prefer when fetching events. @@ -142,3 +153,47 @@ Function addSuccessRateDatum: wCurr[r] = wInit[r] + conn[r] * CW sort rSorted by wCurr ``` + +### Add Relay + +```pseudocode +Constants and Variables: + general // A list of general-purpose relay URLs + inbox // A list of user-defined inbox relay URLs + local // A list of local relay URLs + +Parameters: + r // The relay URL + rType // The relay type (general, inbox, or local) + +Function addRelay: + if rType is "general": + add r to general + sort general by current weights + if rType is "inbox": + add r to inbox + sort inbox by current weights + if rType is "local": + add r to local +``` + +### Get Relay + +``` +Constants and Variables: + general // A sorted list of general-purpose relay URLs + inbox // A sorted list of user-defined inbox relay URLs + local // An unsorted list of local relay URLs + +Parameters: + rank // The requested rank + +Function getRelay: + selected = [] + if local has members: + add all local members to selected + if rank less than length of inbox: + add inbox[rank] to selected + if rank less than length of general: + add general[rank] to selected +``` From 9a96a7aad8ce99cb355b090fd961f5b5e0c4f358 Mon Sep 17 00:00:00 2001 From: silberengel Date: Tue, 5 Aug 2025 17:46:08 +0200 Subject: [PATCH 03/98] fixed gaps in landing page feed. now always 10x column number. --- package-lock.json | 180 +++++++++--------- .../publications/PublicationFeed.svelte | 90 +++++++-- 2 files changed, 168 insertions(+), 102 deletions(-) diff --git a/package-lock.json b/package-lock.json index ed96156..003bf33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2783,19 +2783,39 @@ } }, "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "devOptional": true, + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "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": ">= 14.16.0" + "node": ">= 8.10.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==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, "node_modules/cliui": { @@ -3711,6 +3731,16 @@ } } }, + "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, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -5953,17 +5983,27 @@ } }, "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==", - "devOptional": true, + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, "engines": { - "node": ">= 14.18.0" + "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==", + "license": "MIT", + "engines": { + "node": ">=8.6" }, "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/require-directory": { @@ -6471,6 +6511,36 @@ "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/svelte-eslint-parser": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.3.1.tgz", @@ -6671,54 +6741,6 @@ "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==", - "license": "MIT", - "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==", - "license": "ISC", - "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==", - "license": "MIT", - "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", @@ -6767,30 +6789,6 @@ "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==", - "license": "MIT", - "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==", - "license": "ISC", - "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", @@ -7376,13 +7374,15 @@ } }, "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, + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14.6" } }, "node_modules/yargs": { diff --git a/src/lib/components/publications/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte index 48e4eba..1e6939c 100644 --- a/src/lib/components/publications/PublicationFeed.svelte +++ b/src/lib/components/publications/PublicationFeed.svelte @@ -27,6 +27,7 @@ let loading: boolean = $state(true); let hasInitialized = $state(false); let fallbackTimeout: ReturnType | null = null; + let gridContainer: HTMLElement; // Relay management let allRelays: string[] = $state([]); @@ -35,6 +36,35 @@ // Event management let allIndexEvents: NDKEvent[] = $state([]); + // Calculate the number of columns based on window width + let columnCount = $state(1); + let publicationsToDisplay = $state(10); + + // Update column count and publications when window resizes + $effect(() => { + if (typeof window !== 'undefined') { + const width = window.innerWidth; + let newColumnCount = 1; + if (width >= 1280) newColumnCount = 4; // xl:grid-cols-4 + else if (width >= 1024) newColumnCount = 3; // lg:grid-cols-3 + else if (width >= 768) newColumnCount = 2; // md:grid-cols-2 + + if (columnCount !== newColumnCount) { + columnCount = newColumnCount; + publicationsToDisplay = newColumnCount * 10; + + // Update the view immediately when column count changes + if (allIndexEvents.length > 0) { + const source = props.searchQuery?.trim() + ? filterEventsBySearch(allIndexEvents) + : allIndexEvents; + eventsInView = source.slice(0, publicationsToDisplay); + endOfFeed = eventsInView.length >= source.length; + } + } + } + }); + // Initialize relays and fetch events async function initializeAndFetch() { if (!ndk) { @@ -121,8 +151,8 @@ `[PublicationFeed] Using cached index events (${cachedEvents.length} events)`, ); allIndexEvents = cachedEvents; - eventsInView = allIndexEvents.slice(0, 30); - endOfFeed = allIndexEvents.length <= 30; + eventsInView = allIndexEvents.slice(0, publicationsToDisplay); + endOfFeed = allIndexEvents.length <= publicationsToDisplay; loading = false; return; } @@ -210,8 +240,8 @@ 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; + eventsInView = allIndexEvents.slice(0, publicationsToDisplay); + endOfFeed = allIndexEvents.length <= publicationsToDisplay; console.debug(`[PublicationFeed] Updated view with ${newEvents.length} new events from ${relay}, total: ${allIndexEvents.length}`); } @@ -236,8 +266,8 @@ indexEventCache.set(allRelays, allIndexEvents); // Final update to ensure we have the latest view - eventsInView = allIndexEvents.slice(0, 30); - endOfFeed = allIndexEvents.length <= 30; + eventsInView = allIndexEvents.slice(0, publicationsToDisplay); + endOfFeed = allIndexEvents.length <= publicationsToDisplay; loading = false; } @@ -326,11 +356,11 @@ console.debug("[PublicationFeed] Search query changed:", query); if (query && query.trim()) { const filtered = filterEventsBySearch(allIndexEvents); - eventsInView = filtered.slice(0, 30); - endOfFeed = filtered.length <= 30; + eventsInView = filtered.slice(0, publicationsToDisplay); + endOfFeed = filtered.length <= publicationsToDisplay; } else { - eventsInView = allIndexEvents.slice(0, 30); - endOfFeed = allIndexEvents.length <= 30; + eventsInView = allIndexEvents.slice(0, publicationsToDisplay); + endOfFeed = allIndexEvents.length <= publicationsToDisplay; } }, 300); @@ -354,7 +384,7 @@ let source = props.searchQuery.trim() ? filterEventsBySearch(allIndexEvents) : allIndexEvents; - eventsInView = source.slice(0, current + 30); + eventsInView = source.slice(0, current + publicationsToDisplay); endOfFeed = eventsInView.length >= source.length; loadingMore = false; } @@ -388,14 +418,50 @@ cleanup(); }); - onMount(async () => { + onMount(() => { console.debug('[PublicationFeed] onMount called'); // The effect will handle fetching when relays become available + + // Add window resize listener for responsive updates + const handleResize = () => { + if (typeof window !== 'undefined') { + const width = window.innerWidth; + let newColumnCount = 1; + if (width >= 1280) newColumnCount = 4; // xl:grid-cols-4 + else if (width >= 1024) newColumnCount = 3; // lg:grid-cols-3 + else if (width >= 768) newColumnCount = 2; // md:grid-cols-2 + + if (columnCount !== newColumnCount) { + columnCount = newColumnCount; + publicationsToDisplay = newColumnCount * 10; + + // Update the view immediately when column count changes + if (allIndexEvents.length > 0) { + const source = props.searchQuery?.trim() + ? filterEventsBySearch(allIndexEvents) + : allIndexEvents; + eventsInView = source.slice(0, publicationsToDisplay); + endOfFeed = eventsInView.length >= source.length; + } + } + } + }; + + window.addEventListener('resize', handleResize); + + // Initial calculation + handleResize(); + + // Cleanup function + return () => { + window.removeEventListener('resize', handleResize); + }; });
{#if loading && eventsInView.length === 0} From a93e528949e335d74a911f6a5e27ccdbaee12f4a Mon Sep 17 00:00:00 2001 From: silberengel Date: Tue, 5 Aug 2025 17:57:20 +0200 Subject: [PATCH 04/98] Fixed relay list and profile being empty on page refresh. --- .../publications/PublicationFeed.svelte | 11 ++++++ src/lib/components/util/Profile.svelte | 19 +++++++++- src/lib/stores/userStore.ts | 36 ++++++++++++------- src/lib/utils/nostrUtils.ts | 10 +++--- 4 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/lib/components/publications/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte index 1e6939c..7dc8b82 100644 --- a/src/lib/components/publications/PublicationFeed.svelte +++ b/src/lib/components/publications/PublicationFeed.svelte @@ -86,6 +86,17 @@ if (newRelays.length === 0) { console.debug('[PublicationFeed] No relays available, waiting...'); + // Set up a retry mechanism when relays become available + const unsubscribe = activeInboxRelays.subscribe((relays) => { + if (relays.length > 0 && !hasInitialized) { + console.debug('[PublicationFeed] Relays now available, retrying initialization'); + unsubscribe(); + setTimeout(() => { + hasInitialized = true; + initializeAndFetch(); + }, 1000); + } + }); return; } diff --git a/src/lib/components/util/Profile.svelte b/src/lib/components/util/Profile.svelte index cc5ff4a..d39c286 100644 --- a/src/lib/components/util/Profile.svelte +++ b/src/lib/components/util/Profile.svelte @@ -21,7 +21,7 @@ import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import { onMount } from "svelte"; import { getUserMetadata } from "$lib/utils/nostrUtils"; - import { activeInboxRelays } from "$lib/ndk"; + import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; let { pubkey, isNav = false } = $props<{ pubkey?: string, isNav?: boolean }>(); @@ -187,6 +187,23 @@ try { console.log("Refreshing profile for npub:", userState.npub); + // Check if we have relays available + const inboxRelays = get(activeInboxRelays); + const outboxRelays = get(activeOutboxRelays); + + if (inboxRelays.length === 0 && outboxRelays.length === 0) { + console.log("Profile: No relays available, will retry when relays become available"); + // Set up a retry mechanism when relays become available + const unsubscribe = activeInboxRelays.subscribe((relays) => { + if (relays.length > 0 && !isRefreshingProfile) { + console.log("Profile: Relays now available, retrying profile fetch"); + unsubscribe(); + setTimeout(() => refreshProfile(), 1000); + } + }); + return; + } + // Try using NDK's built-in profile fetching first const ndk = get(ndkInstance); if (ndk && userState.ndkUser) { diff --git a/src/lib/stores/userStore.ts b/src/lib/stores/userStore.ts index df73ab7..1e58f42 100644 --- a/src/lib/stores/userStore.ts +++ b/src/lib/stores/userStore.ts @@ -288,14 +288,20 @@ export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser) { */ export async function loginWithNpub(pubkeyOrNpub: string) { const ndk = get(ndkInstance); - if (!ndk) throw new Error("NDK not initialized"); - // Only clear previous login state after successful login + if (!ndk) { + throw new Error("NDK not initialized"); + } + let hexPubkey: string; - if (pubkeyOrNpub.startsWith("npub")) { + if (pubkeyOrNpub.startsWith("npub1")) { try { - hexPubkey = nip19.decode(pubkeyOrNpub).data as string; + const decoded = nip19.decode(pubkeyOrNpub); + if (decoded.type !== "npub") { + throw new Error("Invalid npub format"); + } + hexPubkey = decoded.data; } catch (e) { - console.error("Failed to decode hex pubkey from npub:", pubkeyOrNpub, e); + console.error("Failed to decode npub:", pubkeyOrNpub, e); throw e; } } else { @@ -313,6 +319,18 @@ export async function loginWithNpub(pubkeyOrNpub: string) { const user = ndk.getUser({ npub }); let profile: NostrProfile | null = null; + + // First, update relay stores to ensure we have relays available + 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); + } + + // Wait a moment for relay stores to be properly initialized + await new Promise(resolve => setTimeout(resolve, 500)); + try { profile = await getUserMetadata(npub, true); // Force fresh fetch console.log("Login with npub - fetched profile:", profile); @@ -344,14 +362,6 @@ export async function loginWithNpub(pubkeyOrNpub: string) { 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"); diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index c36108f..d3be24d 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -5,7 +5,7 @@ import { npubCache } from "./npubCache.ts"; import NDK, { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; import type { NDKKind, NostrEvent } from "@nostr-dev-kit/ndk"; import type { Filter } from "./search_types.ts"; -import { communityRelays, secondaryRelays } from "../consts.ts"; +import { communityRelays, secondaryRelays, searchRelays } from "../consts.ts"; import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts"; import { NDKRelaySet as NDKRelaySetFromNDK } from "@nostr-dev-kit/ndk"; import { sha256 } from "@noble/hashes/sha2.js"; @@ -446,15 +446,17 @@ export async function fetchEventWithFallback( // Use both inbox and outbox relays for better event discovery const inboxRelays = get(activeInboxRelays); const outboxRelays = get(activeOutboxRelays); - const allRelays = [...inboxRelays, ...outboxRelays]; + let 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 (allRelays.length === 0) { - console.warn("fetchEventWithFallback: No relays available for event fetch"); - return null; + console.warn("fetchEventWithFallback: No relays available for event fetch, using fallback relays"); + // Use fallback relays when no relays are available + allRelays = [...secondaryRelays, ...searchRelays]; + console.log("fetchEventWithFallback: Using fallback relays:", allRelays); } // Create relay set from all available relays From 74598a1d3ce442149517bb903dbc12b4a2844463 Mon Sep 17 00:00:00 2001 From: silberengel Date: Tue, 5 Aug 2025 18:11:42 +0200 Subject: [PATCH 05/98] Added a checkbox to the landing page, so that the currently logged-in user can easily find their own publications, both as "p" tag and as "pubkey". --- .../publications/PublicationFeed.svelte | 112 +++++++++++++++--- src/routes/+page.svelte | 22 +++- 2 files changed, 115 insertions(+), 19 deletions(-) diff --git a/src/lib/components/publications/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte index 7dc8b82..5405e7e 100644 --- a/src/lib/components/publications/PublicationFeed.svelte +++ b/src/lib/components/publications/PublicationFeed.svelte @@ -13,9 +13,11 @@ import { searchCache } from "$lib/utils/searchCache"; import { indexEventCache } from "$lib/utils/indexEventCache"; import { isValidNip05Address } from "$lib/utils/search_utility"; + import { userStore } from "$lib/stores/userStore.ts"; const props = $props<{ searchQuery?: string; + showOnlyMyPublications?: boolean; onEventCountUpdate?: (counts: { displayed: number; total: number }) => void; }>(); @@ -55,9 +57,16 @@ // Update the view immediately when column count changes if (allIndexEvents.length > 0) { - const source = props.searchQuery?.trim() - ? filterEventsBySearch(allIndexEvents) - : allIndexEvents; + let source = allIndexEvents; + + // Apply user filter first + source = filterEventsByUser(source); + + // Then apply search filter if query exists + if (props.searchQuery?.trim()) { + source = filterEventsBySearch(source); + } + eventsInView = source.slice(0, publicationsToDisplay); endOfFeed = eventsInView.length >= source.length; } @@ -282,6 +291,47 @@ loading = false; } + // Function to filter events by current user's pubkey + const filterEventsByUser = (events: NDKEvent[]) => { + if (!props.showOnlyMyPublications) return events; + + const currentUser = $userStore; + if (!currentUser.signedIn || !currentUser.pubkey) { + console.debug("[PublicationFeed] User not signed in or no pubkey, showing all events"); + return events; + } + + const userPubkey = currentUser.pubkey.toLowerCase(); + console.debug("[PublicationFeed] Filtering events for user:", userPubkey); + + const filtered = events.filter((event) => { + // Check if user is the author of the event + const eventPubkey = event.pubkey.toLowerCase(); + const isAuthor = eventPubkey === userPubkey; + + // Check if user is listed in "p" tags (participants/contributors) + const pTags = getMatchingTags(event, "p"); + const isInPTags = pTags.some(tag => tag[1]?.toLowerCase() === userPubkey); + + const matches = isAuthor || isInPTags; + + if (matches) { + console.debug("[PublicationFeed] Event matches user filter:", { + id: event.id, + eventPubkey, + userPubkey, + isAuthor, + isInPTags, + pTags: pTags.map(tag => tag[1]) + }); + } + return matches; + }); + + console.debug("[PublicationFeed] Events after user filtering:", filtered.length); + return filtered; + }; + // Function to filter events based on search query const filterEventsBySearch = (events: NDKEvent[]) => { if (!props.searchQuery) return events; @@ -364,21 +414,37 @@ // Debounced search function const debouncedSearch = debounceAsync(async (query: string) => { - console.debug("[PublicationFeed] Search query changed:", query); + console.debug("[PublicationFeed] Search query or user filter changed:", query); + let filtered = allIndexEvents; + + // Apply user filter first + filtered = filterEventsByUser(filtered); + + // Then apply search filter if query exists if (query && query.trim()) { - const filtered = filterEventsBySearch(allIndexEvents); - eventsInView = filtered.slice(0, publicationsToDisplay); - endOfFeed = filtered.length <= publicationsToDisplay; - } else { - eventsInView = allIndexEvents.slice(0, publicationsToDisplay); - endOfFeed = allIndexEvents.length <= publicationsToDisplay; + filtered = filterEventsBySearch(filtered); } + + eventsInView = filtered.slice(0, publicationsToDisplay); + endOfFeed = filtered.length <= publicationsToDisplay; }, 300); + // AI-NOTE: Watch for changes in search query and user filter $effect(() => { + // Trigger search when either search query or user filter changes + // Also watch for changes in user store to update filter when user logs in/out debouncedSearch(props.searchQuery); }); + // AI-NOTE: Watch for changes in the user filter checkbox + $effect(() => { + // Trigger filtering when the user filter checkbox changes + // Access both props to ensure the effect runs when either changes + const searchQuery = props.searchQuery; + const showOnlyMyPublications = props.showOnlyMyPublications; + debouncedSearch(searchQuery); + }); + // Emit event count updates $effect(() => { if (props.onEventCountUpdate) { @@ -392,9 +458,16 @@ async function loadMorePublications() { loadingMore = true; const current = eventsInView.length; - let source = props.searchQuery.trim() - ? filterEventsBySearch(allIndexEvents) - : allIndexEvents; + let source = allIndexEvents; + + // Apply user filter first + source = filterEventsByUser(source); + + // Then apply search filter if query exists + if (props.searchQuery.trim()) { + source = filterEventsBySearch(source); + } + eventsInView = source.slice(0, current + publicationsToDisplay); endOfFeed = eventsInView.length >= source.length; loadingMore = false; @@ -448,9 +521,16 @@ // Update the view immediately when column count changes if (allIndexEvents.length > 0) { - const source = props.searchQuery?.trim() - ? filterEventsBySearch(allIndexEvents) - : allIndexEvents; + let source = allIndexEvents; + + // Apply user filter first + source = filterEventsByUser(source); + + // Then apply search filter if query exists + if (props.searchQuery?.trim()) { + source = filterEventsBySearch(source); + } + eventsInView = source.slice(0, publicationsToDisplay); endOfFeed = eventsInView.length >= source.length; } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 1439a42..038047b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,8 +1,10 @@
From c47abe9cdc3007e240c58302adb1aefccc7acdc8 Mon Sep 17 00:00:00 2001 From: silberengel Date: Tue, 5 Aug 2025 22:41:11 +0200 Subject: [PATCH 07/98] Fixed author display on the 30040 events results --- src/lib/components/EventDetails.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index 4bd78e4..688bacd 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -405,7 +405,7 @@ Author: {@render userBadge( toNpub(event.pubkey) as string, - profile?.display_name || event.pubkey, + profile?.display_name || undefined, )} {:else} From 078cf05cc1ead2536d7ab1ba15581f8b56c44188 Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 06:34:28 +0200 Subject: [PATCH 08/98] comment viewer added --- src/lib/components/CommentViewer.svelte | 316 ++++++++++++++++++++++++ src/routes/events/+page.svelte | 3 + 2 files changed, 319 insertions(+) create mode 100644 src/lib/components/CommentViewer.svelte diff --git a/src/lib/components/CommentViewer.svelte b/src/lib/components/CommentViewer.svelte new file mode 100644 index 0000000..b240aad --- /dev/null +++ b/src/lib/components/CommentViewer.svelte @@ -0,0 +1,316 @@ + + +
+ + Comments ({threadedComments.length}) + + + {#if loading} +
+

Loading comments...

+
+ {:else if error} +
+

{error}

+
+ {:else if threadedComments.length === 0} +
+

No comments yet. Be the first to comment!

+
+ {:else} +
+ {#each threadedComments as node (node.event.id)} +
+
+
+
+ + {getAuthorName(node.event.pubkey)} + + + {formatDate(node.event.created_at || 0)} Kind: {node.event.kind} + +
+
+ + {shortenNevent(getNeventUrl(node.event))} + + +
+
+ +
+ {@html node.event.content || ""} +
+
+ + {#if node.children.length > 0} + {#each node.children as childNode (childNode.event.id)} +
+
+
+
+ + {getAuthorName(childNode.event.pubkey)} + + + {formatDate(childNode.event.created_at || 0)} Kind: {childNode.event.kind} + +
+
+ + {shortenNevent(getNeventUrl(childNode.event))} + + +
+
+ +
+ {@html childNode.event.content || ""} +
+
+
+ {/each} + {/if} +
+ {/each} +
+ {/if} +
\ No newline at end of file diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index 15c469c..65e0c43 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -8,6 +8,7 @@ import EventDetails from "$lib/components/EventDetails.svelte"; import RelayActions from "$lib/components/RelayActions.svelte"; import CommentBox from "$lib/components/CommentBox.svelte"; +import CommentViewer from "$lib/components/CommentViewer.svelte"; import { userStore } from "$lib/stores/userStore"; import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils"; @@ -810,6 +811,8 @@ + + {#if isLoggedIn && userPubkey}
Add Comment From 976771fc655b74b8d23c2e6d84d494cf7c29e61b Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 07:12:42 +0200 Subject: [PATCH 09/98] working kind 1 thread --- src/lib/components/CommentViewer.svelte | 375 +++++++++++++++--------- 1 file changed, 229 insertions(+), 146 deletions(-) diff --git a/src/lib/components/CommentViewer.svelte b/src/lib/components/CommentViewer.svelte index b240aad..078341c 100644 --- a/src/lib/components/CommentViewer.svelte +++ b/src/lib/components/CommentViewer.svelte @@ -1,96 +1,129 @@ + +{#snippet CommentItem(node: CommentNode)} +
+
+
+
+ {#if getAuthorPicture(node.event.pubkey)} + {getAuthorName(node.event.pubkey)} (e.target as HTMLImageElement).style.display = 'none'} + /> + {:else} +
+ + {getAuthorName(node.event.pubkey).charAt(0).toUpperCase()} + +
+ {/if} +
+ + {getAuthorName(node.event.pubkey)} + + + {formatRelativeDate(node.event.created_at || 0)} • Kind: {node.event.kind} + +
+
+
+ + {shortenNevent(getNeventUrl(node.event))} + + +
+
+ +
+ {#await parseContent(node.event.content || "") then parsedContent} + {@html parsedContent} + {:catch} + {@html node.event.content || ""} + {/await} +
+
+ + {#if node.children.length > 0} +
+ {#each node.children as childNode (childNode.event.id)} + {@render CommentItem(childNode)} + {/each} +
+ {/if} +
+{/snippet} +
Comments ({threadedComments.length}) @@ -239,77 +392,7 @@ {:else}
{#each threadedComments as node (node.event.id)} -
-
-
-
- - {getAuthorName(node.event.pubkey)} - - - {formatDate(node.event.created_at || 0)} Kind: {node.event.kind} - -
-
- - {shortenNevent(getNeventUrl(node.event))} - - -
-
- -
- {@html node.event.content || ""} -
-
- - {#if node.children.length > 0} - {#each node.children as childNode (childNode.event.id)} -
-
-
-
- - {getAuthorName(childNode.event.pubkey)} - - - {formatDate(childNode.event.created_at || 0)} Kind: {childNode.event.kind} - -
-
- - {shortenNevent(getNeventUrl(childNode.event))} - - -
-
- -
- {@html childNode.event.content || ""} -
-
-
- {/each} - {/if} -
+ {@render CommentItem(node)} {/each}
{/if} From 550ae84a9f81db09c82624dd6f99f90cb6910c59 Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 07:17:22 +0200 Subject: [PATCH 10/98] fixed commentbox userbadge display --- src/lib/components/CommentBox.svelte | 31 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/lib/components/CommentBox.svelte b/src/lib/components/CommentBox.svelte index fbff0f3..6952279 100644 --- a/src/lib/components/CommentBox.svelte +++ b/src/lib/components/CommentBox.svelte @@ -10,6 +10,7 @@ } from "$lib/utils/search_utility"; import { userPubkey } from "$lib/stores/authStore.Svelte"; + import { userStore } from "$lib/stores/userStore"; import type { NDKEvent } from "$lib/utils/nostrUtils"; import { extractRootEventInfo, @@ -67,17 +68,12 @@ } }); + // Get user profile from userStore $effect(() => { - const trimmedPubkey = $userPubkey?.trim(); - const npub = toNpub(trimmedPubkey); - if (npub) { - // Call an async function, but don't make the effect itself async - getUserMetadata(npub).then((metadata) => { - userProfile = metadata; - }); - } else if (trimmedPubkey) { - userProfile = null; - error = "Invalid public key: must be a 64-character hex string."; + const currentUser = $userStore; + if (currentUser?.signedIn && currentUser.profile) { + userProfile = currentUser.profile; + error = null; } else { userProfile = null; error = null; @@ -590,17 +586,20 @@ {userProfile.name { - const img = e.target as HTMLImageElement; - img.src = `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(img.alt)}`; - }} + class="w-8 h-8 rounded-full object-cover" + onerror={(e) => (e.target as HTMLImageElement).style.display = 'none'} /> + {:else} +
+ + {(userProfile.displayName || userProfile.name || "U").charAt(0).toUpperCase()} + +
{/if} {userProfile.displayName || userProfile.name || - nip19.npubEncode($userPubkey || "").slice(0, 8) + "..."} + `${$userPubkey?.slice(0, 8)}...${$userPubkey?.slice(-4)}`}
{/if} From 939759e5ceea3b4f21b2966ac9f60130b79054d5 Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 07:41:23 +0200 Subject: [PATCH 11/98] made the cache more persistent --- src/lib/utils/npubCache.ts | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/lib/utils/npubCache.ts b/src/lib/utils/npubCache.ts index 4fc4405..8c1c36f 100644 --- a/src/lib/utils/npubCache.ts +++ b/src/lib/utils/npubCache.ts @@ -4,6 +4,47 @@ export type NpubMetadata = NostrProfile; class NpubCache { private cache: Record = {}; + private readonly storageKey = 'alexandria_npub_cache'; + private readonly maxAge = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + + constructor() { + this.loadFromStorage(); + } + + private loadFromStorage(): void { + try { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem(this.storageKey); + if (stored) { + const data = JSON.parse(stored) as Record; + const now = Date.now(); + + // Filter out expired entries + for (const [key, entry] of Object.entries(data)) { + if (entry.timestamp && (now - entry.timestamp) < this.maxAge) { + this.cache[key] = entry.profile; + } + } + } + } + } catch (error) { + console.warn('Failed to load npub cache from storage:', error); + } + } + + private saveToStorage(): void { + try { + if (typeof window !== 'undefined') { + const data: Record = {}; + for (const [key, profile] of Object.entries(this.cache)) { + data[key] = { profile, timestamp: Date.now() }; + } + localStorage.setItem(this.storageKey, JSON.stringify(data)); + } + } catch (error) { + console.warn('Failed to save npub cache to storage:', error); + } + } get(key: string): NpubMetadata | undefined { return this.cache[key]; @@ -11,6 +52,7 @@ class NpubCache { set(key: string, value: NpubMetadata): void { this.cache[key] = value; + this.saveToStorage(); } has(key: string): boolean { @@ -20,6 +62,7 @@ class NpubCache { delete(key: string): boolean { if (key in this.cache) { delete this.cache[key]; + this.saveToStorage(); return true; } return false; @@ -37,6 +80,7 @@ class NpubCache { clear(): void { this.cache = {}; + this.saveToStorage(); } size(): number { From 485601a67b1ac08aa5c20405209c60f447e168ea Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 07:41:58 +0200 Subject: [PATCH 12/98] fixed image display for markup content fields --- src/lib/utils/markup/basicMarkupParser.ts | 45 ++++++++++++++--------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/lib/utils/markup/basicMarkupParser.ts b/src/lib/utils/markup/basicMarkupParser.ts index fd7fd14..2d70c41 100644 --- a/src/lib/utils/markup/basicMarkupParser.ts +++ b/src/lib/utils/markup/basicMarkupParser.ts @@ -78,20 +78,7 @@ function replaceAlexandriaNostrLinks(text: string): string { return `nostr:${bech32Match[0]}`; } } - // For non-Alexandria/localhost URLs, append (View here: nostr:) if a Nostr identifier is present - const hexMatch = url.match(hexPattern); - if (hexMatch) { - try { - const nevent = nip19.neventEncode({ id: hexMatch[0] }); - return `${url} (View here: nostr:${nevent})`; - } catch { - return url; - } - } - const bech32Match = url.match(bech32Pattern); - if (bech32Match) { - return `${url} (View here: nostr:${bech32Match[0]})`; - } + // For non-Alexandria/localhost URLs, just return the URL as-is return url; }); @@ -253,7 +240,18 @@ function processBasicFormatting(content: string): string { } // Only render if the url ends with a direct image extension if (IMAGE_EXTENSIONS.test(url.split("?")[0])) { - return `${alt}`; + return `
+
+
+
🖼️
+
Image
+
+
+ ${alt} + +
`; } // Otherwise, render as a clickable link return `${alt || url}`; @@ -290,7 +288,18 @@ function processBasicFormatting(content: string): string { } // Only render if the url ends with a direct image extension if (IMAGE_EXTENSIONS.test(clean.split("?")[0])) { - return `Embedded media`; + return `
+
+
+
🖼️
+
Image
+
+
+ Embedded media + +
`; } // Otherwise, render as a clickable link return `${clean}`; @@ -310,10 +319,10 @@ function processBasicFormatting(content: string): string { }, ); - // Process hashtags + // Process hashtags as clickable buttons processedText = processedText.replace( HASHTAG_REGEX, - '#$1', + '', ); // --- Improved List Grouping and Parsing --- From 577d8c832a7b33c9a7100585abc009db5d69b876 Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 08:31:46 +0200 Subject: [PATCH 13/98] fix relays and subscription search --- src/lib/components/CommentViewer.svelte | 46 ++--- src/lib/ndk.ts | 88 +++++++++- src/lib/utils/subscription_search.ts | 217 ++++++++++++++++++++---- src/routes/+layout.svelte | 24 ++- src/routes/events/+page.svelte | 27 ++- 5 files changed, 333 insertions(+), 69 deletions(-) diff --git a/src/lib/components/CommentViewer.svelte b/src/lib/components/CommentViewer.svelte index 078341c..20ace2c 100644 --- a/src/lib/components/CommentViewer.svelte +++ b/src/lib/components/CommentViewer.svelte @@ -294,12 +294,6 @@ let parsedContent = await parseBasicmarkup(content); - // Make images blurry until clicked - parsedContent = parsedContent.replace( - /]+)>/g, - '' - ); - return parsedContent; } @@ -313,24 +307,32 @@ >
- {#if getAuthorPicture(node.event.pubkey)} - {getAuthorName(node.event.pubkey)} (e.target as HTMLImageElement).style.display = 'none'} - /> - {:else} -
- - {getAuthorName(node.event.pubkey).charAt(0).toUpperCase()} - -
- {/if} +
- + ([]); export const activeInboxRelays = writable([]); export const activeOutboxRelays = writable([]); +// AI-NOTE: 2025-01-08 - Persistent relay storage to avoid recalculation +let persistentRelaySet: { inboxRelays: string[]; outboxRelays: string[] } | null = null; +let relaySetLastUpdated: number = 0; +const RELAY_SET_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes +const RELAY_SET_STORAGE_KEY = 'alexandria/relay_set_cache'; + +/** + * Load persistent relay set from localStorage + */ +function loadPersistentRelaySet(): { relaySet: { inboxRelays: string[]; outboxRelays: string[] } | null; lastUpdated: number } { + try { + const stored = localStorage.getItem(RELAY_SET_STORAGE_KEY); + if (!stored) return { relaySet: null, lastUpdated: 0 }; + + const data = JSON.parse(stored); + const now = Date.now(); + + // Check if cache is expired + if (now - data.timestamp > RELAY_SET_CACHE_DURATION) { + localStorage.removeItem(RELAY_SET_STORAGE_KEY); + return { relaySet: null, lastUpdated: 0 }; + } + + return { relaySet: data.relaySet, lastUpdated: data.timestamp }; + } catch (error) { + console.warn('[NDK.ts] Failed to load persistent relay set:', error); + localStorage.removeItem(RELAY_SET_STORAGE_KEY); + return { relaySet: null, lastUpdated: 0 }; + } +} + +/** + * Save persistent relay set to localStorage + */ +function savePersistentRelaySet(relaySet: { inboxRelays: string[]; outboxRelays: string[] }): void { + try { + const data = { + relaySet, + timestamp: Date.now() + }; + localStorage.setItem(RELAY_SET_STORAGE_KEY, JSON.stringify(data)); + } catch (error) { + console.warn('[NDK.ts] Failed to save persistent relay set:', error); + } +} + +/** + * Clear persistent relay set from localStorage + */ +function clearPersistentRelaySet(): void { + try { + localStorage.removeItem(RELAY_SET_STORAGE_KEY); + } catch (error) { + console.warn('[NDK.ts] Failed to clear persistent relay set:', error); + } +} + // Subscribe to userStore changes and update ndkSignedIn accordingly userStore.subscribe((userState) => { ndkSignedIn.set(userState.signedIn); @@ -351,15 +408,39 @@ export async function getActiveRelaySet(ndk: NDK): Promise<{ inboxRelays: string /** * Updates the active relay stores and NDK pool with new relay URLs * @param ndk NDK instance + * @param forceUpdate Force update even if cached (default: false) */ -export async function updateActiveRelayStores(ndk: NDK): Promise { +export async function updateActiveRelayStores(ndk: NDK, forceUpdate: boolean = false): Promise { try { + // AI-NOTE: 2025-01-08 - Use persistent relay set to avoid recalculation + const now = Date.now(); + const cacheExpired = now - relaySetLastUpdated > RELAY_SET_CACHE_DURATION; + + // Load from persistent storage if not already loaded + if (!persistentRelaySet) { + const loaded = loadPersistentRelaySet(); + persistentRelaySet = loaded.relaySet; + relaySetLastUpdated = loaded.lastUpdated; + } + + if (!forceUpdate && persistentRelaySet && !cacheExpired) { + console.debug('[NDK.ts] updateActiveRelayStores: Using cached relay set'); + activeInboxRelays.set(persistentRelaySet.inboxRelays); + activeOutboxRelays.set(persistentRelaySet.outboxRelays); + return; + } + 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); + // Cache the relay set + persistentRelaySet = relaySet; + relaySetLastUpdated = now; + savePersistentRelaySet(relaySet); // Save to persistent storage + // Update the stores with the new relay configuration activeInboxRelays.set(relaySet.inboxRelays); activeOutboxRelays.set(relaySet.outboxRelays); @@ -560,6 +641,11 @@ export function logout(user: NDKUser): void { activeInboxRelays.set([]); activeOutboxRelays.set([]); + // AI-NOTE: 2025-01-08 - Clear persistent relay set on logout + persistentRelaySet = null; + relaySetLastUpdated = 0; + clearPersistentRelaySet(); // Clear persistent storage + // Stop network monitoring stopNetworkStatusMonitoring(); diff --git a/src/lib/utils/subscription_search.ts b/src/lib/utils/subscription_search.ts index 17fa093..d992f5b 100644 --- a/src/lib/utils/subscription_search.ts +++ b/src/lib/utils/subscription_search.ts @@ -26,6 +26,17 @@ const normalizeUrl = (url: string): string => { return url.replace(/\/$/, ''); // Remove trailing slash }; +/** + * Filter out unwanted events from search results + * @param events Array of NDKEvent to filter + * @returns Filtered array of NDKEvent + */ +function filterUnwantedEvents(events: NDKEvent[]): NDKEvent[] { + return events.filter( + (event) => !isEmojiReaction(event) && event.kind !== 3 && event.kind !== 5, + ); +} + /** * Search for events by subscription type (d, t, n) */ @@ -35,6 +46,7 @@ export async function searchBySubscription( callbacks?: SearchCallbacks, abortSignal?: AbortSignal, ): Promise { + const startTime = Date.now(); // AI-NOTE: 2025-01-08 - Track search performance const normalizedSearchTerm = searchTerm.toLowerCase().trim(); console.log("subscription_search: Starting search:", { @@ -47,7 +59,22 @@ export async function searchBySubscription( const cachedResult = searchCache.get(searchType, normalizedSearchTerm); if (cachedResult) { console.log("subscription_search: Found cached result:", cachedResult); - return cachedResult; + // AI-NOTE: 2025-01-08 - For profile searches, clear cache if it's empty to force fresh search + if (searchType === "n" && cachedResult.events.length === 0) { + console.log("subscription_search: Clearing empty cached profile result to force fresh search"); + searchCache.clear(); // Clear all cache to force fresh search + } else if (searchType === "n" && cachedResult.events.length > 0 && cachedResult.secondOrder.length === 0) { + // AI-NOTE: 2025-01-08 - Clear cache if we have profile results but no second-order events + // This forces a fresh search that includes second-order searching + console.log("subscription_search: Clearing cached profile result with no second-order events to force fresh search"); + searchCache.clear(); + } else if (searchType === "n") { + // AI-NOTE: 2025-01-08 - For profile searches, always clear cache to ensure fresh second-order search + console.log("subscription_search: Clearing cache for profile search to ensure fresh second-order search"); + searchCache.clear(); + } else { + return cachedResult; + } } const ndk = get(ndkInstance); @@ -64,7 +91,7 @@ export async function searchBySubscription( searchState.timeoutId = setTimeout(() => { console.log("subscription_search: Search timeout reached"); cleanup(); - }, TIMEOUTS.SUBSCRIPTION_SEARCH); + }, searchType === "n" ? 5000 : TIMEOUTS.SUBSCRIPTION_SEARCH); // AI-NOTE: 2025-01-08 - Shorter timeout for profile searches // Check for abort signal if (abortSignal?.aborted) { @@ -125,7 +152,26 @@ export async function searchBySubscription( ); searchCache.set(searchType, normalizedSearchTerm, immediateResult); - // Start Phase 2 in background for additional results + // AI-NOTE: 2025-01-08 - For profile searches, return immediately when found + // but still start background search for second-order results + if (searchType === "n") { + console.log("subscription_search: Profile found, returning immediately but starting background second-order search"); + + // Start Phase 2 in background for second-order results + searchOtherRelaysInBackground( + searchType, + searchFilter, + searchState, + callbacks, + cleanup, + ); + + const elapsed = Date.now() - startTime; + console.log(`subscription_search: Profile search completed in ${elapsed}ms`); + return immediateResult; + } + + // Start Phase 2 in background for additional results (only for non-profile searches) searchOtherRelaysInBackground( searchType, searchFilter, @@ -137,8 +183,72 @@ export async function searchBySubscription( return immediateResult; } else { console.log( - "subscription_search: No results from primary relay, continuing to Phase 2", + "subscription_search: No results from primary relay", ); + + // AI-NOTE: 2025-01-08 - For profile searches, if no results found in search relays, + // try all relays as fallback + if (searchType === "n") { + console.log( + "subscription_search: No profile found in search relays, trying all relays", + ); + // Try with all relays as fallback + const allRelaySet = new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())) as any, ndk); + try { + const fallbackEvents = await ndk.fetchEvents( + searchFilter.filter, + { closeOnEose: true }, + allRelaySet, + ); + + console.log( + "subscription_search: Fallback search returned", + fallbackEvents.size, + "events", + ); + + processPrimaryRelayResults( + fallbackEvents, + searchType, + searchFilter.subscriptionType, + normalizedSearchTerm, + searchState, + abortSignal, + cleanup, + ); + + if (hasResults(searchState, searchType)) { + console.log( + "subscription_search: Found profile in fallback search, returning immediately", + ); + const fallbackResult = createSearchResult( + searchState, + searchType, + normalizedSearchTerm, + ); + searchCache.set(searchType, normalizedSearchTerm, fallbackResult); + const elapsed = Date.now() - startTime; + console.log(`subscription_search: Profile search completed in ${elapsed}ms (fallback)`); + return fallbackResult; + } + } catch (fallbackError) { + console.error("subscription_search: Fallback search failed:", fallbackError); + } + + console.log( + "subscription_search: Profile not found in any relays, returning empty result", + ); + const emptyResult = createEmptySearchResult(searchType, normalizedSearchTerm); + // AI-NOTE: 2025-01-08 - Don't cache empty profile results as they may be due to search issues + // rather than the profile not existing + const elapsed = Date.now() - startTime; + console.log(`subscription_search: Profile search completed in ${elapsed}ms (not found)`); + return emptyResult; + } else { + console.log( + "subscription_search: No results from primary relay, continuing to Phase 2", + ); + } } } catch (error) { console.error( @@ -153,13 +263,21 @@ export async function searchBySubscription( } // Always do Phase 2: Search all other relays in parallel - return searchOtherRelaysInBackground( + const result = await searchOtherRelaysInBackground( searchType, searchFilter, searchState, callbacks, cleanup, ); + + // AI-NOTE: 2025-01-08 - Log performance for non-profile searches + if (searchType !== "n") { + const elapsed = Date.now() - startTime; + console.log(`subscription_search: ${searchType} search completed in ${elapsed}ms`); + } + + return result; } /** @@ -253,7 +371,7 @@ async function createProfileSearchFilter( filter: { kinds: [0], authors: [decoded.data], - limit: SEARCH_LIMITS.SPECIFIC_PROFILE, + limit: 1, // AI-NOTE: 2025-01-08 - Only need 1 result for specific npub search }, subscriptionType: "npub-specific", }; @@ -273,7 +391,7 @@ async function createProfileSearchFilter( filter: { kinds: [0], authors: [npub], - limit: SEARCH_LIMITS.SPECIFIC_PROFILE, + limit: 1, // AI-NOTE: 2025-01-08 - Only need 1 result for specific npub search }, subscriptionType: "nip05-found", }; @@ -299,31 +417,38 @@ 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 search relays first - const profileRelaySet = poolRelays.filter( + // AI-NOTE: 2025-01-08 - For profile searches, prioritize search relays for speed + // Use search relays first, then fall back to all relays if needed + const searchRelaySet = poolRelays.filter( (relay: any) => 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); + + if (searchRelaySet.length > 0) { + console.debug('subscription_search: Profile search - using search relays for speed:', searchRelaySet.map((r: any) => r.url)); + return new NDKRelaySet(new Set(searchRelaySet) as any, ndk); + } else { + // Fallback to all relays if search relays not available + console.debug('subscription_search: Profile search - fallback to all relays:', poolRelays.map((r: any) => r.url)); + return new NDKRelaySet(new Set(poolRelays) as any, ndk); + } } else { // For other searches, use active relays first + const searchRelays = [...get(activeInboxRelays), ...get(activeOutboxRelays)]; + console.debug('subscription_search: Active relay stores:', { + inboxRelays: get(activeInboxRelays), + outboxRelays: get(activeOutboxRelays), + searchRelays + }); + const activeRelaySet = poolRelays.filter( (relay: any) => searchRelays.some( @@ -534,11 +659,9 @@ function searchOtherRelaysInBackground( new Set( Array.from(ndk.pool.relays.values()).filter((relay: any) => { if (searchType === "n") { - // For profile searches, exclude search relays from fallback search - return !searchRelays.some( - (searchRelay: string) => - normalizeUrl(relay.url) === normalizeUrl(searchRelay), - ); + // AI-NOTE: 2025-01-08 - For profile searches, use ALL available relays + // Don't exclude any relays since we want maximum coverage + return true; } else { // For other searches, exclude community relays from fallback search return !communityRelays.some( @@ -652,6 +775,7 @@ function processProfileEoseResults( ) { const targetPubkey = dedupedProfiles[0]?.pubkey; if (targetPubkey) { + console.log("subscription_search: Triggering second-order search for npub-specific profile:", targetPubkey); performSecondOrderSearchInBackground( "n", dedupedProfiles, @@ -660,11 +784,14 @@ function processProfileEoseResults( targetPubkey, callbacks, ); + } else { + console.log("subscription_search: No targetPubkey found for second-order search"); } } else if (searchFilter.subscriptionType === "profile") { // For general profile searches, perform second-order search for each found profile for (const profile of dedupedProfiles) { if (profile.pubkey) { + console.log("subscription_search: Triggering second-order search for general profile:", profile.pubkey); performSecondOrderSearchInBackground( "n", dedupedProfiles, @@ -675,6 +802,8 @@ function processProfileEoseResults( ); } } + } else { + console.log("subscription_search: No second-order search triggered for subscription type:", searchFilter.subscriptionType); } return { @@ -784,6 +913,7 @@ async function performSecondOrderSearchInBackground( callbacks?: SearchCallbacks, ) { try { + console.log("subscription_search: Starting second-order search for", searchType, "with targetPubkey:", targetPubkey); const ndk = get(ndkInstance); let allSecondOrderEvents: NDKEvent[] = []; @@ -797,6 +927,8 @@ async function performSecondOrderSearchInBackground( const searchPromise = (async () => { if (searchType === "n" && targetPubkey) { + console.log("subscription_search: Searching for events mentioning pubkey:", targetPubkey); + // Search for events that mention this pubkey via p-tags const pTagFilter = { "#p": [targetPubkey] }; const pTagEvents = await ndk.fetchEvents( @@ -804,11 +936,25 @@ async function performSecondOrderSearchInBackground( { closeOnEose: true }, new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())), ndk), ); - // Filter out emoji reactions - const filteredEvents = Array.from(pTagEvents).filter( - (event) => !isEmojiReaction(event), + console.log("subscription_search: Found", pTagEvents.size, "events with p-tag for", targetPubkey); + + // AI-NOTE: 2025-01-08 - Also search for events written by this pubkey + const authorFilter = { authors: [targetPubkey] }; + const authorEvents = await ndk.fetchEvents( + authorFilter, + { closeOnEose: true }, + new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())), ndk), ); - allSecondOrderEvents = [...allSecondOrderEvents, ...filteredEvents]; + console.log("subscription_search: Found", authorEvents.size, "events written by", targetPubkey); + + // Filter out unwanted events from both sets + const filteredPTagEvents = filterUnwantedEvents(Array.from(pTagEvents)); + const filteredAuthorEvents = filterUnwantedEvents(Array.from(authorEvents)); + + console.log("subscription_search: After filtering unwanted events:", filteredPTagEvents.length, "p-tag events,", filteredAuthorEvents.length, "author events"); + + // Combine both sets of events + allSecondOrderEvents = [...filteredPTagEvents, ...filteredAuthorEvents]; } else if (searchType === "d") { // Parallel fetch for #e and #a tag events const relaySet = new NDKRelaySet( @@ -831,13 +977,9 @@ async function performSecondOrderSearchInBackground( ) : Promise.resolve([]), ]); - // Filter out emoji reactions - const filteredETagEvents = Array.from(eTagEvents).filter( - (event) => !isEmojiReaction(event), - ); - const filteredATagEvents = Array.from(aTagEvents).filter( - (event) => !isEmojiReaction(event), - ); + // Filter out unwanted events + const filteredETagEvents = filterUnwantedEvents(Array.from(eTagEvents)); + const filteredATagEvents = filterUnwantedEvents(Array.from(aTagEvents)); allSecondOrderEvents = [ ...allSecondOrderEvents, ...filteredETagEvents, @@ -866,6 +1008,8 @@ async function performSecondOrderSearchInBackground( .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) .slice(0, SEARCH_LIMITS.SECOND_ORDER_RESULTS); + console.log("subscription_search: Second-order search completed with", sortedSecondOrder.length, "results"); + // Update the search results with second-order events const result: SearchResult = { events: firstOrderEvents, @@ -882,7 +1026,10 @@ async function performSecondOrderSearchInBackground( // Notify UI of updated results if (callbacks?.onSecondOrderUpdate) { + console.log("subscription_search: Calling onSecondOrderUpdate callback with", sortedSecondOrder.length, "second-order events"); callbacks.onSecondOrderUpdate(result); + } else { + console.log("subscription_search: No onSecondOrderUpdate callback available"); } })(); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 2fff8a9..1ae83af 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -5,7 +5,10 @@ import { page } from "$app/stores"; import { Alert } from "flowbite-svelte"; import { HammerSolid } from "flowbite-svelte-icons"; - import { logCurrentRelayConfiguration } from "$lib/ndk"; + import { logCurrentRelayConfiguration, activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; + + // Define children prop for Svelte 5 + let { children } = $props(); // Get standard metadata for OpenGraph tags let title = "Library of Alexandria"; @@ -16,12 +19,23 @@ let summary = "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages."; + // Reactive effect to log relay configuration when stores change + $effect(() => { + const inboxRelays = $activeInboxRelays; + const outboxRelays = $activeOutboxRelays; + + // Only log if we have relays (not empty arrays) + if (inboxRelays.length > 0 || outboxRelays.length > 0) { + console.log('🔌 Relay Configuration Updated:'); + console.log('📥 Inbox Relays:', inboxRelays); + console.log('📤 Outbox Relays:', outboxRelays); + console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`); + } + }); + onMount(() => { const rect = document.body.getBoundingClientRect(); // document.body.style.height = `${rect.height}px`; - - // Log relay configuration when layout mounts - logCurrentRelayConfiguration(); }); @@ -47,5 +61,5 @@
- + {@render children()}
diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index 65e0c43..b06ea4e 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -151,21 +151,27 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; searchInProgress = loading || (results.length > 0 && secondOrder.length === 0); - // Show second-order search message when we have first-order results but no second-order yet + // AI-NOTE: 2025-01-08 - Only show second-order search message if we're actually searching + // Don't show it for cached results that have no second-order events if ( results.length > 0 && secondOrder.length === 0 && - searchTypeParam === "n" + searchTypeParam === "n" && + !loading // Only show message if we're actively searching, not for cached results ) { secondOrderSearchMessage = `Found ${results.length} profile(s). Starting second-order search for events mentioning these profiles...`; } else if ( results.length > 0 && secondOrder.length === 0 && - searchTypeParam === "d" + searchTypeParam === "d" && + !loading // Only show message if we're actively searching, not for cached results ) { secondOrderSearchMessage = `Found ${results.length} event(s). Starting second-order search for events referencing these events...`; } else if (secondOrder.length > 0) { secondOrderSearchMessage = null; + } else { + // Clear message if we have results but no second-order search is happening + secondOrderSearchMessage = null; } // Check community status for all search results @@ -347,9 +353,18 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; - // Log relay configuration when page mounts - onMount(() => { - logCurrentRelayConfiguration(); + // Reactive effect to log relay configuration when stores change + $effect(() => { + const inboxRelays = $activeInboxRelays; + const outboxRelays = $activeOutboxRelays; + + // Only log if we have relays (not empty arrays) + if (inboxRelays.length > 0 || outboxRelays.length > 0) { + console.log('🔌 Events Page - Relay Configuration Updated:'); + console.log('📥 Inbox Relays:', inboxRelays); + console.log('📤 Outbox Relays:', outboxRelays); + console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`); + } }); From 3eafdf0282f7a7bcf0380b45dce4bb43268f4d8e Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 08:51:49 +0200 Subject: [PATCH 14/98] fixed profile display --- src/lib/components/CommentViewer.svelte | 134 ++++++- src/lib/components/EventDetails.svelte | 28 +- src/routes/events/+page.svelte | 491 +++++++++++++++--------- 3 files changed, 457 insertions(+), 196 deletions(-) diff --git a/src/lib/components/CommentViewer.svelte b/src/lib/components/CommentViewer.svelte index 20ace2c..fb819e1 100644 --- a/src/lib/components/CommentViewer.svelte +++ b/src/lib/components/CommentViewer.svelte @@ -36,12 +36,24 @@ const npub = toNpub(pubkey); if (!npub) return; - const profile = await getUserMetadata(npub); + // Force fetch to ensure we get the latest profile data + const profile = await getUserMetadata(npub, true); const newProfiles = new Map(profiles); newProfiles.set(pubkey, profile); profiles = newProfiles; + + console.log(`[CommentViewer] Fetched profile for ${pubkey}:`, profile); } catch (err) { console.warn(`Failed to fetch profile for ${pubkey}:`, err); + // Set a fallback profile to avoid repeated failed requests + const fallbackProfile = { + name: `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`, + displayName: `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`, + picture: null + }; + const newProfiles = new Map(profiles); + newProfiles.set(pubkey, fallbackProfile); + profiles = newProfiles; } } @@ -54,6 +66,9 @@ comments = []; console.log(`[CommentViewer] Fetching comments for event: ${event.id}`); + console.log(`[CommentViewer] Event kind: ${event.kind}`); + console.log(`[CommentViewer] Event pubkey: ${event.pubkey}`); + console.log(`[CommentViewer] Available relays: ${$activeInboxRelays.length}`); // Wait for relays to be available let attempts = 0; @@ -69,12 +84,35 @@ } try { - activeSub = $ndkInstance.subscribe({ - kinds: [1, 1111], - "#e": [event.id], - }); + // Try multiple filter approaches to find comments + const filters = [ + // Standard comment filter + { + kinds: [1, 1111], + "#e": [event.id], + }, + // Broader search for any events that might reference this event + { + kinds: [1, 1111], + "#e": [event.id], + limit: 100, + }, + // Search for events by the same author that might be replies + { + kinds: [1, 1111], + authors: [event.pubkey], + since: event.created_at ? event.created_at - 86400 : undefined, // Last 24 hours + limit: 50, + } + ]; + + console.log(`[CommentViewer] Setting up subscription with filters:`, filters); + + // Try the first filter (standard comment search) + activeSub = $ndkInstance.subscribe(filters[0]); const timeout = setTimeout(() => { + console.log(`[CommentViewer] Subscription timeout - no comments found`); if (activeSub) { activeSub.stop(); activeSub = null; @@ -84,8 +122,21 @@ activeSub.on("event", (commentEvent: NDKEvent) => { console.log(`[CommentViewer] Received comment: ${commentEvent.id}`); - comments = [...comments, commentEvent]; - fetchProfile(commentEvent.pubkey); + console.log(`[CommentViewer] Comment kind: ${commentEvent.kind}`); + console.log(`[CommentViewer] Comment pubkey: ${commentEvent.pubkey}`); + console.log(`[CommentViewer] Comment content preview: ${commentEvent.content?.slice(0, 100)}...`); + + // Check if this event actually references our target event + const eTags = commentEvent.getMatchingTags("e"); + const referencesTarget = eTags.some(tag => tag[1] === event.id); + + if (referencesTarget) { + console.log(`[CommentViewer] Comment references target event - adding to comments`); + comments = [...comments, commentEvent]; + fetchProfile(commentEvent.pubkey); + } else { + console.log(`[CommentViewer] Comment does not reference target event - skipping`); + } }); activeSub.on("eose", () => { @@ -96,6 +147,14 @@ activeSub = null; } loading = false; + + // Pre-fetch all profiles after comments are loaded + preFetchAllProfiles(); + + // AI-NOTE: 2025-01-24 - Test for comments if none were found + if (comments.length === 0) { + testForComments(); + } }); activeSub.on("error", (err: any) => { @@ -116,6 +175,60 @@ } } + // Pre-fetch all profiles for comments + async function preFetchAllProfiles() { + const uniquePubkeys = new Set(); + comments.forEach(comment => { + if (comment.pubkey && !profiles.has(comment.pubkey)) { + uniquePubkeys.add(comment.pubkey); + } + }); + + console.log(`[CommentViewer] Pre-fetching ${uniquePubkeys.size} profiles`); + + // Fetch profiles in parallel + const profilePromises = Array.from(uniquePubkeys).map(pubkey => fetchProfile(pubkey)); + await Promise.allSettled(profilePromises); + + console.log(`[CommentViewer] Pre-fetching complete`); + } + + // AI-NOTE: 2025-01-24 - Function to manually test for comments + async function testForComments() { + if (!event?.id) return; + + console.log(`[CommentViewer] Testing for comments on event: ${event.id}`); + + try { + // Try a broader search to see if there are any events that might be comments + const testSub = $ndkInstance.subscribe({ + kinds: [1, 1111], + "#e": [event.id], + limit: 10, + }); + + let testComments = 0; + + testSub.on("event", (testEvent: NDKEvent) => { + testComments++; + console.log(`[CommentViewer] Test found event: ${testEvent.id}, kind: ${testEvent.kind}`); + }); + + testSub.on("eose", () => { + console.log(`[CommentViewer] Test search found ${testComments} potential comments`); + testSub.stop(); + }); + + // Stop the test after 5 seconds + setTimeout(() => { + testSub.stop(); + }, 5000); + + } catch (err) { + console.error(`[CommentViewer] Test search error:`, err); + } + } + // Build threaded comment structure function buildCommentThread(events: NDKEvent[]): CommentNode[] { if (events.length === 0) return []; @@ -220,6 +333,9 @@ return neventEncode(commentEvent, $activeInboxRelays); } + // AI-NOTE: 2025-01-24 - View button functionality is working correctly + // This function navigates to the specific event as the main event, allowing + // users to view replies as the primary content function navigateToComment(commentEvent: NDKEvent) { const nevent = getNeventUrl(commentEvent); goto(`/events?id=${encodeURIComponent(nevent)}`); @@ -275,7 +391,9 @@ function getAuthorName(pubkey: string): string { const profile = profiles.get(pubkey); - return profile?.displayName || profile?.name || `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`; + if (profile?.displayName) return profile.displayName; + if (profile?.name) return profile.name; + return `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`; } function getAuthorPicture(pubkey: string): string | null { diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index 4bd78e4..c14d7e6 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -449,20 +449,22 @@ -
- {#if event.kind !== 0} - Content: -
- {@html showFullContent ? parsedContent : contentPreview} - {#if !showFullContent && parsedContent.length > 250} - - {/if} + {#if event.kind !== 0} +
+
+ Content: +
+ {@html showFullContent ? parsedContent : contentPreview} + {#if !showFullContent && parsedContent.length > 250} + + {/if} +
- {/if} -
+
+ {/if} {#if event.kind === 0} diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index b06ea4e..389b8bd 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -11,7 +11,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; import { userStore } from "$lib/stores/userStore"; import { userBadge } from "$lib/snippets/UserSnippets.svelte"; - import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils"; + import { getMatchingTags, toNpub, getUserMetadata } from "$lib/utils/nostrUtils"; import EventInput from "$lib/components/EventInput.svelte"; import { userPubkey, isLoggedIn } from "$lib/stores/authStore.Svelte"; import CopyToClipboard from "$lib/components/util/CopyToClipboard.svelte"; @@ -75,6 +75,25 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; } else { profile = null; } + + // AI-NOTE: 2025-01-24 - Ensure profile is cached for the event author + if (newEvent.pubkey) { + cacheProfileForPubkey(newEvent.pubkey); + } + } + + // AI-NOTE: 2025-01-24 - Function to ensure profile is cached for a pubkey + async function cacheProfileForPubkey(pubkey: string) { + try { + const npub = toNpub(pubkey); + if (npub) { + // Force fetch to ensure profile is cached + await getUserMetadata(npub, true); + console.log(`[Events Page] Cached profile for pubkey: ${pubkey}`); + } + } catch (error) { + console.warn(`[Events Page] Failed to cache profile for ${pubkey}:`, error); + } } // Use Svelte 5 idiomatic effect to update searchValue when $page.url.searchParams.get('id') changes @@ -185,11 +204,32 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; checkCommunityStatusForResults(tTagEvents); } + // AI-NOTE: 2025-01-24 - Cache profiles for all search results + cacheProfilesForEvents([...results, ...secondOrder, ...tTagEvents]); + // Don't clear the current event - let the user continue viewing it // event = null; // profile = null; } + // AI-NOTE: 2025-01-24 - Function to cache profiles for multiple events + async function cacheProfilesForEvents(events: NDKEvent[]) { + const uniquePubkeys = new Set(); + events.forEach(event => { + if (event.pubkey) { + uniquePubkeys.add(event.pubkey); + } + }); + + console.log(`[Events Page] Caching profiles for ${uniquePubkeys.size} unique pubkeys`); + + // Cache profiles in parallel + const cachePromises = Array.from(uniquePubkeys).map(pubkey => cacheProfileForPubkey(pubkey)); + await Promise.allSettled(cachePromises); + + console.log(`[Events Page] Profile caching complete`); + } + function handleClear() { searchType = null; searchTerm = null; @@ -233,48 +273,47 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; originalEventIds: Set, originalAddresses: Set, ): string { - // Check if this event has e-tags referencing original events - const eTags = getMatchingTags(event, "e"); - for (const tag of eTags) { - if (originalEventIds.has(tag[1])) { - return "Reply/Reference (e-tag)"; - } - } + const eTags = event.getMatchingTags("e"); + const aTags = event.getMatchingTags("a"); - // Check if this event has a-tags or e-tags referencing original events - let tags = getMatchingTags(event, "a"); - if (tags.length === 0) { - tags = getMatchingTags(event, "e"); + if (eTags.length > 0) { + const referencedEventId = eTags[eTags.length - 1][1]; + if (originalEventIds.has(referencedEventId)) { + return "Reply"; + } } - for (const tag of tags) { - if (originalAddresses.has(tag[1])) { - return "Reply/Reference (a-tag)"; + if (aTags.length > 0) { + const referencedAddress = aTags[aTags.length - 1][1]; + if (originalAddresses.has(referencedAddress)) { + return "Quote"; } } - // Check if this event has content references - if (event.content) { - for (const id of originalEventIds) { - const neventPattern = new RegExp(`nevent1[a-z0-9]{50,}`, "i"); - const notePattern = new RegExp(`note1[a-z0-9]{50,}`, "i"); - if ( - neventPattern.test(event.content) || - notePattern.test(event.content) - ) { - return "Content Reference"; - } - } + return "Reference"; + } - for (const address of originalAddresses) { - const naddrPattern = new RegExp(`naddr1[a-z0-9]{50,}`, "i"); - if (naddrPattern.test(event.content)) { - return "Content Reference"; - } - } + // AI-NOTE: 2025-01-24 - Function to parse profile content from kind 0 events + function parseProfileContent(event: NDKEvent): { + name?: string; + display_name?: string; + about?: string; + picture?: string; + banner?: string; + website?: string; + lud16?: string; + nip05?: string; + } | null { + if (event.kind !== 0 || !event.content) { + return null; } - return "Reference"; + try { + return JSON.parse(event.content); + } catch (error) { + console.warn("Failed to parse profile content:", error); + return null; + } } function getNeventUrl(event: NDKEvent): string { @@ -427,6 +466,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";
{#each searchResults as result, index} + {@const profileData = parseProfileContent(result)}
- {#if getSummary(result)} -
- {getSummary(result)} + {#if result.kind === 0 && profileData} +
+ {#if profileData.picture} + Profile { + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> + {:else} +
+ + {(profileData.display_name || profileData.name || result.pubkey.slice(0, 1)).toUpperCase()} + +
+ {/if} +
+ {#if profileData.display_name || profileData.name} + + {profileData.display_name || profileData.name} + + {/if} + {#if profileData.about} + + {profileData.about} + + {/if} +
- {/if} - {#if getDeferralNaddr(result)} -
- Read - { - e.stopPropagation(); - navigateToPublication( - getDeferralNaddr(result) || "", - ); - }} - onkeydown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); + {:else} + {#if getSummary(result)} +
+ {getSummary(result)} +
+ {/if} + {#if getDeferralNaddr(result)} +
+ Read + { e.stopPropagation(); navigateToPublication( getDeferralNaddr(result) || "", ); - } - }} - tabindex="0" - role="button" + }} + onkeydown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + e.stopPropagation(); + navigateToPublication( + getDeferralNaddr(result) || "", + ); + } + }} + tabindex="0" + role="button" + > + {getDeferralNaddr(result)} + +
+ {/if} + {#if isAddressableEvent(result)} +
- {getDeferralNaddr(result)} - -
- {/if} - {#if isAddressableEvent(result)} -
- -
- {/if} - {#if result.content} -
- {result.content.slice(0, 200)}{result.content.length > - 200 - ? "..." - : ""} -
+ +
+ {/if} + {#if result.content} +
+ {result.content.slice(0, 200)}{result.content.length > + 200 + ? "..." + : ""} +
+ {/if} {/if}
@@ -551,6 +624,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";

{#each secondOrderResults as result, index} + {@const profileData = parseProfileContent(result)} @@ -675,6 +782,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";

{#each tTagResults as result, index} + {@const profileData = parseProfileContent(result)}
- {#if getSummary(result)} -
- {getSummary(result)} + {#if result.kind === 0 && profileData} +
+ {#if profileData.picture} + Profile { + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> + {:else} +
+ + {(profileData.display_name || profileData.name || result.pubkey.slice(0, 1)).toUpperCase()} + +
+ {/if} +
+ {#if profileData.display_name || profileData.name} + + {profileData.display_name || profileData.name} + + {/if} + {#if profileData.about} + + {profileData.about} + + {/if} +
- {/if} - {#if getDeferralNaddr(result)} -
- Read - { - e.stopPropagation(); - navigateToPublication( - getDeferralNaddr(result) || "", - ); - }} - onkeydown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); + {:else} + {#if getSummary(result)} +
+ {getSummary(result)} +
+ {/if} + {#if getDeferralNaddr(result)} +
+ Read + { e.stopPropagation(); navigateToPublication( getDeferralNaddr(result) || "", ); - } - }} - tabindex="0" - role="button" + }} + onkeydown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + e.stopPropagation(); + navigateToPublication( + getDeferralNaddr(result) || "", + ); + } + }} + tabindex="0" + role="button" + > + {getDeferralNaddr(result)} + +
+ {/if} + {#if isAddressableEvent(result)} +
- {getDeferralNaddr(result)} - -
- {/if} - {#if isAddressableEvent(result)} -
- -
- {/if} - {#if result.content} -
- {result.content.slice(0, 200)}{result.content.length > - 200 - ? "..." - : ""} -
+ +
+ {/if} + {#if result.content} +
+ {result.content.slice(0, 200)}{result.content.length > + 200 + ? "..." + : ""} +
+ {/if} {/if}
From 11c605b5eff23870e41aea99d4e28e0e757bd5c2 Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 09:21:57 +0200 Subject: [PATCH 15/98] improved search some more --- src/lib/components/EventSearch.svelte | 132 ++++++++++++++++++++++++++ src/lib/utils/search_constants.ts | 7 +- src/lib/utils/subscription_search.ts | 50 +++++----- src/routes/events/+page.svelte | 12 ++- 4 files changed, 171 insertions(+), 30 deletions(-) diff --git a/src/lib/components/EventSearch.svelte b/src/lib/components/EventSearch.svelte index 10f888b..67dece6 100644 --- a/src/lib/components/EventSearch.svelte +++ b/src/lib/components/EventSearch.svelte @@ -488,6 +488,61 @@ searchType, searchTerm, }); + + // AI-NOTE: 2025-01-24 - Check cache first for profile searches to provide immediate response + if (searchType === "n") { + try { + const { getUserMetadata } = await import("$lib/utils/nostrUtils"); + const cachedProfile = await getUserMetadata(searchTerm, false); + if (cachedProfile && cachedProfile.name) { + console.log("EventSearch: Found cached profile, displaying immediately:", cachedProfile); + + // Create a mock NDKEvent for the cached profile + const { NDKEvent } = await import("@nostr-dev-kit/ndk"); + const { nip19 } = await import("$lib/utils/nostrUtils"); + + // Decode the npub to get the actual pubkey + let pubkey = searchTerm; + try { + const decoded = nip19.decode(searchTerm); + if (decoded && decoded.type === "npub") { + pubkey = decoded.data; + } + } catch (error) { + console.warn("EventSearch: Failed to decode npub for mock event:", error); + } + + const mockEvent = new NDKEvent(undefined, { + kind: 0, + pubkey: pubkey, + content: JSON.stringify(cachedProfile), + tags: [], + created_at: Math.floor(Date.now() / 1000), + id: "", // Will be computed by NDK + sig: "", // Will be computed by NDK + }); + + // Display the cached profile immediately + handleFoundEvent(mockEvent); + updateSearchState(false, true, 1, "profile-cached"); + + // AI-NOTE: 2025-01-24 - Still perform background search for second-order events + // but with better timeout handling to prevent hanging + setTimeout(async () => { + try { + await performBackgroundProfileSearch(searchType, searchTerm); + } catch (error) { + console.warn("EventSearch: Background profile search failed:", error); + } + }, 100); + + return; + } + } catch (error) { + console.warn("EventSearch: Cache check failed, proceeding with subscription search:", error); + } + } + isResetting = false; // Allow effects to run for new searches localError = null; updateSearchState(true); @@ -663,6 +718,83 @@ } } + // AI-NOTE: 2025-01-24 - Function to perform background profile search without blocking UI + async function performBackgroundProfileSearch( + searchType: "d" | "t" | "n", + searchTerm: string, + ) { + console.log("EventSearch: Performing background profile search:", { + searchType, + searchTerm, + }); + + try { + // Cancel existing search + if (currentAbortController) { + currentAbortController.abort(); + } + currentAbortController = new AbortController(); + + // AI-NOTE: 2025-01-24 - Add timeout to prevent hanging background searches + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error("Background search timeout")); + }, 10000); // 10 second timeout for background searches + }); + + const searchPromise = searchBySubscription( + searchType, + searchTerm, + { + onSecondOrderUpdate: (updatedResult) => { + console.log("EventSearch: Background second order update:", updatedResult); + // Only update if we have new results + if (updatedResult.events.length > 0) { + onSearchResults( + updatedResult.events, + updatedResult.secondOrder, + updatedResult.tTagEvents, + updatedResult.eventIds, + updatedResult.addresses, + updatedResult.searchType, + updatedResult.searchTerm, + ); + } + }, + onSubscriptionCreated: (sub) => { + console.log("EventSearch: Background subscription created:", sub); + if (activeSub) { + activeSub.stop(); + } + activeSub = sub; + }, + }, + currentAbortController.signal, + ); + + // Race between search and timeout + const result = await Promise.race([searchPromise, timeoutPromise]) as any; + + console.log("EventSearch: Background search completed:", result); + + // Only update results if we have new data + if (result.events.length > 0) { + onSearchResults( + result.events, + result.secondOrder, + result.tTagEvents, + result.eventIds, + result.addresses, + result.searchType, + result.searchTerm, + ); + } + } catch (error) { + console.warn("EventSearch: Background profile search failed:", error); + } + } + + // Search utility functions function handleClear() { isResetting = true; searchQuery = ""; diff --git a/src/lib/utils/search_constants.ts b/src/lib/utils/search_constants.ts index 663e985..cc6f677 100644 --- a/src/lib/utils/search_constants.ts +++ b/src/lib/utils/search_constants.ts @@ -17,7 +17,7 @@ export const TIMEOUTS = { SUBSCRIPTION_SEARCH: 10000, /** Timeout for second-order search operations */ - SECOND_ORDER_SEARCH: 5000, + SECOND_ORDER_SEARCH: 3000, // AI-NOTE: 2025-01-24 - Reduced timeout since we limit scope /** Timeout for relay diagnostics */ RELAY_DIAGNOSTICS: 5000, @@ -44,7 +44,10 @@ export const SEARCH_LIMITS = { SPECIFIC_PROFILE: 10, /** Limit for general profile searches */ - GENERAL_PROFILE: 500, + GENERAL_PROFILE: 100, // AI-NOTE: 2025-01-24 - Reduced from 500 to prevent wild searches + + /** Limit for general content searches (t-tag, d-tag, etc.) */ + GENERAL_CONTENT: 100, // AI-NOTE: 2025-01-24 - Added limit for all content searches /** Limit for community relay checks */ COMMUNITY_CHECK: 1, diff --git a/src/lib/utils/subscription_search.ts b/src/lib/utils/subscription_search.ts index d992f5b..d07067e 100644 --- a/src/lib/utils/subscription_search.ts +++ b/src/lib/utils/subscription_search.ts @@ -59,19 +59,11 @@ export async function searchBySubscription( const cachedResult = searchCache.get(searchType, normalizedSearchTerm); if (cachedResult) { console.log("subscription_search: Found cached result:", cachedResult); - // AI-NOTE: 2025-01-08 - For profile searches, clear cache if it's empty to force fresh search - if (searchType === "n" && cachedResult.events.length === 0) { - console.log("subscription_search: Clearing empty cached profile result to force fresh search"); - searchCache.clear(); // Clear all cache to force fresh search - } else if (searchType === "n" && cachedResult.events.length > 0 && cachedResult.secondOrder.length === 0) { - // AI-NOTE: 2025-01-08 - Clear cache if we have profile results but no second-order events - // This forces a fresh search that includes second-order searching - console.log("subscription_search: Clearing cached profile result with no second-order events to force fresh search"); - searchCache.clear(); - } else if (searchType === "n") { - // AI-NOTE: 2025-01-08 - For profile searches, always clear cache to ensure fresh second-order search - console.log("subscription_search: Clearing cache for profile search to ensure fresh second-order search"); - searchCache.clear(); + // AI-NOTE: 2025-01-24 - For profile searches, return cached results immediately + // The EventSearch component now handles cache checking before calling this function + if (searchType === "n") { + console.log("subscription_search: Returning cached profile result immediately"); + return cachedResult; } else { return cachedResult; } @@ -91,7 +83,7 @@ export async function searchBySubscription( searchState.timeoutId = setTimeout(() => { console.log("subscription_search: Search timeout reached"); cleanup(); - }, searchType === "n" ? 5000 : TIMEOUTS.SUBSCRIPTION_SEARCH); // AI-NOTE: 2025-01-08 - Shorter timeout for profile searches + }, TIMEOUTS.SUBSCRIPTION_SEARCH); // AI-NOTE: 2025-01-24 - Use standard timeout since cache is checked first // Check for abort signal if (abortSignal?.aborted) { @@ -332,7 +324,7 @@ async function createSearchFilter( switch (searchType) { case "d": { const dFilter = { - filter: { "#d": [normalizedSearchTerm] }, + filter: { "#d": [normalizedSearchTerm], limit: SEARCH_LIMITS.GENERAL_CONTENT }, subscriptionType: "d-tag", }; console.log("subscription_search: Created d-tag filter:", dFilter); @@ -340,7 +332,7 @@ async function createSearchFilter( } case "t": { const tFilter = { - filter: { "#t": [normalizedSearchTerm] }, + filter: { "#t": [normalizedSearchTerm], limit: SEARCH_LIMITS.GENERAL_CONTENT }, subscriptionType: "t-tag", }; console.log("subscription_search: Created t-tag filter:", tFilter); @@ -929,21 +921,33 @@ async function performSecondOrderSearchInBackground( if (searchType === "n" && targetPubkey) { console.log("subscription_search: Searching for events mentioning pubkey:", targetPubkey); + // AI-NOTE: 2025-01-24 - Use only active relays for second-order profile search to prevent hanging + const activeRelays = [...get(activeInboxRelays), ...get(activeOutboxRelays)]; + const availableRelays = activeRelays + .map(url => ndk.pool.relays.get(url)) + .filter((relay): relay is any => relay !== undefined); + const relaySet = new NDKRelaySet( + new Set(availableRelays), + ndk + ); + + console.log("subscription_search: Using", activeRelays.length, "active relays for second-order search"); + // Search for events that mention this pubkey via p-tags - const pTagFilter = { "#p": [targetPubkey] }; + const pTagFilter = { "#p": [targetPubkey], limit: 50 }; // AI-NOTE: 2025-01-24 - Limit results to prevent hanging const pTagEvents = await ndk.fetchEvents( pTagFilter, { closeOnEose: true }, - new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())), ndk), + relaySet, ); console.log("subscription_search: Found", pTagEvents.size, "events with p-tag for", targetPubkey); - // AI-NOTE: 2025-01-08 - Also search for events written by this pubkey - const authorFilter = { authors: [targetPubkey] }; + // AI-NOTE: 2025-01-24 - Also search for events written by this pubkey with limit + const authorFilter = { authors: [targetPubkey], limit: 50 }; // AI-NOTE: 2025-01-24 - Limit results to prevent hanging const authorEvents = await ndk.fetchEvents( authorFilter, { closeOnEose: true }, - new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())), ndk), + relaySet, ); console.log("subscription_search: Found", authorEvents.size, "events written by", targetPubkey); @@ -964,14 +968,14 @@ async function performSecondOrderSearchInBackground( const [eTagEvents, aTagEvents] = await Promise.all([ eventIds.size > 0 ? ndk.fetchEvents( - { "#e": Array.from(eventIds) }, + { "#e": Array.from(eventIds), limit: SEARCH_LIMITS.SECOND_ORDER_RESULTS }, { closeOnEose: true }, relaySet, ) : Promise.resolve([]), addresses.size > 0 ? ndk.fetchEvents( - { "#a": Array.from(addresses) }, + { "#a": Array.from(addresses), limit: SEARCH_LIMITS.SECOND_ORDER_RESULTS }, { closeOnEose: true }, relaySet, ) diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index 389b8bd..f37d5a8 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -453,15 +453,17 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; {#if searchResults.length > 0}
- + {#if searchType === "n"} - Search Results for name: "{searchTerm}" ({searchResults.length} profiles) + Search Results for name: "{searchTerm && searchTerm.length > 50 ? searchTerm.slice(0, 50) + '...' : searchTerm || ''}" ({searchResults.length} profiles) {:else if searchType === "t"} - Search Results for t-tag: "{searchTerm}" ({searchResults.length} + Search Results for t-tag: "{searchTerm && searchTerm.length > 50 ? searchTerm.slice(0, 50) + '...' : searchTerm || ''}" ({searchResults.length} events) {:else} - Search Results for d-tag: "{searchTerm || - dTagValue?.toLowerCase()}" ({searchResults.length} events) + Search Results for d-tag: "{(() => { + const term = searchTerm || dTagValue?.toLowerCase() || ''; + return term.length > 50 ? term.slice(0, 50) + '...' : term; + })()}" ({searchResults.length} events) {/if}
From 715efad96e6323d41a274810dfd839c9cd766202 Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 17:12:27 +0200 Subject: [PATCH 16/98] fixed kind 1111 comments and added highlights --- src/lib/components/CommentViewer.svelte | 487 +++++++++++++++++++++--- src/lib/consts.ts | 3 + src/lib/utils.ts | 27 +- src/lib/utils/event_search.ts | 64 +++- 4 files changed, 523 insertions(+), 58 deletions(-) diff --git a/src/lib/components/CommentViewer.svelte b/src/lib/components/CommentViewer.svelte index fb819e1..3b44665 100644 --- a/src/lib/components/CommentViewer.svelte +++ b/src/lib/components/CommentViewer.svelte @@ -14,6 +14,8 @@ // AI-NOTE: 2025-01-08 - Clean, efficient comment viewer implementation // This component fetches and displays threaded comments with proper hierarchy // Uses simple, reliable profile fetching and efficient state management + // AI-NOTE: 2025-01-24 - Added support for kind 9802 highlights (NIP-84) + // Highlights are displayed with special styling and include source attribution // State management let comments: NDKEvent[] = $state([]); @@ -84,32 +86,78 @@ } try { - // Try multiple filter approaches to find comments + // Build address for NIP-22 search if this is a replaceable event + let eventAddress: string | null = null; + if (event.kind && event.pubkey) { + const dTag = event.getMatchingTags("d")[0]?.[1]; + if (dTag) { + eventAddress = `${event.kind}:${event.pubkey}:${dTag}`; + } + } + + console.log(`[CommentViewer] Event address for NIP-22: ${eventAddress}`); + + // Use more targeted filters to reduce noise const filters = [ - // Standard comment filter + // Primary filter: events that explicitly reference our target via e-tags { - kinds: [1, 1111], + kinds: [1, 1111, 9802], "#e": [event.id], - }, - // Broader search for any events that might reference this event - { - kinds: [1, 1111], - "#e": [event.id], - limit: 100, - }, - // Search for events by the same author that might be replies - { - kinds: [1, 1111], - authors: [event.pubkey], - since: event.created_at ? event.created_at - 86400 : undefined, // Last 24 hours limit: 50, } ]; - console.log(`[CommentViewer] Setting up subscription with filters:`, filters); + // Add NIP-22 filter only if we have a valid event address + if (eventAddress) { + filters.push({ + kinds: [1111, 9802], + "#a": [eventAddress], + limit: 50, + } as any); + } + + console.log(`[CommentViewer] Setting up subscription with ${filters.length} filters:`, filters); + + // Debug: Check if the provided event would match our filters + console.log(`[CommentViewer] Debug: Checking if event b9a15298f2b203d42ba6d0c56c43def87efc887697460c0febb9542515d5a00b would match our filters`); + console.log(`[CommentViewer] Debug: Target event ID: ${event.id}`); + console.log(`[CommentViewer] Debug: Event address: ${eventAddress}`); + + // Get all available relays for a more comprehensive search + // Use the full NDK pool relays instead of just active relays + const ndkPoolRelays = Array.from($ndkInstance.pool.relays.values()).map(relay => relay.url); + console.log(`[CommentViewer] Using ${ndkPoolRelays.length} NDK pool relays for search:`, ndkPoolRelays); + + // Try all filters to find comments with full relay set + activeSub = $ndkInstance.subscribe(filters); - // Try the first filter (standard comment search) - activeSub = $ndkInstance.subscribe(filters[0]); + // Also try a direct search for the specific comment we're looking for + console.log(`[CommentViewer] Also searching for specific comment: 64173a81c2a8e26342d4a75d3def804da8644377bde99cfdfeaf189dff87f942`); + const specificCommentSub = $ndkInstance.subscribe({ + ids: ["64173a81c2a8e26342d4a75d3def804da8644377bde99cfdfeaf189dff87f942"] + }); + + specificCommentSub.on("event", (specificEvent: NDKEvent) => { + console.log(`[CommentViewer] Found specific comment via direct search:`, specificEvent.id); + console.log(`[CommentViewer] Specific comment tags:`, specificEvent.tags); + + // Check if this specific comment references our target + const eTags = specificEvent.getMatchingTags("e"); + const aTags = specificEvent.getMatchingTags("a"); + console.log(`[CommentViewer] Specific comment e-tags:`, eTags.map(t => t[1])); + console.log(`[CommentViewer] Specific comment a-tags:`, aTags.map(t => t[1])); + + const hasETag = eTags.some(tag => tag[1] === event.id); + const hasATag = eventAddress ? aTags.some(tag => tag[1] === eventAddress) : false; + + console.log(`[CommentViewer] Specific comment has matching e-tag: ${hasETag}`); + console.log(`[CommentViewer] Specific comment has matching a-tag: ${hasATag}`); + }); + + specificCommentSub.on("eose", () => { + console.log(`[CommentViewer] Specific comment search EOSE`); + specificCommentSub.stop(); + }); const timeout = setTimeout(() => { console.log(`[CommentViewer] Subscription timeout - no comments found`); @@ -126,16 +174,54 @@ console.log(`[CommentViewer] Comment pubkey: ${commentEvent.pubkey}`); console.log(`[CommentViewer] Comment content preview: ${commentEvent.content?.slice(0, 100)}...`); + // Special debug for the specific comment we're looking for + if (commentEvent.id === "64173a81c2a8e26342d4a75d3def804da8644377bde99cfdfeaf189dff87f942") { + console.log(`[CommentViewer] DEBUG: Found the specific comment we're looking for!`); + console.log(`[CommentViewer] DEBUG: Comment tags:`, commentEvent.tags); + } + // Check if this event actually references our target event + let referencesTarget = false; + let referenceMethod = ""; + + // Check e-tags (standard format) const eTags = commentEvent.getMatchingTags("e"); - const referencesTarget = eTags.some(tag => tag[1] === event.id); + console.log(`[CommentViewer] Checking e-tags:`, eTags.map(t => t[1])); + console.log(`[CommentViewer] Target event ID: ${event.id}`); + const hasETag = eTags.some(tag => tag[1] === event.id); + console.log(`[CommentViewer] Has matching e-tag: ${hasETag}`); + if (hasETag) { + referencesTarget = true; + referenceMethod = "e-tag"; + } + + // Check a-tags (NIP-22 format) if not found via e-tags + if (!referencesTarget && eventAddress) { + const aTags = commentEvent.getMatchingTags("a"); + console.log(`[CommentViewer] Checking a-tags:`, aTags.map(t => t[1])); + console.log(`[CommentViewer] Expected a-tag: ${eventAddress}`); + const hasATag = aTags.some(tag => tag[1] === eventAddress); + console.log(`[CommentViewer] Has matching a-tag: ${hasATag}`); + if (hasATag) { + referencesTarget = true; + referenceMethod = "a-tag"; + } + } if (referencesTarget) { - console.log(`[CommentViewer] Comment references target event - adding to comments`); + console.log(`[CommentViewer] Comment references target event via ${referenceMethod} - adding to comments`); comments = [...comments, commentEvent]; fetchProfile(commentEvent.pubkey); + + // Fetch nested replies for this comment + fetchNestedReplies(commentEvent.id); } else { console.log(`[CommentViewer] Comment does not reference target event - skipping`); + console.log(`[CommentViewer] e-tags:`, eTags.map(t => t[1])); + if (eventAddress) { + console.log(`[CommentViewer] a-tags:`, commentEvent.getMatchingTags("a").map(t => t[1])); + console.log(`[CommentViewer] expected a-tag:`, eventAddress); + } } }); @@ -151,6 +237,11 @@ // Pre-fetch all profiles after comments are loaded preFetchAllProfiles(); + // AI-NOTE: 2025-01-24 - Fetch nested replies for all found comments + comments.forEach(comment => { + fetchNestedReplies(comment.id); + }); + // AI-NOTE: 2025-01-24 - Test for comments if none were found if (comments.length === 0) { testForComments(); @@ -193,25 +284,35 @@ console.log(`[CommentViewer] Pre-fetching complete`); } - // AI-NOTE: 2025-01-24 - Function to manually test for comments - async function testForComments() { - if (!event?.id) return; - - console.log(`[CommentViewer] Testing for comments on event: ${event.id}`); - - try { - // Try a broader search to see if there are any events that might be comments - const testSub = $ndkInstance.subscribe({ - kinds: [1, 1111], - "#e": [event.id], - limit: 10, - }); + // AI-NOTE: 2025-01-24 - Function to manually test for comments + async function testForComments() { + if (!event?.id) return; + + console.log(`[CommentViewer] Testing for comments on event: ${event.id}`); + + try { + // Try a broader search to see if there are any events that might be comments + const testSub = $ndkInstance.subscribe({ + kinds: [1, 1111, 9802], + "#e": [event.id], + limit: 10, + }); let testComments = 0; testSub.on("event", (testEvent: NDKEvent) => { testComments++; - console.log(`[CommentViewer] Test found event: ${testEvent.id}, kind: ${testEvent.kind}`); + console.log(`[CommentViewer] Test found event: ${testEvent.id}, kind: ${testEvent.kind}, content: ${testEvent.content?.slice(0, 50)}...`); + + // Special debug for the specific comment we're looking for + if (testEvent.id === "64173a81c2a8e26342d4a75d3def804da8644377bde99cfdfeaf189dff87f942") { + console.log(`[CommentViewer] DEBUG: Test found the specific comment we're looking for!`); + console.log(`[CommentViewer] DEBUG: Test comment tags:`, testEvent.tags); + } + + // Show the e-tags to help debug + const eTags = testEvent.getMatchingTags("e"); + console.log(`[CommentViewer] Test event e-tags:`, eTags.map(t => t[1])); }); testSub.on("eose", () => { @@ -266,12 +367,36 @@ } } } else if (event.kind === 1111) { - // Kind 1111: Look for lowercase e-tags (immediate parent) - for (const tag of eTags) { - const referencedId = tag[1]; - if (eventMap.has(referencedId) && referencedId !== event.id) { - parentId = referencedId; - break; + // Kind 1111: Use NIP-22 threading format + // First try to find parent using 'a' tags (NIP-22 parent scope) + const aTags = event.getMatchingTags("a"); + for (const tag of aTags) { + const address = tag[1]; + // Extract event ID from address if it's a coordinate + const parts = address.split(":"); + if (parts.length >= 3) { + const [kind, pubkey, dTag] = parts; + // Look for the parent event with this address + for (const [eventId, parentEvent] of eventMap) { + if (parentEvent.kind === parseInt(kind) && + parentEvent.pubkey === pubkey && + parentEvent.getMatchingTags("d")[0]?.[1] === dTag) { + parentId = eventId; + break; + } + } + if (parentId) break; + } + } + + // Fallback to 'e' tags if no parent found via 'a' tags + if (!parentId) { + for (const tag of eTags) { + const referencedId = tag[1]; + if (eventMap.has(referencedId) && referencedId !== event.id) { + parentId = referencedId; + break; + } } } } @@ -310,6 +435,7 @@ // Fetch comments when event changes $effect(() => { if (event?.id) { + console.log(`[CommentViewer] Event changed, fetching comments for:`, event.id, `kind:`, event.kind); if (activeSub) { activeSub.stop(); activeSub = null; @@ -318,6 +444,110 @@ } }); + // AI-NOTE: 2025-01-24 - Add recursive comment fetching for nested replies + let isFetchingNestedReplies = $state(false); + let nestedReplyIds = $state>(new Set()); + + // Function to fetch nested replies for a given event + async function fetchNestedReplies(eventId: string) { + if (isFetchingNestedReplies || nestedReplyIds.has(eventId)) { + console.log(`[CommentViewer] Skipping nested reply fetch for ${eventId} - already fetching or processed`); + return; + } + + console.log(`[CommentViewer] Starting nested reply fetch for event: ${eventId}`); + isFetchingNestedReplies = true; + nestedReplyIds.add(eventId); + + try { + console.log(`[CommentViewer] Fetching nested replies for event:`, eventId); + + // Search for replies to this specific event + const nestedSub = $ndkInstance.subscribe({ + kinds: [1, 1111, 9802], + "#e": [eventId], + limit: 50, + }); + + let nestedCount = 0; + + nestedSub.on("event", (nestedEvent: NDKEvent) => { + console.log(`[CommentViewer] Found nested reply:`, nestedEvent.id, `kind:`, nestedEvent.kind); + + // Check if this event actually references the target event + const eTags = nestedEvent.getMatchingTags("e"); + const referencesTarget = eTags.some(tag => tag[1] === eventId); + + console.log(`[CommentViewer] Nested reply references target:`, referencesTarget, `eTags:`, eTags); + + if (referencesTarget && !comments.some(c => c.id === nestedEvent.id)) { + console.log(`[CommentViewer] Adding nested reply to comments`); + comments = [...comments, nestedEvent]; + fetchProfile(nestedEvent.pubkey); + + // Recursively fetch replies to this nested reply + fetchNestedReplies(nestedEvent.id); + } else if (!referencesTarget) { + console.log(`[CommentViewer] Nested reply does not reference target, skipping`); + } else { + console.log(`[CommentViewer] Nested reply already exists in comments`); + } + }); + + nestedSub.on("eose", () => { + console.log(`[CommentViewer] Nested replies EOSE, found ${nestedCount} replies`); + nestedSub.stop(); + isFetchingNestedReplies = false; + }); + + // Also search for NIP-22 format nested replies + const event = comments.find(c => c.id === eventId); + if (event && event.kind && event.pubkey) { + const dTag = event.getMatchingTags("d")[0]?.[1]; + if (dTag) { + const eventAddress = `${event.kind}:${event.pubkey}:${dTag}`; + + const nip22Sub = $ndkInstance.subscribe({ + kinds: [1111, 9802], + "#a": [eventAddress], + limit: 50, + }); + + nip22Sub.on("event", (nip22Event: NDKEvent) => { + console.log(`[CommentViewer] Found NIP-22 nested reply:`, nip22Event.id, `kind:`, nip22Event.kind); + + const aTags = nip22Event.getMatchingTags("a"); + const referencesTarget = aTags.some(tag => tag[1] === eventAddress); + + console.log(`[CommentViewer] NIP-22 nested reply references target:`, referencesTarget, `aTags:`, aTags, `eventAddress:`, eventAddress); + + if (referencesTarget && !comments.some(c => c.id === nip22Event.id)) { + console.log(`[CommentViewer] Adding NIP-22 nested reply to comments`); + comments = [...comments, nip22Event]; + fetchProfile(nip22Event.pubkey); + + // Recursively fetch replies to this nested reply + fetchNestedReplies(nip22Event.id); + } else if (!referencesTarget) { + console.log(`[CommentViewer] NIP-22 nested reply does not reference target, skipping`); + } else { + console.log(`[CommentViewer] NIP-22 nested reply already exists in comments`); + } + }); + + nip22Sub.on("eose", () => { + console.log(`[CommentViewer] NIP-22 nested replies EOSE`); + nip22Sub.stop(); + }); + } + } + + } catch (err) { + console.error(`[CommentViewer] Error fetching nested replies:`, err); + isFetchingNestedReplies = false; + } + } + // Cleanup on unmount onMount(() => { return () => { @@ -330,15 +560,31 @@ // Navigation functions function getNeventUrl(commentEvent: NDKEvent): string { - return neventEncode(commentEvent, $activeInboxRelays); + try { + console.log(`[CommentViewer] Generating nevent for:`, commentEvent.id, `kind:`, commentEvent.kind); + const nevent = neventEncode(commentEvent, $activeInboxRelays); + console.log(`[CommentViewer] Generated nevent:`, nevent); + return nevent; + } catch (error) { + console.error(`[CommentViewer] Error generating nevent:`, error); + // Fallback to just the event ID + return commentEvent.id; + } } // AI-NOTE: 2025-01-24 - View button functionality is working correctly // This function navigates to the specific event as the main event, allowing // users to view replies as the primary content function navigateToComment(commentEvent: NDKEvent) { - const nevent = getNeventUrl(commentEvent); - goto(`/events?id=${encodeURIComponent(nevent)}`); + try { + const nevent = getNeventUrl(commentEvent); + console.log(`[CommentViewer] Navigating to comment:`, nevent); + goto(`/events?id=${encodeURIComponent(nevent)}`); + } catch (error) { + console.error(`[CommentViewer] Error navigating to comment:`, error); + // Fallback to event ID + goto(`/events?id=${commentEvent.id}`); + } } // Utility functions @@ -414,6 +660,39 @@ return parsedContent; } + + + + // AI-NOTE: 2025-01-24 - Get highlight source information + function getHighlightSource(highlightEvent: NDKEvent): { type: string; value: string; url?: string } | null { + // Check for e-tags (nostr events) + const eTags = highlightEvent.getMatchingTags("e"); + if (eTags.length > 0) { + return { type: "nostr_event", value: eTags[0][1] }; + } + + // Check for r-tags (URLs) + const rTags = highlightEvent.getMatchingTags("r"); + if (rTags.length > 0) { + return { type: "url", value: rTags[0][1], url: rTags[0][1] }; + } + + return null; + } + + // AI-NOTE: 2025-01-24 - Get highlight attribution + function getHighlightAttribution(highlightEvent: NDKEvent): Array<{ pubkey: string; role?: string }> { + const pTags = highlightEvent.getMatchingTags("p"); + return pTags.map(tag => ({ + pubkey: tag[1], + role: tag[3] || undefined + })); + } + + // AI-NOTE: 2025-01-24 - Check if highlight has comment + function hasHighlightComment(highlightEvent: NDKEvent): boolean { + return highlightEvent.getMatchingTags("comment").length > 0; + } @@ -474,11 +753,84 @@
- {#await parseContent(node.event.content || "") then parsedContent} - {@html parsedContent} - {:catch} - {@html node.event.content || ""} - {/await} + {#if node.event.kind === 9802} + +
+ {#if hasHighlightComment(node.event)} + +
+
+ Highlighted content: +
+ {#if node.event.getMatchingTags("context")[0]?.[1]} +
+ {@html node.event.getMatchingTags("context")[0]?.[1]} +
+ {:else} +
+ {node.event.content || ""} +
+ {/if} + {#if getHighlightSource(node.event)} +
+ Source: {getHighlightSource(node.event)?.type === 'nostr_event' ? 'Nostr Event' : 'URL'} +
+ {/if} +
+
+
+ Comment: +
+ {#await parseContent(node.event.getMatchingTags("comment")[0]?.[1] || "") then parsedContent} + {@html parsedContent} + {:catch} + {@html node.event.getMatchingTags("comment")[0]?.[1] || ""} + {/await} +
+ {:else} + + {#if node.event.getMatchingTags("context")[0]?.[1]} +
+ {@html node.event.getMatchingTags("context")[0]?.[1]} +
+ {:else} +
+ {node.event.content || ""} +
+ {/if} + + {#if getHighlightSource(node.event)} +
+ Source: {getHighlightSource(node.event)?.type === 'nostr_event' ? 'Nostr Event' : 'URL'} +
+ {/if} + {/if} + + {#if getHighlightAttribution(node.event).length > 0} +
+ Attribution: + {#each getHighlightAttribution(node.event) as attribution} + + {/each} +
+ {/if} +
+ {:else} + + {#await parseContent(node.event.content || "") then parsedContent} + {@html parsedContent} + {:catch} + {@html node.event.content || ""} + {/await} + {/if}
@@ -494,7 +846,7 @@
- Comments ({threadedComments.length}) + Comments & Highlights ({threadedComments.length}) {#if loading} @@ -507,7 +859,7 @@
{:else if threadedComments.length === 0}
-

No comments yet. Be the first to comment!

+

No comments or highlights yet. Be the first to engage!

{:else}
@@ -516,4 +868,37 @@ {/each}
{/if} -
\ No newline at end of file +
+ + \ No newline at end of file diff --git a/src/lib/consts.ts b/src/lib/consts.ts index 90afa53..29f4502 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -14,6 +14,9 @@ export const searchRelays = [ "wss://aggr.nostr.land", "wss://relay.noswhere.com", "wss://nostr.wine", + "wss://relay.damus.io", + "wss://relay.nostr.band", + "wss://freelay.sovbit.host" ]; export const secondaryRelays = [ diff --git a/src/lib/utils.ts b/src/lib/utils.ts index ee44929..18fad03 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -19,12 +19,27 @@ export class InvalidKindError extends DecodeError { } export function neventEncode(event: NDKEvent, relays: string[]) { - return nip19.neventEncode({ - id: event.id, - kind: event.kind, - relays, - author: event.pubkey, - }); + try { + console.log(`[neventEncode] Encoding event:`, { + id: event.id, + kind: event.kind, + pubkey: event.pubkey, + relayCount: relays.length + }); + + const nevent = nip19.neventEncode({ + id: event.id, + kind: event.kind, + relays, + author: event.pubkey, + }); + + console.log(`[neventEncode] Generated nevent:`, nevent); + return nevent; + } catch (error) { + console.error(`[neventEncode] Error encoding nevent:`, error); + throw error; + } } export function naddrEncode(event: NDKEvent, relays: string[]) { diff --git a/src/lib/utils/event_search.ts b/src/lib/utils/event_search.ts index 1d5537d..f15b9b3 100644 --- a/src/lib/utils/event_search.ts +++ b/src/lib/utils/event_search.ts @@ -1,5 +1,5 @@ import { ndkInstance } from "../ndk.ts"; -import { fetchEventWithFallback } from "./nostrUtils.ts"; +import { fetchEventWithFallback, NDKRelaySetFromNDK } from "./nostrUtils.ts"; import { nip19 } from "nostr-tools"; import { NDKEvent } from "@nostr-dev-kit/ndk"; import type { Filter } from "./search_types.ts"; @@ -11,6 +11,26 @@ import { TIMEOUTS, VALIDATION } from "./search_constants.ts"; * Search for a single event by ID or filter */ export async function searchEvent(query: string): Promise { + const ndk = get(ndkInstance); + if (!ndk) { + console.warn("[Search] No NDK instance available"); + return null; + } + + // Wait for relays to be available + let attempts = 0; + const maxAttempts = 10; + while (ndk.pool.relays.size === 0 && attempts < maxAttempts) { + console.log(`[Search] Waiting for relays to be available (attempt ${attempts + 1}/${maxAttempts})`); + await new Promise(resolve => setTimeout(resolve, 500)); + attempts++; + } + + if (ndk.pool.relays.size === 0) { + console.warn("[Search] No relays available after waiting"); + return null; + } + // Clean the query and normalize to lowercase const cleanedQuery = query.replace(/^nostr:/, "").toLowerCase(); let filterOrId: Filter | string = cleanedQuery; @@ -51,8 +71,50 @@ export async function searchEvent(query: string): Promise { try { const decoded = nip19.decode(cleanedQuery); if (!decoded) throw new Error("Invalid identifier"); + + console.log(`[Search] Decoded identifier:`, { + type: decoded.type, + data: decoded.data, + query: cleanedQuery + }); + switch (decoded.type) { case "nevent": + console.log(`[Search] Processing nevent:`, { + id: decoded.data.id, + kind: decoded.data.kind, + relays: decoded.data.relays + }); + + // Use the relays from the nevent if available + if (decoded.data.relays && decoded.data.relays.length > 0) { + console.log(`[Search] Using relays from nevent:`, decoded.data.relays); + + // Try to fetch the event using the nevent's relays + try { + // Create a temporary relay set for this search + const neventRelaySet = NDKRelaySetFromNDK.fromRelayUrls(decoded.data.relays, ndk); + + if (neventRelaySet.relays.size > 0) { + console.log(`[Search] Created relay set with ${neventRelaySet.relays.size} relays from nevent`); + + // Try to fetch the event using the nevent's relays + const event = await ndk + .fetchEvent({ ids: [decoded.data.id] }, undefined, neventRelaySet) + .withTimeout(TIMEOUTS.EVENT_FETCH); + + if (event) { + console.log(`[Search] Found event using nevent relays:`, event.id); + return event; + } else { + console.log(`[Search] Event not found on nevent relays, trying default relays`); + } + } + } catch (error) { + console.warn(`[Search] Error fetching from nevent relays:`, error); + } + } + filterOrId = decoded.data.id; break; case "note": From 0b45c3e97c3ab70c095df0f1304f7dd2a665da37 Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 18:05:04 +0200 Subject: [PATCH 17/98] Personal notifications added to "View Profile" --- src/lib/components/EventDetails.svelte | 4 + src/lib/components/Notifications.svelte | 451 ++++++++++++++++++++++++ 2 files changed, 455 insertions(+) create mode 100644 src/lib/components/Notifications.svelte diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index c14d7e6..3a8801e 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -14,6 +14,7 @@ import CopyToClipboard from "$lib/components/util/CopyToClipboard.svelte"; import { navigateToEvent } from "$lib/utils/nostrEventService"; import ContainingIndexes from "$lib/components/util/ContainingIndexes.svelte"; + import Notifications from "$lib/components/Notifications.svelte"; const { event, @@ -473,6 +474,9 @@ {profile} identifiers={getIdentifiers(event, profile)} /> + + + {/if} diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte new file mode 100644 index 0000000..9566bcb --- /dev/null +++ b/src/lib/components/Notifications.svelte @@ -0,0 +1,451 @@ + + +{#if isOwnProfile && $userStore.signedIn} +
+
+ Notifications + + +
+ + +
+
+ + {#if loading} +
+
+ Loading notifications... +
+ {:else if error} +
+

Error loading notifications: {error}

+
+ {:else if notifications.length === 0} +
+

No notifications {notificationMode === "to-me" ? "received" : "sent"} found.

+
+ {:else} +
+
+ {#each notifications.slice(0, 10) as notification} + {@const authorProfile = authorProfiles.get(notification.pubkey)} +
+
+ +
+ {#if authorProfile?.picture} + Author avatar { + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> + {:else} +
+ + {(authorProfile?.displayName || authorProfile?.name || notification.pubkey.slice(0, 1)).toUpperCase()} + +
+ {/if} +
+ + +
+
+ + {getNotificationType(notification)} + + + {notification.created_at ? formatDate(notification.created_at) : "Unknown date"} + +
+ + +
+ + {authorProfile?.displayName || authorProfile?.name || `${notification.pubkey.slice(0, 8)}...${notification.pubkey.slice(-4)}`} + + {#if authorProfile?.name && authorProfile?.displayName && authorProfile.name !== authorProfile.displayName} + + (@{authorProfile.name}) + + {/if} +
+ + {#if notification.content} +
+ {truncateContent(notification.content)} +
+ {/if} + +
+ + + {getNeventUrl(notification).slice(0, 16)}... + +
+
+
+
+ {/each} +
+ + {#if notifications.length > 10} +
+ Showing 10 of {notifications.length} notifications {notificationMode === "to-me" ? "received" : "sent"}. Scroll to see more. +
+ {/if} +
+ {/if} +
+{/if} \ No newline at end of file From 1da3f6ba0d95b50276ff868ccc887171816265d8 Mon Sep 17 00:00:00 2001 From: silberengel Date: Wed, 6 Aug 2025 23:50:09 +0200 Subject: [PATCH 18/98] Worked on the notifications some more --- src/lib/components/EventDetails.svelte | 2 +- src/lib/components/Notifications.svelte | 687 ++++++++++++++---------- 2 files changed, 397 insertions(+), 292 deletions(-) diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index 3a8801e..dcdd0eb 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -454,7 +454,7 @@
Content: -
+
{@html showFullContent ? parsedContent : contentPreview} {#if !showFullContent && parsedContent.length > 250} - + +
+ {#each ["to-me", "from-me", "public-messages"] as mode} + {@const modeLabel = mode === "to-me" ? "To Me" : mode === "from-me" ? "From Me" : "Public Messages"} + + {/each}
{#if loading}
- Loading notifications... + + Loading {notificationMode === "public-messages" ? "public messages" : "notifications"}... +
{:else if error}
-

Error loading notifications: {error}

-
- {:else if notifications.length === 0} -
-

No notifications {notificationMode === "to-me" ? "received" : "sent"} found.

+

Error loading {notificationMode === "public-messages" ? "public messages" : "notifications"}: {error}

- {:else} -
-
- {#each notifications.slice(0, 10) as notification} - {@const authorProfile = authorProfiles.get(notification.pubkey)} -
-
- -
- {#if authorProfile?.picture} - Author avatar { - (e.target as HTMLImageElement).style.display = 'none'; - }} - /> - {:else} -
- - {(authorProfile?.displayName || authorProfile?.name || notification.pubkey.slice(0, 1)).toUpperCase()} + {:else if notificationMode === "public-messages"} + {#if publicMessages.length === 0} +
+

No public messages found.

+
+ {:else} +
+ {#if filteredByUser} +
+
+ + Filtered by user: {authorProfiles.get(filteredByUser)?.displayName || authorProfiles.get(filteredByUser)?.name || `${filteredByUser.slice(0, 8)}...${filteredByUser.slice(-4)}`} + + +
+
+ {/if} +
+ {#each filteredMessages.slice(0, 20) as message} + {@const authorProfile = authorProfiles.get(message.pubkey)} + {@const isFromUser = message.pubkey === $userStore.pubkey} +
+
+ +
+ {#if authorProfile?.picture} + Author avatar (e.target as HTMLImageElement).style.display = 'none'} + /> + {:else} +
+ + {(authorProfile?.displayName || authorProfile?.name || message.pubkey.slice(0, 1)).toUpperCase()} + +
+ {/if} + + + {#if !isFromUser} +
+ +
+ {/if} +
+ + +
+
+ + {isFromUser ? 'Your Message' : 'Public Message'} + + + {message.created_at ? formatDate(message.created_at) : "Unknown date"} + +
+ + +
+ + {authorProfile?.displayName || authorProfile?.name || `${message.pubkey.slice(0, 8)}...${message.pubkey.slice(-4)}`} + {#if authorProfile?.name && authorProfile?.displayName && authorProfile.name !== authorProfile.displayName} + + (@{authorProfile.name}) + + {/if}
- {/if} + + {#if message.content} +
+ {truncateContent(message.content)} +
+ {/if} + +
+ + + {getNeventUrl(message).slice(0, 16)}... + +
+
- - -
-
- - {getNotificationType(notification)} - - - {notification.created_at ? formatDate(notification.created_at) : "Unknown date"} - +
+ {/each} +
+ + {#if filteredMessages.length > 20} +
+ Showing 20 of {filteredMessages.length} messages {filteredByUser ? `(filtered)` : ''}. Scroll to see more. +
+ {/if} +
+ {/if} + {:else} + {#if notifications.length === 0} +
+

No notifications {notificationMode === "to-me" ? "received" : "sent"} found.

+
+ {:else} +
+
+ {#each notifications.slice(0, 10) as notification} + {@const authorProfile = authorProfiles.get(notification.pubkey)} +
+
+ +
+ {#if authorProfile?.picture} + Author avatar (e.target as HTMLImageElement).style.display = 'none'} + /> + {:else} +
+ + {(authorProfile?.displayName || authorProfile?.name || notification.pubkey.slice(0, 1)).toUpperCase()} + +
+ {/if}
- -
- - {authorProfile?.displayName || authorProfile?.name || `${notification.pubkey.slice(0, 8)}...${notification.pubkey.slice(-4)}`} - - {#if authorProfile?.name && authorProfile?.displayName && authorProfile.name !== authorProfile.displayName} - - (@{authorProfile.name}) + +
+
+ + {getNotificationType(notification)} + + {notification.created_at ? formatDate(notification.created_at) : "Unknown date"} + +
+ + +
+ + {authorProfile?.displayName || authorProfile?.name || `${notification.pubkey.slice(0, 8)}...${notification.pubkey.slice(-4)}`} + + {#if authorProfile?.name && authorProfile?.displayName && authorProfile.name !== authorProfile.displayName} + + (@{authorProfile.name}) + + {/if} +
+ + {#if notification.content} +
+ {truncateContent(notification.content)} +
{/if} -
- - {#if notification.content} -
- {truncateContent(notification.content)} + +
+ + + {getNeventUrl(notification).slice(0, 16)}... +
- {/if} - -
- - - {getNeventUrl(notification).slice(0, 16)}... -
+ {/each} +
+ + {#if notifications.length > 10} +
+ Showing 10 of {notifications.length} notifications {notificationMode === "to-me" ? "received" : "sent"}. Scroll to see more.
- {/each} + {/if}
- - {#if notifications.length > 10} -
- Showing 10 of {notifications.length} notifications {notificationMode === "to-me" ? "received" : "sent"}. Scroll to see more. -
- {/if} -
+ {/if} {/if}
{/if} \ No newline at end of file From 49de089e9a4e3c66eafdec29a2594a46e90fb453 Mon Sep 17 00:00:00 2001 From: silberengel Date: Fri, 8 Aug 2025 23:21:56 +0200 Subject: [PATCH 19/98] relay info display in public message replies --- src/lib/components/Notifications.svelte | 240 +++++++++++++++++++- src/lib/components/RelayInfoDisplay.svelte | 92 ++++++++ src/lib/components/RelayInfoList.svelte | 169 ++++++++++++++ src/lib/utils/kind24_utils.ts | 252 +++++++++++++++++++++ src/lib/utils/relay_info_service.ts | 166 ++++++++++++++ 5 files changed, 917 insertions(+), 2 deletions(-) create mode 100644 src/lib/components/RelayInfoDisplay.svelte create mode 100644 src/lib/components/RelayInfoList.svelte create mode 100644 src/lib/utils/kind24_utils.ts create mode 100644 src/lib/utils/relay_info_service.ts diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 867b881..062ecd4 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -11,6 +11,9 @@ import { get } from "svelte/store"; import { nip19 } from "nostr-tools"; import { communityRelays, localRelays } from "$lib/consts"; + import { createKind24Reply, getKind24RelaySet } from "$lib/utils/kind24_utils"; + import RelayDisplay from "$lib/components/RelayDisplay.svelte"; + import RelayInfoList from "$lib/components/RelayInfoList.svelte"; const { event } = $props<{ event: NDKEvent }>(); @@ -23,6 +26,14 @@ let notificationMode = $state<"to-me" | "from-me" | "public-messages">("to-me"); let authorProfiles = $state>(new Map()); let filteredByUser = $state(null); + let replyContent = $state(""); + let replyingTo = $state(null); + let isReplying = $state(false); + let originalMessage = $state(null); + let replyingToMessageId = $state(null); + let replyRelays = $state([]); + let senderOutboxRelays = $state([]); + let recipientInboxRelays = $state([]); // Derived state for filtered messages let filteredMessages = $derived.by(() => { @@ -85,6 +96,19 @@ return content.slice(0, maxLength) + "..."; } + function renderContentWithLinks(content: string): string { + console.log("[Notifications] Rendering content:", content); + + // Parse markdown links [text](url) and convert to HTML + let rendered = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + + // Also handle the new quote format: "> LINK: nevent://..." and convert to button + rendered = rendered.replace(/> LINK: (nevent:\/\/[^\s\n]+)/g, '> '); + + console.log("[Notifications] Rendered content:", rendered); + return rendered; + } + function getNotificationType(event: NDKEvent): string { switch (event.kind) { case 1: return "Reply"; @@ -109,6 +133,141 @@ filteredByUser = null; } + // AI-NOTE: Reply functionality for kind 24 messages + async function startReply(pubkey: string, messageEvent?: NDKEvent) { + replyingTo = pubkey; + isReplying = true; + replyContent = ""; + replyingToMessageId = messageEvent?.id || null; + // Store the original message for q tag + originalMessage = messageEvent || null; + // Clear previous relay information + replyRelays = []; + senderOutboxRelays = []; + recipientInboxRelays = []; + + // Immediately fetch relay information for this recipient + await getReplyRelays(); + } + + function cancelReply() { + replyingTo = null; + isReplying = false; + replyContent = ""; + replyingToMessageId = null; + replyRelays = []; + senderOutboxRelays = []; + recipientInboxRelays = []; + } + + async function sendReply() { + if (!replyingTo || !replyContent.trim()) return; + + try { + // Find the original message being replied to + const originalMessage = publicMessages.find(msg => msg.id === replyingToMessageId); + const result = await createKind24Reply(replyContent, replyingTo, originalMessage); + + if (result.success) { + // Store relay information for display + replyRelays = result.relays || []; + + // Update the inbox/outbox arrays to match the actual relays being used + // Keep only the top 3 that are actually in the reply relay set + const replyRelaySet = new Set(replyRelays); + senderOutboxRelays = senderOutboxRelays + .filter(relay => replyRelaySet.has(relay)) + .slice(0, 3); + recipientInboxRelays = recipientInboxRelays + .filter(relay => replyRelaySet.has(relay)) + .slice(0, 3); + + // Clear reply state + replyingTo = null; + isReplying = false; + replyContent = ""; + replyingToMessageId = null; + // Optionally refresh messages + await fetchPublicMessages(); + } else { + console.error("Failed to send reply:", result.error); + // You could show an error message to the user here + } + } catch (error) { + console.error("Error sending reply:", error); + } + } + + // Function to get relay information before sending + async function getReplyRelays() { + if (!replyingTo) return; + + try { + const originalMessage = publicMessages.find(msg => msg.id === replyingToMessageId); + + // Get sender's outbox relays and recipient's inbox relays + const ndk = get(ndkInstance); + if (ndk?.activeUser) { + // Get sender's outbox relays + const senderUser = ndk.activeUser; + const senderRelayList = await ndk.fetchEvent({ + kinds: [10002], + authors: [senderUser.pubkey], + }); + + if (senderRelayList) { + senderOutboxRelays = senderRelayList.tags + .filter(tag => tag[0] === 'r' && tag[1]) + .map(tag => tag[1]) + .slice(0, 3); // Limit to top 3 outbox relays + } + + // Get recipient's inbox relays + const recipientUser = ndk.getUser({ pubkey: replyingTo }); + const recipientRelayList = await ndk.fetchEvent({ + kinds: [10002], + authors: [replyingTo], + }); + + if (recipientRelayList) { + recipientInboxRelays = recipientRelayList.tags + .filter(tag => tag[0] === 'r' && tag[1]) + .map(tag => tag[1]) + .slice(0, 3); // Limit to top 3 inbox relays + } + } + + // If we have content, use the actual reply function + if (replyContent.trim()) { + const result = await createKind24Reply(replyContent, replyingTo, originalMessage); + replyRelays = result.relays || []; + } else { + // If no content yet, just get the relay set for this recipient + const result = await getKind24RelaySet($userStore.pubkey || '', replyingTo); + replyRelays = result || []; + + // Update the inbox/outbox arrays to match the actual relays being used + // Keep only the top 3 that are actually in the reply relay set + const replyRelaySet = new Set(replyRelays); + senderOutboxRelays = senderOutboxRelays + .filter(relay => replyRelaySet.has(relay)) + .slice(0, 3); + recipientInboxRelays = recipientInboxRelays + .filter(relay => replyRelaySet.has(relay)) + .slice(0, 3); + + console.log('[Notifications] Got relay set:', result); + console.log('[Notifications] Filtered sender outbox relays:', senderOutboxRelays); + console.log('[Notifications] Filtered recipient inbox relays:', recipientInboxRelays); + } + } catch (error) { + console.error("Error getting relay information:", error); + replyRelays = []; + senderOutboxRelays = []; + recipientInboxRelays = []; + } + } + // AI-NOTE: Simplified profile fetching with better error handling async function fetchAuthorProfiles(events: NDKEvent[]) { const uniquePubkeys = new Set(); @@ -321,6 +480,13 @@ authorProfiles.clear(); } }); + + // Fetch relay information when reply content changes (for updates) + $effect(() => { + if (isReplying && replyingTo && replyContent.trim() && replyRelays.length === 0) { + getReplyRelays(); + } + }); {#if isOwnProfile && $userStore.signedIn} @@ -400,7 +566,19 @@ {#if !isFromUser} -
+
+ + +
+ + + {#if isReplying && replyingToMessageId === message.id} + {@const recipientProfile = authorProfiles.get(message.pubkey)} +
+
+ + Replying to: {recipientProfile?.displayName || recipientProfile?.name || `${message.pubkey.slice(0, 8)}...${message.pubkey.slice(-4)}`} + + +
+
+ + +
+ + +
+ {#if replyRelays.length > 0} + {@const debugInfo = console.log('[Notifications] Rendering RelayInfoList with:', { replyRelays, recipientInboxRelays, senderOutboxRelays })} + + {:else} +
+
+ Loading relay information... +
+ {/if} +
+
+ {/if}
{/each}
diff --git a/src/lib/components/RelayInfoDisplay.svelte b/src/lib/components/RelayInfoDisplay.svelte new file mode 100644 index 0000000..c72dc24 --- /dev/null +++ b/src/lib/components/RelayInfoDisplay.svelte @@ -0,0 +1,92 @@ + + +
+ {#if showIcon && relayIcon} + Relay icon (e.target as HTMLImageElement).style.display = 'none'} + /> + {:else if showIcon} + +
+ + + +
+ {/if} + +
+ {#if showName} + + {isLoading ? 'Loading...' : displayName} + + {/if} + + {#if showType} + + {relayType} + + {/if} +
+ + {#if error} + + ⚠️ + + {/if} +
diff --git a/src/lib/components/RelayInfoList.svelte b/src/lib/components/RelayInfoList.svelte new file mode 100644 index 0000000..0443453 --- /dev/null +++ b/src/lib/components/RelayInfoList.svelte @@ -0,0 +1,169 @@ + + +
+ {#if showLabels && !compact} + {@const categorizedCount = categorizedRelays().length} + {@const debugCategorized = console.log('[RelayInfoList] Debug - categorized relays:', categorizedRelays())} +
+ Publishing to {categorizedCount} relay(s): +
+ {/if} + + {#if isLoading} +
+
+ Loading relay info... +
+ {:else} + {@const categorized = categorizedRelays()} + {@const debugCategorized = console.log('[RelayInfoList] Debug - categorized relays:', categorized)} + +
+ {#each categorized as { relay, category, label }} +
+
+ + {relay} + + {#if category === 'both'} + + common relay + + {/if} +
+
+ {/each} +
+ {/if} +
diff --git a/src/lib/utils/kind24_utils.ts b/src/lib/utils/kind24_utils.ts new file mode 100644 index 0000000..e3b6e14 --- /dev/null +++ b/src/lib/utils/kind24_utils.ts @@ -0,0 +1,252 @@ +import { get } from "svelte/store"; +import { ndkInstance } from "../ndk"; +import { userStore } from "../stores/userStore"; +import { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; +import type NDK from "@nostr-dev-kit/ndk"; +import { nip19 } from "nostr-tools"; + +/** + * 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 + */ +async function getUseroutboxRelays(ndk: NDK, user: NDKUser): Promise { + try { + console.debug('[kind24_utils] Fetching outbox relays for user:', user.pubkey); + const relayList = await ndk.fetchEvent( + { + kinds: [10002], + authors: [user.pubkey], + } + ); + + if (!relayList) { + console.debug('[kind24_utils] No relay list found for user'); + return []; + } + + console.debug('[kind24_utils] Found relay list event:', relayList.id); + console.debug('[kind24_utils] Relay list tags:', relayList.tags); + + const outboxRelays: string[] = []; + relayList.tags.forEach((tag) => { + console.debug('[kind24_utils] Processing tag:', tag); + if (tag[0] === 'r' && tag[1]) { + // NIP-65: r tags with optional inbox/outbox markers + const marker = tag[2]; + if (!marker || marker === 'outbox' || marker === 'inbox') { + // If no marker or marker is 'outbox', it's a outbox relay + // If marker is 'inbox', it's also a outbox relay (NIP-65 allows both) + outboxRelays.push(tag[1]); + console.debug('[kind24_utils] Added outbox relay:', tag[1]); + } + } + }); + + console.debug('[kind24_utils] Final outbox relays:', outboxRelays); + return outboxRelays; + } catch (error) { + console.info('[kind24_utils] Error fetching user outbox relays:', error); + return []; + } +} + +/** + * Fetches user's inbox relays from NIP-65 relay list + * @param ndk NDK instance + * @param user User to fetch inbox relays for + * @returns Promise that resolves to array of inbox relay URLs + */ +async function getUserinboxRelays(ndk: NDK, user: NDKUser): Promise { + try { + console.debug('[kind24_utils] Fetching inbox relays for user:', user.pubkey); + const relayList = await ndk.fetchEvent( + { + kinds: [10002], + authors: [user.pubkey], + } + ); + + if (!relayList) { + console.debug('[kind24_utils] No relay list found for user'); + return []; + } + + console.debug('[kind24_utils] Found relay list event:', relayList.id); + console.debug('[kind24_utils] Relay list tags:', relayList.tags); + + const inboxRelays: string[] = []; + relayList.tags.forEach((tag) => { + console.debug('[kind24_utils] Processing tag:', tag); + if (tag[0] === 'r' && tag[1]) { + // NIP-65: r tags with optional inbox/outbox markers + const marker = tag[2]; + if (!marker || marker === 'inbox' || marker === 'outbox') { + // If no marker or marker is 'inbox', it's a inbox relay + // If marker is 'outbox', it's also a inbox relay (NIP-65 allows both) + inboxRelays.push(tag[1]); + console.debug('[kind24_utils] Added inbox relay:', tag[1]); + } + } + }); + + console.debug('[kind24_utils] Final inbox relays:', inboxRelays); + return inboxRelays; + } catch (error) { + console.info('[kind24_utils] Error fetching user inbox relays:', error); + return []; + } +} + +/** + * Creates a kind 24 public message reply according to NIP-A4 + * @param content The message content + * @param recipientPubkey The recipient's pubkey + * @param originalEvent The original event being replied to (optional) + * @returns Promise resolving to publish result with relay information + */ +export async function createKind24Reply( + content: string, + recipientPubkey: string, + originalEvent?: NDKEvent +): Promise<{ success: boolean; eventId?: string; error?: string; relays?: string[] }> { + const ndk = get(ndkInstance); + if (!ndk?.activeUser) { + return { success: false, error: "Not logged in" }; + } + + if (!content.trim()) { + return { success: false, error: "Message content cannot be empty" }; + } + + try { + // Get sender's outbox relays (NIP-65) + const senderoutboxRelays = await getUseroutboxRelays(ndk, ndk.activeUser); + + // Get recipient's inbox relays (NIP-65) + const recipientUser = ndk.getUser({ pubkey: recipientPubkey }); + const recipientinboxRelays = await getUserinboxRelays(ndk, recipientUser); + + // According to NIP-A4: Messages MUST be sent to the NIP-65 inbox relays of each receiver + // and the outbox relay of the sender + const targetRelays = [...new Set([...senderoutboxRelays, ...recipientinboxRelays])]; + + // Prioritize common relays between sender and recipient for better privacy + const commonRelays = senderoutboxRelays.filter(relay => + recipientinboxRelays.includes(relay) + ); + const senderOnlyRelays = senderoutboxRelays.filter(relay => + !recipientinboxRelays.includes(relay) + ); + const recipientOnlyRelays = recipientinboxRelays.filter(relay => + !senderoutboxRelays.includes(relay) + ); + + // Prioritize: common relays first, then sender outbox, then recipient inbox + const prioritizedRelays = [...commonRelays, ...senderOnlyRelays, ...recipientOnlyRelays]; + + if (prioritizedRelays.length === 0) { + return { success: false, error: "No relays available for publishing" }; + } + + // Create the kind 24 event + const event = new NDKEvent(ndk); + event.kind = 24; + + // Build content with quoted message if replying + let finalContent = content; + if (originalEvent) { + // Use multiple relays for better discoverability + const nevent = nip19.neventEncode({ + id: originalEvent.id, + relays: prioritizedRelays.slice(0, 3) // Use first 3 relays + }); + const quotedContent = originalEvent.content ? originalEvent.content.slice(0, 200) : "No content"; + // Use a more visible quote format with a clickable link + finalContent = `> QUOTED: ${quotedContent}\n> LINK: ${nevent}\n\n${content}`; + console.log("[kind24_utils] Reply content:", finalContent); + } + + event.content = finalContent; + event.created_at = Math.floor(Date.now() / 1000); + + // Add p tag for recipient with relay URL + const tags: string[][] = [ + ["p", recipientPubkey, prioritizedRelays[0]] // Use first relay as primary + ]; + + // Add q tag if replying to an original event + if (originalEvent) { + const nevent = nip19.neventEncode({ + id: originalEvent.id, + relays: prioritizedRelays.slice(0, 3) // Use first 3 relays + }); + tags.push(["q", nevent, prioritizedRelays[0]]); + } + + event.tags = tags; + event.pubkey = ndk.activeUser.pubkey; + + // Sign the event + await event.sign(); + + // Publish to relays + const relaySet = NDKRelaySet.fromRelayUrls(prioritizedRelays, ndk); + const publishedToRelays = await event.publish(relaySet); + + if (publishedToRelays.size > 0) { + return { success: true, eventId: event.id, relays: prioritizedRelays }; + } else { + return { success: false, error: "Failed to publish to any relays", relays: prioritizedRelays }; + } + } catch (error) { + console.error("[kind24_utils] Error creating kind 24 reply:", error); + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error" + }; + } +} + +/** + * Gets optimal relay set for kind 24 messages between two users + * @param senderPubkey The sender's pubkey + * @param recipientPubkey The recipient's pubkey + * @returns Promise resolving to relay URLs prioritized by commonality + */ +export async function getKind24RelaySet( + senderPubkey: string, + recipientPubkey: string +): Promise { + const ndk = get(ndkInstance); + if (!ndk) { + throw new Error("NDK not available"); + } + + // Get sender's outbox relays (NIP-65) + const senderUser = ndk.getUser({ pubkey: senderPubkey }); + const senderoutboxRelays = await getUseroutboxRelays(ndk, senderUser); + + // Get recipient's inbox relays (NIP-65) + const recipientUser = ndk.getUser({ pubkey: recipientPubkey }); + const recipientinboxRelays = await getUserinboxRelays(ndk, recipientUser); + + // According to NIP-A4: Messages MUST be sent to the NIP-65 inbox relays of each receiver + // and the outbox relay of the sender + const targetRelays = [...new Set([...senderoutboxRelays, ...recipientinboxRelays])]; + + // Prioritize common relays between sender and recipient for better privacy + const commonRelays = senderoutboxRelays.filter((relay: string) => + recipientinboxRelays.includes(relay) + ); + const senderOnlyRelays = senderoutboxRelays.filter((relay: string) => + !recipientinboxRelays.includes(relay) + ); + const recipientOnlyRelays = recipientinboxRelays.filter((relay: string) => + !senderoutboxRelays.includes(relay) + ); + + // Prioritize: common relays first, then sender outbox, then recipient inbox + return [...commonRelays, ...senderOnlyRelays, ...recipientOnlyRelays]; +} diff --git a/src/lib/utils/relay_info_service.ts b/src/lib/utils/relay_info_service.ts new file mode 100644 index 0000000..8b978a0 --- /dev/null +++ b/src/lib/utils/relay_info_service.ts @@ -0,0 +1,166 @@ +/** + * Simplifies a URL by removing protocol and common prefixes + * @param url The URL to simplify + * @returns Simplified URL string + */ +function simplifyUrl(url: string): string { + try { + const urlObj = new URL(url); + return urlObj.hostname + (urlObj.port ? `:${urlObj.port}` : ''); + } catch { + // If URL parsing fails, return the original string + return url; + } +} + +export interface RelayInfo { + name?: string; + description?: string; + icon?: string; + pubkey?: string; + contact?: string; + supported_nips?: number[]; + software?: string; + version?: string; + tags?: string[]; + payments_url?: string; + limitation?: { + auth_required?: boolean; + payment_required?: boolean; + }; +} + +export interface RelayInfoWithMetadata extends RelayInfo { + url: string; + shortUrl: string; + hasNip11: boolean; + triedNip11: boolean; +} + +/** + * Fetches relay information using NIP-11 + * @param url The relay URL to fetch info for + * @returns Promise resolving to relay info or undefined if failed + */ +export async function fetchRelayInfo(url: string): Promise { + try { + // Convert WebSocket URL to HTTP URL for NIP-11 + const httpUrl = url.replace('ws://', 'http://').replace('wss://', 'https://'); + + const response = await fetch(httpUrl, { + headers: { + 'Accept': 'application/nostr+json', + 'User-Agent': 'Alexandria/1.0' + }, + // Add timeout to prevent hanging + signal: AbortSignal.timeout(5000) + }); + + if (!response.ok) { + console.warn(`[RelayInfo] HTTP ${response.status} for ${url}`); + return { + url, + shortUrl: simplifyUrl(url), + hasNip11: false, + triedNip11: true + }; + } + + const relayInfo = await response.json() as RelayInfo; + + return { + ...relayInfo, + url, + shortUrl: simplifyUrl(url), + hasNip11: Object.keys(relayInfo).length > 0, + triedNip11: true + }; + } catch (error) { + console.warn(`[RelayInfo] Failed to fetch info for ${url}:`, error); + return { + url, + shortUrl: simplifyUrl(url), + hasNip11: false, + triedNip11: true + }; + } +} + +/** + * Fetches relay information for multiple relays in parallel + * @param urls Array of relay URLs to fetch info for + * @returns Promise resolving to array of relay info objects + */ +export async function fetchRelayInfos(urls: string[]): Promise { + if (urls.length === 0) { + return []; + } + + const promises = urls.map(url => fetchRelayInfo(url)); + const results = await Promise.allSettled(promises); + + return results + .map(result => result.status === 'fulfilled' ? result.value : undefined) + .filter((info): info is RelayInfoWithMetadata => info !== undefined); +} + +/** + * Gets relay type label based on relay URL and info + * @param relayUrl The relay URL + * @param relayInfo Optional relay info + * @returns String describing the relay type + */ +export function getRelayTypeLabel(relayUrl: string, relayInfo?: RelayInfoWithMetadata): string { + // Check if it's a local relay + if (relayUrl.includes('localhost') || relayUrl.includes('127.0.0.1')) { + return 'Local'; + } + + // Check if it's a community relay + if (relayUrl.includes('nostr.band') || relayUrl.includes('noswhere.com') || + relayUrl.includes('damus.io') || relayUrl.includes('nostr.wine')) { + return 'Community'; + } + + // Check if it's a user's relay (likely inbox/outbox) + if (relayUrl.includes('relay.nsec.app') || relayUrl.includes('relay.snort.social')) { + return 'User'; + } + + // Use relay name if available + if (relayInfo?.name) { + return relayInfo.name; + } + + // Fallback to domain + try { + const domain = new URL(relayUrl).hostname; + return domain.replace('www.', ''); + } catch { + return 'Unknown'; + } +} + +/** + * Gets relay icon URL or fallback + * @param relayInfo Relay info object + * @param relayUrl Relay URL as fallback + * @returns Icon URL or undefined + */ +export function getRelayIcon(relayInfo?: RelayInfoWithMetadata, relayUrl?: string): string | undefined { + if (relayInfo?.icon) { + return relayInfo.icon; + } + + // Generate favicon URL from relay URL + if (relayUrl) { + try { + const url = new URL(relayUrl); + return `${url.protocol}//${url.hostname}/favicon.ico`; + } catch { + // Invalid URL, return undefined + } + } + + return undefined; +} From b6c30352e215fdd212e138d3259cd27e57ab1d77 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 00:08:22 +0200 Subject: [PATCH 20/98] gray bar navigation --- src/lib/components/Notifications.svelte | 123 +++++++++++++++++------- src/lib/components/RelayInfoList.svelte | 34 +------ src/lib/utils/kind24_utils.ts | 28 ++---- 3 files changed, 103 insertions(+), 82 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 062ecd4..1604532 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -17,6 +17,22 @@ const { event } = $props<{ event: NDKEvent }>(); + // Handle navigation events from quoted messages + $effect(() => { + if (typeof window !== 'undefined') { + const handleJumpToMessage = (e: Event) => { + const customEvent = e as CustomEvent; + jumpToMessageInFeed(customEvent.detail); + }; + + window.addEventListener('jump-to-message', handleJumpToMessage); + + return () => { + window.removeEventListener('jump-to-message', handleJumpToMessage); + }; + } + }); + // Component state let notifications = $state([]); let publicMessages = $state([]); @@ -76,7 +92,9 @@ function getNeventUrl(event: NDKEvent): string { const relays = getAvailableRelays(); - return neventEncode(event, relays); + const nevent = neventEncode(event, relays); + console.log('Generated nevent for event:', event.id, '→', nevent); + return nevent; } function formatDate(timestamp: number): string { @@ -97,15 +115,28 @@ } function renderContentWithLinks(content: string): string { - console.log("[Notifications] Rendering content:", content); - // Parse markdown links [text](url) and convert to HTML let rendered = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); - // Also handle the new quote format: "> LINK: nevent://..." and convert to button - rendered = rendered.replace(/> LINK: (nevent:\/\/[^\s\n]+)/g, '> '); + // Handle quote format and convert to small gray bars like Jumble + const patterns = [ + /> QUOTED: ([^•]*?) • LINK:\s*\n(nevent[^\s]*)/g, + /> QUOTED: ([^\n]*?)\n> LINK: (nevent[^\s]*)/g, + /> QUOTED: ([^•]*?) • LINK:\s*(nevent[^\s]*)/g, + ]; + + for (const pattern of patterns) { + const beforeReplace = rendered; + rendered = rendered.replace(pattern, (match, quotedText, neventUrl) => { + const encodedUrl = neventUrl.replace(/'/g, '''); + const cleanQuotedText = quotedText.trim(); + return `
${cleanQuotedText}
`; + }); + if (beforeReplace !== rendered) { + break; + } + } - console.log("[Notifications] Rendered content:", rendered); return rendered; } @@ -122,9 +153,43 @@ } function navigateToEvent(nevent: string) { + // Navigate to the events search page with this specific event goto(`/events?id=${nevent}`); } + function jumpToMessageInFeed(nevent: string) { + // Switch to public messages tab and scroll to the specific message + notificationMode = "public-messages"; + + // Try to find and scroll to the specific message + setTimeout(() => { + try { + // Decode the nevent to get the event ID + const decoded = nip19.decode(nevent); + if (decoded.type === 'nevent' && decoded.data.id) { + const eventId = decoded.data.id; + + // Find the message in our public messages + const targetMessage = publicMessages.find(msg => msg.id === eventId); + if (targetMessage) { + // Try to scroll to the element if it exists in the DOM + const element = document.querySelector(`[data-event-id="${eventId}"]`); + if (element) { + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + // Briefly highlight the message + element.classList.add('ring-2', 'ring-blue-500'); + setTimeout(() => { + element.classList.remove('ring-2', 'ring-blue-500'); + }, 2000); + } + } + } + } catch (error) { + console.warn('Failed to jump to message:', error); + } + }, 100); + } + function filterByUser(pubkey: string) { filteredByUser = filteredByUser === pubkey ? null : pubkey; } @@ -256,9 +321,7 @@ .filter(relay => replyRelaySet.has(relay)) .slice(0, 3); - console.log('[Notifications] Got relay set:', result); - console.log('[Notifications] Filtered sender outbox relays:', senderOutboxRelays); - console.log('[Notifications] Filtered recipient inbox relays:', recipientInboxRelays); + } } catch (error) { console.error("Error getting relay information:", error); @@ -545,7 +608,7 @@ {#each filteredMessages.slice(0, 20) as message} {@const authorProfile = authorProfiles.get(message.pubkey)} {@const isFromUser = message.pubkey === $userStore.pubkey} -
+
@@ -602,6 +665,13 @@ {message.created_at ? formatDate(message.created_at) : "Unknown date"} +
@@ -622,17 +692,7 @@
{/if} -
- - - {getNeventUrl(message).slice(0, 16)}... - -
+
@@ -676,7 +736,7 @@
{#if replyRelays.length > 0} - {@const debugInfo = console.log('[Notifications] Rendering RelayInfoList with:', { replyRelays, recipientInboxRelays, senderOutboxRelays })} + {notification.created_at ? formatDate(notification.created_at) : "Unknown date"} +
@@ -763,17 +830,7 @@
{/if} -
- - - {getNeventUrl(notification).slice(0, 16)}... - -
+
diff --git a/src/lib/components/RelayInfoList.svelte b/src/lib/components/RelayInfoList.svelte index 0443453..62d6b8b 100644 --- a/src/lib/components/RelayInfoList.svelte +++ b/src/lib/components/RelayInfoList.svelte @@ -25,18 +25,10 @@ label: string; }; - // AI-NOTE: Updated to show only top-3 inboxes and top-3 outboxes as intended + // Categorize relays by their function (inbox/outbox/both) const categorizedRelays = $derived(() => { const inbox = new Set(inboxRelays); const outbox = new Set(outboxRelays); - - console.log('[RelayInfoList] Categorizing relays:', { - relays: relays.length, - inboxRelays: inboxRelays.length, - outboxRelays: outboxRelays.length - }); - - // Create a map of all relays with their categories const relayCategories = new Map(); // Process inbox relays (up to top 3) @@ -58,29 +50,19 @@ } }); - // Only include relays that are actually in the top-3 lists - // This ensures we only show the intended top-3 inboxes and top-3 outboxes - const categorized = Array.from(relayCategories.values()); - console.log('[RelayInfoList] Categorized relays count:', categorized.length); - return categorized; + return Array.from(relayCategories.values()); }); - // Group by category + // Group by category for display const groupedRelays = $derived(() => { const categorized = categorizedRelays(); - console.log('[RelayInfoList] Grouping categorized relays'); - const groups = { + return { both: categorized.filter((r: CategorizedRelay) => r.category === 'both'), inbox: categorized.filter((r: CategorizedRelay) => r.category === 'inbox'), outbox: categorized.filter((r: CategorizedRelay) => r.category === 'outbox'), other: categorized.filter((r: CategorizedRelay) => r.category === 'other') }; - - console.log('[RelayInfoList] Grouped relays:', Object.fromEntries( - Object.entries(groups).map(([key, relays]) => [key, relays.length]) - )); - return groups; }); async function loadRelayInfos() { @@ -99,12 +81,6 @@ // Load relay info when categorized relays change $effect(() => { const categorized = categorizedRelays(); - console.log('[RelayInfoList] Categorized relays changed:', { - total: categorized.length, - byCategory: Object.fromEntries( - Object.entries(groupedRelays()).map(([key, relays]) => [key, relays.length]) - ) - }); if (categorized.length > 0) { loadRelayInfos(); } @@ -134,7 +110,6 @@
{#if showLabels && !compact} {@const categorizedCount = categorizedRelays().length} - {@const debugCategorized = console.log('[RelayInfoList] Debug - categorized relays:', categorizedRelays())}
Publishing to {categorizedCount} relay(s):
@@ -147,7 +122,6 @@
{:else} {@const categorized = categorizedRelays()} - {@const debugCategorized = console.log('[RelayInfoList] Debug - categorized relays:', categorized)}
{#each categorized as { relay, category, label }} diff --git a/src/lib/utils/kind24_utils.ts b/src/lib/utils/kind24_utils.ts index e3b6e14..62a27d0 100644 --- a/src/lib/utils/kind24_utils.ts +++ b/src/lib/utils/kind24_utils.ts @@ -13,7 +13,7 @@ import { nip19 } from "nostr-tools"; */ async function getUseroutboxRelays(ndk: NDK, user: NDKUser): Promise { try { - console.debug('[kind24_utils] Fetching outbox relays for user:', user.pubkey); + const relayList = await ndk.fetchEvent( { kinds: [10002], @@ -22,16 +22,11 @@ async function getUseroutboxRelays(ndk: NDK, user: NDKUser): Promise { ); if (!relayList) { - console.debug('[kind24_utils] No relay list found for user'); return []; } - console.debug('[kind24_utils] Found relay list event:', relayList.id); - console.debug('[kind24_utils] Relay list tags:', relayList.tags); - const outboxRelays: string[] = []; relayList.tags.forEach((tag) => { - console.debug('[kind24_utils] Processing tag:', tag); if (tag[0] === 'r' && tag[1]) { // NIP-65: r tags with optional inbox/outbox markers const marker = tag[2]; @@ -39,15 +34,15 @@ async function getUseroutboxRelays(ndk: NDK, user: NDKUser): Promise { // If no marker or marker is 'outbox', it's a outbox relay // If marker is 'inbox', it's also a outbox relay (NIP-65 allows both) outboxRelays.push(tag[1]); - console.debug('[kind24_utils] Added outbox relay:', tag[1]); + } } }); - console.debug('[kind24_utils] Final outbox relays:', outboxRelays); + return outboxRelays; } catch (error) { - console.info('[kind24_utils] Error fetching user outbox relays:', error); + return []; } } @@ -60,7 +55,7 @@ async function getUseroutboxRelays(ndk: NDK, user: NDKUser): Promise { */ async function getUserinboxRelays(ndk: NDK, user: NDKUser): Promise { try { - console.debug('[kind24_utils] Fetching inbox relays for user:', user.pubkey); + const relayList = await ndk.fetchEvent( { kinds: [10002], @@ -69,16 +64,11 @@ async function getUserinboxRelays(ndk: NDK, user: NDKUser): Promise { ); if (!relayList) { - console.debug('[kind24_utils] No relay list found for user'); return []; } - console.debug('[kind24_utils] Found relay list event:', relayList.id); - console.debug('[kind24_utils] Relay list tags:', relayList.tags); - const inboxRelays: string[] = []; relayList.tags.forEach((tag) => { - console.debug('[kind24_utils] Processing tag:', tag); if (tag[0] === 'r' && tag[1]) { // NIP-65: r tags with optional inbox/outbox markers const marker = tag[2]; @@ -86,15 +76,15 @@ async function getUserinboxRelays(ndk: NDK, user: NDKUser): Promise { // If no marker or marker is 'inbox', it's a inbox relay // If marker is 'outbox', it's also a inbox relay (NIP-65 allows both) inboxRelays.push(tag[1]); - console.debug('[kind24_utils] Added inbox relay:', tag[1]); + } } }); - console.debug('[kind24_utils] Final inbox relays:', inboxRelays); + return inboxRelays; } catch (error) { - console.info('[kind24_utils] Error fetching user inbox relays:', error); + return []; } } @@ -165,7 +155,7 @@ export async function createKind24Reply( const quotedContent = originalEvent.content ? originalEvent.content.slice(0, 200) : "No content"; // Use a more visible quote format with a clickable link finalContent = `> QUOTED: ${quotedContent}\n> LINK: ${nevent}\n\n${content}`; - console.log("[kind24_utils] Reply content:", finalContent); + } event.content = finalContent; From dab4995dcf717c58478e9f7d738d886721f19ddc Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 00:21:24 +0200 Subject: [PATCH 21/98] moved notifications to the top --- src/lib/components/EventDetails.svelte | 8 +++++--- src/lib/components/Notifications.svelte | 12 +++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index dcdd0eb..6e6712a 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -401,6 +401,11 @@ {/if} + + {#if event.kind === 0} + + {/if} +
{#if toNpub(event.pubkey)} - - - {/if} diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 1604532..37cf746 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -92,9 +92,7 @@ function getNeventUrl(event: NDKEvent): string { const relays = getAvailableRelays(); - const nevent = neventEncode(event, relays); - console.log('Generated nevent for event:', event.id, '→', nevent); - return nevent; + return neventEncode(event, relays); } function formatDate(timestamp: number): string { @@ -605,7 +603,7 @@
{/if}
- {#each filteredMessages.slice(0, 20) as message} + {#each filteredMessages.slice(0, 100) as message} {@const authorProfile = authorProfiles.get(message.pubkey)} {@const isFromUser = message.pubkey === $userStore.pubkey}
@@ -772,7 +770,7 @@ {:else}
- {#each notifications.slice(0, 10) as notification} + {#each notifications.slice(0, 100) as notification} {@const authorProfile = authorProfiles.get(notification.pubkey)}
@@ -837,9 +835,9 @@ {/each}
- {#if notifications.length > 10} + {#if notifications.length > 100}
- Showing 10 of {notifications.length} notifications {notificationMode === "to-me" ? "received" : "sent"}. Scroll to see more. + Showing 100 of {notifications.length} notifications {notificationMode === "to-me" ? "received" : "sent"}. Scroll to see more.
{/if}
From eb4c19a9d720171fe9cf1cb8975dafc87c11e58b Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 01:55:19 +0200 Subject: [PATCH 22/98] fixed multi-recipients --- src/lib/components/Notifications.svelte | 33 +++++++++++++------------ 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 37cf746..4488952 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -14,6 +14,9 @@ import { createKind24Reply, getKind24RelaySet } from "$lib/utils/kind24_utils"; import RelayDisplay from "$lib/components/RelayDisplay.svelte"; import RelayInfoList from "$lib/components/RelayInfoList.svelte"; + import { Modal, Button } from "flowbite-svelte"; + import { searchProfiles } from "$lib/utils/search_utility"; + import type { NostrProfile } from "$lib/utils/search_types"; const { event } = $props<{ event: NDKEvent }>(); @@ -570,7 +573,7 @@
{#if loading} -
+
Loading {notificationMode === "public-messages" ? "public messages" : "notifications"}... @@ -586,9 +589,9 @@

No public messages found.

{:else} -
+
{#if filteredByUser} -
+
Filtered by user: {authorProfiles.get(filteredByUser)?.displayName || authorProfiles.get(filteredByUser)?.name || `${filteredByUser.slice(0, 8)}...${filteredByUser.slice(-4)}`} @@ -602,11 +605,11 @@
{/if} -
+
{#each filteredMessages.slice(0, 100) as message} {@const authorProfile = authorProfiles.get(message.pubkey)} {@const isFromUser = message.pubkey === $userStore.pubkey} -
+
@@ -752,11 +755,11 @@
{/if}
- {/each} + {/each}
{#if filteredMessages.length > 20} -
+
Showing 20 of {filteredMessages.length} messages {filteredByUser ? `(filtered)` : ''}. Scroll to see more.
{/if} @@ -768,11 +771,10 @@

No notifications {notificationMode === "to-me" ? "received" : "sent"} found.

{:else} -
-
- {#each notifications.slice(0, 100) as notification} - {@const authorProfile = authorProfiles.get(notification.pubkey)} -
+
+ {#each notifications.slice(0, 100) as notification} + {@const authorProfile = authorProfiles.get(notification.pubkey)} +
@@ -831,12 +833,11 @@
-
- {/each} -
+
+ {/each} {#if notifications.length > 100} -
+
Showing 100 of {notifications.length} notifications {notificationMode === "to-me" ? "received" : "sent"}. Scroll to see more.
{/if} From 49752931173ec91000dc1846f24a08be7c3c22a3 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 07:49:55 +0200 Subject: [PATCH 23/98] made the kind 24 public messages ephemeral for 4 weeks --- src/lib/consts.ts | 2 ++ src/lib/utils/nostrEventService.ts | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/lib/consts.ts b/src/lib/consts.ts index 29f4502..f141e7b 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -51,5 +51,7 @@ export enum FeedType { UserRelays = "user", } +export const EXPIRATION_DURATION = 28 * 24 * 60 * 60; // 4 weeks in seconds + export const loginStorageKey = "alexandria/login/pubkey"; export const feedTypeStorageKey = "alexandria/feed/type"; diff --git a/src/lib/utils/nostrEventService.ts b/src/lib/utils/nostrEventService.ts index cdea5e1..459275c 100644 --- a/src/lib/utils/nostrEventService.ts +++ b/src/lib/utils/nostrEventService.ts @@ -3,6 +3,7 @@ import { getEventHash, signEvent, prefixNostrAddresses } from "./nostrUtils.ts"; import { get } from "svelte/store"; import { goto } from "$app/navigation"; import { EVENT_KINDS, TIME_CONSTANTS } from "./search_constants.ts"; +import { EXPIRATION_DURATION } from "../consts.ts"; import { ndkInstance } from "../ndk.ts"; import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk"; @@ -320,12 +321,19 @@ export async function createSignedEvent( ): Promise<{ id: string; sig: string; event: any }> { const prefixedContent = prefixNostrAddresses(content); + // Add expiration tag for kind 24 events (NIP-40) + const finalTags = [...tags]; + if (kind === 24) { + const expirationTimestamp = Math.floor(Date.now() / TIME_CONSTANTS.UNIX_TIMESTAMP_FACTOR) + EXPIRATION_DURATION; + finalTags.push(["expiration", String(expirationTimestamp)]); + } + const eventToSign = { kind: Number(kind), created_at: Number( Math.floor(Date.now() / TIME_CONSTANTS.UNIX_TIMESTAMP_FACTOR), ), - tags: tags.map((tag) => [ + tags: finalTags.map((tag) => [ String(tag[0]), String(tag[1]), String(tag[2] || ""), From 49e8293fbeae40f61b789abefefa5731fc94d122 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 08:01:56 +0200 Subject: [PATCH 24/98] unified the kind24 publishing --- src/lib/utils/kind24_utils.ts | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/lib/utils/kind24_utils.ts b/src/lib/utils/kind24_utils.ts index 62a27d0..edf362f 100644 --- a/src/lib/utils/kind24_utils.ts +++ b/src/lib/utils/kind24_utils.ts @@ -4,6 +4,7 @@ import { userStore } from "../stores/userStore"; import { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; import type NDK from "@nostr-dev-kit/ndk"; import { nip19 } from "nostr-tools"; +import { createSignedEvent } from "./nostrEventService.ts"; /** * Fetches user's outbox relays from NIP-65 relay list @@ -140,10 +141,6 @@ export async function createKind24Reply( return { success: false, error: "No relays available for publishing" }; } - // Create the kind 24 event - const event = new NDKEvent(ndk); - event.kind = 24; - // Build content with quoted message if replying let finalContent = content; if (originalEvent) { @@ -155,13 +152,9 @@ export async function createKind24Reply( const quotedContent = originalEvent.content ? originalEvent.content.slice(0, 200) : "No content"; // Use a more visible quote format with a clickable link finalContent = `> QUOTED: ${quotedContent}\n> LINK: ${nevent}\n\n${content}`; - } - event.content = finalContent; - event.created_at = Math.floor(Date.now() / 1000); - - // Add p tag for recipient with relay URL + // Build tags for the kind 24 event const tags: string[][] = [ ["p", recipientPubkey, prioritizedRelays[0]] // Use first relay as primary ]; @@ -175,11 +168,16 @@ export async function createKind24Reply( tags.push(["q", nevent, prioritizedRelays[0]]); } - event.tags = tags; - event.pubkey = ndk.activeUser.pubkey; - - // Sign the event - await event.sign(); + // Create and sign the event using the unified function (includes expiration tag) + const { event: signedEventData } = await createSignedEvent( + finalContent, + ndk.activeUser.pubkey, + 24, + tags + ); + + // Create NDKEvent from the signed event data + const event = new NDKEvent(ndk, signedEventData); // Publish to relays const relaySet = NDKRelaySet.fromRelayUrls(prioritizedRelays, ndk); From de183ed6ffd7cfe9af3f305eec52b3b19c09cf19 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 08:48:02 +0200 Subject: [PATCH 25/98] reinstate missing New Message button --- src/lib/components/Notifications.svelte | 754 ++++++++++++++++++------ src/lib/utils/profile_search.ts | 27 +- 2 files changed, 589 insertions(+), 192 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 4488952..45ee83e 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -17,6 +17,7 @@ import { Modal, Button } from "flowbite-svelte"; import { searchProfiles } from "$lib/utils/search_utility"; import type { NostrProfile } from "$lib/utils/search_types"; + import { PlusOutline } from "flowbite-svelte-icons"; const { event } = $props<{ event: NDKEvent }>(); @@ -45,14 +46,26 @@ let notificationMode = $state<"to-me" | "from-me" | "public-messages">("to-me"); let authorProfiles = $state>(new Map()); let filteredByUser = $state(null); - let replyContent = $state(""); - let replyingTo = $state(null); - let isReplying = $state(false); - let originalMessage = $state(null); - let replyingToMessageId = $state(null); - let replyRelays = $state([]); - let senderOutboxRelays = $state([]); - let recipientInboxRelays = $state([]); + + + // New Message Modal state + let showNewMessageModal = $state(false); + let newMessageContent = $state(""); + let selectedRecipients = $state([]); + let newMessageRelays = $state([]); + let isComposingMessage = $state(false); + let replyToMessage = $state(null); + let quotedContent = $state(""); + + // Recipient Selection Modal state + let showRecipientModal = $state(false); + let recipientSearch = $state(""); + let recipientResults = $state([]); + let recipientLoading = $state(false); + let recipientSearchInput = $state(); + let recipientSearchTimeout: ReturnType | null = null; + let recipientCommunityStatus: Record = $state({}); + let isRecipientSearching = $state(false); // Derived state for filtered messages let filteredMessages = $derived.by(() => { @@ -121,9 +134,10 @@ // Handle quote format and convert to small gray bars like Jumble const patterns = [ - /> QUOTED: ([^•]*?) • LINK:\s*\n(nevent[^\s]*)/g, - /> QUOTED: ([^\n]*?)\n> LINK: (nevent[^\s]*)/g, - /> QUOTED: ([^•]*?) • LINK:\s*(nevent[^\s]*)/g, + /> QUOTED: ([^•]*?) • LINK:\s*\n((?:nostr:)?nevent[^\s]*)/g, + /> QUOTED: ([^\n]*?)\n> LINK: ((?:nostr:)?nevent[^\s]*)/g, + /> QUOTED: ([^•]*?) • LINK:\s*((?:nostr:)?nevent[^\s]*)/g, + /> QUOTED: ([^•]*?) • LINK: ((?:nostr:)?nevent[^\s]*)/g, // Without optional whitespace ]; for (const pattern of patterns) { @@ -199,139 +213,301 @@ filteredByUser = null; } - // AI-NOTE: Reply functionality for kind 24 messages - async function startReply(pubkey: string, messageEvent?: NDKEvent) { - replyingTo = pubkey; - isReplying = true; - replyContent = ""; - replyingToMessageId = messageEvent?.id || null; - // Store the original message for q tag - originalMessage = messageEvent || null; - // Clear previous relay information - replyRelays = []; - senderOutboxRelays = []; - recipientInboxRelays = []; + + + // AI-NOTE: New Message Modal Functions + function openNewMessageModal(messageToReplyTo?: NDKEvent) { + showNewMessageModal = true; + newMessageContent = ""; + selectedRecipients = []; + newMessageRelays = []; + isComposingMessage = false; + replyToMessage = messageToReplyTo || null; - // Immediately fetch relay information for this recipient - await getReplyRelays(); + // If replying, set up the quote and pre-select all original recipients plus sender + if (messageToReplyTo) { + // Store clean content for UI display (no markdown formatting) + quotedContent = messageToReplyTo.content.length > 200 + ? messageToReplyTo.content.slice(0, 200) + "..." + : messageToReplyTo.content; + + // Collect all recipients: original sender + all p-tag recipients + const recipientPubkeys = new Set(); + + // Add the original sender + recipientPubkeys.add(messageToReplyTo.pubkey); + + // Add all p-tag recipients from the original message + const pTags = messageToReplyTo.getMatchingTags("p"); + pTags.forEach(tag => { + if (tag[1]) { + recipientPubkeys.add(tag[1]); + } + }); + + // Remove the current user from recipients (don't reply to yourself) + const currentUserPubkey = $userStore.pubkey; + if (currentUserPubkey) { + recipientPubkeys.delete(currentUserPubkey); + } + + // Build the recipient list with profile information + selectedRecipients = Array.from(recipientPubkeys).map(pubkey => { + const profile = authorProfiles.get(pubkey); + return { + pubkey: pubkey, + name: profile?.name || "", + displayName: profile?.displayName || "", + picture: profile?.picture || "", + about: "", // We don't store about in authorProfiles + nip05: "", // We don't store nip05 in authorProfiles + }; + }).filter(recipient => recipient.pubkey); // Ensure we have valid pubkeys + + console.log(`Pre-loaded ${selectedRecipients.length} recipients for reply:`, selectedRecipients.map(r => r.displayName || r.name || r.pubkey?.slice(0, 8))); + } else { + quotedContent = ""; + } + } + + function closeNewMessageModal() { + showNewMessageModal = false; + newMessageContent = ""; + selectedRecipients = []; + newMessageRelays = []; + isComposingMessage = false; + replyToMessage = null; + quotedContent = ""; + } + + // AI-NOTE: Recipient Selection Modal Functions + function openRecipientModal() { + showRecipientModal = true; + recipientSearch = ""; + recipientResults = []; + recipientLoading = false; + recipientCommunityStatus = {}; + isRecipientSearching = false; + // Focus the search input after a brief delay to ensure modal is rendered + setTimeout(() => { + recipientSearchInput?.focus(); + }, 100); } - function cancelReply() { - replyingTo = null; - isReplying = false; - replyContent = ""; - replyingToMessageId = null; - replyRelays = []; - senderOutboxRelays = []; - recipientInboxRelays = []; + function closeRecipientModal() { + showRecipientModal = false; + recipientSearch = ""; + recipientResults = []; + recipientLoading = false; + recipientCommunityStatus = {}; + isRecipientSearching = false; + + // Clear any pending search timeout + if (recipientSearchTimeout) { + clearTimeout(recipientSearchTimeout); + recipientSearchTimeout = null; + } } - async function sendReply() { - if (!replyingTo || !replyContent.trim()) return; + async function searchRecipients() { + if (!recipientSearch.trim()) { + recipientResults = []; + recipientCommunityStatus = {}; + return; + } + + // Prevent multiple concurrent searches + if (isRecipientSearching) { + return; + } + + console.log("Starting recipient search for:", recipientSearch.trim()); + + // Set loading state + recipientLoading = true; + isRecipientSearching = true; try { - // Find the original message being replied to - const originalMessage = publicMessages.find(msg => msg.id === replyingToMessageId); - const result = await createKind24Reply(replyContent, replyingTo, originalMessage); - - if (result.success) { - // Store relay information for display - replyRelays = result.relays || []; - - // Update the inbox/outbox arrays to match the actual relays being used - // Keep only the top 3 that are actually in the reply relay set - const replyRelaySet = new Set(replyRelays); - senderOutboxRelays = senderOutboxRelays - .filter(relay => replyRelaySet.has(relay)) - .slice(0, 3); - recipientInboxRelays = recipientInboxRelays - .filter(relay => replyRelaySet.has(relay)) - .slice(0, 3); - - // Clear reply state - replyingTo = null; - isReplying = false; - replyContent = ""; - replyingToMessageId = null; - // Optionally refresh messages - await fetchPublicMessages(); - } else { - console.error("Failed to send reply:", result.error); - // You could show an error message to the user here - } + console.log("Recipient search promise created, waiting for result..."); + const result = await searchProfiles(recipientSearch.trim()); + console.log("Recipient search completed, found profiles:", result.profiles.length); + console.log("Profile details:", result.profiles); + console.log("Community status:", result.Status); + + // Update state + recipientResults = result.profiles; + recipientCommunityStatus = result.Status; + + console.log( + "State updated - recipientResults length:", + recipientResults.length, + ); + console.log( + "State updated - recipientCommunityStatus keys:", + Object.keys(recipientCommunityStatus), + ); } catch (error) { - console.error("Error sending reply:", error); + console.error("Error searching recipients:", error); + recipientResults = []; + recipientCommunityStatus = {}; + } finally { + recipientLoading = false; + isRecipientSearching = false; + console.log( + "Recipient search finished - loading:", + recipientLoading, + "searching:", + isRecipientSearching, + ); + } + } + + // Reactive search with debouncing + $effect(() => { + // Clear existing timeout + if (recipientSearchTimeout) { + clearTimeout(recipientSearchTimeout); + } + + // If search is empty, clear results immediately + if (!recipientSearch.trim()) { + recipientResults = []; + recipientCommunityStatus = {}; + recipientLoading = false; + return; + } + + // Set loading state immediately for better UX + recipientLoading = true; + + // Debounce the search with 300ms delay + recipientSearchTimeout = setTimeout(() => { + searchRecipients(); + }, 300); + }); + + function selectRecipient(profile: NostrProfile) { + // Check if recipient is already selected + if (selectedRecipients.some(r => r.pubkey === profile.pubkey)) { + console.log("Recipient already selected:", profile.displayName || profile.name); + return; } + + // Add recipient to selection + selectedRecipients = [...selectedRecipients, profile]; + console.log("Selected recipient:", profile.displayName || profile.name); + + // Close the recipient modal (New Message modal stays open) + closeRecipientModal(); } - // Function to get relay information before sending - async function getReplyRelays() { - if (!replyingTo) return; + async function sendNewMessage() { + if (!newMessageContent.trim() || selectedRecipients.length === 0) return; try { - const originalMessage = publicMessages.find(msg => msg.id === replyingToMessageId); + isComposingMessage = true; - // Get sender's outbox relays and recipient's inbox relays - const ndk = get(ndkInstance); - if (ndk?.activeUser) { - // Get sender's outbox relays - const senderUser = ndk.activeUser; - const senderRelayList = await ndk.fetchEvent({ - kinds: [10002], - authors: [senderUser.pubkey], - }); - - if (senderRelayList) { - senderOutboxRelays = senderRelayList.tags - .filter(tag => tag[0] === 'r' && tag[1]) - .map(tag => tag[1]) - .slice(0, 3); // Limit to top 3 outbox relays - } - - // Get recipient's inbox relays - const recipientUser = ndk.getUser({ pubkey: replyingTo }); - const recipientRelayList = await ndk.fetchEvent({ - kinds: [10002], - authors: [replyingTo], - }); - - if (recipientRelayList) { - recipientInboxRelays = recipientRelayList.tags - .filter(tag => tag[0] === 'r' && tag[1]) - .map(tag => tag[1]) - .slice(0, 3); // Limit to top 3 inbox relays - } + // Create p-tags for all recipients + const pTags = selectedRecipients.map(recipient => ["p", recipient.pubkey!]); + + // Get all recipient pubkeys for relay calculation + const recipientPubkeys = selectedRecipients.map(r => r.pubkey!); + + // Calculate relay set using the same logic as kind24_utils + const senderPubkey = $userStore.pubkey; + if (!senderPubkey) { + throw new Error("No sender pubkey available"); } - // If we have content, use the actual reply function - if (replyContent.trim()) { - const result = await createKind24Reply(replyContent, replyingTo, originalMessage); - replyRelays = result.relays || []; + // Get relay sets for all recipients and combine them + const relaySetPromises = recipientPubkeys.map(recipientPubkey => + getKind24RelaySet(senderPubkey, recipientPubkey) + ); + const relaySets = await Promise.all(relaySetPromises); + + // Combine and deduplicate all relay sets + const allRelays = relaySets.flat(); + const uniqueRelays = [...new Set(allRelays)]; + newMessageRelays = uniqueRelays; + + // Create the kind 24 event with quoted content if replying + let finalContent = newMessageContent; + if (replyToMessage && quotedContent) { + // Generate the markdown quote format for the actual message + const neventUrl = getNeventUrl(replyToMessage); + const markdownQuote = `> QUOTED: ${quotedContent} • LINK: ${neventUrl}`; + finalContent = markdownQuote + "\n\n" + newMessageContent; + } + + const eventData = { + kind: 24, + content: finalContent, + tags: pTags, + pubkey: $userStore.pubkey || '', + created_at: Math.floor(Date.now() / 1000) + }; + + // Sign the event + let signedEvent; + if (typeof window !== "undefined" && window.nostr && window.nostr.signEvent) { + signedEvent = await window.nostr.signEvent(eventData); } else { - // If no content yet, just get the relay set for this recipient - const result = await getKind24RelaySet($userStore.pubkey || '', replyingTo); - replyRelays = result || []; - - // Update the inbox/outbox arrays to match the actual relays being used - // Keep only the top 3 that are actually in the reply relay set - const replyRelaySet = new Set(replyRelays); - senderOutboxRelays = senderOutboxRelays - .filter(relay => replyRelaySet.has(relay)) - .slice(0, 3); - recipientInboxRelays = recipientInboxRelays - .filter(relay => replyRelaySet.has(relay)) - .slice(0, 3); - + throw new Error("No signing method available"); + } + // Publish to relays using WebSocket pool like other components + const { WebSocketPool } = await import("$lib/data_structures/websocket_pool"); + let publishedToAny = false; + + for (const relayUrl of newMessageRelays) { + try { + const ws = await WebSocketPool.instance.acquire(relayUrl); + + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + WebSocketPool.instance.release(ws); + reject(new Error("Timeout")); + }, 5000); + + ws.onmessage = (e) => { + const [type, id, ok, message] = JSON.parse(e.data); + if (type === "OK" && id === signedEvent.id) { + clearTimeout(timeout); + if (ok) { + publishedToAny = true; + WebSocketPool.instance.release(ws); + resolve(); + } else { + WebSocketPool.instance.release(ws); + reject(new Error(message)); + } + } + }; + + ws.send(JSON.stringify(["EVENT", signedEvent])); + }); + } catch (e) { + console.warn(`Failed to publish to ${relayUrl}:`, e); + } + } + + if (publishedToAny) { + // Close modal and refresh messages + closeNewMessageModal(); + await fetchPublicMessages(); + } else { + throw new Error("Failed to publish to any relay"); } } catch (error) { - console.error("Error getting relay information:", error); - replyRelays = []; - senderOutboxRelays = []; - recipientInboxRelays = []; + console.error("Error sending new message:", error); + // You could show an error message to the user here + } finally { + isComposingMessage = false; } } + + // AI-NOTE: Simplified profile fetching with better error handling async function fetchAuthorProfiles(events: NDKEvent[]) { const uniquePubkeys = new Set(); @@ -545,10 +721,30 @@ } }); - // Fetch relay information when reply content changes (for updates) + + + // Calculate relay set when recipients change $effect(() => { - if (isReplying && replyingTo && replyContent.trim() && replyRelays.length === 0) { - getReplyRelays(); + const senderPubkey = $userStore.pubkey; + if (selectedRecipients.length > 0 && senderPubkey) { + const recipientPubkeys = selectedRecipients.map(r => r.pubkey!); + + // Get relay sets for all recipients and combine them + const relaySetPromises = recipientPubkeys.map(recipientPubkey => + getKind24RelaySet(senderPubkey, recipientPubkey) + ); + + Promise.all(relaySetPromises).then(relaySets => { + // Combine and deduplicate all relay sets + const allRelays = relaySets.flat(); + const uniqueRelays = [...new Set(allRelays)]; + newMessageRelays = uniqueRelays; + }).catch(error => { + console.error("Error getting relay set:", error); + newMessageRelays = []; + }); + } else { + newMessageRelays = []; } }); @@ -558,6 +754,18 @@
Notifications +
+ + +
{#each ["to-me", "from-me", "public-messages"] as mode} @@ -569,6 +777,7 @@ {modeLabel} {/each} +
@@ -634,12 +843,16 @@ @@ -697,63 +910,6 @@
- - {#if isReplying && replyingToMessageId === message.id} - {@const recipientProfile = authorProfiles.get(message.pubkey)} -
-
- - Replying to: {recipientProfile?.displayName || recipientProfile?.name || `${message.pubkey.slice(0, 8)}...${message.pubkey.slice(-4)}`} - - -
-
- - -
- - -
- {#if replyRelays.length > 0} - - - {:else} -
-
- Loading relay information... -
- {/if} -
-
- {/if}
{/each}
@@ -845,4 +1001,236 @@ {/if} {/if}
+ + + +
+
+

+ {replyToMessage ? 'Reply to Message' : 'New Public Message'} +

+
+ + + {#if quotedContent} +
+
Replying to:
+
+ {@html renderContentWithLinks(quotedContent)} +
+
+ {/if} + + +
+
+ + Sending to {selectedRecipients.length} recipient{selectedRecipients.length !== 1 ? 's' : ''}: + + +
+ + {#if selectedRecipients.length === 0} +
+

+ No recipients selected. Click "Edit Recipients" to add recipients. +

+
+ {:else} +
+ {#each selectedRecipients as recipient} + + {recipient.displayName || recipient.name || `${recipient.pubkey?.slice(0, 8)}...`} + + + {/each} +
+ {/if} +
+ + + {#if selectedRecipients.length > 0 && newMessageRelays.length > 0} +
+ + Publishing to {newMessageRelays.length} relay{newMessageRelays.length !== 1 ? 's' : ''}: + +
+
+ {#each newMessageRelays as relay} +
+ {relay} +
+ {/each} +
+
+
+ {/if} + + +
+ + +
+ + +
+ + +
+
+
+ + + +
+
+

Select Recipients

+
+ +
+
+ + {#if recipientLoading} +
+
+
+ {/if} +
+ + {#if recipientResults.length > 0} +
+
    + {#each recipientResults as profile} + {@const isAlreadySelected = selectedRecipients.some(r => r.pubkey === profile.pubkey)} + + {/each} +
+
+ {:else if recipientSearch.trim()} +
No results found
+ {:else} +
+ Enter a search term to find users +
+ {/if} +
+
+
{/if} \ No newline at end of file diff --git a/src/lib/utils/profile_search.ts b/src/lib/utils/profile_search.ts index eeac332..ecf43ec 100644 --- a/src/lib/utils/profile_search.ts +++ b/src/lib/utils/profile_search.ts @@ -1,8 +1,8 @@ -import { ndkInstance } from "../ndk.ts"; +import { ndkInstance, activeInboxRelays } from "../ndk.ts"; import { getUserMetadata, getNpubFromNip05 } from "./nostrUtils.ts"; import NDK, { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk"; import { searchCache } from "./searchCache.ts"; -import { communityRelays, secondaryRelays } from "../consts.ts"; +import { searchRelays, communityRelays, secondaryRelays } from "../consts.ts"; import { get } from "svelte/store"; import type { NostrProfile, ProfileSearchResult } from "./search_types.ts"; import { @@ -264,12 +264,21 @@ async function quickRelaySearch( const normalizedSearchTerm = normalizeSearchTerm(searchTerm); console.log("Normalized search term for relay search:", normalizedSearchTerm); - // Use all profile relays for better coverage - const quickRelayUrls = [...communityRelays, ...secondaryRelays]; // Use all available relays - console.log("Using all relays for search:", quickRelayUrls); + // Use search relays (optimized for profiles) + user's inbox relays + community relays + const userInboxRelays = get(activeInboxRelays); + const quickRelayUrls = [ + ...searchRelays, // Dedicated profile search relays + ...userInboxRelays, // User's personal inbox relays + ...communityRelays, // Community relays + ...secondaryRelays // Secondary relays as fallback + ]; + + // Deduplicate relay URLs + const uniqueRelayUrls = [...new Set(quickRelayUrls)]; + console.log("Using relays for profile search:", uniqueRelayUrls); // Create relay sets for parallel search - const relaySets = quickRelayUrls + const relaySets = uniqueRelayUrls .map((url) => { try { return NDKRelaySet.fromRelayUrls([url], ndk); @@ -289,7 +298,7 @@ async function quickRelaySearch( let eventCount = 0; console.log( - `Starting search on relay ${index + 1}: ${quickRelayUrls[index]}`, + `Starting search on relay ${index + 1}: ${uniqueRelayUrls[index]}`, ); const sub = ndk.subscribe( @@ -354,7 +363,7 @@ async function quickRelaySearch( sub.on("eose", () => { console.log( - `Relay ${index + 1} (${quickRelayUrls[index]}) search completed, processed ${eventCount} events, found ${foundInRelay.length} matches`, + `Relay ${index + 1} (${uniqueRelayUrls[index]}) search completed, processed ${eventCount} events, found ${foundInRelay.length} matches`, ); resolve(foundInRelay); }); @@ -362,7 +371,7 @@ async function quickRelaySearch( // Short timeout for quick search setTimeout(() => { console.log( - `Relay ${index + 1} (${quickRelayUrls[index]}) search timed out after 1.5s, processed ${eventCount} events, found ${foundInRelay.length} matches`, + `Relay ${index + 1} (${uniqueRelayUrls[index]}) search timed out after 1.5s, processed ${eventCount} events, found ${foundInRelay.length} matches`, ); sub.stop(); resolve(foundInRelay); From a10f62c1e1d4d2eac4821db19bbc40493b83d0d2 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 09:12:05 +0200 Subject: [PATCH 26/98] show all 100 messages --- src/lib/components/Notifications.svelte | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 45ee83e..548896b 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -17,7 +17,7 @@ import { Modal, Button } from "flowbite-svelte"; import { searchProfiles } from "$lib/utils/search_utility"; import type { NostrProfile } from "$lib/utils/search_types"; - import { PlusOutline } from "flowbite-svelte-icons"; + import { PlusOutline, ReplyOutline } from "flowbite-svelte-icons"; const { event } = $props<{ event: NDKEvent }>(); @@ -410,6 +410,13 @@ // Create p-tags for all recipients const pTags = selectedRecipients.map(recipient => ["p", recipient.pubkey!]); + // Add q tag if replying to a message (for jump-to functionality) + if (replyToMessage) { + // Get the first relay from newMessageRelays or use a fallback + const relayUrl = newMessageRelays[0] || "wss://freelay.sovbit.host/"; + pTags.push(["q", replyToMessage.id, relayUrl, replyToMessage.pubkey]); + } + // Get all recipient pubkeys for relay calculation const recipientPubkeys = selectedRecipients.map(r => r.pubkey!); @@ -851,9 +858,7 @@ title="Reply to this message" aria-label="Reply to this message" > - - - +
From 493c08daeeb9f7d7c8cc7eb7f98c1e8a75d4d5d3 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 09:51:07 +0200 Subject: [PATCH 27/98] fixed jump quotes --- src/lib/components/Notifications.svelte | 55 ++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 548896b..136917e 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -128,6 +128,59 @@ return content.slice(0, maxLength) + "..."; } + function truncateRenderedContent(renderedHtml: string, maxLength: number = 300): string { + // If the rendered HTML is short enough, return as-is + if (renderedHtml.length <= maxLength) return renderedHtml; + + // Check if there are any gray quote boxes (jump-to-message divs) + const hasQuoteBoxes = renderedHtml.includes('jump-to-message'); + + if (hasQuoteBoxes) { + // Split content into quote boxes and regular text + const quoteBoxPattern = /
]*>[^<]*<\/div>/g; + const quoteBoxes = renderedHtml.match(quoteBoxPattern) || []; + + // Remove quote boxes temporarily to measure text length + let textOnly = renderedHtml.replace(quoteBoxPattern, '|||QUOTEBOX|||'); + + // If text without quote boxes is still too long, truncate it + if (textOnly.length > maxLength) { + const availableLength = maxLength - (quoteBoxes.join('').length); + if (availableLength > 50) { // Leave some reasonable space for text + textOnly = textOnly.slice(0, availableLength) + "..."; + } else { + // If quote boxes take up too much space, just show them with minimal text + textOnly = textOnly.slice(0, 50) + "..."; + } + } + + // Restore quote boxes + let result = textOnly; + quoteBoxes.forEach(box => { + result = result.replace('|||QUOTEBOX|||', box); + }); + + return result; + } else { + // No quote boxes, simple truncation with HTML awareness + if (renderedHtml.includes('<')) { + // Has HTML tags, do a simple truncation but try to avoid breaking tags + const truncated = renderedHtml.slice(0, maxLength); + const lastTagStart = truncated.lastIndexOf('<'); + const lastTagEnd = truncated.lastIndexOf('>'); + + if (lastTagStart > lastTagEnd) { + // We're in the middle of a tag, truncate before it + return renderedHtml.slice(0, lastTagStart) + "..."; + } + return truncated + "..."; + } else { + // Plain text, simple truncation + return renderedHtml.slice(0, maxLength) + "..."; + } + } + } + function renderContentWithLinks(content: string): string { // Parse markdown links [text](url) and convert to HTML let rendered = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); @@ -907,7 +960,7 @@ {#if message.content}
- {@html renderContentWithLinks(truncateContent(message.content))} + {@html truncateRenderedContent(renderContentWithLinks(message.content), 300)}
{/if} From e8ce9bb8eb5289d849fdc92b419630ea31453423 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 10:25:24 +0200 Subject: [PATCH 28/98] fix inbox/outbox selection --- src/lib/components/Notifications.svelte | 74 ++++++++++++++++++++++--- src/lib/utils/kind24_utils.ts | 54 +++++++++++++----- 2 files changed, 107 insertions(+), 21 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 136917e..8c6b992 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -460,8 +460,22 @@ try { isComposingMessage = true; - // Create p-tags for all recipients - const pTags = selectedRecipients.map(recipient => ["p", recipient.pubkey!]); + // Create p-tags for all recipients (ensure hex format) + const pTags = selectedRecipients.map(recipient => { + let pubkey = recipient.pubkey!; + // Convert npub to hex if needed + if (pubkey.startsWith('npub')) { + try { + const decoded = nip19.decode(pubkey); + if (decoded.type === 'npub') { + pubkey = decoded.data; + } + } catch (e) { + console.warn("[Send Message] Failed to decode npub:", pubkey, e); + } + } + return ["p", pubkey]; + }); // Add q tag if replying to a message (for jump-to functionality) if (replyToMessage) { @@ -470,8 +484,22 @@ pTags.push(["q", replyToMessage.id, relayUrl, replyToMessage.pubkey]); } - // Get all recipient pubkeys for relay calculation - const recipientPubkeys = selectedRecipients.map(r => r.pubkey!); + // Get all recipient pubkeys for relay calculation (ensure hex format) + const recipientPubkeys = selectedRecipients.map(r => { + let pubkey = r.pubkey!; + // Convert npub to hex if needed + if (pubkey.startsWith('npub')) { + try { + const decoded = nip19.decode(pubkey); + if (decoded.type === 'npub') { + pubkey = decoded.data; + } + } catch (e) { + console.warn("[Send Message Relay Calc] Failed to decode npub:", pubkey, e); + } + } + return pubkey; + }); // Calculate relay set using the same logic as kind24_utils const senderPubkey = $userStore.pubkey; @@ -786,8 +814,25 @@ // Calculate relay set when recipients change $effect(() => { const senderPubkey = $userStore.pubkey; + console.log("[Relay Effect] Recipients changed:", selectedRecipients.length, "Sender:", senderPubkey?.slice(0, 8)); + if (selectedRecipients.length > 0 && senderPubkey) { - const recipientPubkeys = selectedRecipients.map(r => r.pubkey!); + const recipientPubkeys = selectedRecipients.map(r => { + const pubkey = r.pubkey!; + // Convert npub to hex if needed + if (pubkey.startsWith('npub')) { + try { + const decoded = nip19.decode(pubkey); + if (decoded.type === 'npub') { + return decoded.data; + } + } catch (e) { + console.warn("[Relay Effect] Failed to decode npub:", pubkey, e); + } + } + return pubkey; + }); + console.log("[Relay Effect] Getting relay sets for recipients (hex):", recipientPubkeys.map(p => p.slice(0, 8))); // Get relay sets for all recipients and combine them const relaySetPromises = recipientPubkeys.map(recipientPubkey => @@ -795,15 +840,28 @@ ); Promise.all(relaySetPromises).then(relaySets => { + console.log("[Relay Effect] Received relay sets:", relaySets); // Combine and deduplicate all relay sets const allRelays = relaySets.flat(); const uniqueRelays = [...new Set(allRelays)]; - newMessageRelays = uniqueRelays; + console.log("[Relay Effect] Final relay list:", uniqueRelays); + + // If no relays found from NIP-65, use fallback relays + if (uniqueRelays.length === 0) { + console.log("[Relay Effect] No NIP-65 relays found, using fallback"); + const fallbackRelays = getAvailableRelays(); + newMessageRelays = fallbackRelays.slice(0, 5); // Limit to first 5 for performance + } else { + newMessageRelays = uniqueRelays; + } }).catch(error => { - console.error("Error getting relay set:", error); - newMessageRelays = []; + console.error("[Relay Effect] Error getting relay set:", error); + console.log("[Relay Effect] Using fallback relays due to error"); + const fallbackRelays = getAvailableRelays(); + newMessageRelays = fallbackRelays.slice(0, 5); }); } else { + console.log("[Relay Effect] Clearing relays - no recipients or sender"); newMessageRelays = []; } }); diff --git a/src/lib/utils/kind24_utils.ts b/src/lib/utils/kind24_utils.ts index edf362f..9d1271e 100644 --- a/src/lib/utils/kind24_utils.ts +++ b/src/lib/utils/kind24_utils.ts @@ -31,12 +31,11 @@ async function getUseroutboxRelays(ndk: NDK, user: NDKUser): Promise { if (tag[0] === 'r' && tag[1]) { // NIP-65: r tags with optional inbox/outbox markers const marker = tag[2]; - if (!marker || marker === 'outbox' || marker === 'inbox') { - // If no marker or marker is 'outbox', it's a outbox relay - // If marker is 'inbox', it's also a outbox relay (NIP-65 allows both) + if (!marker || marker === 'outbox' || marker === 'both') { + // If no marker, marker is 'outbox', or marker is 'both', it's an outbox relay outboxRelays.push(tag[1]); - } + // Note: inbox-only relays are NOT included in outbox relays } }); @@ -56,6 +55,7 @@ async function getUseroutboxRelays(ndk: NDK, user: NDKUser): Promise { */ async function getUserinboxRelays(ndk: NDK, user: NDKUser): Promise { try { + console.log(`[getUserinboxRelays] Fetching kind 10002 for user: ${user.pubkey.slice(0, 8)}`); const relayList = await ndk.fetchEvent( { @@ -65,27 +65,31 @@ async function getUserinboxRelays(ndk: NDK, user: NDKUser): Promise { ); if (!relayList) { + console.log(`[getUserinboxRelays] No kind 10002 relay list found for user: ${user.pubkey.slice(0, 8)}`); return []; } + console.log(`[getUserinboxRelays] Found relay list for user: ${user.pubkey.slice(0, 8)}, tags:`, relayList.tags); + const inboxRelays: string[] = []; relayList.tags.forEach((tag) => { if (tag[0] === 'r' && tag[1]) { // NIP-65: r tags with optional inbox/outbox markers const marker = tag[2]; - if (!marker || marker === 'inbox' || marker === 'outbox') { - // If no marker or marker is 'inbox', it's a inbox relay - // If marker is 'outbox', it's also a inbox relay (NIP-65 allows both) + console.log(`[getUserinboxRelays] Processing relay tag:`, tag, `marker: ${marker}`); + if (!marker || marker === 'inbox' || marker === 'both') { + // If no marker, marker is 'inbox', or marker is 'both', it's an inbox relay inboxRelays.push(tag[1]); - + console.log(`[getUserinboxRelays] Added inbox relay: ${tag[1]} (marker: ${marker || 'none'})`); } + // Note: outbox-only relays are NOT included in inbox relays } }); - + console.log(`[getUserinboxRelays] Final inbox relays for user ${user.pubkey.slice(0, 8)}:`, inboxRelays); return inboxRelays; } catch (error) { - + console.error(`[getUserinboxRelays] Error fetching inbox relays for user ${user.pubkey.slice(0, 8)}:`, error); return []; } } @@ -117,7 +121,13 @@ export async function createKind24Reply( // Get recipient's inbox relays (NIP-65) const recipientUser = ndk.getUser({ pubkey: recipientPubkey }); - const recipientinboxRelays = await getUserinboxRelays(ndk, recipientUser); + let recipientinboxRelays = await getUserinboxRelays(ndk, recipientUser); + + // Fallback: if no inbox relays found, use recipient's outbox relays + if (recipientinboxRelays.length === 0) { + console.log(`[createKind24Reply] No inbox relays found for recipient, falling back to outbox relays`); + recipientinboxRelays = await getUseroutboxRelays(ndk, recipientUser); + } // According to NIP-A4: Messages MUST be sent to the NIP-65 inbox relays of each receiver // and the outbox relay of the sender @@ -212,17 +222,29 @@ export async function getKind24RelaySet( throw new Error("NDK not available"); } + console.log(`[getKind24RelaySet] Getting relays for sender: ${senderPubkey.slice(0, 8)} -> recipient: ${recipientPubkey.slice(0, 8)}`); + // Get sender's outbox relays (NIP-65) const senderUser = ndk.getUser({ pubkey: senderPubkey }); const senderoutboxRelays = await getUseroutboxRelays(ndk, senderUser); + console.log(`[getKind24RelaySet] Sender outbox relays:`, senderoutboxRelays); // Get recipient's inbox relays (NIP-65) const recipientUser = ndk.getUser({ pubkey: recipientPubkey }); - const recipientinboxRelays = await getUserinboxRelays(ndk, recipientUser); + let recipientinboxRelays = await getUserinboxRelays(ndk, recipientUser); + console.log(`[getKind24RelaySet] Recipient inbox relays:`, recipientinboxRelays); + + // Fallback: if no inbox relays found, use recipient's outbox relays + if (recipientinboxRelays.length === 0) { + console.log(`[getKind24RelaySet] No inbox relays found for recipient, falling back to outbox relays`); + recipientinboxRelays = await getUseroutboxRelays(ndk, recipientUser); + console.log(`[getKind24RelaySet] Recipient outbox relays (used as fallback):`, recipientinboxRelays); + } // According to NIP-A4: Messages MUST be sent to the NIP-65 inbox relays of each receiver // and the outbox relay of the sender const targetRelays = [...new Set([...senderoutboxRelays, ...recipientinboxRelays])]; + console.log(`[getKind24RelaySet] Combined target relays:`, targetRelays); // Prioritize common relays between sender and recipient for better privacy const commonRelays = senderoutboxRelays.filter((relay: string) => @@ -235,6 +257,12 @@ export async function getKind24RelaySet( !senderoutboxRelays.includes(relay) ); + console.log(`[getKind24RelaySet] Common relays:`, commonRelays); + console.log(`[getKind24RelaySet] Sender-only relays:`, senderOnlyRelays); + console.log(`[getKind24RelaySet] Recipient-only relays:`, recipientOnlyRelays); + // Prioritize: common relays first, then sender outbox, then recipient inbox - return [...commonRelays, ...senderOnlyRelays, ...recipientOnlyRelays]; + const finalRelays = [...commonRelays, ...senderOnlyRelays, ...recipientOnlyRelays]; + console.log(`[getKind24RelaySet] Final relay list:`, finalRelays); + return finalRelays; } From d31b8bfc13e3333aa5c370e567e35208c5bb8256 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 10:29:15 +0200 Subject: [PATCH 29/98] got rid of noise --- src/lib/utils.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 18fad03..60237f8 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -20,13 +20,6 @@ export class InvalidKindError extends DecodeError { export function neventEncode(event: NDKEvent, relays: string[]) { try { - console.log(`[neventEncode] Encoding event:`, { - id: event.id, - kind: event.kind, - pubkey: event.pubkey, - relayCount: relays.length - }); - const nevent = nip19.neventEncode({ id: event.id, kind: event.kind, @@ -34,7 +27,6 @@ export function neventEncode(event: NDKEvent, relays: string[]) { author: event.pubkey, }); - console.log(`[neventEncode] Generated nevent:`, nevent); return nevent; } catch (error) { console.error(`[neventEncode] Error encoding nevent:`, error); From e39febc25fc8dd811b50b1139f080e4c9e842201 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 9 Aug 2025 10:37:55 +0200 Subject: [PATCH 30/98] reinstate ephemeral tag --- src/lib/components/Notifications.svelte | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 8c6b992..fa8e8f4 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -12,6 +12,7 @@ import { nip19 } from "nostr-tools"; import { communityRelays, localRelays } from "$lib/consts"; import { createKind24Reply, getKind24RelaySet } from "$lib/utils/kind24_utils"; + import { createSignedEvent } from "$lib/utils/nostrEventService"; import RelayDisplay from "$lib/components/RelayDisplay.svelte"; import RelayInfoList from "$lib/components/RelayInfoList.svelte"; import { Modal, Button } from "flowbite-svelte"; @@ -527,21 +528,13 @@ finalContent = markdownQuote + "\n\n" + newMessageContent; } - const eventData = { - kind: 24, - content: finalContent, - tags: pTags, - pubkey: $userStore.pubkey || '', - created_at: Math.floor(Date.now() / 1000) - }; - - // Sign the event - let signedEvent; - if (typeof window !== "undefined" && window.nostr && window.nostr.signEvent) { - signedEvent = await window.nostr.signEvent(eventData); - } else { - throw new Error("No signing method available"); - } + // Create and sign the event using the unified function (includes expiration tag) + const { event: signedEvent } = await createSignedEvent( + finalContent, + $userStore.pubkey || '', + 24, + pTags + ); // Publish to relays using WebSocket pool like other components const { WebSocketPool } = await import("$lib/data_structures/websocket_pool"); From 9ea1462bcb2b1e0af22cde65dee2665b0f81a461 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 10 Aug 2025 15:11:55 +0200 Subject: [PATCH 31/98] Fixed reactivity inefficiency --- src/lib/components/Notifications.svelte | 65 +++++++++++++++---------- src/routes/+layout.svelte | 16 +++--- src/routes/events/+page.svelte | 16 +++--- 3 files changed, 58 insertions(+), 39 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index fa8e8f4..f46607a 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -804,18 +804,32 @@ - // Calculate relay set when recipients change + // AI-NOTE: Refactored to avoid blocking $effect with async operations + // Calculate relay set when recipients change - non-blocking approach $effect(() => { const senderPubkey = $userStore.pubkey; console.log("[Relay Effect] Recipients changed:", selectedRecipients.length, "Sender:", senderPubkey?.slice(0, 8)); if (selectedRecipients.length > 0 && senderPubkey) { - const recipientPubkeys = selectedRecipients.map(r => { + // Start async relay set calculation without blocking the effect + updateRelaySet(selectedRecipients, senderPubkey); + } else { + console.log("[Relay Effect] Clearing relays - no recipients or sender"); + newMessageRelays = []; + } + }); + + /** + * Updates relay set asynchronously to avoid blocking the reactive system + */ + async function updateRelaySet(recipients: any[], senderPubkey: string) { + try { + const recipientPubkeys = recipients.map(r => { const pubkey = r.pubkey!; // Convert npub to hex if needed if (pubkey.startsWith('npub')) { try { - const decoded = nip19.decode(pubkey); + const decoded = nip19.decode(pubkey) as unknown as { type: string; data: string }; if (decoded.type === 'npub') { return decoded.data; } @@ -832,32 +846,29 @@ getKind24RelaySet(senderPubkey, recipientPubkey) ); - Promise.all(relaySetPromises).then(relaySets => { - console.log("[Relay Effect] Received relay sets:", relaySets); - // Combine and deduplicate all relay sets - const allRelays = relaySets.flat(); - const uniqueRelays = [...new Set(allRelays)]; - console.log("[Relay Effect] Final relay list:", uniqueRelays); - - // If no relays found from NIP-65, use fallback relays - if (uniqueRelays.length === 0) { - console.log("[Relay Effect] No NIP-65 relays found, using fallback"); - const fallbackRelays = getAvailableRelays(); - newMessageRelays = fallbackRelays.slice(0, 5); // Limit to first 5 for performance - } else { - newMessageRelays = uniqueRelays; - } - }).catch(error => { - console.error("[Relay Effect] Error getting relay set:", error); - console.log("[Relay Effect] Using fallback relays due to error"); + const relaySets = await Promise.all(relaySetPromises); + console.log("[Relay Effect] Received relay sets:", relaySets); + + // Combine and deduplicate all relay sets + const allRelays = relaySets.flat(); + const uniqueRelays = [...new Set(allRelays)]; + console.log("[Relay Effect] Final relay list:", uniqueRelays); + + // If no relays found from NIP-65, use fallback relays + if (uniqueRelays.length === 0) { + console.log("[Relay Effect] No NIP-65 relays found, using fallback"); const fallbackRelays = getAvailableRelays(); - newMessageRelays = fallbackRelays.slice(0, 5); - }); - } else { - console.log("[Relay Effect] Clearing relays - no recipients or sender"); - newMessageRelays = []; + newMessageRelays = fallbackRelays.slice(0, 5); // Limit to first 5 for performance + } else { + newMessageRelays = uniqueRelays; + } + } catch (error) { + console.error("[Relay Effect] Error getting relay set:", error); + console.log("[Relay Effect] Using fallback relays due to error"); + const fallbackRelays = getAvailableRelays(); + newMessageRelays = fallbackRelays.slice(0, 5); } - }); + } {#if isOwnProfile && $userStore.signedIn} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 1ae83af..3aae73f 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -19,17 +19,21 @@ let summary = "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages."; - // Reactive effect to log relay configuration when stores change - $effect(() => { + // AI-NOTE: Refactored to avoid blocking $effect with logging operations + // Reactive effect to log relay configuration when stores change - non-blocking approach + $effect.pre(() => { const inboxRelays = $activeInboxRelays; const outboxRelays = $activeOutboxRelays; // Only log if we have relays (not empty arrays) if (inboxRelays.length > 0 || outboxRelays.length > 0) { - console.log('🔌 Relay Configuration Updated:'); - console.log('📥 Inbox Relays:', inboxRelays); - console.log('📤 Outbox Relays:', outboxRelays); - console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`); + // Defer logging to avoid blocking the reactive system + requestAnimationFrame(() => { + console.log('🔌 Relay Configuration Updated:'); + console.log('📥 Inbox Relays:', inboxRelays); + console.log('📤 Outbox Relays:', outboxRelays); + console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`); + }); } }); diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index f37d5a8..fc86dc5 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -392,17 +392,21 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; - // Reactive effect to log relay configuration when stores change - $effect(() => { + // AI-NOTE: Refactored to avoid blocking $effect with logging operations + // Reactive effect to log relay configuration when stores change - non-blocking approach + $effect.pre(() => { const inboxRelays = $activeInboxRelays; const outboxRelays = $activeOutboxRelays; // Only log if we have relays (not empty arrays) if (inboxRelays.length > 0 || outboxRelays.length > 0) { - console.log('🔌 Events Page - Relay Configuration Updated:'); - console.log('📥 Inbox Relays:', inboxRelays); - console.log('📤 Outbox Relays:', outboxRelays); - console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`); + // Defer logging to avoid blocking the reactive system + requestAnimationFrame(() => { + console.log('🔌 Events Page - Relay Configuration Updated:'); + console.log('📥 Inbox Relays:', inboxRelays); + console.log('📤 Outbox Relays:', outboxRelays); + console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`); + }); } }); From 176e710dff435df3b56b42e97878ce44a307e7d5 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 10 Aug 2025 15:49:56 +0200 Subject: [PATCH 32/98] simplified quote structure and corrected msg jump --- src/lib/components/Notifications.svelte | 139 ++++++++++++++++-------- src/lib/utils/kind24_utils.ts | 14 +-- 2 files changed, 98 insertions(+), 55 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index f46607a..e915315 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -19,6 +19,7 @@ import { searchProfiles } from "$lib/utils/search_utility"; import type { NostrProfile } from "$lib/utils/search_types"; import { PlusOutline, ReplyOutline } from "flowbite-svelte-icons"; + import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; const { event } = $props<{ event: NDKEvent }>(); @@ -182,31 +183,46 @@ } } - function renderContentWithLinks(content: string): string { - // Parse markdown links [text](url) and convert to HTML - let rendered = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + async function parseContent(content: string): Promise { + if (!content) return ""; - // Handle quote format and convert to small gray bars like Jumble - const patterns = [ - /> QUOTED: ([^•]*?) • LINK:\s*\n((?:nostr:)?nevent[^\s]*)/g, - /> QUOTED: ([^\n]*?)\n> LINK: ((?:nostr:)?nevent[^\s]*)/g, - /> QUOTED: ([^•]*?) • LINK:\s*((?:nostr:)?nevent[^\s]*)/g, - /> QUOTED: ([^•]*?) • LINK: ((?:nostr:)?nevent[^\s]*)/g, // Without optional whitespace - ]; + let parsedContent = await parseBasicmarkup(content); - for (const pattern of patterns) { - const beforeReplace = rendered; - rendered = rendered.replace(pattern, (match, quotedText, neventUrl) => { - const encodedUrl = neventUrl.replace(/'/g, '''); - const cleanQuotedText = quotedText.trim(); - return `
${cleanQuotedText}
`; - }); - if (beforeReplace !== rendered) { - break; + return parsedContent; + } + + function renderQuotedContent(message: NDKEvent): string { + const qTags = message.getMatchingTags("q"); + if (qTags.length === 0) return ""; + + const qTag = qTags[0]; + const nevent = qTag[1]; + + // Extract event ID from nevent + let eventId = ''; + try { + const decoded = nip19.decode(nevent); + if (decoded.type === 'nevent' && decoded.data.id) { + eventId = decoded.data.id; + } + } catch (error) { + // If decode fails, try to extract hex ID directly + const hexMatch = nevent.match(/[a-f0-9]{64}/i); + if (hexMatch) { + eventId = hexMatch[0]; + } + } + + if (eventId) { + // Find the quoted message in our public messages + const quotedMessage = publicMessages.find(msg => msg.id === eventId); + if (quotedMessage) { + const quotedContent = quotedMessage.content ? quotedMessage.content.slice(0, 200) : "No content"; + return `
${quotedContent}
`; } } - return rendered; + return ""; } function getNotificationType(event: NDKEvent): string { @@ -226,35 +242,59 @@ goto(`/events?id=${nevent}`); } - function jumpToMessageInFeed(nevent: string) { + function jumpToMessageInFeed(eventIdOrNevent: string) { // Switch to public messages tab and scroll to the specific message notificationMode = "public-messages"; // Try to find and scroll to the specific message setTimeout(() => { - try { - // Decode the nevent to get the event ID - const decoded = nip19.decode(nevent); - if (decoded.type === 'nevent' && decoded.data.id) { - const eventId = decoded.data.id; + let eventId = eventIdOrNevent; + + // If it's a nevent URL, try to extract the event ID + if (eventIdOrNevent.startsWith('nostr:nevent') || eventIdOrNevent.startsWith('nevent')) { + try { + const decoded = nip19.decode(eventIdOrNevent); + if (decoded.type === 'nevent' && decoded.data.id) { + eventId = decoded.data.id; + } + } catch (error) { + // If decode fails, try to extract hex ID directly + const hexMatch = eventIdOrNevent.match(/[a-f0-9]{64}/i); + if (hexMatch) { + eventId = hexMatch[0]; + } else { + console.warn('Failed to extract event ID from nevent:', eventIdOrNevent); + return; + } + } + } + + // Find the message in our public messages + const targetMessage = publicMessages.find(msg => msg.id === eventId); + if (targetMessage) { + // Try to find the element in the DOM + const element = document.querySelector(`[data-event-id="${eventId}"]`); + if (element) { + // Check if element is in viewport + const rect = element.getBoundingClientRect(); + const isInView = ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); - // Find the message in our public messages - const targetMessage = publicMessages.find(msg => msg.id === eventId); - if (targetMessage) { - // Try to scroll to the element if it exists in the DOM - const element = document.querySelector(`[data-event-id="${eventId}"]`); - if (element) { - element.scrollIntoView({ behavior: 'smooth', block: 'center' }); - // Briefly highlight the message - element.classList.add('ring-2', 'ring-blue-500'); - setTimeout(() => { - element.classList.remove('ring-2', 'ring-blue-500'); - }, 2000); - } + // Only scroll if not in view + if (!isInView) { + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); } + + // ALWAYS highlight the message in blue + element.classList.add('ring-2', 'ring-blue-500'); + setTimeout(() => { + element.classList.remove('ring-2', 'ring-blue-500'); + }, 2000); } - } catch (error) { - console.warn('Failed to jump to message:', error); } }, 100); } @@ -1020,9 +1060,18 @@ {/if}
+ {#if message.getMatchingTags("q").length > 0} +
+ {@html renderQuotedContent(message)} +
+ {/if} {#if message.content}
- {@html truncateRenderedContent(renderContentWithLinks(message.content), 300)} + {#await parseContent(message.content) then parsedContent} + {@html parsedContent} + {:catch} + {@html message.content} + {/await}
{/if} @@ -1136,7 +1185,11 @@
Replying to:
- {@html renderContentWithLinks(quotedContent)} + {#await parseContent(quotedContent) then parsedContent} + {@html parsedContent} + {:catch} + {@html quotedContent} + {/await}
{/if} diff --git a/src/lib/utils/kind24_utils.ts b/src/lib/utils/kind24_utils.ts index 9d1271e..cb43e57 100644 --- a/src/lib/utils/kind24_utils.ts +++ b/src/lib/utils/kind24_utils.ts @@ -151,18 +151,8 @@ export async function createKind24Reply( return { success: false, error: "No relays available for publishing" }; } - // Build content with quoted message if replying - let finalContent = content; - if (originalEvent) { - // Use multiple relays for better discoverability - const nevent = nip19.neventEncode({ - id: originalEvent.id, - relays: prioritizedRelays.slice(0, 3) // Use first 3 relays - }); - const quotedContent = originalEvent.content ? originalEvent.content.slice(0, 200) : "No content"; - // Use a more visible quote format with a clickable link - finalContent = `> QUOTED: ${quotedContent}\n> LINK: ${nevent}\n\n${content}`; - } + // Use the content as-is, quoted content is handled via q tag + const finalContent = content; // Build tags for the kind 24 event const tags: string[][] = [ From 22235235b31cd97b322ba89e1cfe5e9f42877c54 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 10 Aug 2025 16:08:48 +0200 Subject: [PATCH 33/98] got rid of hard-coded relays --- src/lib/components/EventInput.svelte | 5 +-- src/lib/components/Notifications.svelte | 56 +++++++------------------ src/lib/consts.ts | 2 + src/lib/utils/kind24_utils.ts | 7 +--- src/routes/contact/+page.svelte | 10 ++--- 5 files changed, 24 insertions(+), 56 deletions(-) diff --git a/src/lib/components/EventInput.svelte b/src/lib/components/EventInput.svelte index 0519692..cec2cde 100644 --- a/src/lib/components/EventInput.svelte +++ b/src/lib/components/EventInput.svelte @@ -29,6 +29,7 @@ import { Button } from "flowbite-svelte"; import { goto } from "$app/navigation"; import { WebSocketPool } from "$lib/data_structures/websocket_pool"; + import { anonymousRelays } from "$lib/consts"; let kind = $state(30040); let tags = $state<[string, string][]>([]); @@ -384,9 +385,7 @@ // Try to publish to relays directly const relays = [ - "wss://relay.damus.io", - "wss://relay.nostr.band", - "wss://nos.lol", + ...anonymousRelays, ...$activeOutboxRelays, ...$activeInboxRelays, ]; diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index e915315..4a98bc3 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -10,7 +10,7 @@ import { goto } from "$app/navigation"; import { get } from "svelte/store"; import { nip19 } from "nostr-tools"; - import { communityRelays, localRelays } from "$lib/consts"; + import { communityRelays, localRelays, anonymousRelays, searchRelays } from "$lib/consts"; import { createKind24Reply, getKind24RelaySet } from "$lib/utils/kind24_utils"; import { createSignedEvent } from "$lib/utils/nostrEventService"; import RelayDisplay from "$lib/components/RelayDisplay.svelte"; @@ -191,38 +191,24 @@ return parsedContent; } - function renderQuotedContent(message: NDKEvent): string { + async function renderQuotedContent(message: NDKEvent): Promise { const qTags = message.getMatchingTags("q"); if (qTags.length === 0) return ""; const qTag = qTags[0]; - const nevent = qTag[1]; - - // Extract event ID from nevent - let eventId = ''; - try { - const decoded = nip19.decode(nevent); - if (decoded.type === 'nevent' && decoded.data.id) { - eventId = decoded.data.id; - } - } catch (error) { - // If decode fails, try to extract hex ID directly - const hexMatch = nevent.match(/[a-f0-9]{64}/i); - if (hexMatch) { - eventId = hexMatch[0]; - } - } + const eventId = qTag[1]; if (eventId) { // Find the quoted message in our public messages const quotedMessage = publicMessages.find(msg => msg.id === eventId); if (quotedMessage) { const quotedContent = quotedMessage.content ? quotedMessage.content.slice(0, 200) : "No content"; - return `
${quotedContent}
`; + const parsedContent = await parseBasicmarkup(quotedContent); + return `
${parsedContent}
`; } } - return ""; + return ""; } function getNotificationType(event: NDKEvent): string { @@ -520,9 +506,7 @@ // Add q tag if replying to a message (for jump-to functionality) if (replyToMessage) { - // Get the first relay from newMessageRelays or use a fallback - const relayUrl = newMessageRelays[0] || "wss://freelay.sovbit.host/"; - pTags.push(["q", replyToMessage.id, relayUrl, replyToMessage.pubkey]); + pTags.push(["q", replyToMessage.id, newMessageRelays[0] || anonymousRelays[0]]); } // Get all recipient pubkeys for relay calculation (ensure hex format) @@ -559,14 +543,8 @@ const uniqueRelays = [...new Set(allRelays)]; newMessageRelays = uniqueRelays; - // Create the kind 24 event with quoted content if replying - let finalContent = newMessageContent; - if (replyToMessage && quotedContent) { - // Generate the markdown quote format for the actual message - const neventUrl = getNeventUrl(replyToMessage); - const markdownQuote = `> QUOTED: ${quotedContent} • LINK: ${neventUrl}`; - finalContent = markdownQuote + "\n\n" + newMessageContent; - } + // Use the content as-is, quoted content is handled via q tag + const finalContent = newMessageContent; // Create and sign the event using the unified function (includes expiration tag) const { event: signedEvent } = await createSignedEvent( @@ -649,16 +627,6 @@ } // Try search relays - const searchRelays = [ - "wss://profiles.nostr1.com", - "wss://aggr.nostr.land", - "wss://relay.noswhere.com", - "wss://nostr.wine", - "wss://relay.damus.io", - "wss://relay.nostr.band", - "wss://freelay.sovbit.host" - ]; - for (const relay of searchRelays) { try { const ndk = get(ndkInstance); @@ -1062,7 +1030,11 @@ {#if message.getMatchingTags("q").length > 0}
- {@html renderQuotedContent(message)} + {#await renderQuotedContent(message) then quotedHtml} + {@html quotedHtml} + {:catch} + + {/await}
{/if} {#if message.content} diff --git a/src/lib/consts.ts b/src/lib/consts.ts index f141e7b..b8e7f0d 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -33,6 +33,8 @@ export const secondaryRelays = [ export const anonymousRelays = [ "wss://freelay.sovbit.host", "wss://thecitadel.nostr1.com", + "wss://relay.damus.io", + "wss://relay.nostr.band" ]; export const lowbandwidthRelays = [ diff --git a/src/lib/utils/kind24_utils.ts b/src/lib/utils/kind24_utils.ts index cb43e57..69c59ed 100644 --- a/src/lib/utils/kind24_utils.ts +++ b/src/lib/utils/kind24_utils.ts @@ -5,6 +5,7 @@ import { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; import type NDK from "@nostr-dev-kit/ndk"; import { nip19 } from "nostr-tools"; import { createSignedEvent } from "./nostrEventService.ts"; +import { anonymousRelays } from "../consts"; /** * Fetches user's outbox relays from NIP-65 relay list @@ -161,11 +162,7 @@ export async function createKind24Reply( // Add q tag if replying to an original event if (originalEvent) { - const nevent = nip19.neventEncode({ - id: originalEvent.id, - relays: prioritizedRelays.slice(0, 3) // Use first 3 relays - }); - tags.push(["q", nevent, prioritizedRelays[0]]); + tags.push(["q", originalEvent.id, prioritizedRelays[0] || anonymousRelays[0]]); } // Create and sign the event using the unified function (includes expiration tag) diff --git a/src/routes/contact/+page.svelte b/src/routes/contact/+page.svelte index 4137220..b520ddc 100644 --- a/src/routes/contact/+page.svelte +++ b/src/routes/contact/+page.svelte @@ -11,7 +11,7 @@ } from "flowbite-svelte"; import { ndkInstance, ndkSignedIn, activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; import { userStore } from "$lib/stores/userStore"; - import { communityRelays } from "$lib/consts"; + import { communityRelays, anonymousRelays } 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,13 +62,11 @@ const repoAddress = "naddr1qvzqqqrhnypzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqy88wumn8ghj7mn0wvhxcmmv9uqq5stvv4uxzmnywf5kz2elajr"; - // Use the new relay management system instead of hardcoded relays + // Use the new relay management system with anonymous relays as fallbacks const allRelays = [ - "wss://relay.damus.io", - "wss://relay.nostr.band", - "wss://nos.lol", ...$activeInboxRelays, ...$activeOutboxRelays, + ...anonymousRelays, ]; // Hard-coded repository owner pubkey and ID from the task @@ -213,7 +211,7 @@ ...(ndk.pool ? Array.from(ndk.pool.relays.values()) .filter( - (relay) => relay.url && !relay.url.includes("wss://nos.lol"), + (relay) => relay.url, ) .map((relay) => normalizeRelayUrl(relay.url)) : []), From 8479a8c7dceecae02e0b00536258ad1801a4455b Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 10 Aug 2025 17:39:41 +0200 Subject: [PATCH 34/98] Redid the formatting. --- src/lib/components/Notifications.svelte | 397 ++++++------------------ src/lib/utils/kind24_utils.ts | 240 ++++---------- src/lib/utils/notification_utils.ts | 225 ++++++++++++++ 3 files changed, 386 insertions(+), 476 deletions(-) create mode 100644 src/lib/utils/notification_utils.ts diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 4a98bc3..f0995c7 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -5,8 +5,6 @@ import { userStore } from "$lib/stores/userStore"; import { userPubkey, isLoggedIn } from "$lib/stores/authStore.Svelte"; import { ndkInstance, activeInboxRelays } from "$lib/ndk"; - import { neventEncode } from "$lib/utils"; - import { getUserMetadata, NDKRelaySetFromNDK } from "$lib/utils/nostrUtils"; import { goto } from "$app/navigation"; import { get } from "svelte/store"; import { nip19 } from "nostr-tools"; @@ -19,7 +17,19 @@ import { searchProfiles } from "$lib/utils/search_utility"; import type { NostrProfile } from "$lib/utils/search_types"; import { PlusOutline, ReplyOutline } from "flowbite-svelte-icons"; + import { + truncateContent, + truncateRenderedContent, + parseContent, + renderQuotedContent, + getNotificationType, + fetchAuthorProfiles + } from "$lib/utils/notification_utils"; + import { buildCompleteRelaySet } from "$lib/utils/relay_management"; + import { formatDate, neventEncode } from "$lib/utils"; + import { toNpub, getUserMetadata, NDKRelaySetFromNDK } from "$lib/utils/nostrUtils"; import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; + import { userBadge } from "$lib/snippets/UserSnippets.svelte"; const { event } = $props<{ event: NDKEvent }>(); @@ -79,148 +89,9 @@ }); // AI-NOTE: Utility functions extracted to reduce code duplication - function getAvailableRelays(): string[] { - const userInboxRelays = $userStore.relays.inbox || []; - const userOutboxRelays = $userStore.relays.outbox || []; - const activeInboxRelayList = get(activeInboxRelays); - - const allRelays = [ - ...userInboxRelays, - ...userOutboxRelays, - ...localRelays, - ...communityRelays, - ...activeInboxRelayList - ]; - - return [...new Set(allRelays)]; - } - - function toNpub(pubkey: string): string | null { - if (!pubkey) return null; - try { - if (/^[a-f0-9]{64}$/i.test(pubkey)) { - return nip19.npubEncode(pubkey); - } - if (pubkey.startsWith("npub1")) return pubkey; - return null; - } catch { - return null; - } - } - function getNeventUrl(event: NDKEvent): string { - const relays = getAvailableRelays(); - return neventEncode(event, relays); - } - - function formatDate(timestamp: number): string { - const date = new Date(timestamp * 1000); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); - - if (diffDays === 0) return "Today"; - if (diffDays === 1) return "Yesterday"; - if (diffDays < 7) return `${diffDays} days ago`; - return date.toLocaleDateString(); - } - - function truncateContent(content: string, maxLength: number = 300): string { - if (content.length <= maxLength) return content; - return content.slice(0, maxLength) + "..."; - } - - function truncateRenderedContent(renderedHtml: string, maxLength: number = 300): string { - // If the rendered HTML is short enough, return as-is - if (renderedHtml.length <= maxLength) return renderedHtml; - - // Check if there are any gray quote boxes (jump-to-message divs) - const hasQuoteBoxes = renderedHtml.includes('jump-to-message'); - - if (hasQuoteBoxes) { - // Split content into quote boxes and regular text - const quoteBoxPattern = /
]*>[^<]*<\/div>/g; - const quoteBoxes = renderedHtml.match(quoteBoxPattern) || []; - - // Remove quote boxes temporarily to measure text length - let textOnly = renderedHtml.replace(quoteBoxPattern, '|||QUOTEBOX|||'); - - // If text without quote boxes is still too long, truncate it - if (textOnly.length > maxLength) { - const availableLength = maxLength - (quoteBoxes.join('').length); - if (availableLength > 50) { // Leave some reasonable space for text - textOnly = textOnly.slice(0, availableLength) + "..."; - } else { - // If quote boxes take up too much space, just show them with minimal text - textOnly = textOnly.slice(0, 50) + "..."; - } - } - - // Restore quote boxes - let result = textOnly; - quoteBoxes.forEach(box => { - result = result.replace('|||QUOTEBOX|||', box); - }); - - return result; - } else { - // No quote boxes, simple truncation with HTML awareness - if (renderedHtml.includes('<')) { - // Has HTML tags, do a simple truncation but try to avoid breaking tags - const truncated = renderedHtml.slice(0, maxLength); - const lastTagStart = truncated.lastIndexOf('<'); - const lastTagEnd = truncated.lastIndexOf('>'); - - if (lastTagStart > lastTagEnd) { - // We're in the middle of a tag, truncate before it - return renderedHtml.slice(0, lastTagStart) + "..."; - } - return truncated + "..."; - } else { - // Plain text, simple truncation - return renderedHtml.slice(0, maxLength) + "..."; - } - } - } - - async function parseContent(content: string): Promise { - if (!content) return ""; - - let parsedContent = await parseBasicmarkup(content); - - return parsedContent; - } - - async function renderQuotedContent(message: NDKEvent): Promise { - const qTags = message.getMatchingTags("q"); - if (qTags.length === 0) return ""; - - const qTag = qTags[0]; - const eventId = qTag[1]; - - if (eventId) { - // Find the quoted message in our public messages - const quotedMessage = publicMessages.find(msg => msg.id === eventId); - if (quotedMessage) { - const quotedContent = quotedMessage.content ? quotedMessage.content.slice(0, 200) : "No content"; - const parsedContent = await parseBasicmarkup(quotedContent); - return `
${parsedContent}
`; - } - } - - return ""; - } - - function getNotificationType(event: NDKEvent): string { - switch (event.kind) { - case 1: return "Reply"; - case 1111: return "Custom Reply"; - case 9802: return "Highlight"; - case 6: return "Repost"; - case 16: return "Generic Repost"; - case 24: return "Public Message"; - default: return `Kind ${event.kind}`; - } + // Use empty relay list for nevent encoding - relays will be discovered by the events page + return neventEncode(event, []); } function navigateToEvent(nevent: string) { @@ -605,88 +476,6 @@ } } - - - // AI-NOTE: Simplified profile fetching with better error handling - async function fetchAuthorProfiles(events: NDKEvent[]) { - const uniquePubkeys = new Set(); - events.forEach(event => { - if (event.pubkey) uniquePubkeys.add(event.pubkey); - }); - - const profilePromises = Array.from(uniquePubkeys).map(async (pubkey) => { - try { - const npub = toNpub(pubkey); - if (!npub) return; - - // Try cache first - let profile = await getUserMetadata(npub, false); - if (profile && (profile.name || profile.displayName || profile.picture)) { - authorProfiles.set(pubkey, profile); - return; - } - - // Try search relays - for (const relay of searchRelays) { - try { - const ndk = get(ndkInstance); - if (!ndk) break; - - const relaySet = NDKRelaySetFromNDK.fromRelayUrls([relay], ndk); - const profileEvent = await ndk.fetchEvent( - { kinds: [0], authors: [pubkey] }, - undefined, - relaySet - ); - - if (profileEvent) { - const profileData = JSON.parse(profileEvent.content); - authorProfiles.set(pubkey, { - name: profileData.name, - displayName: profileData.display_name || profileData.displayName, - picture: profileData.picture || profileData.image - }); - return; - } - } catch (error) { - console.warn(`[Notifications] Failed to fetch profile from ${relay}:`, error); - } - } - - // Try all available relays as fallback - const relays = getAvailableRelays(); - if (relays.length > 0) { - try { - const ndk = get(ndkInstance); - if (!ndk) return; - - const relaySet = NDKRelaySetFromNDK.fromRelayUrls(relays, ndk); - const profileEvent = await ndk.fetchEvent( - { kinds: [0], authors: [pubkey] }, - undefined, - relaySet - ); - - if (profileEvent) { - const profileData = JSON.parse(profileEvent.content); - authorProfiles.set(pubkey, { - name: profileData.name, - displayName: profileData.display_name || profileData.displayName, - picture: profileData.picture || profileData.image - }); - } - } catch (error) { - console.warn(`[Notifications] Failed to fetch profile from all relays:`, error); - } - } - } catch (error) { - console.warn(`[Notifications] Failed to fetch profile for ${pubkey}:`, error); - } - }); - - await Promise.allSettled(profilePromises); - } - // AI-NOTE: Simplified notification fetching async function fetchNotifications() { if (!$userStore.pubkey || !isOwnProfile) return; @@ -697,8 +486,11 @@ try { const ndk = get(ndkInstance); if (!ndk) throw new Error("No NDK instance available"); - - const relays = getAvailableRelays(); + + const userStoreValue = get(userStore); + const user = userStoreValue.signedIn && userStoreValue.pubkey ? ndk.getUser({ pubkey: userStoreValue.pubkey }) : null; + const relaySet = await buildCompleteRelaySet(ndk, user); + const relays = [...relaySet.inboxRelays, ...relaySet.outboxRelays]; if (relays.length === 0) throw new Error("No relays available"); const filter = { @@ -710,8 +502,8 @@ limit: 100, }; - const relaySet = NDKRelaySetFromNDK.fromRelayUrls(relays, ndk); - const events = await ndk.fetchEvents(filter, undefined, relaySet); + const ndkRelaySet = NDKRelaySetFromNDK.fromRelayUrls(relays, ndk); + const events = await ndk.fetchEvents(filter, undefined, ndkRelaySet); const eventArray = Array.from(events); // Filter out self-referential events @@ -729,7 +521,7 @@ .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) .slice(0, 100); - await fetchAuthorProfiles(notifications); + authorProfiles = await fetchAuthorProfiles(notifications); } catch (err) { console.error("[Notifications] Error fetching notifications:", err); error = err instanceof Error ? err.message : "Failed to fetch notifications"; @@ -749,15 +541,18 @@ const ndk = get(ndkInstance); if (!ndk) throw new Error("No NDK instance available"); - const relays = getAvailableRelays(); + const userStoreValue = get(userStore); + const user = userStoreValue.signedIn && userStoreValue.pubkey ? ndk.getUser({ pubkey: userStoreValue.pubkey }) : null; + const relaySet = await buildCompleteRelaySet(ndk, user); + const relays = [...relaySet.inboxRelays, ...relaySet.outboxRelays]; if (relays.length === 0) throw new Error("No relays available"); - const relaySet = NDKRelaySetFromNDK.fromRelayUrls(relays, ndk); + const ndkRelaySet = NDKRelaySetFromNDK.fromRelayUrls(relays, ndk); // Fetch only kind 24 messages const [messagesEvents, userMessagesEvents] = await Promise.all([ - ndk.fetchEvents({ kinds: [24 as any], "#p": [$userStore.pubkey], limit: 200 }, undefined, relaySet), - ndk.fetchEvents({ kinds: [24 as any], authors: [$userStore.pubkey], limit: 200 }, undefined, relaySet) + ndk.fetchEvents({ kinds: [24 as any], "#p": [$userStore.pubkey], limit: 200 }, undefined, ndkRelaySet), + ndk.fetchEvents({ kinds: [24 as any], authors: [$userStore.pubkey], limit: 200 }, undefined, ndkRelaySet) ]); const allMessages = [ @@ -774,7 +569,7 @@ .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) .slice(0, 200); - await fetchAuthorProfiles(publicMessages); + authorProfiles = await fetchAuthorProfiles(publicMessages); } catch (err) { console.error("[PublicMessages] Error fetching public messages:", err); error = err instanceof Error ? err.message : "Failed to fetch public messages"; @@ -865,16 +660,32 @@ // If no relays found from NIP-65, use fallback relays if (uniqueRelays.length === 0) { console.log("[Relay Effect] No NIP-65 relays found, using fallback"); - const fallbackRelays = getAvailableRelays(); - newMessageRelays = fallbackRelays.slice(0, 5); // Limit to first 5 for performance + const ndk = get(ndkInstance); + if (ndk) { + const userStoreValue = get(userStore); + const user = userStoreValue.signedIn && userStoreValue.pubkey ? ndk.getUser({ pubkey: userStoreValue.pubkey }) : null; + const relaySet = await buildCompleteRelaySet(ndk, user); + const fallbackRelays = [...relaySet.inboxRelays, ...relaySet.outboxRelays]; + newMessageRelays = fallbackRelays.slice(0, 5); // Limit to first 5 for performance + } else { + newMessageRelays = []; + } } else { newMessageRelays = uniqueRelays; } } catch (error) { console.error("[Relay Effect] Error getting relay set:", error); console.log("[Relay Effect] Using fallback relays due to error"); - const fallbackRelays = getAvailableRelays(); - newMessageRelays = fallbackRelays.slice(0, 5); + const ndk = get(ndkInstance); + if (ndk) { + const userStoreValue = get(userStore); + const user = userStoreValue.signedIn && userStoreValue.pubkey ? ndk.getUser({ pubkey: userStoreValue.pubkey }) : null; + const relaySet = await buildCompleteRelaySet(ndk, user); + const fallbackRelays = [...relaySet.inboxRelays, ...relaySet.outboxRelays]; + newMessageRelays = fallbackRelays.slice(0, 5); + } else { + newMessageRelays = []; + } } } @@ -933,7 +744,7 @@
- Filtered by user: {authorProfiles.get(filteredByUser)?.displayName || authorProfiles.get(filteredByUser)?.name || `${filteredByUser.slice(0, 8)}...${filteredByUser.slice(-4)}`} + Filtered by user: {@render userBadge(filteredByUser, authorProfiles.get(filteredByUser)?.displayName || authorProfiles.get(filteredByUser)?.name)}
- -
- - {authorProfile?.displayName || authorProfile?.name || `${message.pubkey.slice(0, 8)}...${message.pubkey.slice(-4)}`} - - {#if authorProfile?.name && authorProfile?.displayName && authorProfile.name !== authorProfile.displayName} - - (@{authorProfile.name}) - - {/if} -
+ {#if message.getMatchingTags("q").length > 0}
- {#await renderQuotedContent(message) then quotedHtml} + {#await renderQuotedContent(message, publicMessages) then quotedHtml} {@html quotedHtml} {:catch} @@ -1073,22 +879,27 @@ {@const authorProfile = authorProfiles.get(notification.pubkey)}
- +
- {#if authorProfile?.picture} - Author avatar (e.target as HTMLImageElement).style.display = 'none'} - /> - {:else} -
- - {(authorProfile?.displayName || authorProfile?.name || notification.pubkey.slice(0, 1)).toUpperCase()} - -
- {/if} +
+ {#if authorProfile?.picture} + Author avatar (e.target as HTMLImageElement).style.display = 'none'} + /> + {:else} +
+ + {(authorProfile?.displayName || authorProfile?.name || notification.pubkey.slice(0, 1)).toUpperCase()} + +
+ {/if} + + {@render userBadge(notification.pubkey, authorProfile?.displayName || authorProfile?.name)} + +
@@ -1109,21 +920,15 @@
- -
- - {authorProfile?.displayName || authorProfile?.name || `${notification.pubkey.slice(0, 8)}...${notification.pubkey.slice(-4)}`} - - {#if authorProfile?.name && authorProfile?.displayName && authorProfile.name !== authorProfile.displayName} - - (@{authorProfile.name}) - - {/if} -
+ {#if notification.content}
- {truncateContent(notification.content)} + {#await parseContent(notification.content) then parsedContent} + {@html parsedContent} + {:catch} + {@html truncateContent(notification.content)} + {/await}
{/if} @@ -1194,7 +999,7 @@
{#each selectedRecipients as recipient} - {recipient.displayName || recipient.name || `${recipient.pubkey?.slice(0, 8)}...`} + {@render userBadge(recipient.pubkey!, recipient.displayName || recipient.name)} + +
`; } // Otherwise, render as a clickable link From f89bfc0b9b373628dbb25dd0fd64c42d6584afc0 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 10 Aug 2025 22:51:55 +0200 Subject: [PATCH 41/98] fixed broken publications --- src/lib/data_structures/publication_tree.ts | 140 +++++++++++++++++--- src/lib/utils/websocket_utils.ts | 10 +- 2 files changed, 128 insertions(+), 22 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 6871044..c507b9f 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -2,6 +2,10 @@ import { Lazy } from "./lazy.ts"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type NDK from "@nostr-dev-kit/ndk"; import { fetchEventById } from "../utils/websocket_utils.ts"; +import { fetchEventWithFallback, NDKRelaySetFromNDK } from "../utils/nostrUtils.ts"; +import { get } from "svelte/store"; +import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts"; +import { searchRelays, secondaryRelays } from "../consts.ts"; enum PublicationTreeNodeType { Branch, @@ -685,24 +689,108 @@ export class PublicationTree implements AsyncIterable { if (!event) { const [kind, pubkey, dTag] = address.split(":"); - const fetchedEvent = await this.#ndk.fetchEvent({ + + // AI-NOTE: 2025-01-24 - Enhanced event fetching with comprehensive fallback + // First try to fetch using the enhanced fetchEventWithFallback function + // which includes search relay fallback logic + return fetchEventWithFallback(this.#ndk, { kinds: [parseInt(kind)], authors: [pubkey], "#d": [dTag], - }); - - // Cache the event if found - if (fetchedEvent) { - this.#eventCache.set(address, fetchedEvent); - event = fetchedEvent; - } + }, 5000) // 5 second timeout for publication events + .then(fetchedEvent => { + if (fetchedEvent) { + // Cache the event if found + this.#eventCache.set(address, fetchedEvent); + event = fetchedEvent; + } + + if (!event) { + console.warn( + `[PublicationTree] Event with address ${address} not found on primary relays, trying search relays.`, + ); + + // If still not found, try a more aggressive search using search relays + return this.#trySearchRelayFallback(address, kind, pubkey, dTag, parentNode); + } + + return this.#buildNodeFromEvent(event, address, parentNode); + }) + .catch(error => { + console.warn(`[PublicationTree] Error fetching event for address ${address}:`, error); + + // Try search relay fallback even on error + return this.#trySearchRelayFallback(address, kind, pubkey, dTag, parentNode); + }); } - if (!event) { - console.debug( - `[PublicationTree] Event with address ${address} not found.`, - ); + return Promise.resolve(this.#buildNodeFromEvent(event, address, parentNode)); + } + /** + * AI-NOTE: 2025-01-24 - Aggressive search relay fallback for publication events + * This method tries to find events on search relays when they're not found on primary relays + */ + async #trySearchRelayFallback( + address: string, + kind: string, + pubkey: string, + dTag: string, + parentNode: PublicationTreeNode + ): Promise { + try { + console.log(`[PublicationTree] Trying search relay fallback for address: ${address}`); + + // Get current relay configuration + const inboxRelays = get(activeInboxRelays); + const outboxRelays = get(activeOutboxRelays); + + // Create a comprehensive relay set including search relays + const allRelays = [...inboxRelays, ...outboxRelays, ...searchRelays, ...secondaryRelays]; + const uniqueRelays = [...new Set(allRelays)]; // Remove duplicates + + console.log(`[PublicationTree] Trying ${uniqueRelays.length} relays for fallback search:`, uniqueRelays); + + // Try each relay individually with a shorter timeout + for (const relay of uniqueRelays) { + try { + const relaySet = NDKRelaySetFromNDK.fromRelayUrls([relay], this.#ndk); + + const fetchedEvent = await this.#ndk.fetchEvent({ + kinds: [parseInt(kind)], + authors: [pubkey], + "#d": [dTag], + }, undefined, relaySet).withTimeout(3000); // 3 second timeout per relay + + if (fetchedEvent) { + console.log(`[PublicationTree] Found event ${fetchedEvent.id} on search relay: ${relay}`); + + // Cache the event + this.#eventCache.set(address, fetchedEvent); + this.#events.set(address, fetchedEvent); + + return this.#buildNodeFromEvent(fetchedEvent, address, parentNode); + } + } catch (error) { + console.debug(`[PublicationTree] Failed to fetch from relay ${relay}:`, error); + continue; // Try next relay + } + } + + // If we get here, the event was not found on any relay + console.warn(`[PublicationTree] Event with address ${address} not found on any relay after fallback search.`); + + return { + type: PublicationTreeNodeType.Leaf, + status: PublicationTreeNodeStatus.Error, + address, + parent: parentNode, + children: [], + }; + + } catch (error) { + console.error(`[PublicationTree] Error in search relay fallback for ${address}:`, error); + return { type: PublicationTreeNodeType.Leaf, status: PublicationTreeNodeStatus.Error, @@ -711,7 +799,17 @@ export class PublicationTree implements AsyncIterable { children: [], }; } + } + /** + * AI-NOTE: 2025-01-24 - Helper method to build a node from an event + * This extracts the common logic for building nodes from events + */ + #buildNodeFromEvent( + event: NDKEvent, + address: string, + parentNode: PublicationTreeNode + ): PublicationTreeNode { this.#events.set(address, event); const childAddresses = event.tags @@ -754,14 +852,11 @@ export class PublicationTree implements AsyncIterable { } }); - const resolvedAddresses = await Promise.all(eTagPromises); - const validAddresses = resolvedAddresses.filter(addr => addr !== null) as string[]; - - console.debug(`[PublicationTree] Resolved ${validAddresses.length} valid addresses from e-tags:`, validAddresses); - - if (validAddresses.length > 0) { - childAddresses.push(...validAddresses); - } + // Note: We can't await here since this is a synchronous method + // The e-tag resolution will happen when the children are processed + // For now, we'll add the e-tags as potential child addresses + const eTagAddresses = eTags.map(tag => tag[1]); + childAddresses.push(...eTagAddresses); } const node: PublicationTreeNode = { @@ -772,10 +867,13 @@ export class PublicationTree implements AsyncIterable { children: [], }; + // Add children asynchronously const childPromises = childAddresses.map(address => this.addEventByAddress(address, event) ); - await Promise.all(childPromises); + Promise.all(childPromises).catch(error => { + console.warn(`[PublicationTree] Error adding children for ${address}:`, error); + }); this.#nodeResolvedObservers.forEach((observer) => observer(address)); diff --git a/src/lib/utils/websocket_utils.ts b/src/lib/utils/websocket_utils.ts index 834bca3..aeeeb65 100644 --- a/src/lib/utils/websocket_utils.ts +++ b/src/lib/utils/websocket_utils.ts @@ -93,8 +93,16 @@ export async function fetchNostrEvent(filter: NostrFilter): Promise { + const relayPromises = uniqueRelays.map(async (relay) => { try { const ws = await WebSocketPool.instance.acquire(relay); const subId = crypto.randomUUID(); From 1283265155f3275cdb2082bef37a31a2c025b005 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 10 Aug 2025 23:18:35 +0200 Subject: [PATCH 42/98] fix the author in the publication cards --- src/lib/components/publications/PublicationHeader.svelte | 2 +- src/lib/components/util/Details.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/components/publications/PublicationHeader.svelte b/src/lib/components/publications/PublicationHeader.svelte index c1c6222..5cab792 100644 --- a/src/lib/components/publications/PublicationHeader.svelte +++ b/src/lib/components/publications/PublicationHeader.svelte @@ -35,7 +35,7 @@ let title: string = $derived(event.getMatchingTags("title")[0]?.[1]); let author: string = $derived( - event.getMatchingTags(event, "author")[0]?.[1] ?? "unknown", + event.getMatchingTags("author")[0]?.[1] ?? "unknown", ); let version: string = $derived( event.getMatchingTags("version")[0]?.[1] ?? "1", diff --git a/src/lib/components/util/Details.svelte b/src/lib/components/util/Details.svelte index 5ce8b28..ad5c423 100644 --- a/src/lib/components/util/Details.svelte +++ b/src/lib/components/util/Details.svelte @@ -62,7 +62,7 @@

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

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

From 2bc742497ebb289d6bfb211a58e75a1243e73cd0 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 10 Aug 2025 23:50:51 +0200 Subject: [PATCH 43/98] changed to Svelte 5 syntax --- src/lib/components/CommentBox.svelte | 16 ++++++++-------- src/lib/components/RelayActions.svelte | 2 +- src/lib/components/ZettelEditor.svelte | 2 +- .../components/publications/Publication.svelte | 2 +- src/routes/[...catchall]/+page.svelte | 4 ++-- src/routes/my-notes/+page.svelte | 8 ++++---- src/routes/new/compose/+page.svelte | 2 +- src/routes/new/edit/+page.svelte | 8 ++++---- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/lib/components/CommentBox.svelte b/src/lib/components/CommentBox.svelte index 6952279..9d7e978 100644 --- a/src/lib/components/CommentBox.svelte +++ b/src/lib/components/CommentBox.svelte @@ -364,12 +364,12 @@
{#each markupButtons as button} - + {/each} - - +
@@ -519,12 +519,12 @@ class="mb-4" />
- Insert @@ -552,7 +552,7 @@ {error} {#if showOtherRelays} - {/if} @@ -560,7 +560,7 @@ handleSubmit(false, true)}>Try Fallback Relays {/if} @@ -604,7 +604,7 @@
{/if} diff --git a/src/lib/components/ZettelEditor.svelte b/src/lib/components/ZettelEditor.svelte index da96f74..c077398 100644 --- a/src/lib/components/ZettelEditor.svelte +++ b/src/lib/components/ZettelEditor.svelte @@ -187,7 +187,7 @@ Note content here... {:else if !isDone} - + {:else}

You've reached the end of the publication. diff --git a/src/routes/[...catchall]/+page.svelte b/src/routes/[...catchall]/+page.svelte index 0224b3d..1e3a0b1 100644 --- a/src/routes/[...catchall]/+page.svelte +++ b/src/routes/[...catchall]/+page.svelte @@ -11,13 +11,13 @@ >The page you are looking for does not exist or has been moved.

- window.history.back()}>Go Back
diff --git a/src/routes/my-notes/+page.svelte b/src/routes/my-notes/+page.svelte index 1e02ef2..4841b4c 100644 --- a/src/routes/my-notes/+page.svelte +++ b/src/routes/my-notes/+page.svelte @@ -183,7 +183,7 @@ {selectedTagTypes.has(type) ? 'border-2 border-amber-800' : 'border border-amber-200'}" - on:click={() => toggleTagType(type)} + onclick={() => toggleTagType(type)} > {#if type.length === 1} {type} @@ -200,7 +200,7 @@ {#if tagsToShow.length > 0} @@ -240,7 +240,7 @@
{getTitle(event)}
+ +
+
+ From c7e3cbf993614f2cb12ffaa2f53fcb61a57c4dd8 Mon Sep 17 00:00:00 2001 From: silberengel Date: Mon, 11 Aug 2025 07:27:05 +0200 Subject: [PATCH 45/98] switched to npub --- .../publications/PublicationFeed.svelte | 30 ++++--------------- src/lib/utils/nostrUtils.ts | 17 +++++++++-- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/lib/components/publications/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte index e95ddf5..0a2593b 100644 --- a/src/lib/components/publications/PublicationFeed.svelte +++ b/src/lib/components/publications/PublicationFeed.svelte @@ -7,6 +7,7 @@ import { onMount, onDestroy } from "svelte"; import { getMatchingTags, + toNpub, } from "$lib/utils/nostrUtils"; import { WebSocketPool } from "$lib/data_structures/websocket_pool"; import { NDKEvent } from "@nostr-dev-kit/ndk"; @@ -292,32 +293,13 @@ loading = false; } - // Function to convert various Nostr identifiers to npub + // Function to convert various Nostr identifiers to npub using the utility function const convertToNpub = (input: string): string | null => { - try { - // If it's already an npub, return it - if (input.startsWith('npub')) { - return input; - } - - // If it's a hex pubkey, convert to npub - if (input.length === 64 && /^[0-9a-fA-F]+$/.test(input)) { - return nip19.npubEncode(input); - } - - // If it's an nprofile, decode and extract npub - if (input.startsWith('nprofile')) { - const decoded = nip19.decode(input); - if (decoded.type === 'nprofile') { - return decoded.data.pubkey ? nip19.npubEncode(decoded.data.pubkey) : null; - } - } - - return null; - } catch (error) { - console.debug("[PublicationFeed] Failed to convert to npub:", input, error); - return null; + const result = toNpub(input); + if (!result) { + console.debug("[PublicationFeed] Failed to convert to npub:", input); } + return result; }; // Function to filter events by npub (author or p tags) diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index d3be24d..06ae2bf 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -519,15 +519,28 @@ export async function fetchEventWithFallback( } /** - * Converts a hex pubkey to npub, or returns npub if already encoded. + * Converts various Nostr identifiers to npub format. + * Handles hex pubkeys, npub strings, and nprofile strings. */ export function toNpub(pubkey: string | undefined): string | null { if (!pubkey) return null; try { + // If it's already an npub, return it + if (pubkey.startsWith("npub")) return pubkey; + + // If it's a hex pubkey, convert to npub if (new RegExp(`^[a-f0-9]{${VALIDATION.HEX_LENGTH}}$`, "i").test(pubkey)) { return nip19.npubEncode(pubkey); } - if (pubkey.startsWith("npub1")) return pubkey; + + // If it's an nprofile, decode and extract npub + if (pubkey.startsWith("nprofile")) { + const decoded = nip19.decode(pubkey); + if (decoded.type === 'nprofile') { + return decoded.data.pubkey ? nip19.npubEncode(decoded.data.pubkey) : null; + } + } + return null; } catch { return null; From c240bf8873fe7e8f1dfa77ccaceed0f9bf7567cc Mon Sep 17 00:00:00 2001 From: silberengel Date: Thu, 14 Aug 2025 06:43:55 +0200 Subject: [PATCH 46/98] removed if-browser from layout --- src/routes/publication/[type]/[identifier]/+layout.svelte | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/routes/publication/[type]/[identifier]/+layout.svelte b/src/routes/publication/[type]/[identifier]/+layout.svelte index c14d288..9ebd55c 100644 --- a/src/routes/publication/[type]/[identifier]/+layout.svelte +++ b/src/routes/publication/[type]/[identifier]/+layout.svelte @@ -29,6 +29,4 @@ -{#if browser} - {@render children()} -{/if} +{@render children()} \ No newline at end of file From 9946493740f06c496f07b1b5838bc4485c76fe66 Mon Sep 17 00:00:00 2001 From: silberengel Date: Thu, 14 Aug 2025 06:49:45 +0200 Subject: [PATCH 47/98] Added missing orly address --- src/lib/consts.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/consts.ts b/src/lib/consts.ts index b8e7f0d..219d676 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -46,6 +46,7 @@ export const lowbandwidthRelays = [ export const localRelays: string[] = [ "wss://localhost:8080", "wss://localhost:4869", + "wss://localhost:3334" ]; export enum FeedType { From b92f3a0a9b4b97312386b735300eeec47a07bc66 Mon Sep 17 00:00:00 2001 From: silberengel Date: Thu, 14 Aug 2025 06:53:13 +0200 Subject: [PATCH 48/98] Revert "Added missing orly address" This reverts commit 9946493740f06c496f07b1b5838bc4485c76fe66. --- src/lib/consts.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/consts.ts b/src/lib/consts.ts index 219d676..b8e7f0d 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -46,7 +46,6 @@ export const lowbandwidthRelays = [ export const localRelays: string[] = [ "wss://localhost:8080", "wss://localhost:4869", - "wss://localhost:3334" ]; export enum FeedType { From 43a7dcd14ca5154aa83dd5ca4d62e661ddc85f7f Mon Sep 17 00:00:00 2001 From: silberengel Date: Thu, 14 Aug 2025 08:00:42 +0200 Subject: [PATCH 49/98] fixed relay connections --- src/lib/consts.ts | 9 +- src/lib/ndk.ts | 150 ++++++++++++----- src/lib/stores/userStore.ts | 4 +- src/lib/utils/relay_management.ts | 263 +++++++++++++++++++++++------- 4 files changed, 324 insertions(+), 102 deletions(-) diff --git a/src/lib/consts.ts b/src/lib/consts.ts index b8e7f0d..4f78a56 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -32,9 +32,7 @@ export const secondaryRelays = [ export const anonymousRelays = [ "wss://freelay.sovbit.host", - "wss://thecitadel.nostr1.com", - "wss://relay.damus.io", - "wss://relay.nostr.band" + "wss://thecitadel.nostr1.com" ]; export const lowbandwidthRelays = [ @@ -44,8 +42,9 @@ export const lowbandwidthRelays = [ ]; export const localRelays: string[] = [ - "wss://localhost:8080", - "wss://localhost:4869", + "ws://localhost:8080", + "ws://localhost:4869", + "ws://localhost:3334" ]; export enum FeedType { diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index f89a9f7..45e9097 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -9,6 +9,7 @@ import NDK, { import { writable, get, type Writable } from "svelte/store"; import { loginStorageKey, + anonymousRelays, } from "./consts.ts"; import { buildCompleteRelaySet, @@ -91,8 +92,18 @@ function clearPersistentRelaySet(): void { } // Subscribe to userStore changes and update ndkSignedIn accordingly -userStore.subscribe((userState) => { +userStore.subscribe(async (userState) => { ndkSignedIn.set(userState.signedIn); + + // Refresh relay stores when user state changes + const ndk = get(ndkInstance); + if (ndk) { + try { + await refreshRelayStores(ndk); + } catch (error) { + console.warn('[NDK.ts] Failed to refresh relay stores on user state change:', error); + } + } }); /** @@ -322,15 +333,21 @@ export function clearPersistedRelays(user: NDKUser): void { /** * Ensures a relay URL uses secure WebSocket protocol * @param url The relay URL to secure - * @returns The URL with wss:// protocol + * @returns The URL with appropriate protocol (ws:// for localhost, wss:// for remote) */ function ensureSecureWebSocket(url: string): string { - // Replace ws:// with wss:// if present + // 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( - `[NDK.ts] Protocol downgrade detected: ${url} -> ${secureUrl}`, + `[NDK.ts] Protocol upgrade for remote relay: ${url} -> ${secureUrl}`, ); } @@ -341,46 +358,85 @@ function ensureSecureWebSocket(url: string): string { * Creates a relay with proper authentication handling */ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { - console.debug(`[NDK.ts] Creating relay with URL: ${url}`); + try { + console.debug(`[NDK.ts] Creating relay with URL: ${url}`); - // Ensure the URL is using wss:// protocol - const secureUrl = ensureSecureWebSocket(url); + // Ensure the URL is using appropriate protocol + const secureUrl = ensureSecureWebSocket(url); - // Add connection timeout and error handling - const relay = new NDKRelay( - secureUrl, - NDKRelayAuthPolicies.signIn({ ndk }), - ndk, - ); + // Add connection timeout and error handling + const relay = new NDKRelay( + secureUrl, + NDKRelayAuthPolicies.signIn({ ndk }), + ndk, + ); - // Set up connection timeout - const connectionTimeout = setTimeout(() => { - console.warn(`[NDK.ts] Connection timeout for ${secureUrl}`); - relay.disconnect(); - }, 5000); // 5 second timeout - - // Set up custom authentication handling only if user is signed in - if (ndk.signer && ndk.activeUser) { - const authPolicy = new CustomRelayAuthPolicy(ndk); - relay.on("connect", () => { - console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); - clearTimeout(connectionTimeout); - authPolicy.authenticate(relay); - }); - } else { - relay.on("connect", () => { - console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); - clearTimeout(connectionTimeout); - }); - } + // Set up connection timeout + const connectionTimeout = setTimeout(() => { + try { + console.warn(`[NDK.ts] Connection timeout for ${secureUrl}`); + relay.disconnect(); + } catch { + // Silently ignore disconnect errors + } + }, 5000); // 5 second timeout - // Add error handling - relay.on("disconnect", () => { - console.debug(`[NDK.ts] Relay disconnected: ${secureUrl}`); - clearTimeout(connectionTimeout); - }); + // Set up custom authentication handling only if user is signed in + if (ndk.signer && ndk.activeUser) { + const authPolicy = new CustomRelayAuthPolicy(ndk); + relay.on("connect", () => { + try { + console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); + clearTimeout(connectionTimeout); + authPolicy.authenticate(relay); + } catch { + // Silently handle connect handler errors + } + }); + } else { + relay.on("connect", () => { + try { + console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); + clearTimeout(connectionTimeout); + } catch { + // Silently handle connect handler errors + } + }); + } + + // Add error handling + relay.on("disconnect", () => { + try { + console.debug(`[NDK.ts] Relay disconnected: ${secureUrl}`); + clearTimeout(connectionTimeout); + } catch { + // Silently handle disconnect handler errors + } + }); - return relay; + return relay; + } catch (error) { + // If relay creation fails, try to use an anonymous relay as fallback + console.debug(`[NDK.ts] Failed to create relay for ${url}, trying anonymous relay fallback`); + + // Find an anonymous relay that's not the same as the failed URL + const fallbackUrl = anonymousRelays.find(relay => relay !== url) || anonymousRelays[0]; + + if (fallbackUrl) { + console.debug(`[NDK.ts] Using anonymous relay as fallback: ${fallbackUrl}`); + try { + const fallbackRelay = new NDKRelay(fallbackUrl, NDKRelayAuthPolicies.signIn({ ndk }), ndk); + return fallbackRelay; + } catch (fallbackError) { + console.debug(`[NDK.ts] Fallback relay creation also failed: ${fallbackError}`); + } + } + + // If all else fails, create a minimal relay that will fail gracefully + console.debug(`[NDK.ts] All fallback attempts failed, creating minimal relay for ${url}`); + const minimalRelay = new NDKRelay(url, undefined, ndk); + return minimalRelay; + } } @@ -478,13 +534,27 @@ export function logCurrentRelayConfiguration(): void { console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`); } +/** + * Clears the relay set cache to force a rebuild + */ +export function clearRelaySetCache(): void { + console.debug('[NDK.ts] Clearing relay set cache'); + persistentRelaySet = null; + relaySetLastUpdated = 0; + // Clear from localStorage as well + if (typeof localStorage !== 'undefined') { + localStorage.removeItem('alexandria/relay_set_cache'); + } +} + /** * Updates relay stores when user state changes * @param ndk NDK instance */ export async function refreshRelayStores(ndk: NDK): Promise { console.debug('[NDK.ts] Refreshing relay stores due to user state change'); - await updateActiveRelayStores(ndk); + clearRelaySetCache(); // Clear cache when user state changes + await updateActiveRelayStores(ndk, true); // Force update } /** diff --git a/src/lib/stores/userStore.ts b/src/lib/stores/userStore.ts index 1e58f42..6189158 100644 --- a/src/lib/stores/userStore.ts +++ b/src/lib/stores/userStore.ts @@ -206,7 +206,7 @@ export async function loginWithExtension() { // Update relay stores with the new user's relays try { console.debug('[userStore.ts] loginWithExtension: Updating relay stores for authenticated user'); - await updateActiveRelayStores(ndk); + await updateActiveRelayStores(ndk, true); // Force update to rebuild relay set for authenticated user } catch (error) { console.warn('[userStore.ts] loginWithExtension: Failed to update relay stores:', error); } @@ -273,7 +273,7 @@ export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser) { // Update relay stores with the new user's relays try { console.debug('[userStore.ts] loginWithAmber: Updating relay stores for authenticated user'); - await updateActiveRelayStores(ndk); + await updateActiveRelayStores(ndk, true); // Force update to rebuild relay set for authenticated user } catch (error) { console.warn('[userStore.ts] loginWithAmber: Failed to update relay stores:', error); } diff --git a/src/lib/utils/relay_management.ts b/src/lib/utils/relay_management.ts index a4f41fa..c9c10a0 100644 --- a/src/lib/utils/relay_management.ts +++ b/src/lib/utils/relay_management.ts @@ -43,12 +43,12 @@ export function deduplicateRelayUrls(urls: string[]): string[] { } /** - * Tests connection to a relay and returns connection status - * @param relayUrl The relay URL to test + * Tests connection to a local relay (ws:// protocol) + * @param relayUrl The local relay URL to test (should be ws://) * @param ndk The NDK instance * @returns Promise that resolves to connection status */ -export function testRelayConnection( +export function testLocalRelayConnection( relayUrl: string, ndk: NDK, ): Promise<{ @@ -58,8 +58,135 @@ export function testRelayConnection( actualUrl?: string; }> { return new Promise((resolve) => { - // Ensure the URL is using wss:// protocol - const secureUrl = ensureSecureWebSocket(relayUrl); + try { + // Ensure the URL is using ws:// protocol for local relays + const localUrl = relayUrl.replace(/^wss:\/\//, "ws://"); + + // Use the existing NDK instance instead of creating a new one + const relay = new NDKRelay(localUrl, undefined, ndk); + let authRequired = false; + let connected = false; + let error: string | undefined; + let actualUrl: string | undefined; + + const timeout = setTimeout(() => { + try { + relay.disconnect(); + } catch { + // Silently ignore disconnect errors + } + resolve({ + connected: false, + requiresAuth: authRequired, + error: "Connection timeout", + actualUrl, + }); + }, 3000); + + // Wrap all event handlers in try-catch to prevent errors from bubbling up + relay.on("connect", () => { + try { + connected = true; + actualUrl = localUrl; + clearTimeout(timeout); + relay.disconnect(); + resolve({ + connected: true, + requiresAuth: authRequired, + error, + actualUrl, + }); + } catch { + // Silently handle any errors in connect handler + clearTimeout(timeout); + resolve({ + connected: false, + requiresAuth: false, + error: "Connection handler error", + actualUrl: localUrl, + }); + } + }); + + relay.on("notice", (message: string) => { + try { + if (message.includes("auth-required")) { + authRequired = true; + } + } catch { + // Silently ignore notice handler errors + } + }); + + relay.on("disconnect", () => { + try { + if (!connected) { + error = "Connection failed"; + clearTimeout(timeout); + resolve({ + connected: false, + requiresAuth: authRequired, + error, + actualUrl, + }); + } + } catch { + // Silently handle any errors in disconnect handler + clearTimeout(timeout); + resolve({ + connected: false, + requiresAuth: false, + error: "Disconnect handler error", + actualUrl: localUrl, + }); + } + }); + + // Wrap the connect call in try-catch + try { + relay.connect(); + } catch (connectError) { + // Silently handle connection errors + clearTimeout(timeout); + resolve({ + connected: false, + requiresAuth: false, + error: "Connection failed", + actualUrl: localUrl, + }); + } + } catch (outerError) { + // Catch any other errors that might occur during setup + resolve({ + connected: false, + requiresAuth: false, + error: "Setup failed", + actualUrl: relayUrl, + }); + } + }); +} + +/** + * Tests connection to a remote relay (wss:// protocol) + * @param relayUrl The remote relay URL to test + * @param ndk The NDK instance + * @returns Promise that resolves to connection status + */ +export function testRemoteRelayConnection( + relayUrl: string, + ndk: NDK, +): Promise<{ + connected: boolean; + requiresAuth: boolean; + error?: string; + actualUrl?: string; +}> { + return new Promise((resolve) => { + // Ensure the URL is using wss:// protocol for remote relays + const secureUrl = relayUrl.replace(/^ws:\/\//, "wss://"); + + console.debug(`[relay_management.ts] Testing remote relay connection: ${secureUrl}`); // Use the existing NDK instance instead of creating a new one const relay = new NDKRelay(secureUrl, undefined, ndk); @@ -69,6 +196,7 @@ export function testRelayConnection( let actualUrl: string | undefined; const timeout = setTimeout(() => { + console.debug(`[relay_management.ts] Relay ${secureUrl} connection timeout`); relay.disconnect(); resolve({ connected: false, @@ -76,9 +204,10 @@ export function testRelayConnection( error: "Connection timeout", actualUrl, }); - }, 3000); // Increased timeout to 3 seconds to give relays more time + }, 3000); relay.on("connect", () => { + console.debug(`[relay_management.ts] Relay ${secureUrl} connected successfully`); connected = true; actualUrl = secureUrl; clearTimeout(timeout); @@ -99,6 +228,7 @@ export function testRelayConnection( relay.on("disconnect", () => { if (!connected) { + console.debug(`[relay_management.ts] Relay ${secureUrl} disconnected without connecting`); error = "Connection failed"; clearTimeout(timeout); resolve({ @@ -113,30 +243,31 @@ export function testRelayConnection( 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) + * 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 */ -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}`, - ); +export function testRelayConnection( + relayUrl: string, + ndk: NDK, +): Promise<{ + connected: boolean; + requiresAuth: boolean; + error?: string; + actualUrl?: string; +}> { + // Determine if this is a local or remote relay + if (relayUrl.includes('localhost') || relayUrl.includes('127.0.0.1')) { + return testLocalRelayConnection(relayUrl, ndk); + } else { + return testRemoteRelayConnection(relayUrl, ndk); } - - return secureUrl; } + + /** * Tests connection to local relays @@ -145,33 +276,38 @@ function ensureSecureWebSocket(url: string): string { * @returns Promise that resolves to array of working local relay URLs */ async function testLocalRelays(localRelayUrls: string[], ndk: NDK): Promise { - const workingRelays: string[] = []; - - if (localRelayUrls.length === 0) { + try { + const workingRelays: string[] = []; + + if (localRelayUrls.length === 0) { + return workingRelays; + } + + // Test local relays quietly, without logging failures + await Promise.all( + localRelayUrls.map(async (url) => { + try { + const result = await testLocalRelayConnection(url, ndk); + if (result.connected) { + workingRelays.push(url); + console.debug(`[relay_management.ts] Local relay connected: ${url}`); + } + // Don't log failures - local relays are optional + } catch { + // Silently ignore local relay failures - they're optional + } + }) + ); + + if (workingRelays.length > 0) { + console.info(`[relay_management.ts] Found ${workingRelays.length} working local relays`); + } return workingRelays; + } catch { + // If anything goes wrong with the entire local relay testing process, + // just return an empty array silently + return []; } - - 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 { - // 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; } /** @@ -391,12 +527,18 @@ async function testRelaySet(relayUrls: string[], ndk: NDK): Promise { const workingRelays: string[] = []; const maxConcurrent = 2; // Reduce to 2 relays at a time to avoid overwhelming them + console.debug(`[relay_management.ts] Testing ${relayUrls.length} relays in batches of ${maxConcurrent}`); + console.debug(`[relay_management.ts] Relay URLs to test:`, relayUrls); + for (let i = 0; i < relayUrls.length; i += maxConcurrent) { const batch = relayUrls.slice(i, i + maxConcurrent); + console.debug(`[relay_management.ts] Testing batch ${Math.floor(i/maxConcurrent) + 1}:`, batch); const batchPromises = batch.map(async (url) => { try { + console.debug(`[relay_management.ts] Testing relay: ${url}`); const result = await testRelayConnection(url, ndk); + console.debug(`[relay_management.ts] Relay ${url} test result:`, result); return result.connected ? url : null; } catch (error) { console.debug(`[relay_management.ts] Failed to test relay ${url}:`, error); @@ -409,9 +551,12 @@ async function testRelaySet(relayUrls: string[], ndk: NDK): Promise { .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map(result => result.value) .filter((url): url is string => url !== null); + + console.debug(`[relay_management.ts] Batch ${Math.floor(i/maxConcurrent) + 1} working relays:`, batchWorkingRelays); workingRelays.push(...batchWorkingRelays); } + console.debug(`[relay_management.ts] Total working relays after testing:`, workingRelays); return workingRelays; } @@ -496,9 +641,16 @@ export async function buildCompleteRelaySet( }; } - // Use tested relays and deduplicate - const inboxRelays = testedInboxRelays.length > 0 ? deduplicateRelayUrls(testedInboxRelays) : deduplicateRelayUrls(secondaryRelays); - const outboxRelays = testedOutboxRelays.length > 0 ? deduplicateRelayUrls(testedOutboxRelays) : deduplicateRelayUrls(secondaryRelays); + // Always include some remote relays as fallback, even when local relays are working + const fallbackRelays = deduplicateRelayUrls([...anonymousRelays, ...secondaryRelays]); + + // Use tested relays and add fallback relays + const inboxRelays = testedInboxRelays.length > 0 + ? deduplicateRelayUrls([...testedInboxRelays, ...fallbackRelays]) + : deduplicateRelayUrls(fallbackRelays); + const outboxRelays = testedOutboxRelays.length > 0 + ? deduplicateRelayUrls([...testedOutboxRelays, ...fallbackRelays]) + : deduplicateRelayUrls(fallbackRelays); // Apply network condition optimization const currentNetworkCondition = get(networkCondition); @@ -515,8 +667,9 @@ export async function buildCompleteRelaySet( outboxRelays: deduplicateRelayUrls(networkOptimizedRelaySet.outboxRelays.filter((r: string) => !blockedRelays.includes(r))) }; - // If no relays are working, use anonymous relays as fallback + // Ensure we always have at least some relays if (finalRelaySet.inboxRelays.length === 0 && finalRelaySet.outboxRelays.length === 0) { + console.warn('[relay_management.ts] No relays available, using anonymous relays as final fallback'); return { inboxRelays: deduplicateRelayUrls(anonymousRelays), outboxRelays: deduplicateRelayUrls(anonymousRelays) From 0e720f70e833f80de3c86f0549c7dde9ee9304a1 Mon Sep 17 00:00:00 2001 From: silberengel Date: Thu, 14 Aug 2025 08:08:24 +0200 Subject: [PATCH 50/98] corrected my-notes going blank on refresh and showing when no one is logged-in --- src/lib/components/Navigation.svelte | 4 ++- src/lib/ndk.ts | 31 ++++++++++++----------- src/routes/my-notes/+page.svelte | 37 ++++++++++++++++++---------- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/src/lib/components/Navigation.svelte b/src/lib/components/Navigation.svelte index e155c03..186a93f 100644 --- a/src/lib/components/Navigation.svelte +++ b/src/lib/components/Navigation.svelte @@ -31,7 +31,9 @@ Visualize Getting Started Events - My Notes + {#if userState.signedIn} + My Notes + {/if} About Contact diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index 45e9097..92d2430 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -91,20 +91,8 @@ function clearPersistentRelaySet(): void { } } -// Subscribe to userStore changes and update ndkSignedIn accordingly -userStore.subscribe(async (userState) => { - ndkSignedIn.set(userState.signedIn); - - // Refresh relay stores when user state changes - const ndk = get(ndkInstance); - if (ndk) { - try { - await refreshRelayStores(ndk); - } catch (error) { - console.warn('[NDK.ts] Failed to refresh relay stores on user state change:', error); - } - } -}); +// AI-NOTE: userStore subscription moved to initNdk function to prevent initialization errors +// The subscription will be set up after userStore is properly initialized /** * Custom authentication policy that handles NIP-42 authentication manually @@ -655,6 +643,21 @@ export function initNdk(): NDK { attemptConnection(); + // AI-NOTE: Set up userStore subscription after NDK initialization to prevent initialization errors + userStore.subscribe(async (userState) => { + ndkSignedIn.set(userState.signedIn); + + // Refresh relay stores when user state changes + const ndk = get(ndkInstance); + if (ndk) { + try { + await refreshRelayStores(ndk); + } catch (error) { + console.warn('[NDK.ts] Failed to refresh relay stores on user state change:', error); + } + } + }); + return ndk; } diff --git a/src/routes/my-notes/+page.svelte b/src/routes/my-notes/+page.svelte index 4841b4c..852d31e 100644 --- a/src/routes/my-notes/+page.svelte +++ b/src/routes/my-notes/+page.svelte @@ -1,5 +1,6 @@ From f199f356a83060c450f2285c68d628d0ded8ee86 Mon Sep 17 00:00:00 2001 From: silberengel Date: Thu, 14 Aug 2025 09:13:30 +0200 Subject: [PATCH 51/98] allow for differentiation: ssr versus client rendering --- .../publications/PublicationFeed.svelte | 5 + src/lib/ndk.ts | 36 ++++++- src/lib/stores/userStore.ts | 96 ++++++++++++------- .../utils/markup/asciidoctorPostProcessor.ts | 8 +- src/lib/utils/markup/basicMarkupParser.ts | 4 +- src/lib/utils/relay_management.ts | 20 ++++ src/routes/+layout.svelte | 46 ++++++++- src/routes/+layout.ts | 9 +- 8 files changed, 176 insertions(+), 48 deletions(-) diff --git a/src/lib/components/publications/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte index 0a2593b..931cf9d 100644 --- a/src/lib/components/publications/PublicationFeed.svelte +++ b/src/lib/components/publications/PublicationFeed.svelte @@ -551,6 +551,11 @@ } function getSkeletonIds(): string[] { + // Only access window on client-side + if (typeof window === 'undefined') { + return ['skeleton-0', 'skeleton-1', 'skeleton-2']; // Default fallback for SSR + } + 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 = []; diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index 92d2430..74ed95a 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -44,6 +44,9 @@ const RELAY_SET_STORAGE_KEY = 'alexandria/relay_set_cache'; * Load persistent relay set from localStorage */ function loadPersistentRelaySet(): { relaySet: { inboxRelays: string[]; outboxRelays: string[] } | null; lastUpdated: number } { + // Only load from localStorage on client-side + if (typeof window === 'undefined') return { relaySet: null, lastUpdated: 0 }; + try { const stored = localStorage.getItem(RELAY_SET_STORAGE_KEY); if (!stored) return { relaySet: null, lastUpdated: 0 }; @@ -69,6 +72,9 @@ function loadPersistentRelaySet(): { relaySet: { inboxRelays: string[]; outboxRe * Save persistent relay set to localStorage */ function savePersistentRelaySet(relaySet: { inboxRelays: string[]; outboxRelays: string[] }): void { + // Only save to localStorage on client-side + if (typeof window === 'undefined') return; + try { const data = { relaySet, @@ -84,6 +90,9 @@ function savePersistentRelaySet(relaySet: { inboxRelays: string[]; outboxRelays: * Clear persistent relay set from localStorage */ function clearPersistentRelaySet(): void { + // Only clear from localStorage on client-side + if (typeof window === 'undefined') return; + try { localStorage.removeItem(RELAY_SET_STORAGE_KEY); } catch (error) { @@ -281,6 +290,9 @@ export function checkWebSocketSupport(): void { * sessions. */ export function getPersistedLogin(): string | null { + // Only access localStorage on client-side + if (typeof window === 'undefined') return null; + const pubkey = localStorage.getItem(loginStorageKey); return pubkey; } @@ -292,6 +304,9 @@ export function getPersistedLogin(): string | null { * time. */ export function persistLogin(user: NDKUser): void { + // Only access localStorage on client-side + if (typeof window === 'undefined') return; + localStorage.setItem(loginStorageKey, user.pubkey); } @@ -300,6 +315,9 @@ export function persistLogin(user: NDKUser): void { * @remarks Use this function when the user logs out. */ export function clearLogin(): void { + // Only access localStorage on client-side + if (typeof window === 'undefined') return; + localStorage.removeItem(loginStorageKey); } @@ -314,6 +332,9 @@ function getRelayStorageKey(user: NDKUser, type: "inbox" | "outbox"): string { } export function clearPersistedRelays(user: NDKUser): void { + // Only access localStorage on client-side + if (typeof window === 'undefined') return; + localStorage.removeItem(getRelayStorageKey(user, "inbox")); localStorage.removeItem(getRelayStorageKey(user, "outbox")); } @@ -529,8 +550,8 @@ export function clearRelaySetCache(): void { console.debug('[NDK.ts] Clearing relay set cache'); persistentRelaySet = null; relaySetLastUpdated = 0; - // Clear from localStorage as well - if (typeof localStorage !== 'undefined') { + // Clear from localStorage as well (client-side only) + if (typeof window !== 'undefined') { localStorage.removeItem('alexandria/relay_set_cache'); } } @@ -613,6 +634,12 @@ export function initNdk(): NDK { const maxRetries = 1; // Reduce to 1 retry const attemptConnection = async () => { + // Only attempt connection on client-side + if (typeof window === 'undefined') { + console.debug("[NDK.ts] Skipping NDK connection during SSR"); + return; + } + try { await ndk.connect(); console.debug("[NDK.ts] NDK connected successfully"); @@ -641,7 +668,10 @@ export function initNdk(): NDK { } }; - attemptConnection(); + // Only attempt connection on client-side + if (typeof window !== 'undefined') { + attemptConnection(); + } // AI-NOTE: Set up userStore subscription after NDK initialization to prevent initialization errors userStore.subscribe(async (userState) => { diff --git a/src/lib/stores/userStore.ts b/src/lib/stores/userStore.ts index 6189158..0b96dd0 100644 --- a/src/lib/stores/userStore.ts +++ b/src/lib/stores/userStore.ts @@ -45,6 +45,9 @@ function persistRelays( inboxes: Set, outboxes: Set, ): void { + // Only access localStorage on client-side + if (typeof window === 'undefined') return; + localStorage.setItem( getRelayStorageKey(user, "inbox"), JSON.stringify(Array.from(inboxes).map((relay) => relay.url)), @@ -56,6 +59,11 @@ function persistRelays( } function getPersistedRelays(user: NDKUser): [Set, Set] { + // Only access localStorage on client-side + if (typeof window === 'undefined') { + return [new Set(), new Set()]; + } + const inboxes = new Set( JSON.parse(localStorage.getItem(getRelayStorageKey(user, "inbox")) ?? "[]"), ); @@ -135,6 +143,9 @@ async function getUserPreferredRelays( export const loginMethodStorageKey = "alexandria/login/method"; function persistLogin(user: NDKUser, method: "extension" | "amber" | "npub") { + // Only access localStorage on client-side + if (typeof window === 'undefined') return; + localStorage.setItem(loginStorageKey, user.pubkey); localStorage.setItem(loginMethodStorageKey, method); } @@ -212,7 +223,10 @@ export async function loginWithExtension() { } clearLogin(); - localStorage.removeItem("alexandria/logout/flag"); + // Only access localStorage on client-side + if (typeof window !== 'undefined') { + localStorage.removeItem("alexandria/logout/flag"); + } persistLogin(user, "extension"); } @@ -279,7 +293,10 @@ export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser) { } clearLogin(); - localStorage.removeItem("alexandria/logout/flag"); + // Only access localStorage on client-side + if (typeof window !== 'undefined') { + localStorage.removeItem("alexandria/logout/flag"); + } persistLogin(user, "amber"); } @@ -363,7 +380,10 @@ export async function loginWithNpub(pubkeyOrNpub: string) { userPubkey.set(user.pubkey); clearLogin(); - localStorage.removeItem("alexandria/logout/flag"); + // Only access localStorage on client-side + if (typeof window !== 'undefined') { + localStorage.removeItem("alexandria/logout/flag"); + } persistLogin(user, "npub"); } @@ -373,47 +393,51 @@ export async function loginWithNpub(pubkeyOrNpub: string) { export function logoutUser() { console.log("Logging out user..."); const currentUser = get(userStore); - if (currentUser.ndkUser) { - // Clear persisted relays for the user - localStorage.removeItem(getRelayStorageKey(currentUser.ndkUser, "inbox")); - localStorage.removeItem(getRelayStorageKey(currentUser.ndkUser, "outbox")); - } + + // Only access localStorage on client-side + if (typeof window !== 'undefined') { + if (currentUser.ndkUser) { + // Clear persisted relays for the user + localStorage.removeItem(getRelayStorageKey(currentUser.ndkUser, "inbox")); + localStorage.removeItem(getRelayStorageKey(currentUser.ndkUser, "outbox")); + } - // Clear all possible login states from localStorage - clearLogin(); + // Clear all possible login states from localStorage + clearLogin(); - // Also clear any other potential login keys that might exist - const keysToRemove = []; - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if ( - key && - (key.includes("login") || - key.includes("nostr") || - key.includes("user") || - key.includes("alexandria") || - key === "pubkey") - ) { - keysToRemove.push(key); + // Also clear any other potential login keys that might exist + const keysToRemove = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if ( + key && + (key.includes("login") || + key.includes("nostr") || + key.includes("user") || + key.includes("alexandria") || + key === "pubkey") + ) { + keysToRemove.push(key); + } } - } - // Specifically target the login storage key - keysToRemove.push("alexandria/login/pubkey"); - keysToRemove.push("alexandria/login/method"); + // Specifically target the login storage key + keysToRemove.push("alexandria/login/pubkey"); + keysToRemove.push("alexandria/login/method"); - keysToRemove.forEach((key) => { - console.log("Removing localStorage key:", key); - localStorage.removeItem(key); - }); + keysToRemove.forEach((key) => { + console.log("Removing localStorage key:", key); + localStorage.removeItem(key); + }); - // Clear Amber-specific flags - localStorage.removeItem("alexandria/amber/fallback"); + // Clear Amber-specific flags + localStorage.removeItem("alexandria/amber/fallback"); - // Set a flag to prevent auto-login on next page load - localStorage.setItem("alexandria/logout/flag", "true"); + // Set a flag to prevent auto-login on next page load + localStorage.setItem("alexandria/logout/flag", "true"); - console.log("Cleared all login data from localStorage"); + console.log("Cleared all login data from localStorage"); + } userStore.set({ pubkey: null, diff --git a/src/lib/utils/markup/asciidoctorPostProcessor.ts b/src/lib/utils/markup/asciidoctorPostProcessor.ts index f2d9318..82a1dc7 100644 --- a/src/lib/utils/markup/asciidoctorPostProcessor.ts +++ b/src/lib/utils/markup/asciidoctorPostProcessor.ts @@ -26,8 +26,8 @@ function replaceWikilinks(html: string): string { const display = (label || target).trim(); const url = `/events?d=${normalized}`; // Output as a clickable with the [[display]] format and matching link colors - // Use onclick to bypass SvelteKit routing and navigate directly - return `${display}`; + // Remove onclick handler to avoid breaking amber session - will be handled by global click handler + return `${display}`; }, ); } @@ -39,8 +39,8 @@ function replaceAsciiDocAnchors(html: string): string { return html.replace(/<\/a>/g, (_match, id) => { const normalized = normalizeDTag(id.trim()); const url = `/events?d=${normalized}`; - // Use onclick to bypass SvelteKit routing and navigate directly - return `${id}`; + // Remove onclick handler to avoid breaking amber session - will be handled by global click handler + return `${id}`; }); } diff --git a/src/lib/utils/markup/basicMarkupParser.ts b/src/lib/utils/markup/basicMarkupParser.ts index d4b35bd..10ebbe7 100644 --- a/src/lib/utils/markup/basicMarkupParser.ts +++ b/src/lib/utils/markup/basicMarkupParser.ts @@ -149,8 +149,8 @@ function replaceWikilinks(text: string): string { const display = (label || target).trim(); const url = `/events?d=${normalized}`; // Output as a clickable with the [[display]] format and matching link colors - // Use onclick to bypass SvelteKit routing and navigate directly - return `${display}`; + // Remove onclick handler to avoid breaking amber session - will be handled by global click handler + return `${display}`; }, ); } diff --git a/src/lib/utils/relay_management.ts b/src/lib/utils/relay_management.ts index c9c10a0..c75a801 100644 --- a/src/lib/utils/relay_management.ts +++ b/src/lib/utils/relay_management.ts @@ -57,6 +57,16 @@ export function testLocalRelayConnection( error?: string; actualUrl?: string; }> { + // Only test connections on client-side + if (typeof window === 'undefined') { + return Promise.resolve({ + connected: false, + requiresAuth: false, + error: "Server-side rendering - connection test skipped", + actualUrl: relayUrl, + }); + } + return new Promise((resolve) => { try { // Ensure the URL is using ws:// protocol for local relays @@ -182,6 +192,16 @@ export function testRemoteRelayConnection( error?: string; actualUrl?: string; }> { + // Only test connections on client-side + if (typeof window === 'undefined') { + return Promise.resolve({ + connected: false, + requiresAuth: false, + error: "Server-side rendering - connection test skipped", + actualUrl: relayUrl, + }); + } + return new Promise((resolve) => { // Ensure the URL is using wss:// protocol for remote relays const secureUrl = relayUrl.replace(/^ws:\/\//, "wss://"); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 3aae73f..dd7f835 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,8 +1,9 @@ - diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index ac50221..a1253a9 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -10,14 +10,18 @@ import type { LayoutLoad } from "./$types"; import { get } from "svelte/store"; import { browser } from "$app/environment"; -// AI-NOTE: Leave SSR off until event fetches are implemented server-side. -export const ssr = false; +// AI-NOTE: SSR enabled for better SEO and OpenGraph support +export const ssr = true; /** * Attempts to restore the user's authentication session from localStorage. * Handles extension, Amber (NIP-46), and npub login methods. + * Only runs on client-side. */ function restoreAuthSession() { + // Only run on client-side + if (!browser) return; + try { const pubkey = getPersistedLogin(); const loginMethod = localStorage.getItem(loginMethodStorageKey); @@ -122,6 +126,7 @@ export const load: LayoutLoad = () => { const ndk = initNdk(); ndkInstance.set(ndk); + // Only restore auth session on client-side if (browser) { restoreAuthSession(); } From 27fdbde8df21aabe3371fae17a9e51e58d73a533 Mon Sep 17 00:00:00 2001 From: silberengel Date: Thu, 14 Aug 2025 09:27:04 +0200 Subject: [PATCH 52/98] fixed build and worked on memory leak --- Dockerfile | 9 ++++++ deno.json | 10 +++++++ import_map.json | 19 ++++++++---- src/lib/data_structures/websocket_pool.ts | 24 ++++++++++++++-- src/lib/ndk.ts | 35 ++++++++++++++++++++++- src/routes/+layout.svelte | 10 +++++-- 6 files changed, 95 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index c8ecacc..9bdfec7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,11 @@ FROM denoland/deno:alpine AS build WORKDIR /app/src COPY . . + +# Set memory limits for Deno to prevent memory leaks +ENV DENO_MEMORY_LIMIT=512MB +ENV DENO_GC_INTERVAL=1000 + RUN deno install RUN deno task build @@ -11,6 +16,10 @@ COPY --from=build /app/src/import_map.json . ENV ORIGIN=http://localhost:3000 +# Set memory limits for runtime to prevent memory leaks +ENV DENO_MEMORY_LIMIT=512MB +ENV DENO_GC_INTERVAL=1000 + RUN deno cache --import-map=import_map.json ./build/index.js EXPOSE 3000 diff --git a/deno.json b/deno.json index 9e2ecc6..350316e 100644 --- a/deno.json +++ b/deno.json @@ -2,5 +2,15 @@ "importMap": "./import_map.json", "compilerOptions": { "lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"] + }, + "tasks": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --plugin-search-dir . --check . && eslint .", + "format": "prettier --plugin-search-dir . --write .", + "test": "vitest" } } diff --git a/import_map.json b/import_map.json index b5aa95c..3c34c52 100644 --- a/import_map.json +++ b/import_map.json @@ -2,18 +2,25 @@ "imports": { "he": "npm:he@1.2.x", "@nostr-dev-kit/ndk": "npm:@nostr-dev-kit/ndk@^2.14.32", - "@nostr-dev-kit/ndk-cache-dexie": "npm:@nostr-dev-kit/ndk-cache-dexie@^2.6.33", + "@nostr-dev-kit/ndk-cache-dexie": "npm:@nostr-dev-kit/ndk-cache-dexie@2.6.x", "@popperjs/core": "npm:@popperjs/core@2.11.x", "@tailwindcss/forms": "npm:@tailwindcss/forms@0.5.x", "@tailwindcss/typography": "npm:@tailwindcss/typography@0.5.x", "asciidoctor": "npm:asciidoctor@3.0.x", - "d3": "npm:d3@7.9.x", - "nostr-tools": "npm:nostr-tools@^2.15.1", + "d3": "npm:d3@^7.9.0", + "nostr-tools": "npm:nostr-tools@2.15.x", "tailwind-merge": "npm:tailwind-merge@^3.3.1", "svelte": "npm:svelte@^5.36.8", - "flowbite": "npm:flowbite@^3.1.2", - "flowbite-svelte": "npm:flowbite-svelte@^1.10.10", - "flowbite-svelte-icons": "npm:flowbite-svelte-icons@^2.2.1", + "flowbite": "npm:flowbite@2.x", + "flowbite-svelte": "npm:flowbite-svelte@0.48.x", + "flowbite-svelte-icons": "npm:flowbite-svelte-icons@2.1.x", + "@noble/curves": "npm:@noble/curves@^1.9.4", + "@noble/hashes": "npm:@noble/hashes@^1.8.0", + "bech32": "npm:bech32@^2.0.0", + "highlight.js": "npm:highlight.js@^11.11.1", + "node-emoji": "npm:node-emoji@^2.2.0", + "plantuml-encoder": "npm:plantuml-encoder@^1.4.0", + "qrcode": "npm:qrcode@^1.5.4", "child_process": "node:child_process" } } diff --git a/src/lib/data_structures/websocket_pool.ts b/src/lib/data_structures/websocket_pool.ts index fca0325..5efcdf5 100644 --- a/src/lib/data_structures/websocket_pool.ts +++ b/src/lib/data_structures/websocket_pool.ts @@ -191,20 +191,27 @@ export class WebSocketPool { * Closes all WebSocket connections and "drains" the pool. */ public drain(): void { + console.debug(`[WebSocketPool] Draining pool with ${this.#pool.size} connections and ${this.#waitingQueue.length} waiting requests`); + // Clear all idle timers first for (const handle of this.#pool.values()) { this.#clearIdleTimer(handle); } + // Reject all waiting requests for (const { reject } of this.#waitingQueue) { reject(new Error('[WebSocketPool] Draining pool.')); } this.#waitingQueue = []; + // Close all connections and clean up for (const handle of this.#pool.values()) { - handle.ws.close(); + if (handle.ws && handle.ws.readyState === WebSocket.OPEN) { + handle.ws.close(); + } } this.#pool.clear(); + console.debug('[WebSocketPool] Pool drained successfully'); } // #endregion @@ -243,8 +250,18 @@ export class WebSocketPool { #removeSocket(handle: WebSocketHandle): void { this.#clearIdleTimer(handle); - handle.ws.onopen = handle.ws.onerror = handle.ws.onclose = null; - this.#pool.delete(this.#normalizeUrl(handle.ws.url)); + + // Clean up event listeners to prevent memory leaks + if (handle.ws) { + handle.ws.onopen = null; + handle.ws.onerror = null; + handle.ws.onclose = null; + handle.ws.onmessage = null; + } + + const url = this.#normalizeUrl(handle.ws.url); + this.#pool.delete(url); + console.debug(`[WebSocketPool] Removed socket for ${url}, pool size: ${this.#pool.size}`); this.#processWaitingQueue(); } @@ -261,6 +278,7 @@ export class WebSocketPool { handle.idleTimer = setTimeout(() => { const refCount = handle.refCount; if (refCount === 0 && handle.ws.readyState === WebSocket.OPEN) { + console.debug(`[WebSocketPool] Closing idle connection to ${handle.ws.url}`); handle.ws.close(); this.#removeSocket(handle); } diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index 74ed95a..55afb4d 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -654,7 +654,10 @@ export function initNdk(): NDK { if (retryCount < maxRetries) { retryCount++; console.debug(`[NDK.ts] Attempting to reconnect (${retryCount}/${maxRetries})...`); - setTimeout(attemptConnection, 2000); // Reduce timeout to 2 seconds + // Use a more reasonable retry delay and prevent memory leaks + setTimeout(() => { + attemptConnection(); + }, 2000 * retryCount); // Exponential backoff } else { console.warn("[NDK.ts] Max retries reached, continuing with limited functionality"); // Still try to update relay stores even if connection failed @@ -691,6 +694,36 @@ export function initNdk(): NDK { return ndk; } +/** + * Cleans up NDK resources to prevent memory leaks + * Should be called when the application is shutting down or when NDK needs to be reset + */ +export function cleanupNdk(): void { + console.debug("[NDK.ts] Cleaning up NDK resources"); + + const ndk = get(ndkInstance); + if (ndk) { + try { + // Disconnect from all relays + if (ndk.pool) { + for (const relay of ndk.pool.relays.values()) { + relay.disconnect(); + } + } + + // Drain the WebSocket pool + WebSocketPool.instance.drain(); + + // Stop network monitoring + stopNetworkStatusMonitoring(); + + console.debug("[NDK.ts] NDK cleanup completed"); + } catch (error) { + console.warn("[NDK.ts] Error during NDK cleanup:", error); + } + } +} + /** * Signs in with a NIP-07 browser extension using the new relay management system * @returns The user's profile, if it is available diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index dd7f835..43bfb5c 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -6,7 +6,7 @@ import { goto } from "$app/navigation"; import { Alert } from "flowbite-svelte"; import { HammerSolid } from "flowbite-svelte-icons"; - import { logCurrentRelayConfiguration, activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; + import { logCurrentRelayConfiguration, activeInboxRelays, activeOutboxRelays, cleanupNdk } from "$lib/ndk"; // Define children prop for Svelte 5 let { children } = $props(); @@ -83,7 +83,13 @@ } document.addEventListener("click", handleInternalLinkClick); - return () => document.removeEventListener("click", handleInternalLinkClick); + + // Cleanup function to prevent memory leaks + return () => { + document.removeEventListener("click", handleInternalLinkClick); + // Clean up NDK resources when component unmounts + cleanupNdk(); + }; }); From f3a589b5954d378e8286c095e0447a66f7b5b190 Mon Sep 17 00:00:00 2001 From: silberengel Date: Thu, 14 Aug 2025 15:42:58 +0200 Subject: [PATCH 53/98] corrected relay management --- import_map.json | 3 +- package-lock.json | 592 +++++++------- package.json | 1 + src/lib/components/EventSearch.svelte | 761 ++++++++---------- .../publications/PublicationFeed.svelte | 42 +- src/lib/ndk.ts | 25 +- src/lib/services/event_search_service.ts | 84 ++ src/lib/services/search_state_manager.ts | 62 ++ src/lib/utils/event_search.ts | 28 +- src/lib/utils/nostrUtils.ts | 16 +- src/lib/utils/search_result_formatter.ts | 26 + src/lib/utils/subscription_search.ts | 57 +- vite.config.ts | 5 + 13 files changed, 938 insertions(+), 764 deletions(-) create mode 100644 src/lib/services/event_search_service.ts create mode 100644 src/lib/services/search_state_manager.ts create mode 100644 src/lib/utils/search_result_formatter.ts diff --git a/import_map.json b/import_map.json index 3c34c52..d536e63 100644 --- a/import_map.json +++ b/import_map.json @@ -21,6 +21,7 @@ "node-emoji": "npm:node-emoji@^2.2.0", "plantuml-encoder": "npm:plantuml-encoder@^1.4.0", "qrcode": "npm:qrcode@^1.5.4", - "child_process": "node:child_process" + "child_process": "node:child_process", + "process": "node:process" } } diff --git a/package-lock.json b/package-lock.json index 003bf33..97568ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,20 +69,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@asciidoctor/cli": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@asciidoctor/cli/-/cli-4.0.0.tgz", @@ -149,12 +135,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.2" }, "bin": { "parser": "bin/babel-parser.js" @@ -177,9 +163,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", - "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", "cpu": [ "ppc64" ], @@ -194,9 +180,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", - "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "cpu": [ "arm" ], @@ -211,9 +197,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", - "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "cpu": [ "arm64" ], @@ -228,9 +214,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", - "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", "cpu": [ "x64" ], @@ -245,9 +231,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", - "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "cpu": [ "arm64" ], @@ -262,9 +248,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", - "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "cpu": [ "x64" ], @@ -279,9 +265,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", - "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "cpu": [ "arm64" ], @@ -296,9 +282,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", - "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "cpu": [ "x64" ], @@ -313,9 +299,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", - "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "cpu": [ "arm" ], @@ -330,9 +316,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", - "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", "cpu": [ "arm64" ], @@ -347,9 +333,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", - "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", "cpu": [ "ia32" ], @@ -364,9 +350,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", - "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", "cpu": [ "loong64" ], @@ -381,9 +367,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", - "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", "cpu": [ "mips64el" ], @@ -398,9 +384,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", - "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", "cpu": [ "ppc64" ], @@ -415,9 +401,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", - "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", "cpu": [ "riscv64" ], @@ -432,9 +418,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", - "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "cpu": [ "s390x" ], @@ -449,9 +435,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", - "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "cpu": [ "x64" ], @@ -466,9 +452,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", - "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", "cpu": [ "arm64" ], @@ -483,9 +469,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", - "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", "cpu": [ "x64" ], @@ -500,9 +486,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", - "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", "cpu": [ "arm64" ], @@ -517,9 +503,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", - "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", "cpu": [ "x64" ], @@ -534,9 +520,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", - "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", "cpu": [ "arm64" ], @@ -551,9 +537,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", - "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", "cpu": [ "x64" ], @@ -568,9 +554,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", - "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", "cpu": [ "arm64" ], @@ -585,9 +571,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", - "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", "cpu": [ "ia32" ], @@ -602,9 +588,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", - "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", "cpu": [ "x64" ], @@ -678,9 +664,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -689,9 +675,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -728,9 +714,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", - "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", "dev": true, "license": "MIT", "peer": true, @@ -753,14 +739,14 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -963,15 +949,26 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -982,15 +979,15 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1663,9 +1660,9 @@ } }, "node_modules/@sveltejs/adapter-auto": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-6.0.1.tgz", - "integrity": "sha512-mcWud3pYGPWM2Pphdj8G9Qiq24nZ8L4LB7coCUckUEy5Y7wOWGJ/enaZ4AtJTcSm5dNK1rIkBRoqt+ae4zlxcQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-6.1.0.tgz", + "integrity": "sha512-shOuLI5D2s+0zTv2ab5M5PqfknXqWbKi+0UwB9yLTRIdzsK1R93JOO8jNhIYSHdW+IYXIYnLniu+JZqXs7h9Wg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1673,9 +1670,9 @@ } }, "node_modules/@sveltejs/adapter-node": { - "version": "5.2.13", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.13.tgz", - "integrity": "sha512-yS2TVFmIrxjGhYaV5/iIUrJ3mJl6zjaYn0lBD70vTLnYvJeqf3cjvLXeXCUCuYinhSBoyF4DpfGla49BnIy7sQ==", + "version": "5.2.14", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.14.tgz", + "integrity": "sha512-TjJvfw0HZlbBGGAW2vFtdGjdKhqpGW3ZDIz0nzy8Zx6Ki6oFmYTjV5Kwn3LWTsyjbsUSXhfFPCuYop3z1iS9qQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1689,9 +1686,9 @@ } }, "node_modules/@sveltejs/adapter-static": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.8.tgz", - "integrity": "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.9.tgz", + "integrity": "sha512-aytHXcMi7lb9ljsWUzXYQ0p5X1z9oWud2olu/EpmH7aCu4m84h7QLvb5Wp+CFirKcwoNnYvYWhyP/L8Vh1ztdw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1699,9 +1696,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.27.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.27.0.tgz", - "integrity": "sha512-pEX1Z2Km8tqmkni+ykIIou+ojp/7gb3M9tpllN5nDWNo9zlI0dI8/hDKFyBwQvb4jYR+EyLriFtrmgJ6GvbnBA==", + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.29.1.tgz", + "integrity": "sha512-0D3dkz5ay5OslGSv8hyZJY892kTw+1G16zFR/ZXbV9XOd6s6Y9wW66vbkr0AtV0BXYtsXCXyt15H/OshAWKfDA==", "dev": true, "license": "MIT", "dependencies": { @@ -1732,13 +1729,13 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.1.0.tgz", - "integrity": "sha512-+U6lz1wvGEG/BvQyL4z/flyNdQ9xDNv5vrh+vWBWTHaebqT0c9RNggpZTo/XSPoHsSCWBlYaTlRX8pZ9GATXCw==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.1.2.tgz", + "integrity": "sha512-7v+7OkUYelC2dhhYDAgX1qO2LcGscZ18Hi5kKzJQq7tQeXpH215dd0+J/HnX2zM5B3QKcIrTVqCGkZXAy5awYw==", "dev": true, "license": "MIT", "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0-next.1", + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", @@ -2143,13 +2140,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", - "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "version": "24.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", + "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.10.0" } }, "node_modules/@types/qrcode": { @@ -2581,7 +2578,9 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2600,9 +2599,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", + "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", "dev": true, "funding": [ { @@ -2620,8 +2619,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", + "caniuse-lite": "^1.0.30001733", + "electron-to-chromium": "^1.5.199", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -2701,9 +2700,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001731", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", - "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "version": "1.0.30001735", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", + "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", "dev": true, "funding": [ { @@ -2742,7 +2741,9 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2783,39 +2784,19 @@ } }, "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, "license": "MIT", "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==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" } }, "node_modules/cliui": { @@ -2877,7 +2858,9 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" + "dev": true, + "license": "MIT", + "peer": true }, "node_modules/constantinople": { "version": "4.0.1", @@ -3462,9 +3445,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.194", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz", - "integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==", + "version": "1.5.200", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.200.tgz", + "integrity": "sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==", "dev": true, "license": "ISC" }, @@ -3518,9 +3501,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", - "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3531,32 +3514,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.8", - "@esbuild/android-arm": "0.25.8", - "@esbuild/android-arm64": "0.25.8", - "@esbuild/android-x64": "0.25.8", - "@esbuild/darwin-arm64": "0.25.8", - "@esbuild/darwin-x64": "0.25.8", - "@esbuild/freebsd-arm64": "0.25.8", - "@esbuild/freebsd-x64": "0.25.8", - "@esbuild/linux-arm": "0.25.8", - "@esbuild/linux-arm64": "0.25.8", - "@esbuild/linux-ia32": "0.25.8", - "@esbuild/linux-loong64": "0.25.8", - "@esbuild/linux-mips64el": "0.25.8", - "@esbuild/linux-ppc64": "0.25.8", - "@esbuild/linux-riscv64": "0.25.8", - "@esbuild/linux-s390x": "0.25.8", - "@esbuild/linux-x64": "0.25.8", - "@esbuild/netbsd-arm64": "0.25.8", - "@esbuild/netbsd-x64": "0.25.8", - "@esbuild/openbsd-arm64": "0.25.8", - "@esbuild/openbsd-x64": "0.25.8", - "@esbuild/openharmony-arm64": "0.25.8", - "@esbuild/sunos-x64": "0.25.8", - "@esbuild/win32-arm64": "0.25.8", - "@esbuild/win32-ia32": "0.25.8", - "@esbuild/win32-x64": "0.25.8" + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" } }, "node_modules/escalade": { @@ -3583,9 +3566,9 @@ } }, "node_modules/eslint": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", - "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, "license": "MIT", "peer": true, @@ -3593,11 +3576,11 @@ "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.32.0", - "@eslint/plugin-kit": "^0.3.4", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3731,16 +3714,6 @@ } } }, - "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, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -4340,7 +4313,9 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -4631,15 +4606,14 @@ } }, "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", "license": "Apache-2.0", "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", + "async": "^3.2.6", "filelist": "^1.0.4", - "minimatch": "^3.1.2" + "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" @@ -4921,7 +4895,9 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5983,27 +5959,17 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "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, "license": "MIT", "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": { @@ -6441,7 +6407,9 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -6462,13 +6430,13 @@ } }, "node_modules/svelte": { - "version": "5.37.3", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.37.3.tgz", - "integrity": "sha512-7t/ejshehHd+95z3Z7ebS7wsqHDQxi/8nBTuTRwpMgNegfRBfuitCSKTUDKIBOExqfT2+DhQ2VLG8Xn+cBXoaQ==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.1.tgz", + "integrity": "sha512-fO6CLDfJYWHgfo6lQwkQU2vhCiHc2MBl6s3vEhK+sSZru17YL4R5s1v14ndRpqKAIkq8nCz6MTk1yZbESZWeyQ==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", + "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", @@ -6488,9 +6456,9 @@ } }, "node_modules/svelte-check": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.0.tgz", - "integrity": "sha512-Iz8dFXzBNAM7XlEIsUjUGQhbEE+Pvv9odb9+0+ITTgFWZBGeJRRYqHUUglwe2EkLD5LIsQaAc4IUJyvtKuOO5w==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.1.tgz", + "integrity": "sha512-lkh8gff5gpHLjxIV+IaApMxQhTGnir2pNUAqcNgeKkvK5bT/30Ey/nzBxNLDlkztCH4dP7PixkMt9SWEKFPBWg==", "dev": true, "license": "MIT", "dependencies": { @@ -6511,36 +6479,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, - "license": "MIT", - "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, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/svelte-eslint-parser": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.3.1.tgz", @@ -6741,6 +6679,54 @@ "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==", + "license": "MIT", + "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==", + "license": "ISC", + "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==", + "license": "MIT", + "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", @@ -6789,6 +6775,30 @@ "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==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/tailwindcss/node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "license": "ISC", + "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", @@ -6966,9 +6976,9 @@ } }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, "license": "MIT" }, @@ -7374,15 +7384,13 @@ } }, "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, "engines": { - "node": ">= 14.6" + "node": ">= 6" } }, "node_modules/yargs": { diff --git a/package.json b/package.json index 2225a7a..5426173 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite dev", + "dev:debug": "DEBUG_RELAYS=true vite dev", "dev:node": "node --version && vite dev", "build": "vite build", "preview": "vite preview", diff --git a/src/lib/components/EventSearch.svelte b/src/lib/components/EventSearch.svelte index 67dece6..0304107 100644 --- a/src/lib/components/EventSearch.svelte +++ b/src/lib/components/EventSearch.svelte @@ -10,12 +10,11 @@ searchNip05, } from "$lib/utils/search_utility"; import { neventEncode, naddrEncode, nprofileEncode } from "$lib/utils"; - import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; + import { activeInboxRelays, activeOutboxRelays, ndkInstance } 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 { loading, @@ -52,7 +51,7 @@ let localError = $state(null); let foundEvent = $state(null); let searching = $state(false); - let searchCompleted = $state(false); + let searchCompleted = $state(false); let searchResultCount = $state(null); let searchResultType = $state(null); let isResetting = $state(false); @@ -75,7 +74,10 @@ let isWaitingForSearchResult = $state(false); let isUserEditing = $state(false); - // Move search handler functions above all $effect runes + // Debounced search timeout + let searchTimeout: ReturnType | null = null; + + // AI-NOTE: 2025-01-24 - Core search handlers extracted for better organization async function handleNip05Search(query: string) { try { const foundEvent = await searchNip05(query); @@ -83,42 +85,11 @@ handleFoundEvent(foundEvent); updateSearchState(false, true, 1, "nip05"); } else { - // relayStatuses = {}; // This line was removed as per the edit hint - if (activeSub) { - try { - activeSub.stop(); - } catch (e) { - console.warn("Error stopping subscription:", e); - } - activeSub = null; - } - if (currentAbortController) { - currentAbortController.abort(); - currentAbortController = null; - } + cleanupSearch(); updateSearchState(false, true, 0, "nip05"); } } catch (error) { - localError = - error instanceof Error ? error.message : "NIP-05 lookup failed"; - // relayStatuses = {}; // This line was removed as per the edit hint - 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; - lastSearchValue = null; - lastSearchValue = null; + handleSearchError(error, "NIP-05 lookup failed"); } } @@ -128,19 +99,7 @@ if (!foundEvent) { console.warn("[Events] Event not found for query:", query); localError = "Event not found"; - // relayStatuses = {}; // This line was removed as per the edit hint - if (activeSub) { - try { - activeSub.stop(); - } catch (e) { - console.warn("Error stopping subscription:", e); - } - activeSub = null; - } - if (currentAbortController) { - currentAbortController.abort(); - currentAbortController = null; - } + cleanupSearch(); updateSearchState(false, false, null, null); } else { console.log("[Events] Event found:", foundEvent); @@ -148,23 +107,7 @@ 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 = {}; // This line was removed as per the edit hint - 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; + handleSearchError(err, "Error fetching event. Please check the ID and try again."); } } @@ -176,72 +119,123 @@ console.log("EventSearch: Already searching, skipping"); return; } + resetSearchState(); localError = null; - updateSearchState(true); + updateSearchState(true, false); isResetting = false; - isUserEditing = false; // Reset user editing flag when search starts - const query = ( - queryOverride !== undefined ? queryOverride || "" : searchQuery || "" - ).trim(); + isUserEditing = false; + + const query = (queryOverride !== undefined ? queryOverride || "" : searchQuery || "").trim(); if (!query) { updateSearchState(false, false, null, null); return; } - if (query.toLowerCase().startsWith("d:")) { + + // Handle different search types + const searchType = getSearchType(query); + if (searchType) { + await handleSearchByType(searchType, query, clearInput); + return; + } + + if (clearInput) { + navigateToSearch(query, "id"); + } + await handleEventSearch(query); + } + + // AI-NOTE: 2025-01-24 - Helper functions for better code organization + function getSearchType(query: string): { type: string; term: string } | null { + const lowerQuery = query.toLowerCase(); + + if (lowerQuery.startsWith("d:")) { const dTag = query.slice(2).trim().toLowerCase(); - if (dTag) { - console.log("EventSearch: Processing d-tag search:", dTag); - navigateToSearch(dTag, "d"); - updateSearchState(false, false, null, null); - return; - } + return dTag ? { type: "d", term: dTag } : null; } - if (query.toLowerCase().startsWith("t:")) { + + if (lowerQuery.startsWith("t:")) { const searchTerm = query.slice(2).trim(); - if (searchTerm) { - await handleSearchBySubscription("t", searchTerm); - return; - } + return searchTerm ? { type: "t", term: searchTerm } : null; } - if (query.toLowerCase().startsWith("n:")) { + + if (lowerQuery.startsWith("n:")) { const searchTerm = query.slice(2).trim(); - if (searchTerm) { - await handleSearchBySubscription("n", searchTerm); - return; - } + return searchTerm ? { type: "n", term: searchTerm } : null; } + if (query.includes("@")) { - await handleNip05Search(query); + return { type: "nip05", term: query }; + } + + return null; + } + + async function handleSearchByType( + searchType: { type: string; term: string }, + query: string, + clearInput: boolean + ) { + const { type, term } = searchType; + + if (type === "d") { + console.log("EventSearch: Processing d-tag search:", term); + navigateToSearch(term, "d"); + updateSearchState(false, false, null, null); return; } - if (clearInput) { - navigateToSearch(query, "id"); - // Don't clear searchQuery here - let the effect handle it + + if (type === "nip05") { + await handleNip05Search(term); + return; } - await handleEventSearch(query); + + if (type === "t" || type === "n") { + await handleSearchBySubscription(type as "t" | "n", term); + return; + } + } + + function handleSearchError(error: unknown, defaultMessage: string) { + localError = error instanceof Error ? error.message : defaultMessage; + cleanupSearch(); + updateSearchState(false, false, null, null); + isProcessingSearch = false; + currentProcessingSearchValue = null; + lastSearchValue = null; } - // Keep searchQuery in sync with searchValue and dTagValue props + function cleanupSearch() { + if (activeSub) { + try { + activeSub.stop(); + } catch (e) { + console.warn("Error stopping subscription:", e); + } + activeSub = null; + } + + if (currentAbortController) { + currentAbortController.abort(); + currentAbortController = null; + } + } + + // AI-NOTE: 2025-01-24 - Effects organized for better readability $effect(() => { - // Only sync if we're not currently searching, resetting, or if the user is editing if (searching || isResetting || isUserEditing) { return; } if (dTagValue) { - // If dTagValue is set, show it as "d:tag" in the search bar searchQuery = `d:${dTagValue}`; } else if (searchValue) { - // searchValue should already be in the correct format (t:, n:, d:, etc.) searchQuery = searchValue; } else if (!searchQuery) { - // Only clear if searchQuery is empty to avoid clearing user input searchQuery = ""; } }); - // Debounced effect to handle searchValue changes $effect(() => { if ( !searchValue || @@ -253,76 +247,19 @@ 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; - let currentNaddr = null; - let currentNevent = null; - let currentNpub = null; - try { - currentNevent = neventEncode(foundEvent, $activeInboxRelays); - } catch {} - try { - currentNaddr = getMatchingTags(foundEvent, "d")[0]?.[1] - ? naddrEncode(foundEvent, $activeInboxRelays) - : null; - } catch {} - try { - currentNpub = foundEvent.kind === 0 ? toNpub(foundEvent.pubkey) : null; - } 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, - ); - - // 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 - ) { - try { - currentNprofile = nprofileEncode(foundEvent.pubkey, $activeInboxRelays); - } catch {} - } - - if ( - searchValue === currentEventId || - (currentNaddr && searchValue === currentNaddr) || - (currentNevent && searchValue === currentNevent) || - (currentNpub && searchValue === currentNpub) || - (currentNprofile && searchValue === currentNprofile) - ) { - // Already displaying the event for this searchValue - lastProcessedSearchValue = searchValue; - return; - } + if (foundEvent && isCurrentEventMatch(searchValue, foundEvent)) { + lastProcessedSearchValue = searchValue; + return; } - // Otherwise, trigger a search for the new value if (searchTimeout) { clearTimeout(searchTimeout); } + searchTimeout = setTimeout(() => { isProcessingSearch = true; isWaitingForSearchResult = true; @@ -333,10 +270,6 @@ }, 300); }); - // Add debouncing to prevent rapid successive searches - let searchTimeout: ReturnType | null = null; - - // Cleanup function to clear timeout when component is destroyed $effect(() => { return () => { if (searchTimeout) { @@ -345,7 +278,6 @@ }; }); - // Simple effect to handle dTagValue changes $effect(() => { if ( dTagValue && @@ -356,7 +288,6 @@ console.log("EventSearch: Processing dTagValue:", dTagValue); lastProcessedDTagValue = dTagValue; - // Add a small delay to prevent rapid successive calls setTimeout(() => { if (!searching && !isResetting) { handleSearchBySubscription("d", dTagValue); @@ -365,14 +296,53 @@ } }); - // Simple effect to handle event prop changes $effect(() => { if (event && !searching && !isResetting) { foundEvent = event; } }); - // Search utility functions + // AI-NOTE: 2025-01-24 - Utility functions for event matching and state management + function isCurrentEventMatch(searchValue: string, event: NDKEvent): boolean { + const currentEventId = event.id; + let currentNaddr: string | null = null; + let currentNevent: string | null = null; + let currentNpub: string | null = null; + let currentNprofile: string | null = null; + + try { + currentNevent = neventEncode(event, $activeInboxRelays); + } catch {} + + try { + currentNaddr = getMatchingTags(event, "d")[0]?.[1] + ? naddrEncode(event, $activeInboxRelays) + : null; + } catch {} + + try { + currentNpub = event.kind === 0 ? toNpub(event.pubkey) : null; + } catch {} + + if ( + searchValue && + searchValue.startsWith("nprofile1") && + event.kind === 0 + ) { + try { + currentNprofile = nprofileEncode(event.pubkey, $activeInboxRelays); + } catch {} + } + + return !!( + searchValue === currentEventId || + (currentNaddr && searchValue === currentNaddr) || + (currentNevent && searchValue === currentNevent) || + (currentNpub && searchValue === currentNpub) || + (currentNprofile && searchValue === currentNprofile) + ); + } + function updateSearchState( isSearching: boolean, completed: boolean = false, @@ -399,32 +369,14 @@ 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); - } - activeSub = null; - } - - // Clear search results + cleanupSearch(); 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; }, 100); @@ -432,37 +384,20 @@ function handleFoundEvent(event: NDKEvent) { foundEvent = event; - localError = null; // Clear local error when event is found - - // Stop any ongoing subscription - if (activeSub) { - try { - activeSub.stop(); - } catch (e) { - console.warn("Error stopping subscription:", e); - } - activeSub = null; - } + localError = null; - // Abort any ongoing fetch - if (currentAbortController) { - currentAbortController.abort(); - currentAbortController = null; - } + cleanupSearch(); - // Clear search state searching = false; searchCompleted = true; searchResultCount = 1; 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; @@ -479,7 +414,7 @@ }); } - // Search handlers + // AI-NOTE: 2025-01-24 - Main subscription search handler with improved error handling async function handleSearchBySubscription( searchType: "d" | "t" | "n", searchTerm: string, @@ -489,236 +424,226 @@ searchTerm, }); - // AI-NOTE: 2025-01-24 - Check cache first for profile searches to provide immediate response if (searchType === "n") { - try { - const { getUserMetadata } = await import("$lib/utils/nostrUtils"); - const cachedProfile = await getUserMetadata(searchTerm, false); - if (cachedProfile && cachedProfile.name) { - console.log("EventSearch: Found cached profile, displaying immediately:", cachedProfile); - - // Create a mock NDKEvent for the cached profile - const { NDKEvent } = await import("@nostr-dev-kit/ndk"); - const { nip19 } = await import("$lib/utils/nostrUtils"); - - // Decode the npub to get the actual pubkey - let pubkey = searchTerm; + const cachedResult = await handleCachedProfileSearch(searchTerm); + if (cachedResult) { + return; + } + } + + isResetting = false; + localError = null; + updateSearchState(true, false); + + await waitForRelays(); + + try { + await performSubscriptionSearch(searchType, searchTerm); + } catch (error) { + handleSubscriptionSearchError(error); + } + } + + async function handleCachedProfileSearch(searchTerm: string): Promise { + if (!searchTerm.startsWith("npub") && !searchTerm.startsWith("nprofile")) { + return false; + } + + try { + const { getUserMetadata } = await import("$lib/utils/nostrUtils"); + const cachedProfile = await getUserMetadata(searchTerm, false); + + if (cachedProfile && cachedProfile.name) { + const mockEvent = await createMockProfileEvent(searchTerm, cachedProfile); + handleFoundEvent(mockEvent); + updateSearchState(false, true, 1, "profile-cached"); + + setTimeout(async () => { try { - const decoded = nip19.decode(searchTerm); - if (decoded && decoded.type === "npub") { - pubkey = decoded.data; - } + await performBackgroundProfileSearch("n", searchTerm); } catch (error) { - console.warn("EventSearch: Failed to decode npub for mock event:", error); + console.warn("EventSearch: Background profile search failed:", error); } - - const mockEvent = new NDKEvent(undefined, { - kind: 0, - pubkey: pubkey, - content: JSON.stringify(cachedProfile), - tags: [], - created_at: Math.floor(Date.now() / 1000), - id: "", // Will be computed by NDK - sig: "", // Will be computed by NDK - }); - - // Display the cached profile immediately - handleFoundEvent(mockEvent); - updateSearchState(false, true, 1, "profile-cached"); - - // AI-NOTE: 2025-01-24 - Still perform background search for second-order events - // but with better timeout handling to prevent hanging - setTimeout(async () => { - try { - await performBackgroundProfileSearch(searchType, searchTerm); - } catch (error) { - console.warn("EventSearch: Background profile search failed:", error); - } - }, 100); - - return; - } - } catch (error) { - console.warn("EventSearch: Cache check failed, proceeding with subscription search:", error); + }, 100); + + return true; } + } catch (error) { + console.warn("EventSearch: Cache check failed, proceeding with subscription search:", error); } + + return false; + } - isResetting = false; // Allow effects to run for new searches - localError = null; - updateSearchState(true); + async function createMockProfileEvent(searchTerm: string, profile: any): Promise { + const { NDKEvent } = await import("@nostr-dev-kit/ndk"); + const { nip19 } = await import("$lib/utils/nostrUtils"); - // Wait for relays to be available (with timeout) + let pubkey = searchTerm; + try { + const decoded = nip19.decode(searchTerm); + if (decoded && decoded.type === "npub") { + pubkey = decoded.data; + } + } catch (error) { + console.warn("EventSearch: Failed to decode npub for mock event:", error); + } + + return new NDKEvent(undefined, { + kind: 0, + pubkey: pubkey, + content: JSON.stringify(profile), + tags: [], + created_at: Math.floor(Date.now() / 1000), + id: "", + sig: "", + }); + } + + async function waitForRelays(): Promise { let retryCount = 0; - const maxRetries = 20; // Wait up to 10 seconds (20 * 500ms) for user login to complete + const maxRetries = 10; // Reduced retry count since we'll use all available relays - while ($activeInboxRelays.length === 0 && $activeOutboxRelays.length === 0 && retryCount < maxRetries) { + // AI-NOTE: 2025-01-24 - Wait for any relays to be available, not just specific types + // This ensures searches can proceed even if some relay types are not available + while (retryCount < maxRetries) { + // Check if we have any relays in the NDK pool + const ndk = get(ndkInstance); + if (ndk && ndk.pool && ndk.pool.relays && ndk.pool.relays.size > 0) { + console.debug(`EventSearch: Found ${ndk.pool.relays.size} relays in NDK pool`); + break; + } + + // Also check active relay stores as fallback + if ($activeInboxRelays.length > 0 || $activeOutboxRelays.length > 0) { + console.debug(`EventSearch: Found active relays - inbox: ${$activeInboxRelays.length}, outbox: ${$activeOutboxRelays.length}`); + break; + } + console.debug(`EventSearch: Waiting for relays... (attempt ${retryCount + 1}/${maxRetries})`); - await new Promise(resolve => setTimeout(resolve, 500)); // Wait 500ms + await new Promise(resolve => setTimeout(resolve, 500)); 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++; - } + // AI-NOTE: 2025-01-24 - Don't fail if no relays are available, let the search functions handle fallbacks + // The search functions will use all available relays including fallback relays + const ndk = get(ndkInstance); + const poolRelayCount = ndk?.pool?.relays?.size || 0; + + console.log("EventSearch: Relay status for search:", { + poolRelayCount, + inboxCount: $activeInboxRelays.length, + outboxCount: $activeOutboxRelays.length, + willUseAllRelays: poolRelayCount > 0 || $activeInboxRelays.length > 0 || $activeOutboxRelays.length > 0 + }); + + // If we have any relays available, proceed with search + if (poolRelayCount > 0 || $activeInboxRelays.length > 0 || $activeOutboxRelays.length > 0) { + console.log("EventSearch: Relays available, proceeding with search"); + } else { + console.warn("EventSearch: No relays detected, but proceeding with search - fallback relays will be used"); } + } + + async function performSubscriptionSearch(searchType: "d" | "t" | "n", searchTerm: string): Promise { + if (currentAbortController) { + currentAbortController.abort(); + } + currentAbortController = new AbortController(); - // Check if we have any relays available - if ($activeInboxRelays.length === 0 && $activeOutboxRelays.length === 0) { - console.warn("EventSearch: No relays available after waiting, failing search"); - localError = "No relays available. Please check your connection and try again."; - updateSearchState(false, false, null, null); + const searchPromise = searchBySubscription( + searchType, + searchTerm, + { + onSecondOrderUpdate: (updatedResult) => { + console.log("EventSearch: Second order update:", updatedResult); + onSearchResults( + updatedResult.events, + updatedResult.secondOrder, + updatedResult.tTagEvents, + updatedResult.eventIds, + updatedResult.addresses, + updatedResult.searchType, + updatedResult.searchTerm, + ); + }, + onSubscriptionCreated: (sub) => { + console.log("EventSearch: Subscription created:", sub); + if (activeSub) { + activeSub.stop(); + } + activeSub = sub; + }, + }, + currentAbortController.signal, + ); + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error("Search timeout: No results received within 30 seconds")); + }, 30000); + }); + + const result = await Promise.race([searchPromise, timeoutPromise]) as any; + console.log("EventSearch: Search completed:", result); + + onSearchResults( + result.events, + result.secondOrder, + result.tTagEvents, + result.eventIds, + result.addresses, + result.searchType, + result.searchTerm, + ); + + const totalCount = result.events.length + result.secondOrder.length + result.tTagEvents.length; + localError = null; + + cleanupSearch(); + updateSearchState(false, true, totalCount, searchType); + isProcessingSearch = false; + currentProcessingSearchValue = null; + isWaitingForSearchResult = false; + + if (searchValue) { + lastProcessedSearchValue = searchValue; + } + } + + function handleSubscriptionSearchError(error: unknown): void { + if (error instanceof Error && error.message === "Search cancelled") { isProcessingSearch = false; currentProcessingSearchValue = null; isWaitingForSearchResult = false; - searching = false; return; } - console.log("EventSearch: Relays available, proceeding with search:", { - inboxCount: $activeInboxRelays.length, - outboxCount: $activeOutboxRelays.length - }); + console.error("EventSearch: Search failed:", error); - try { - // Cancel existing search - if (currentAbortController) { - currentAbortController.abort(); - } - currentAbortController = new AbortController(); - // Add a timeout to prevent hanging searches - const searchPromise = searchBySubscription( - searchType, - searchTerm, - { - onSecondOrderUpdate: (updatedResult) => { - console.log("EventSearch: Second order update:", updatedResult); - onSearchResults( - updatedResult.events, - updatedResult.secondOrder, - updatedResult.tTagEvents, - updatedResult.eventIds, - updatedResult.addresses, - updatedResult.searchType, - updatedResult.searchTerm, - ); - }, - onSubscriptionCreated: (sub) => { - console.log("EventSearch: Subscription created:", sub); - if (activeSub) { - activeSub.stop(); - } - activeSub = sub; - }, - }, - currentAbortController.signal, - ); - - // Add a 30-second timeout - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => { - reject(new Error("Search timeout: No results received within 30 seconds")); - }, 30000); - }); - - const result = await Promise.race([searchPromise, timeoutPromise]) as any; - console.log("EventSearch: Search completed:", result); - onSearchResults( - result.events, - result.secondOrder, - result.tTagEvents, - result.eventIds, - result.addresses, - result.searchType, - result.searchTerm, - ); - const totalCount = - result.events.length + - result.secondOrder.length + - result.tTagEvents.length; - localError = null; // Clear local error when search completes - // Stop any ongoing subscription - if (activeSub) { - try { - activeSub.stop(); - } catch (e) { - console.warn("Error stopping subscription:", e); - } - activeSub = null; - } - // Abort any ongoing fetch - if (currentAbortController) { - currentAbortController.abort(); - currentAbortController = null; - } - updateSearchState(false, true, totalCount, searchType); - 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; - currentProcessingSearchValue = null; - isWaitingForSearchResult = false; - return; - } - console.error("EventSearch: Search failed:", error); - 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."; - } else { - localError = `Search failed: ${error.message}`; - } - } - localError = null; // Clear local error when search fails - // Stop any ongoing subscription - if (activeSub) { - try { - activeSub.stop(); - } catch (e) { - console.warn("Error stopping subscription:", e); - } - activeSub = null; - } - // Abort any ongoing fetch - if (currentAbortController) { - currentAbortController.abort(); - currentAbortController = null; - } - updateSearchState(false, false, null, null); - isProcessingSearch = false; - currentProcessingSearchValue = null; - isWaitingForSearchResult = false; - - // Update last processed search value to prevent re-processing even on error - if (searchValue) { - lastProcessedSearchValue = searchValue; + 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."; + } else { + localError = `Search failed: ${error.message}`; } + } else { + localError = "Search failed"; + } + + cleanupSearch(); + updateSearchState(false, false, null, null); + isProcessingSearch = false; + currentProcessingSearchValue = null; + isWaitingForSearchResult = false; + + if (searchValue) { + lastProcessedSearchValue = searchValue; } } - // AI-NOTE: 2025-01-24 - Function to perform background profile search without blocking UI async function performBackgroundProfileSearch( searchType: "d" | "t" | "n", searchTerm: string, @@ -729,17 +654,15 @@ }); try { - // Cancel existing search if (currentAbortController) { currentAbortController.abort(); } currentAbortController = new AbortController(); - // AI-NOTE: 2025-01-24 - Add timeout to prevent hanging background searches const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error("Background search timeout")); - }, 10000); // 10 second timeout for background searches + }, 10000); }); const searchPromise = searchBySubscription( @@ -748,7 +671,6 @@ { onSecondOrderUpdate: (updatedResult) => { console.log("EventSearch: Background second order update:", updatedResult); - // Only update if we have new results if (updatedResult.events.length > 0) { onSearchResults( updatedResult.events, @@ -772,12 +694,10 @@ currentAbortController.signal, ); - // Race between search and timeout const result = await Promise.race([searchPromise, timeoutPromise]) as any; console.log("EventSearch: Background search completed:", result); - // Only update results if we have new data if (result.events.length > 0) { onSearchResults( result.events, @@ -794,21 +714,18 @@ } } - // Search utility functions function handleClear() { isResetting = true; searchQuery = ""; - isUserEditing = false; // Reset user editing flag + isUserEditing = false; resetSearchState(); - // Clear URL parameters to reset the page goto("", { replaceState: true, keepFocus: true, noScroll: true, }); - // Ensure all search state is cleared searching = false; searchCompleted = false; searchResultCount = null; @@ -820,7 +737,6 @@ lastSearchValue = null; isWaitingForSearchResult = false; - // Clear any pending timeout if (searchTimeout) { clearTimeout(searchTimeout); searchTimeout = null; @@ -830,7 +746,6 @@ onClear(); } - // Reset the flag after a short delay to allow effects to settle setTimeout(() => { isResetting = false; }, 100); diff --git a/src/lib/components/publications/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte index 931cf9d..d913de0 100644 --- a/src/lib/components/publications/PublicationFeed.svelte +++ b/src/lib/components/publications/PublicationFeed.svelte @@ -77,6 +77,8 @@ }); // Initialize relays and fetch events + // AI-NOTE: This function is called when the component mounts and when relay configuration changes + // It ensures that events are fetched from the current set of active relays async function initializeAndFetch() { if (!ndk) { console.debug('[PublicationFeed] No NDK instance available'); @@ -122,11 +124,12 @@ } } - // Watch for relay store changes + // Watch for relay store changes and user authentication state $effect(() => { const inboxRelays = $activeInboxRelays; const outboxRelays = $activeOutboxRelays; const newRelays = [...inboxRelays, ...outboxRelays]; + const userState = $userStore; if (newRelays.length > 0 && !hasInitialized) { console.debug('[PublicationFeed] Relays available, initializing'); @@ -145,6 +148,18 @@ initializeAndFetch(); }, 3000); } + } else if (hasInitialized && newRelays.length > 0) { + // AI-NOTE: Re-fetch events when user authentication state changes or relays are updated + // This ensures that when a user logs in and their relays are loaded, we fetch events from those relays + const currentRelaysString = allRelays.sort().join(','); + const newRelaysString = newRelays.sort().join(','); + + if (currentRelaysString !== newRelaysString) { + console.debug('[PublicationFeed] Relay configuration changed, re-fetching events'); + // Clear cache to force fresh fetch from new relays + indexEventCache.clear(); + setTimeout(() => initializeAndFetch(), 0); + } } }); @@ -513,6 +528,31 @@ debouncedSearch(props.searchQuery); }); + // AI-NOTE: Watch for user authentication state changes to re-fetch events when user logs in/out + $effect(() => { + const userState = $userStore; + + if (hasInitialized && userState.signedIn) { + console.debug('[PublicationFeed] User signed in, checking if we need to re-fetch events'); + // Check if we have user-specific relays that we haven't fetched from yet + const inboxRelays = $activeInboxRelays; + const outboxRelays = $activeOutboxRelays; + const newRelays = [...inboxRelays, ...outboxRelays]; + + if (newRelays.length > 0) { + const currentRelaysString = allRelays.sort().join(','); + const newRelaysString = newRelays.sort().join(','); + + if (currentRelaysString !== newRelaysString) { + console.debug('[PublicationFeed] User logged in with new relays, re-fetching events'); + // Clear cache to force fresh fetch from user's relays + indexEventCache.clear(); + setTimeout(() => initializeAndFetch(), 0); + } + } + } + }); + // AI-NOTE: Watch for changes in the user filter checkbox $effect(() => { // Trigger filtering when the user filter checkbox changes diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index 55afb4d..fed11c6 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -368,7 +368,10 @@ function ensureSecureWebSocket(url: string): string { */ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { try { - console.debug(`[NDK.ts] Creating relay with URL: ${url}`); + // Reduce verbosity in development - only log relay creation if debug mode is enabled + if (process.env.NODE_ENV === 'development' && process.env.DEBUG_RELAYS) { + console.debug(`[NDK.ts] Creating relay with URL: ${url}`); + } // Ensure the URL is using appropriate protocol const secureUrl = ensureSecureWebSocket(url); @@ -383,7 +386,10 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { // Set up connection timeout const connectionTimeout = setTimeout(() => { try { - console.warn(`[NDK.ts] Connection timeout for ${secureUrl}`); + // Only log connection timeouts if debug mode is enabled + if (process.env.NODE_ENV === 'development' && process.env.DEBUG_RELAYS) { + console.debug(`[NDK.ts] Connection timeout for ${secureUrl}`); + } relay.disconnect(); } catch { // Silently ignore disconnect errors @@ -395,7 +401,10 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { const authPolicy = new CustomRelayAuthPolicy(ndk); relay.on("connect", () => { try { - console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); + // Only log successful connections if debug mode is enabled + if (process.env.NODE_ENV === 'development' && process.env.DEBUG_RELAYS) { + console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); + } clearTimeout(connectionTimeout); authPolicy.authenticate(relay); } catch { @@ -405,7 +414,10 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { } else { relay.on("connect", () => { try { - console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); + // Only log successful connections if debug mode is enabled + if (process.env.NODE_ENV === 'development' && process.env.DEBUG_RELAYS) { + console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); + } clearTimeout(connectionTimeout); } catch { // Silently handle connect handler errors @@ -513,7 +525,10 @@ export async function updateActiveRelayStores(ndk: NDK, forceUpdate: boolean = f // 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'); + // Reduce verbosity in development - only log relay addition if debug mode is enabled + if (process.env.NODE_ENV === 'development' && process.env.DEBUG_RELAYS) { + console.debug('[NDK.ts] updateActiveRelayStores: Adding', allRelayUrls.length, 'relays to NDK pool'); + } for (const url of allRelayUrls) { try { diff --git a/src/lib/services/event_search_service.ts b/src/lib/services/event_search_service.ts new file mode 100644 index 0000000..76ee6ca --- /dev/null +++ b/src/lib/services/event_search_service.ts @@ -0,0 +1,84 @@ +/** + * Service class for handling event search operations + * AI-NOTE: 2025-01-24 - Extracted from EventSearch component for better separation of concerns + */ +export class EventSearchService { + /** + * Determines the search type from a query string + */ + getSearchType(query: string): { type: string; term: string } | null { + const lowerQuery = query.toLowerCase(); + + if (lowerQuery.startsWith("d:")) { + const dTag = query.slice(2).trim().toLowerCase(); + return dTag ? { type: "d", term: dTag } : null; + } + + if (lowerQuery.startsWith("t:")) { + const searchTerm = query.slice(2).trim(); + return searchTerm ? { type: "t", term: searchTerm } : null; + } + + if (lowerQuery.startsWith("n:")) { + const searchTerm = query.slice(2).trim(); + return searchTerm ? { type: "n", term: searchTerm } : null; + } + + if (query.includes("@")) { + return { type: "nip05", term: query }; + } + + return null; + } + + /** + * Checks if a search value matches the current event + */ + isCurrentEventMatch(searchValue: string, event: any, relays: string[]): boolean { + const currentEventId = event.id; + let currentNaddr = null; + let currentNevent = null; + let currentNpub = null; + let currentNprofile = null; + + try { + const { neventEncode, naddrEncode, nprofileEncode } = require("$lib/utils"); + const { getMatchingTags, toNpub } = require("$lib/utils/nostrUtils"); + + currentNevent = neventEncode(event, relays); + } catch {} + + try { + const { naddrEncode } = require("$lib/utils"); + const { getMatchingTags } = require("$lib/utils/nostrUtils"); + + currentNaddr = getMatchingTags(event, "d")[0]?.[1] + ? naddrEncode(event, relays) + : null; + } catch {} + + try { + const { toNpub } = require("$lib/utils/nostrUtils"); + currentNpub = event.kind === 0 ? toNpub(event.pubkey) : null; + } catch {} + + if ( + searchValue && + searchValue.startsWith("nprofile1") && + event.kind === 0 + ) { + try { + const { nprofileEncode } = require("$lib/utils"); + currentNprofile = nprofileEncode(event.pubkey, relays); + } catch {} + } + + return ( + searchValue === currentEventId || + (currentNaddr && searchValue === currentNaddr) || + (currentNevent && searchValue === currentNevent) || + (currentNpub && searchValue === currentNpub) || + (currentNprofile && searchValue === currentNprofile) + ); + } +} diff --git a/src/lib/services/search_state_manager.ts b/src/lib/services/search_state_manager.ts new file mode 100644 index 0000000..d673b9d --- /dev/null +++ b/src/lib/services/search_state_manager.ts @@ -0,0 +1,62 @@ +/** + * Service class for managing search state operations + * AI-NOTE: 2025-01-24 - Extracted from EventSearch component for better separation of concerns + */ +export class SearchStateManager { + /** + * Updates the search state with new values + */ + updateSearchState( + state: { + searching: boolean; + searchCompleted: boolean; + searchResultCount: number | null; + searchResultType: string | null; + }, + onLoadingChange?: (loading: boolean) => void + ): void { + if (onLoadingChange) { + onLoadingChange(state.searching); + } + } + + /** + * Resets all search state to initial values + */ + resetSearchState( + callbacks: { + onSearchResults: (events: any[], secondOrder: any[], tTagEvents: any[], eventIds: Set, addresses: Set) => void; + cleanupSearch: () => void; + clearTimeout: () => void; + } + ): void { + callbacks.cleanupSearch(); + callbacks.onSearchResults([], [], [], new Set(), new Set()); + callbacks.clearTimeout(); + } + + /** + * Handles search errors with consistent error handling + */ + handleSearchError( + error: unknown, + defaultMessage: string, + callbacks: { + setLocalError: (error: string | null) => void; + cleanupSearch: () => void; + updateSearchState: (state: any) => void; + resetProcessingFlags: () => void; + } + ): void { + const errorMessage = error instanceof Error ? error.message : defaultMessage; + callbacks.setLocalError(errorMessage); + callbacks.cleanupSearch(); + callbacks.updateSearchState({ + searching: false, + searchCompleted: false, + searchResultCount: null, + searchResultType: null + }); + callbacks.resetProcessingFlags(); + } +} diff --git a/src/lib/utils/event_search.ts b/src/lib/utils/event_search.ts index f15b9b3..5407be4 100644 --- a/src/lib/utils/event_search.ts +++ b/src/lib/utils/event_search.ts @@ -6,6 +6,7 @@ import type { Filter } from "./search_types.ts"; import { get } from "svelte/store"; import { wellKnownUrl, isValidNip05Address } from "./search_utils.ts"; import { TIMEOUTS, VALIDATION } from "./search_constants.ts"; +import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts"; /** * Search for a single event by ID or filter @@ -17,18 +18,35 @@ export async function searchEvent(query: string): Promise { return null; } - // Wait for relays to be available + // AI-NOTE: 2025-01-24 - Wait for any relays to be available, not just pool relays + // This ensures searches can proceed even if some relay types are not available let attempts = 0; - const maxAttempts = 10; - while (ndk.pool.relays.size === 0 && attempts < maxAttempts) { + const maxAttempts = 5; // Reduced since we'll use fallback relays + + while (attempts < maxAttempts) { + // Check if we have any relays in the pool + if (ndk.pool.relays.size > 0) { + console.log(`[Search] Found ${ndk.pool.relays.size} relays in NDK pool`); + break; + } + + // Also check if we have any active relays + const inboxRelays = get(activeInboxRelays); + const outboxRelays = get(activeOutboxRelays); + if (inboxRelays.length > 0 || outboxRelays.length > 0) { + console.log(`[Search] Found active relays - inbox: ${inboxRelays.length}, outbox: ${outboxRelays.length}`); + break; + } + console.log(`[Search] Waiting for relays to be available (attempt ${attempts + 1}/${maxAttempts})`); await new Promise(resolve => setTimeout(resolve, 500)); attempts++; } + // AI-NOTE: 2025-01-24 - Don't fail if no relays are available, let fetchEventWithFallback handle fallbacks + // The fetchEventWithFallback function will use all available relays including fallback relays if (ndk.pool.relays.size === 0) { - console.warn("[Search] No relays available after waiting"); - return null; + console.warn("[Search] No relays in pool, but proceeding with search - fallback relays will be used"); } // Clean the query and normalize to lowercase diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index 06ae2bf..7f2fbb2 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -5,7 +5,7 @@ import { npubCache } from "./npubCache.ts"; import NDK, { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; import type { NDKKind, NostrEvent } from "@nostr-dev-kit/ndk"; import type { Filter } from "./search_types.ts"; -import { communityRelays, secondaryRelays, searchRelays } from "../consts.ts"; +import { communityRelays, secondaryRelays, searchRelays, anonymousRelays } from "../consts.ts"; import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts"; import { NDKRelaySet as NDKRelaySetFromNDK } from "@nostr-dev-kit/ndk"; import { sha256 } from "@noble/hashes/sha2.js"; @@ -443,19 +443,27 @@ export async function fetchEventWithFallback( filterOrId: string | Filter, timeoutMs: number = 3000, ): Promise { - // Use both inbox and outbox relays for better event discovery + // AI-NOTE: 2025-01-24 - Use ALL available relays for comprehensive event discovery + // This ensures we don't miss events that might be on any available relay + + // Get all relays from NDK pool first (most comprehensive) + const poolRelays = Array.from(ndk.pool.relays.values()).map((r: any) => r.url); const inboxRelays = get(activeInboxRelays); const outboxRelays = get(activeOutboxRelays); - let allRelays = [...inboxRelays, ...outboxRelays]; + // Combine all available relays, prioritizing pool relays + let allRelays = [...new Set([...poolRelays, ...inboxRelays, ...outboxRelays])]; + + console.log("fetchEventWithFallback: Using pool relays:", poolRelays); console.log("fetchEventWithFallback: Using inbox relays:", inboxRelays); console.log("fetchEventWithFallback: Using outbox relays:", outboxRelays); + console.log("fetchEventWithFallback: Total unique relays:", allRelays.length); // Check if we have any relays available if (allRelays.length === 0) { console.warn("fetchEventWithFallback: No relays available for event fetch, using fallback relays"); // Use fallback relays when no relays are available - allRelays = [...secondaryRelays, ...searchRelays]; + allRelays = [...secondaryRelays, ...searchRelays, ...anonymousRelays]; console.log("fetchEventWithFallback: Using fallback relays:", allRelays); } diff --git a/src/lib/utils/search_result_formatter.ts b/src/lib/utils/search_result_formatter.ts new file mode 100644 index 0000000..3488b83 --- /dev/null +++ b/src/lib/utils/search_result_formatter.ts @@ -0,0 +1,26 @@ +/** + * Utility class for formatting search result messages + * AI-NOTE: 2025-01-24 - Extracted from EventSearch component for better separation of concerns + */ +export class SearchResultFormatter { + /** + * Formats a result message based on search count and type + */ + formatResultMessage(searchResultCount: number | null, searchResultType: string | null): string { + 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 + ? `Search completed. Found 1 ${typeLabel}.` + : `Search completed. Found ${searchResultCount} ${countLabel}.`; + } +} diff --git a/src/lib/utils/subscription_search.ts b/src/lib/utils/subscription_search.ts index d07067e..169cfb6 100644 --- a/src/lib/utils/subscription_search.ts +++ b/src/lib/utils/subscription_search.ts @@ -403,7 +403,8 @@ async function createProfileSearchFilter( } /** - * Create primary relay set based on search type + * Create primary relay set for search operations + * AI-NOTE: 2025-01-24 - Updated to use all available relays to prevent search failures */ function createPrimaryRelaySet( searchType: SearchSubscriptionType, @@ -413,9 +414,11 @@ function createPrimaryRelaySet( const poolRelays = Array.from(ndk.pool.relays.values()); console.debug('subscription_search: NDK pool relays:', poolRelays.map((r: any) => r.url)); + // AI-NOTE: 2025-01-24 - Use ALL available relays for comprehensive search coverage + // This ensures searches don't fail due to missing relays and provides maximum event discovery + if (searchType === "n") { - // AI-NOTE: 2025-01-08 - For profile searches, prioritize search relays for speed - // Use search relays first, then fall back to all relays if needed + // For profile searches, prioritize search relays for speed but include all relays const searchRelaySet = poolRelays.filter( (relay: any) => searchRelays.some( @@ -426,30 +429,27 @@ function createPrimaryRelaySet( if (searchRelaySet.length > 0) { console.debug('subscription_search: Profile search - using search relays for speed:', searchRelaySet.map((r: any) => r.url)); - return new NDKRelaySet(new Set(searchRelaySet) as any, ndk); + // Still include all relays for comprehensive coverage + console.debug('subscription_search: Profile search - also including all relays for comprehensive coverage'); + return new NDKRelaySet(new Set(poolRelays) as any, ndk); } else { - // Fallback to all relays if search relays not available - console.debug('subscription_search: Profile search - fallback to all relays:', poolRelays.map((r: any) => r.url)); + // Use all relays if search relays not available + console.debug('subscription_search: Profile search - using all relays:', poolRelays.map((r: any) => r.url)); return new NDKRelaySet(new Set(poolRelays) as any, ndk); } } else { - // For other searches, use active relays first - const searchRelays = [...get(activeInboxRelays), ...get(activeOutboxRelays)]; + // For all other searches, use ALL available relays for maximum coverage + const activeRelays = [...get(activeInboxRelays), ...get(activeOutboxRelays)]; console.debug('subscription_search: Active relay stores:', { inboxRelays: get(activeInboxRelays), outboxRelays: get(activeOutboxRelays), - searchRelays + activeRelays }); - const activeRelaySet = poolRelays.filter( - (relay: any) => - searchRelays.some( - (searchRelay: string) => - normalizeUrl(relay.url) === normalizeUrl(searchRelay), - ), - ); - console.debug('subscription_search: Active relay set:', activeRelaySet.map((r: any) => r.url)); - return new NDKRelaySet(new Set(activeRelaySet) as any, ndk); + // AI-NOTE: 2025-01-24 - Use all pool relays instead of filtering to active relays only + // This ensures we don't miss events that might be on other relays + console.debug('subscription_search: Using ALL pool relays for comprehensive search coverage:', poolRelays.map((r: any) => r.url)); + return new NDKRelaySet(new Set(poolRelays) as any, ndk); } } @@ -647,24 +647,15 @@ function searchOtherRelaysInBackground( ): Promise { const ndk = get(ndkInstance); + // AI-NOTE: 2025-01-24 - Use ALL available relays for comprehensive search coverage + // This ensures we don't miss events that might be on any available relay const otherRelays = new NDKRelaySet( - new Set( - Array.from(ndk.pool.relays.values()).filter((relay: any) => { - if (searchType === "n") { - // AI-NOTE: 2025-01-08 - For profile searches, use ALL available relays - // Don't exclude any relays since we want maximum coverage - return true; - } else { - // For other searches, exclude community relays from fallback search - return !communityRelays.some( - (communityRelay: string) => - normalizeUrl(relay.url) === normalizeUrl(communityRelay), - ); - } - }), - ), + new Set(Array.from(ndk.pool.relays.values())), ndk, ); + + console.debug('subscription_search: Background search using ALL relays:', + Array.from(ndk.pool.relays.values()).map((r: any) => r.url)); // Subscribe to events from other relays const sub = ndk.subscribe( diff --git a/vite.config.ts b/vite.config.ts index 82206c3..0872066 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -42,6 +42,8 @@ export default defineConfig({ define: { // Expose the app version as a global variable "import.meta.env.APP_VERSION": JSON.stringify(getAppVersionString()), + // Enable debug logging for relays when needed + "process.env.DEBUG_RELAYS": JSON.stringify(process.env.DEBUG_RELAYS || "false"), }, optimizeDeps: { esbuildOptions: { @@ -54,5 +56,8 @@ export default defineConfig({ fs: { allow: ['..'], }, + hmr: { + overlay: false, // Disable HMR overlay to prevent ESM URL scheme errors + }, }, }); From e9405c836125c57e90c6a97f1f5b888924c91336 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Thu, 14 Aug 2025 08:46:12 -0500 Subject: [PATCH 54/98] Lock Deno version in Dockerfile This avoids a memory leak bug in the latest version of Deno. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9bdfec7..5be0979 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM denoland/deno:alpine AS build +FROM denoland/deno:alpine-2.4.2 AS build WORKDIR /app/src COPY . . From 53d3c33ee8aca5e025505fb45e9dabd79b58dfa1 Mon Sep 17 00:00:00 2001 From: silberengel Date: Thu, 14 Aug 2025 15:46:13 +0200 Subject: [PATCH 55/98] fixed my notes redirection --- src/routes/my-notes/+page.svelte | 52 +++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/src/routes/my-notes/+page.svelte b/src/routes/my-notes/+page.svelte index 852d31e..58b3634 100644 --- a/src/routes/my-notes/+page.svelte +++ b/src/routes/my-notes/+page.svelte @@ -13,6 +13,7 @@ let events: NDKEvent[] = $state([]); let loading = $state(true); let error: string | null = $state(null); + let checkingAuth = $state(true); // Track authentication check state - prevents premature redirects during auth restoration let showTags: Record = $state({}); let renderedContent: Record = $state({}); @@ -167,17 +168,52 @@ }); }); - // AI-NOTE: Check authentication status immediately and redirect if not logged in + // AI-NOTE: Check authentication status and redirect if not logged in + // Wait for authentication state to be properly initialized before checking + let authCheckTimeout: ReturnType | null = null; + $effect(() => { - const user = get(userStore); - if (!user.signedIn) { - // Redirect to home page if not logged in - goto('/'); + const user = $userStore; + + // Clear any existing timeout + if (authCheckTimeout) { + clearTimeout(authCheckTimeout); + authCheckTimeout = null; + } + + // If user is signed in, we're good + if (user.signedIn) { + checkingAuth = false; return; } + + // If user is not signed in, wait a bit for auth restoration to complete + // This handles the case where the page loads before auth restoration finishes + authCheckTimeout = setTimeout(() => { + const currentUser = get(userStore); + if (!currentUser.signedIn) { + console.debug('[MyNotes] User not signed in after auth restoration, redirecting to home page'); + goto('/'); + } else { + checkingAuth = false; + } + }, 1500); // 1.5 second delay to allow auth restoration to complete + + // Cleanup function + return () => { + if (authCheckTimeout) { + clearTimeout(authCheckTimeout); + authCheckTimeout = null; + } + }; }); - onMount(fetchMyNotes); + // AI-NOTE: Only fetch notes after authentication is confirmed + $effect(() => { + if (!checkingAuth && $userStore.signedIn) { + fetchMyNotes(); + } + });

My Notes

- {#if loading} + {#if checkingAuth} +
Checking authentication...
+ {:else if loading}
Loading…
{:else if error}
{error}
From 42cc8aa6d97bd61ec3d8a464f7a27fdcebc33f3b Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Thu, 14 Aug 2025 08:47:53 -0500 Subject: [PATCH 56/98] Revert memory limit env vars --- Dockerfile | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5be0979..2f09ee6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,10 +2,6 @@ FROM denoland/deno:alpine-2.4.2 AS build WORKDIR /app/src COPY . . -# Set memory limits for Deno to prevent memory leaks -ENV DENO_MEMORY_LIMIT=512MB -ENV DENO_GC_INTERVAL=1000 - RUN deno install RUN deno task build @@ -16,10 +12,6 @@ COPY --from=build /app/src/import_map.json . ENV ORIGIN=http://localhost:3000 -# Set memory limits for runtime to prevent memory leaks -ENV DENO_MEMORY_LIMIT=512MB -ENV DENO_GC_INTERVAL=1000 - RUN deno cache --import-map=import_map.json ./build/index.js EXPOSE 3000 From 1380c3c66b9111ee6c8387138cac6545fc5d8a27 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Thu, 14 Aug 2025 08:53:36 -0500 Subject: [PATCH 57/98] Add explanatory note for LLMs around WS pool --- src/lib/data_structures/websocket_pool.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/data_structures/websocket_pool.ts b/src/lib/data_structures/websocket_pool.ts index 5efcdf5..5eda81a 100644 --- a/src/lib/data_structures/websocket_pool.ts +++ b/src/lib/data_structures/websocket_pool.ts @@ -211,6 +211,7 @@ export class WebSocketPool { } } this.#pool.clear(); + console.debug('[WebSocketPool] Pool drained successfully'); } @@ -252,6 +253,8 @@ export class WebSocketPool { this.#clearIdleTimer(handle); // Clean up event listeners to prevent memory leaks + // AI-NOTE: Code that checks out connections should clean up its own listener callbacks before + // releasing the connection to the pool. if (handle.ws) { handle.ws.onopen = null; handle.ws.onerror = null; @@ -261,7 +264,9 @@ export class WebSocketPool { const url = this.#normalizeUrl(handle.ws.url); this.#pool.delete(url); + console.debug(`[WebSocketPool] Removed socket for ${url}, pool size: ${this.#pool.size}`); + this.#processWaitingQueue(); } From 0bd5a3f4532a39dc49f9b516adaf18111e6cb9c6 Mon Sep 17 00:00:00 2001 From: silberengel Date: Thu, 14 Aug 2025 17:09:20 +0200 Subject: [PATCH 58/98] fix event result display --- src/lib/components/CommentViewer.svelte | 14 +- src/lib/components/EventDetails.svelte | 186 +++++++++++++----------- src/lib/components/RelayActions.svelte | 18 --- src/routes/events/+page.svelte | 128 +++++++++------- 4 files changed, 191 insertions(+), 155 deletions(-) diff --git a/src/lib/components/CommentViewer.svelte b/src/lib/components/CommentViewer.svelte index 3b44665..7e9576e 100644 --- a/src/lib/components/CommentViewer.svelte +++ b/src/lib/components/CommentViewer.svelte @@ -8,6 +8,7 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; + import { parseRepostContent, parseContent as parseNotificationContent } from "$lib/utils/notification_utils"; const { event } = $props<{ event: NDKEvent }>(); @@ -653,12 +654,15 @@ return `${actualLevel * 16}px`; } - async function parseContent(content: string): Promise { + async function parseContent(content: string, eventKind?: number): Promise { if (!content) return ""; - let parsedContent = await parseBasicmarkup(content); - - return parsedContent; + // Use parseRepostContent for kind 6 and 16 events (reposts) + if (eventKind === 6 || eventKind === 16) { + return await parseRepostContent(content); + } else { + return await parseNotificationContent(content); + } } @@ -825,7 +829,7 @@
{:else} - {#await parseContent(node.event.content || "") then parsedContent} + {#await parseContent(node.event.content || "", node.event.kind) then parsedContent} {@html parsedContent} {:catch} {@html node.event.content || ""} diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index 65cd830..d59f955 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -15,6 +15,8 @@ import { navigateToEvent } from "$lib/utils/nostrEventService"; import ContainingIndexes from "$lib/components/util/ContainingIndexes.svelte"; import Notifications from "$lib/components/Notifications.svelte"; + import { parseRepostContent } from "$lib/utils/notification_utils"; + import RelayActions from "$lib/components/RelayActions.svelte"; const { event, @@ -305,10 +307,18 @@ $effect(() => { if (event && event.kind !== 0 && event.content) { - parseBasicmarkup(event.content).then((html) => { - parsedContent = html; - contentPreview = html.slice(0, 250); - }); + // Use parseRepostContent for kind 6 and 16 events (reposts) + if (event.kind === 6 || event.kind === 16) { + parseRepostContent(event.content).then((html) => { + parsedContent = html; + contentPreview = html.slice(0, 250); + }); + } else { + parseBasicmarkup(event.content).then((html) => { + parsedContent = html; + contentPreview = html.slice(0, 250); + }); + } } }); @@ -436,30 +446,17 @@
{/if} - {#if getEventHashtags(event).length} -
- Tags: -
- {#each getEventHashtags(event) as tag} - - {/each} -
-
- {/if} + {#if event.kind !== 0} -
+
Content: -
+
{@html showFullContent ? parsedContent : contentPreview} {#if !showFullContent && parsedContent.length > 250} - {/if} - {/each} -
-
- {/if} -
- Show Raw Event JSON + Show details -
- + + +
+

Identifiers:

+
+ {#each getIdentifiers(event, profile) as identifier} +
+ {identifier.label}: +
+ + {identifier.value.slice(0, 20)}...{identifier.value.slice(-8)} + + +
+
+ {/each} +
-
+
+    
+    {#if event.tags && event.tags.length}
+      
+

Event Tags:

+
+ {#each event.tags as tag} + {@const tagInfo = getTagButtonInfo(tag)} + {#if tagInfo.text && tagInfo.gotoValue} + + {/if} + {/each} +
+
+ {/if} + + +
+

Raw Event JSON:

+
+
+ +
+
 {JSON.stringify(event.rawEvent(), null, 2)}
-    
+
+
+
diff --git a/src/lib/components/RelayActions.svelte b/src/lib/components/RelayActions.svelte index 4fd827d..88a01b3 100644 --- a/src/lib/components/RelayActions.svelte +++ b/src/lib/components/RelayActions.svelte @@ -72,24 +72,6 @@ } -
- -
- -{#if foundRelays.length > 0} -
- Found on {foundRelays.length} relay(s): -
- {#each foundRelays as relay} - - {/each} -
-
-{/if} -
Found on:
diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index fc86dc5..57748b0 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -20,6 +20,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; import { getEventType } from "$lib/utils/mime"; import ViewPublicationLink from "$lib/components/util/ViewPublicationLink.svelte"; import { checkCommunity } from "$lib/utils/search_utility"; + import { parseRepostContent, parseContent } from "$lib/utils/notification_utils"; let loading = $state(false); let error = $state(null); @@ -49,22 +50,24 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; let searchInProgress = $state(false); let secondOrderSearchMessage = $state(null); let communityStatus = $state>({}); + let searchResultsCollapsed = $state(false); userStore.subscribe((val) => (user = val)); function handleEventFound(newEvent: NDKEvent) { event = newEvent; showSidePanel = true; - // Clear search results when showing a single event - searchResults = []; - secondOrderResults = []; - tTagResults = []; - originalEventIds = new Set(); - originalAddresses = new Set(); - searchType = null; - searchTerm = null; - searchInProgress = false; - secondOrderSearchMessage = null; + // AI-NOTE: 2025-01-24 - Preserve search results to allow navigation through them + // Don't clear search results when showing a single event - this allows users to browse through results + // searchResults = []; + // secondOrderResults = []; + // tTagResults = []; + // originalEventIds = new Set(); + // originalAddresses = new Set(); + // searchType = null; + // searchTerm = null; + // searchInProgress = false; + // secondOrderSearchMessage = null; if (newEvent.kind === 0) { try { @@ -255,6 +258,10 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; secondOrderSearchMessage = null; } + function toggleSearchResults() { + searchResultsCollapsed = !searchResultsCollapsed; + } + function navigateToPublication(dTag: string) { goto(`/publications?d=${encodeURIComponent(dTag.toLowerCase())}`); } @@ -419,14 +426,24 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";
Events - {#if showSidePanel} - - {/if} +
+ {#if showSidePanel && (searchResults.length > 0 || secondOrderResults.length > 0 || tTagResults.length > 0)} + + {/if} + {#if showSidePanel} + + {/if} +

@@ -457,19 +474,20 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; {#if searchResults.length > 0}

- - {#if searchType === "n"} - Search Results for name: "{searchTerm && searchTerm.length > 50 ? searchTerm.slice(0, 50) + '...' : searchTerm || ''}" ({searchResults.length} profiles) - {:else if searchType === "t"} - Search Results for t-tag: "{searchTerm && searchTerm.length > 50 ? searchTerm.slice(0, 50) + '...' : searchTerm || ''}" ({searchResults.length} - events) - {:else} - Search Results for d-tag: "{(() => { - const term = searchTerm || dTagValue?.toLowerCase() || ''; - return term.length > 50 ? term.slice(0, 50) + '...' : term; - })()}" ({searchResults.length} events) - {/if} - +
+ + {#if searchType === "n"} + Search Results for name: "{searchTerm && searchTerm.length > 50 ? searchTerm.slice(0, 50) + '...' : searchTerm || ''}" ({searchResults.length} profiles) + {:else if searchType === "t"} + Search Results for t-tag: "{searchTerm && searchTerm.length > 50 ? searchTerm.slice(0, 50) + '...' : searchTerm || ''}" ({searchResults.length} + events) + {:else} + Search Results for d-tag: "{(() => { + const term = searchTerm || dTagValue?.toLowerCase() || ''; + return term.length > 50 ? term.slice(0, 50) + '...' : term; + })()}" ({searchResults.length} events) + {/if} +
{#each searchResults as result, index} {@const profileData = parseProfileContent(result)} @@ -599,10 +617,11 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";
- {result.content.slice(0, 200)}{result.content.length > - 200 - ? "..." - : ""} + {#await ((result.kind === 6 || result.kind === 16) ? parseRepostContent(result.content) : parseContent(result.content)) then parsedContent} + {@html parsedContent.slice(0, 200)}{parsedContent.length > 200 ? "..." : ""} + {:catch} + {result.content.slice(0, 200)}{result.content.length > 200 ? "..." : ""} + {/await}
{/if} {/if} @@ -610,15 +629,17 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; {/each}
+
{/if} {#if secondOrderResults.length > 0}
- - Second-Order Events (References, Replies, Quotes) ({secondOrderResults.length} - events) - +
+ + Second-Order Events (References, Replies, Quotes) ({secondOrderResults.length} + events) + {#if (searchType === "n" || searchType === "d") && secondOrderResults.length === 100}

Showing the 100 newest events. More results may be available. @@ -763,10 +784,11 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";

- {result.content.slice(0, 200)}{result.content.length > - 200 - ? "..." - : ""} + {#await ((result.kind === 6 || result.kind === 16) ? parseRepostContent(result.content) : parseContent(result.content)) then parsedContent} + {@html parsedContent.slice(0, 200)}{parsedContent.length > 200 ? "..." : ""} + {:catch} + {result.content.slice(0, 200)}{result.content.length > 200 ? "..." : ""} + {/await}
{/if} {/if} @@ -774,15 +796,17 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; {/each}
+
{/if} {#if tTagResults.length > 0}
- - Search Results for t-tag: "{searchTerm || - dTagValue?.toLowerCase()}" ({tTagResults.length} events) - +
+ + Search Results for t-tag: "{searchTerm || + dTagValue?.toLowerCase()}" ({tTagResults.length} events) +

Events that are tagged with the t-tag.

@@ -914,10 +938,11 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";
- {result.content.slice(0, 200)}{result.content.length > - 200 - ? "..." - : ""} + {#await ((result.kind === 6 || result.kind === 16) ? parseRepostContent(result.content) : parseContent(result.content)) then parsedContent} + {@html parsedContent.slice(0, 200)}{parsedContent.length > 200 ? "..." : ""} + {:catch} + {result.content.slice(0, 200)}{result.content.length > 200 ? "..." : ""} + {/await}
{/if} {/if} @@ -925,6 +950,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; {/each}
+
{/if} From 93c25f48265c10075b55e0d9a0e288026b1f70f1 Mon Sep 17 00:00:00 2001 From: silberengel Date: Thu, 14 Aug 2025 18:50:44 +0200 Subject: [PATCH 59/98] fixed image display fixed user outline display fixed visualization node links and added backlink consolidated common parser functions --- src/lib/components/CommentBox.svelte | 15 +- src/lib/components/Notifications.svelte | 26 +- src/lib/components/cards/ProfileHeader.svelte | 5 +- src/lib/components/util/ArticleNav.svelte | 46 +++- .../navigator/EventNetwork/NodeTooltip.svelte | 4 +- .../utils/markup/asciidoctorPostProcessor.ts | 80 +++--- src/lib/utils/markup/basicMarkupParser.ts | 246 +++--------------- src/lib/utils/markup/markupServices.ts | 223 ++++++++++++++++ 8 files changed, 343 insertions(+), 302 deletions(-) create mode 100644 src/lib/utils/markup/markupServices.ts diff --git a/src/lib/components/CommentBox.svelte b/src/lib/components/CommentBox.svelte index 9d7e978..b25557f 100644 --- a/src/lib/components/CommentBox.svelte +++ b/src/lib/components/CommentBox.svelte @@ -1,5 +1,6 @@ + +
+ {@html parsedContent} + + + {#each embeddedEvents as eventInfo} +
+ +
+ {/each} +
+ + diff --git a/src/lib/components/EmbeddedEvent.svelte b/src/lib/components/EmbeddedEvent.svelte new file mode 100644 index 0000000..f94d68b --- /dev/null +++ b/src/lib/components/EmbeddedEvent.svelte @@ -0,0 +1,352 @@ + + +{#if nestingLevel >= MAX_NESTING_LEVEL} + + +{:else if loading} + +
+
+
+ Loading event... +
+
+{:else if error} + + +{:else if event} + +
+ +
+
+ + Kind {event.kind} + + + ({getEventType(event.kind || 0)}) + + {#if event.pubkey} + + Author: +
+ {#if toNpub(event.pubkey)} + {@render userBadge( + toNpub(event.pubkey) as string, + authorDisplayName, + )} + {:else} + + {authorDisplayName || event.pubkey.slice(0, 8)}...{event.pubkey.slice(-4)} + + {/if} +
+ {/if} +
+ +
+ + + {#if getEventTitle(event)} +

+ {getEventTitle(event)} +

+ {/if} + + + {#if event.kind !== 1 && getEventSummary(event)} +
+

+ {getEventSummary(event)} +

+
+ {/if} + + + {#if event.kind === 1 && parsedContent} +
+ + {#if parsedContent.length > 300} + ... + {/if} +
+ {/if} + + + {#if event.kind === 0 && profile} +
+ {#if profile.picture} + Profile + {/if} + {#if profile.about} +

+ {profile.about.slice(0, 200)} + {#if profile.about.length > 200} + ... + {/if} +

+ {/if} +
+ {/if} + + +
+
+ ID: + + {event.id.slice(0, 8)}...{event.id.slice(-4)} + + {#if isAddressableEvent(event)} + Address: + + {getNaddrUrl(event).slice(0, 12)}...{getNaddrUrl(event).slice(-8)} + + {/if} +
+
+
+{/if} diff --git a/src/lib/components/EmbeddedEventRenderer.svelte b/src/lib/components/EmbeddedEventRenderer.svelte new file mode 100644 index 0000000..d1752e9 --- /dev/null +++ b/src/lib/components/EmbeddedEventRenderer.svelte @@ -0,0 +1,83 @@ + + +
+ {@html renderContent()} + + + {#each embeddedEvents as eventInfo} +
+ +
+ {/each} +
+ + diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index c3b14f6..ab3b865 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -1,5 +1,7 @@ -
+
{#if event.kind !== 0 && getEventTitle(event)} -

+

{getEventTitle(event)}

{/if} @@ -417,33 +438,33 @@ {/if} -
+
{#if toNpub(event.pubkey)} - Author: {@render userBadge( toNpub(event.pubkey) as string, profile?.display_name || undefined, )} {:else} - Author: {profile?.display_name || event.pubkey} {/if}
-
- Kind: - {event.kind} - + Kind: + {event.kind} + ({getEventTypeDisplay(event)})
{#if getEventSummary(event)} -
+
Summary: -

{getEventSummary(event)}

+

{getEventSummary(event)}

{/if} @@ -455,15 +476,21 @@ {#if event.kind !== 0}
-
+
Content: -
- {@html showFullContent ? parsedContent : contentPreview} - {#if !showFullContent && parsedContent.length > 250} - +
+ {#if contentProcessing} +
Processing content...
+ {:else} +
+ +
+ {#if shouldTruncate} + + {/if} {/if}
@@ -491,12 +518,12 @@

Identifiers:

-
+
{#each getIdentifiers(event, profile) as identifier}
{identifier.label}:
- + {identifier.value.slice(0, 20)}...{identifier.value.slice(-8)}

Event Tags:

-
+
{#each event.tags as tag} {@const tagInfo = getTagButtonInfo(tag)} {#if tagInfo.text && tagInfo.gotoValue} @@ -548,7 +575,7 @@ goto(`/events?id=${tagInfo.gotoValue!}`); } }} - class="text-primary-700 dark:text-primary-300 cursor-pointer bg-transparent border-none p-0 text-left hover:text-primary-900 dark:hover:text-primary-100 break-all" + class="text-primary-700 dark:text-primary-300 cursor-pointer bg-transparent border-none p-0 text-left hover:text-primary-900 dark:hover:text-primary-100 break-all max-w-full" > {tagInfo.text} @@ -561,7 +588,7 @@

Raw Event JSON:

-
+
 {JSON.stringify(event.rawEvent(), null, 2)}
         
diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 5086fc9..805ea0e 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -30,8 +30,8 @@ import { buildCompleteRelaySet } from "$lib/utils/relay_management"; import { formatDate, neventEncode } from "$lib/utils"; import { toNpub, getUserMetadata, NDKRelaySetFromNDK } from "$lib/utils/nostrUtils"; - import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; import { userBadge } from "$lib/snippets/UserSnippets.svelte"; + import EmbeddedEventRenderer from "./EmbeddedEventRenderer.svelte"; const { event } = $props<{ event: NDKEvent }>(); @@ -849,7 +849,7 @@
{#await ((message.kind === 6 || message.kind === 16) ? parseRepostContent(message.content) : parseContent(message.content)) then parsedContent} - {@html parsedContent} + {:catch} {@html message.content} {/await} @@ -930,7 +930,7 @@
{#await ((notification.kind === 6 || notification.kind === 16) ? parseRepostContent(notification.content) : parseContent(notification.content)) then parsedContent} - {@html parsedContent} + {:catch} {@html truncateContent(notification.content)} {/await} @@ -969,7 +969,7 @@
Replying to:
{#await parseContent(quotedContent) then parsedContent} - {@html parsedContent} + {:catch} {@html quotedContent} {/await} diff --git a/src/lib/components/Preview.svelte b/src/lib/components/Preview.svelte index 036098a..72a02ab 100644 --- a/src/lib/components/Preview.svelte +++ b/src/lib/components/Preview.svelte @@ -22,6 +22,7 @@ import BlogHeader from "$components/cards/BlogHeader.svelte"; import { getMatchingTags } from "$lib/utils/nostrUtils"; import { onMount } from "svelte"; + import LazyImage from "$components/util/LazyImage.svelte"; // TODO: Fix move between parents. @@ -250,8 +251,14 @@ {#snippet coverImage(rootId: string, index: number, depth: number)} {#if hasCoverImage(rootId, index)} + {@const event = blogEntries[index][1]}
- {title} +
{/if} {/snippet} diff --git a/src/lib/components/cards/ProfileHeader.svelte b/src/lib/components/cards/ProfileHeader.svelte index cc29ae1..c3838d7 100644 --- a/src/lib/components/cards/ProfileHeader.svelte +++ b/src/lib/components/cards/ProfileHeader.svelte @@ -61,7 +61,7 @@ {#if profile} - +
{#if profile.banner} @@ -79,25 +79,27 @@
{/if}
-
+
{#if profile.picture} Profile avatar { (e.target as HTMLImageElement).src = "/favicon.png"; }} /> {/if} -
- {@render userBadge( - toNpub(event.pubkey) as string, - profile.displayName || - profile.display_name || - profile.name || - event.pubkey, - )} +
+
+ {@render userBadge( + toNpub(event.pubkey) as string, + profile.displayName || + profile.display_name || + profile.name || + event.pubkey, + )} +
{#if communityStatus === true}
-
+
{#if profile.name} -
-
Name:
-
{profile.name}
+
+
Name:
+
{profile.name}
{/if} {#if profile.displayName} -
-
Display Name:
-
{profile.displayName}
+
+
Display Name:
+
{profile.displayName}
{/if} {#if profile.about} -
-
About:
-
{profile.about}
+
+
About:
+
{profile.about}
{/if} {#if profile.website} -

Scan the QR code or copy the address

{#if lnurl} -

+

diff --git a/src/lib/components/util/CardActions.svelte b/src/lib/components/util/CardActions.svelte index dddbb8a..89647a6 100644 --- a/src/lib/components/util/CardActions.svelte +++ b/src/lib/components/util/CardActions.svelte @@ -12,6 +12,7 @@ import { userStore } from "$lib/stores/userStore"; import { goto } from "$app/navigation"; import type { NDKEvent } from "$lib/utils/nostrUtils"; + import LazyImage from "$components/util/LazyImage.svelte"; // Component props let { event } = $props<{ event: NDKEvent }>(); @@ -191,10 +192,11 @@
- Publication cover
{/if} diff --git a/src/lib/utils/markup/embeddedMarkupParser.ts b/src/lib/utils/markup/embeddedMarkupParser.ts new file mode 100644 index 0000000..548f4ba --- /dev/null +++ b/src/lib/utils/markup/embeddedMarkupParser.ts @@ -0,0 +1,263 @@ +import * as emoji from "node-emoji"; +import { nip19 } from "nostr-tools"; +import { + processImageWithReveal, + processMediaUrl, + processNostrIdentifiersInText, + processEmojiShortcodes, + processWebSocketUrls, + processHashtags, + processBasicTextFormatting, + processBlockquotes, + processWikilinks, + processNostrIdentifiersWithEmbeddedEvents, + stripTrackingParams +} from "./markupServices"; + +/* Regex constants for basic markup parsing */ + +// Links and media +const MARKUP_LINK = /\[([^\]]+)\]\(([^)]+)\)/g; +const MARKUP_IMAGE = /!\[([^\]]*)\]\(([^)]+)\)/g; +const DIRECT_LINK = /(?"]+)(?!["'])/g; + +// Add this helper function near the top: +function replaceAlexandriaNostrLinks(text: string): string { + // Regex for Alexandria/localhost URLs + const alexandriaPattern = + /^https?:\/\/((next-)?alexandria\.gitcitadel\.(eu|com)|localhost(:\d+)?)/i; + // Regex for bech32 Nostr identifiers + const bech32Pattern = /(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{20,}/; + // Regex for 64-char hex + const hexPattern = /\b[a-fA-F0-9]{64}\b/; + + // 1. Alexandria/localhost markup links + text = text.replace( + /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, + (match, _label, url) => { + if (alexandriaPattern.test(url)) { + if (/[?&]d=/.test(url)) return match; + const hexMatch = url.match(hexPattern); + if (hexMatch) { + try { + const nevent = nip19.neventEncode({ id: hexMatch[0] }); + return `nostr:${nevent}`; + } catch { + return match; + } + } + const bech32Match = url.match(bech32Pattern); + if (bech32Match) { + return `nostr:${bech32Match[0]}`; + } + } + return match; + }, + ); + + // 2. Alexandria/localhost bare URLs and non-Alexandria/localhost URLs with Nostr identifiers + text = text.replace(/https?:\/\/[^\s)\]]+/g, (url) => { + if (alexandriaPattern.test(url)) { + if (/[?&]d=/.test(url)) return url; + const hexMatch = url.match(hexPattern); + if (hexMatch) { + try { + const nevent = nip19.neventEncode({ id: hexMatch[0] }); + return `nostr:${nevent}`; + } catch { + return url; + } + } + const bech32Match = url.match(bech32Pattern); + if (bech32Match) { + return `nostr:${bech32Match[0]}`; + } + } + // For non-Alexandria/localhost URLs, just return the URL as-is + return url; + }); + + return text; +} + +function renderListGroup(lines: string[], typeHint?: "ol" | "ul"): string { + function parseList( + start: number, + indent: number, + type: "ol" | "ul", + ): [string, number] { + let html = ""; + let i = start; + html += `<${type} class="${type === "ol" ? "list-decimal" : "list-disc"} ml-6 mb-2">`; + while (i < lines.length) { + const line = lines[i]; + const match = line.match(/^([ \t]*)([*+-]|\d+\.)[ \t]+(.*)$/); + if (!match) break; + const lineIndent = match[1].replace(/\t/g, " ").length; + const isOrdered = /\d+\./.test(match[2]); + const itemType = isOrdered ? "ol" : "ul"; + if (lineIndent > indent) { + // Nested list + const [nestedHtml, consumed] = parseList(i, lineIndent, itemType); + html = html.replace(/<\/li>$/, "") + nestedHtml + ""; + i = consumed; + continue; + } + if (lineIndent < indent || itemType !== type) { + break; + } + html += `
  • ${match[3]}`; + // Check for next line being a nested list + if (i + 1 < lines.length) { + const nextMatch = lines[i + 1].match(/^([ \t]*)([*+-]|\d+\.)[ \t]+/); + if (nextMatch) { + const nextIndent = nextMatch[1].replace(/\t/g, " ").length; + const nextType = /\d+\./.test(nextMatch[2]) ? "ol" : "ul"; + if (nextIndent > lineIndent) { + const [nestedHtml, consumed] = parseList( + i + 1, + nextIndent, + nextType, + ); + html += nestedHtml; + i = consumed - 1; + } + } + } + html += "
  • "; + i++; + } + html += ``; + return [html, i]; + } + if (!lines.length) return ""; + const firstLine = lines[0]; + const match = firstLine.match(/^([ \t]*)([*+-]|\d+\.)[ \t]+/); + const indent = match ? match[1].replace(/\t/g, " ").length : 0; + const type = typeHint || (match && /\d+\./.test(match[2]) ? "ol" : "ul"); + const [html] = parseList(0, indent, type); + return html; +} + +function processBasicFormatting(content: string): string { + if (!content) return ""; + + let processedText = content; + + try { + // Sanitize Alexandria Nostr links before further processing + processedText = replaceAlexandriaNostrLinks(processedText); + + // Process markup images first + processedText = processedText.replace(MARKUP_IMAGE, (_match, alt, url) => { + return processImageWithReveal(url, alt); + }); + + // Process markup links + processedText = processedText.replace( + MARKUP_LINK, + (_match, text, url) => + `
    ${text}`, + ); + + // Process WebSocket URLs using shared services + processedText = processWebSocketUrls(processedText); + + // Process direct media URLs and auto-link all URLs + processedText = processedText.replace(DIRECT_LINK, (match) => { + return processMediaUrl(match); + }); + + // Process text formatting using shared services + processedText = processBasicTextFormatting(processedText); + + // Process hashtags using shared services + processedText = processHashtags(processedText); + + // --- Improved List Grouping and Parsing --- + const lines = processedText.split("\n"); + let output = ""; + let buffer: string[] = []; + let inList = false; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (/^([ \t]*)([*+-]|\d+\.)[ \t]+/.test(line)) { + buffer.push(line); + inList = true; + } else { + if (inList) { + const firstLine = buffer[0]; + const isOrdered = /^\s*\d+\.\s+/.test(firstLine); + output += renderListGroup(buffer, isOrdered ? "ol" : "ul"); + buffer = []; + inList = false; + } + output += (output && !output.endsWith("\n") ? "\n" : "") + line + "\n"; + } + } + if (buffer.length) { + const firstLine = buffer[0]; + const isOrdered = /^\s*\d+\.\s+/.test(firstLine); + output += renderListGroup(buffer, isOrdered ? "ol" : "ul"); + } + processedText = output; + // --- End Improved List Grouping and Parsing --- + } catch (e: unknown) { + console.error("Error in processBasicFormatting:", e); + } + + return processedText; +} + +/** + * Parse markup with support for embedded Nostr events + * AI-NOTE: 2025-01-24 - Enhanced markup parser that supports nested Nostr event embedding + * Up to 3 levels of nesting are supported, after which events are shown as links + */ +export async function parseEmbeddedMarkup(text: string, nestingLevel: number = 0): Promise { + if (!text) return ""; + + try { + // Process basic text formatting first + let processedText = processBasicFormatting(text); + + // Process emoji shortcuts + processedText = processEmojiShortcodes(processedText); + + // Process blockquotes + processedText = processBlockquotes(processedText); + + // Process paragraphs - split by double newlines and wrap in p tags + // Skip wrapping if content already contains block-level elements + processedText = processedText + .split(/\n\n+/) + .map((para) => para.trim()) + .filter((para) => para.length > 0) + .map((para) => { + // Skip wrapping if para already contains block-level elements or math blocks + if ( + /(]*class=["'][^"']*math-block[^"']*["'])|<(div|h[1-6]|blockquote|table|pre|ul|ol|hr)/i.test( + para, + ) + ) { + return para; + } + return `

    ${para}

    `; + }) + .join("\n"); + + // Process profile identifiers (npub, nprofile) first using the regular processor + processedText = await processNostrIdentifiersInText(processedText); + + // Then process event identifiers with embedded events (only event-related identifiers) + processedText = processNostrIdentifiersWithEmbeddedEvents(processedText, nestingLevel); + + // Replace wikilinks + processedText = processWikilinks(processedText); + + return processedText; + } catch (e: unknown) { + console.error("Error in parseEmbeddedMarkup:", e); + return `
    Error processing markup: ${(e as Error)?.message ?? "Unknown error"}
    `; + } +} diff --git a/src/lib/utils/markup/markupServices.ts b/src/lib/utils/markup/markupServices.ts index 09157dc..f4ce0a5 100644 --- a/src/lib/utils/markup/markupServices.ts +++ b/src/lib/utils/markup/markupServices.ts @@ -1,4 +1,4 @@ -import { processNostrIdentifiers } from "../nostrUtils.ts"; +import { processNostrIdentifiers, NOSTR_PROFILE_REGEX } from "../nostrUtils.ts"; import * as emoji from "node-emoji"; // Media URL patterns @@ -7,40 +7,30 @@ const VIDEO_URL_REGEX = /https?:\/\/[^\s<]+\.(?:mp4|webm|mov|avi)(?:[^\s<]*)?/i; const AUDIO_URL_REGEX = /https?:\/\/[^\s<]+\.(?:mp3|wav|ogg|m4a)(?:[^\s<]*)?/i; const YOUTUBE_URL_REGEX = /https?:\/\/(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})(?:[^\s<]*)?/; + + /** - * Shared service for processing images with reveal/enlarge functionality + * Shared service for processing images with expand functionality */ export function processImageWithReveal(src: string, alt: string = "Image"): string { if (!src || !IMAGE_EXTENSIONS.test(src.split("?")[0])) { return `${alt}`; } - return `
    - -
    - -
    -
    -
    - - -
    + return `
    + ${alt} - - ${alt} - - -
    + +
    ${parsedOriginalContent}
    `; } catch (error) { - // If JSON parsing fails, fall back to basic markup - console.warn("Failed to parse repost content as JSON, falling back to basic markup:", error); - return await parseBasicmarkup(content); + // If JSON parsing fails, fall back to embedded markup + console.warn("Failed to parse repost content as JSON, falling back to embedded markup:", error); + return await parseEmbeddedMarkup(content, 0); } } @@ -155,7 +178,7 @@ export async function renderQuotedContent(message: NDKEvent, publicMessages: NDK if (quotedMessage) { const quotedContent = quotedMessage.content ? quotedMessage.content.slice(0, 200) : "No content"; - const parsedContent = await parseBasicmarkup(quotedContent); + const parsedContent = await parseEmbeddedMarkup(quotedContent, 0); return `
    ${parsedContent}
    `; } else { // Fallback to nevent link - only if eventId is valid diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index 57748b0..fce8b7d 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -964,11 +964,11 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; {#if showSidePanel && event} -
    -
    - Event Details +
    +
    + Event Details
    {#if event.kind !== 0} -
    +
    {/if} - - +
    + +
    +
    + +
    - +
    + +
    {#if isLoggedIn && userPubkey} -
    - Add Comment +
    + Add Comment
    {:else} -
    +

    Please sign in to add comments.

    {/if} From 1435a74a3017b87428ff253686b4628727445384 Mon Sep 17 00:00:00 2001 From: silberengel Date: Thu, 14 Aug 2025 23:58:00 +0200 Subject: [PATCH 64/98] fixed markup images --- src/lib/utils/markup/advancedMarkupParser.ts | 26 +++++--------------- src/lib/utils/markup/basicMarkupParser.ts | 15 +++++++---- src/lib/utils/markup/embeddedMarkupParser.ts | 15 +++++++---- 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/lib/utils/markup/advancedMarkupParser.ts b/src/lib/utils/markup/advancedMarkupParser.ts index 8777390..e05d970 100644 --- a/src/lib/utils/markup/advancedMarkupParser.ts +++ b/src/lib/utils/markup/advancedMarkupParser.ts @@ -233,24 +233,7 @@ function processFootnotes(content: string): string { } } -/** - * Process blockquotes - */ -function processBlockquotes(content: string): string { - // Match blockquotes that might span multiple lines - const blockquoteRegex = /^>[ \t]?(.+(?:\n>[ \t]?.+)*)/gm; - - return content.replace(blockquoteRegex, (match) => { - // Remove the '>' prefix from each line and preserve line breaks - const text = match - .split("\n") - .map((line) => line.replace(/^>[ \t]?/, "")) - .join("\n") - .trim(); - - return `
    ${text}
    `; - }); -} + /** * Process code blocks by finding consecutive code lines and preserving their content @@ -689,6 +672,8 @@ function isLaTeXContent(content: string): boolean { return latexPatterns.some((pattern) => pattern.test(trimmed)); } + + /** * Parse markup text with advanced formatting */ @@ -706,9 +691,10 @@ export async function parseAdvancedmarkup(text: string): Promise { // Step 3: Process LaTeX math expressions ONLY within inline code blocks (legacy support) processedText = processMathExpressions(processedText); - // Step 4: Process block-level elements (tables, blockquotes, headings, horizontal rules) + // Step 4: Process block-level elements (tables, headings, horizontal rules) + // AI-NOTE: 2025-01-24 - Removed duplicate processBlockquotes call to fix image rendering issues + // Blockquotes are now processed only by parseBasicmarkup to avoid double-processing conflicts processedText = processTables(processedText); - processedText = processBlockquotes(processedText); processedText = processHeadings(processedText); processedText = processHorizontalRules(processedText); diff --git a/src/lib/utils/markup/basicMarkupParser.ts b/src/lib/utils/markup/basicMarkupParser.ts index 043680a..dddd31d 100644 --- a/src/lib/utils/markup/basicMarkupParser.ts +++ b/src/lib/utils/markup/basicMarkupParser.ts @@ -18,7 +18,8 @@ import { // Links and media const MARKUP_LINK = /\[([^\]]+)\]\(([^)]+)\)/g; const MARKUP_IMAGE = /!\[([^\]]*)\]\(([^)]+)\)/g; -const DIRECT_LINK = /(?"]+)(?!["'])/g; +// AI-NOTE: 2025-01-24 - Added negative lookbehind (?"]+)(?!["'])/g; @@ -156,8 +157,11 @@ function processBasicFormatting(content: string): string { processedText = replaceAlexandriaNostrLinks(processedText); // Process markup images first - processedText = processedText.replace(MARKUP_IMAGE, (_match, alt, url) => { - return processImageWithReveal(url, alt); + processedText = processedText.replace(MARKUP_IMAGE, (match, alt, url) => { + // Clean the URL and alt text + const cleanUrl = url.trim(); + const cleanAlt = alt ? alt.trim() : ""; + return processImageWithReveal(cleanUrl, cleanAlt); }); // Process markup links @@ -242,9 +246,10 @@ export async function parseBasicmarkup(text: string): Promise { .map((para) => para.trim()) .filter((para) => para.length > 0) .map((para) => { - // Skip wrapping if para already contains block-level elements or math blocks + // AI-NOTE: 2025-01-24 - Added img tag to skip wrapping to prevent image rendering issues + // Skip wrapping if para already contains block-level elements, math blocks, or images if ( - /(]*class=["'][^"']*math-block[^"']*["'])|<(div|h[1-6]|blockquote|table|pre|ul|ol|hr)/i.test( + /(]*class=["'][^"']*math-block[^"']*["'])|<(div|h[1-6]|blockquote|table|pre|ul|ol|hr|img)/i.test( para, ) ) { diff --git a/src/lib/utils/markup/embeddedMarkupParser.ts b/src/lib/utils/markup/embeddedMarkupParser.ts index 548f4ba..ab34ddf 100644 --- a/src/lib/utils/markup/embeddedMarkupParser.ts +++ b/src/lib/utils/markup/embeddedMarkupParser.ts @@ -19,7 +19,8 @@ import { // Links and media const MARKUP_LINK = /\[([^\]]+)\]\(([^)]+)\)/g; const MARKUP_IMAGE = /!\[([^\]]*)\]\(([^)]+)\)/g; -const DIRECT_LINK = /(?"]+)(?!["'])/g; +// AI-NOTE: 2025-01-24 - Added negative lookbehind (?"]+)(?!["'])/g; // Add this helper function near the top: function replaceAlexandriaNostrLinks(text: string): string { @@ -149,8 +150,11 @@ function processBasicFormatting(content: string): string { processedText = replaceAlexandriaNostrLinks(processedText); // Process markup images first - processedText = processedText.replace(MARKUP_IMAGE, (_match, alt, url) => { - return processImageWithReveal(url, alt); + processedText = processedText.replace(MARKUP_IMAGE, (match, alt, url) => { + // Clean the URL and alt text + const cleanUrl = url.trim(); + const cleanAlt = alt ? alt.trim() : ""; + return processImageWithReveal(cleanUrl, cleanAlt); }); // Process markup links @@ -234,9 +238,10 @@ export async function parseEmbeddedMarkup(text: string, nestingLevel: number = 0 .map((para) => para.trim()) .filter((para) => para.length > 0) .map((para) => { - // Skip wrapping if para already contains block-level elements or math blocks + // AI-NOTE: 2025-01-24 - Added img tag to skip wrapping to prevent image rendering issues + // Skip wrapping if para already contains block-level elements, math blocks, or images if ( - /(]*class=["'][^"']*math-block[^"']*["'])|<(div|h[1-6]|blockquote|table|pre|ul|ol|hr)/i.test( + /(]*class=["'][^"']*math-block[^"']*["'])|<(div|h[1-6]|blockquote|table|pre|ul|ol|hr|img)/i.test( para, ) ) { From 300dc2c66dea95c4314821c2fc18bc56e7f37cdd Mon Sep 17 00:00:00 2001 From: silberengel Date: Fri, 15 Aug 2025 00:26:07 +0200 Subject: [PATCH 65/98] Corrected build for docker --- Dockerfile | 2 +- import_map.json | 3 +++ vite.config.ts | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2f09ee6..12ad673 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,4 +15,4 @@ ENV ORIGIN=http://localhost:3000 RUN deno cache --import-map=import_map.json ./build/index.js EXPOSE 3000 -CMD [ "deno", "run", "--allow-env", "--allow-read", "--allow-net", "--import-map=import_map.json", "./build/index.js" ] +CMD [ "deno", "run", "--allow-env", "--allow-read", "--allow-net", "--allow-sys", "--import-map=import_map.json", "./build/index.js" ] diff --git a/import_map.json b/import_map.json index d536e63..3c9c287 100644 --- a/import_map.json +++ b/import_map.json @@ -15,7 +15,10 @@ "flowbite-svelte": "npm:flowbite-svelte@0.48.x", "flowbite-svelte-icons": "npm:flowbite-svelte-icons@2.1.x", "@noble/curves": "npm:@noble/curves@^1.9.4", + "@noble/curves/secp256k1": "npm:@noble/curves@^1.9.4/secp256k1", "@noble/hashes": "npm:@noble/hashes@^1.8.0", + "@noble/hashes/sha2.js": "npm:@noble/hashes@^1.8.0/sha2.js", + "@noble/hashes/utils": "npm:@noble/hashes@^1.8.0/utils", "bech32": "npm:bech32@^2.0.0", "highlight.js": "npm:highlight.js@^11.11.1", "node-emoji": "npm:node-emoji@^2.2.0", diff --git a/vite.config.ts b/vite.config.ts index 0872066..47552d4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -33,7 +33,7 @@ export default defineConfig({ }, build: { rollupOptions: { - external: ["bech32"], + // Removed bech32 from externals since it's needed on client side }, }, test: { From d8c64260b388e1a30acca81d4bf9cbe56910033f Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 16 Aug 2025 00:17:19 -0500 Subject: [PATCH 66/98] Refactor markup generator for embedded events into Svelte snippets - Eliminate a component that is no longer needed. - Reduce duplicate code. - Tidy up code along the way. - Ran `deno fmt` to auto-format code (hence the large diff). --- README.md | 40 +- deno.lock | 2617 +---------------- playwright.config.ts | 7 +- src/app.css | 83 +- src/app.d.ts | 4 +- src/app.html | 10 +- src/lib/components/CommentViewer.svelte | 29 +- src/lib/components/EmbeddedEvent.svelte | 33 +- src/lib/components/EventDetails.svelte | 194 +- src/lib/components/Notifications.svelte | 69 +- .../publications/PublicationSection.svelte | 1 - .../publications/table_of_contents.svelte.ts | 10 +- src/lib/components/util/Notifications.svelte | 321 ++ src/lib/consts.ts | 7 +- .../docs/relay_selector_design.md | 85 +- src/lib/data_structures/publication_tree.ts | 310 +- src/lib/data_structures/websocket_pool.ts | 65 +- src/lib/navigator/EventNetwork/types.ts | 2 +- .../navigator/EventNetwork/utils/common.ts | 2 +- .../EventNetwork/utils/forceSimulation.ts | 325 +- .../EventNetwork/utils/networkBuilder.ts | 439 +-- .../utils/personNetworkBuilder.ts | 100 +- .../EventNetwork/utils/starForceSimulation.ts | 83 +- .../EventNetwork/utils/starNetworkBuilder.ts | 186 +- .../EventNetwork/utils/tagNetworkBuilder.ts | 29 +- src/lib/ndk.ts | 298 +- src/lib/parser.ts | 8 +- src/lib/services/event_search_service.ts | 28 +- src/lib/services/publisher.ts | 57 +- src/lib/services/search_state_manager.ts | 20 +- src/lib/state.ts | 2 +- src/lib/stores/authStore.Svelte.ts | 2 +- src/lib/stores/networkStore.ts | 26 +- src/lib/stores/userStore.ts | 119 +- src/lib/stores/visualizationConfig.ts | 43 +- src/lib/utils.ts | 24 +- src/lib/utils/ZettelParser.ts | 10 +- src/lib/utils/asciidoc_metadata.ts | 273 +- src/lib/utils/community_checker.ts | 4 +- src/lib/utils/displayLimits.ts | 59 +- src/lib/utils/eventColors.ts | 101 +- src/lib/utils/eventDeduplication.ts | 152 +- src/lib/utils/event_input_utils.ts | 72 +- src/lib/utils/event_kind_utils.ts | 71 +- src/lib/utils/event_search.ts | 78 +- src/lib/utils/image_utils.ts | 12 +- src/lib/utils/kind24_utils.ts | 76 +- src/lib/utils/markup/MarkupInfo.md | 58 +- .../advancedAsciidoctorPostProcessor.ts | 3 +- src/lib/utils/markup/advancedMarkupParser.ts | 44 +- .../utils/markup/asciidoctorPostProcessor.ts | 24 +- src/lib/utils/markup/basicMarkupParser.ts | 51 +- src/lib/utils/markup/embeddedMarkupParser.ts | 56 +- src/lib/utils/markup/markupServices.ts | 147 +- src/lib/utils/markup/tikzRenderer.ts | 4 +- src/lib/utils/mime.ts | 2 +- src/lib/utils/network_detection.ts | 106 +- src/lib/utils/nostrEventService.ts | 41 +- src/lib/utils/nostrUtils.ts | 124 +- src/lib/utils/nostr_identifiers.ts | 40 +- src/lib/utils/notification_utils.ts | 306 -- src/lib/utils/npubCache.ts | 22 +- src/lib/utils/profileCache.ts | 69 +- src/lib/utils/profile_search.ts | 32 +- src/lib/utils/relayDiagnostics.ts | 7 +- src/lib/utils/relay_info_service.ts | 84 +- src/lib/utils/relay_management.ts | 380 ++- src/lib/utils/search_result_formatter.ts | 16 +- src/lib/utils/search_utility.ts | 14 +- src/lib/utils/subscription_search.ts | 272 +- src/lib/utils/tag_event_fetch.ts | 117 +- src/lib/utils/websocket_utils.ts | 91 +- src/routes/+layout.ts | 141 - src/routes/events/+page.svelte | 69 +- src/routes/proxy+layout.ts | 5 - src/routes/publication/+page.server.ts | 4 +- .../[type]/[identifier]/+layout.server.ts | 15 +- .../publication/[type]/[identifier]/+page.ts | 52 +- src/routes/visualize/+page.ts | 10 +- src/styles/notifications.css | 8 +- src/styles/publications.css | 20 +- src/styles/scrollbar.css | 6 +- src/styles/visualize.css | 28 +- test_data/LaTeXtestfile.md | 85 +- tests/e2e/my_notes_layout.pw.spec.ts | 26 +- tests/unit/ZettelEditor.test.ts | 275 +- tests/unit/eventInput30040.test.ts | 337 ++- tests/unit/latexRendering.test.ts | 2 +- tests/unit/metadataExtraction.test.ts | 124 +- tests/unit/nostr_identifiers.test.ts | 132 +- tests/unit/relayDeduplication.test.ts | 742 +++-- tests/unit/tagExpansion.test.ts | 353 ++- vite.config.ts | 8 +- 93 files changed, 4955 insertions(+), 6183 deletions(-) create mode 100644 src/lib/components/util/Notifications.svelte delete mode 100644 src/lib/utils/notification_utils.ts delete mode 100644 src/routes/+layout.ts delete mode 100644 src/routes/proxy+layout.ts diff --git a/README.md b/README.md index 274657e..3273302 100644 --- a/README.md +++ b/README.md @@ -3,19 +3,31 @@ # Alexandria Alexandria is a reader and writer for curated publications, including e-books. -For a thorough introduction, please refer to our [project documention](https://next-alexandria.gitcitadel.eu/publication?d=gitcitadel-project-documentation-by-stella-v-1), viewable on Alexandria, or to the Alexandria [About page](https://next-alexandria.gitcitadel.eu/about). +For a thorough introduction, please refer to our +[project documention](https://next-alexandria.gitcitadel.eu/publication?d=gitcitadel-project-documentation-by-stella-v-1), +viewable on Alexandria, or to the Alexandria +[About page](https://next-alexandria.gitcitadel.eu/about). -It also contains a [universal event viewer](https://next-alexandria.gitcitadel.eu/events), with which you can search our relays, some aggregator relays, and your own relay list, to find and view event data. +It also contains a +[universal event viewer](https://next-alexandria.gitcitadel.eu/events), with +which you can search our relays, some aggregator relays, and your own relay +list, to find and view event data. ## Issues and Patches -If you would like to suggest a feature or report a bug, please use the [Alexandria Contact page](https://next-alexandria.gitcitadel.eu/contact). +If you would like to suggest a feature or report a bug, please use the +[Alexandria Contact page](https://next-alexandria.gitcitadel.eu/contact). -You can also contact us [on Nostr](https://next-alexandria.gitcitadel.eu/events?id=nprofile1qqsggm4l0xs23qfjwnkfwf6fqcs66s3lz637gaxhl4nwd2vtle8rnfqprfmhxue69uhhg6r9vehhyetnwshxummnw3erztnrdaks5zhueg), directly. +You can also contact us +[on Nostr](https://next-alexandria.gitcitadel.eu/events?id=nprofile1qqsggm4l0xs23qfjwnkfwf6fqcs66s3lz637gaxhl4nwd2vtle8rnfqprfmhxue69uhhg6r9vehhyetnwshxummnw3erztnrdaks5zhueg), +directly. ## Developing -Make sure that you have [Node.js](https://nodejs.org/en/download/package-manager) (v22 or above) or [Deno](https://docs.deno.com/runtime/getting_started/installation/) (v2) installed. +Make sure that you have +[Node.js](https://nodejs.org/en/download/package-manager) (v22 or above) or +[Deno](https://docs.deno.com/runtime/getting_started/installation/) (v2) +installed. Once you've cloned this repo, install dependencies with NPM: @@ -43,7 +55,8 @@ deno task dev ## Building -Alexandria is configured to run on a Node server. The [Node adapter](https://svelte.dev/docs/kit/adapter-node) works on Deno as well. +Alexandria is configured to run on a Node server. The +[Node adapter](https://svelte.dev/docs/kit/adapter-node) works on Deno as well. To build a production version of your app with Node, use: @@ -71,7 +84,8 @@ deno task preview ## Docker + Deno -This application is configured to use the Deno runtime. A Docker container is provided to handle builds and deployments. +This application is configured to use the Deno runtime. A Docker container is +provided to handle builds and deployments. To build the app for local development: @@ -87,9 +101,11 @@ docker run -d -p 3000:3000 local-alexandria ## Testing -_These tests are under development, but will run. They will later be added to the container._ +_These tests are under development, but will run. They will later be added to +the container._ -To run the Vitest suite we've built, install the program locally and run the tests. +To run the Vitest suite we've built, install the program locally and run the +tests. ```bash npm run test @@ -103,4 +119,8 @@ npx playwright test ## Markup Support -Alexandria supports both Markdown and AsciiDoc markup for different content types. For a detailed list of supported tags and features in the basic and advanced markdown parsers, as well as information about AsciiDoc usage for publications and wikis, see [MarkupInfo.md](./src/lib/utils/markup/MarkupInfo.md). +Alexandria supports both Markdown and AsciiDoc markup for different content +types. For a detailed list of supported tags and features in the basic and +advanced markdown parsers, as well as information about AsciiDoc usage for +publications and wikis, see +[MarkupInfo.md](./src/lib/utils/markup/MarkupInfo.md). diff --git a/deno.lock b/deno.lock index ef86772..ceb1ed5 100644 --- a/deno.lock +++ b/deno.lock @@ -1,105 +1,22 @@ { "version": "5", "specifiers": { - "npm:@noble/curves@^1.9.4": "1.9.4", - "npm:@noble/hashes@^1.8.0": "1.8.0", - "npm:@nostr-dev-kit/ndk-cache-dexie@2.6": "2.6.33_nostr-tools@2.15.1__typescript@5.8.3_typescript@5.8.3", - "npm:@nostr-dev-kit/ndk-cache-dexie@^2.6.33": "2.6.33_nostr-tools@2.15.1__typescript@5.8.3_typescript@5.8.3", - "npm:@nostr-dev-kit/ndk@^2.14.32": "2.14.32_nostr-tools@2.15.1__typescript@5.8.3_typescript@5.8.3", "npm:@playwright/test@^1.54.1": "1.54.1", - "npm:@popperjs/core@2.11": "2.11.8", - "npm:@tailwindcss/forms@0.5": "0.5.10_tailwindcss@3.4.17__postcss@8.5.6", - "npm:@tailwindcss/typography@0.5": "0.5.16_tailwindcss@3.4.17__postcss@8.5.6", "npm:@types/d3@^7.4.3": "7.4.3", "npm:@types/he@1.2": "1.2.3", "npm:@types/mathjax@^0.0.40": "0.0.40", "npm:@types/node@^24.0.15": "24.0.15", "npm:@types/qrcode@^1.5.5": "1.5.5", - "npm:asciidoctor@3.0": "3.0.4_@asciidoctor+core@3.0.4", - "npm:autoprefixer@^10.4.21": "10.4.21_postcss@8.5.6", "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@^3.11.0": "3.11.0_eslint@9.31.0_svelte@5.36.8__acorn@8.15.0_postcss@8.5.6", - "npm:flowbite-svelte-icons@2.1": "2.1.1_svelte@5.36.8__acorn@8.15.0_tailwind-merge@3.3.1", - "npm:flowbite-svelte-icons@^2.2.1": "2.2.1_svelte@5.36.8__acorn@8.15.0", - "npm:flowbite-svelte@0.48": "0.48.6_svelte@5.36.8__acorn@8.15.0", - "npm:flowbite-svelte@^1.10.10": "1.10.10_svelte@5.36.8__acorn@8.15.0_tailwindcss@3.4.17__postcss@8.5.6", - "npm:flowbite@2": "2.5.2", - "npm:flowbite@^3.1.2": "3.1.2", "npm:he@1.2": "1.2.0", "npm:highlight.js@^11.11.1": "11.11.1", "npm:node-emoji@^2.2.0": "2.2.0", - "npm:nostr-tools@2.15": "2.15.1_typescript@5.8.3", - "npm:nostr-tools@^2.15.1": "2.15.1_typescript@5.8.3", "npm:plantuml-encoder@^1.4.0": "1.4.0", "npm:playwright@^1.50.1": "1.54.1", "npm:playwright@^1.54.1": "1.54.1", - "npm:postcss-load-config@6": "6.0.1_postcss@8.5.6", - "npm:postcss@^8.5.6": "8.5.6", - "npm:prettier-plugin-svelte@^3.4.0": "3.4.0_prettier@3.6.2_svelte@5.36.8__acorn@8.15.0", - "npm:prettier@^3.6.2": "3.6.2", - "npm:qrcode@^1.5.4": "1.5.4", - "npm:svelte-check@4": "4.3.0_svelte@5.36.8__acorn@8.15.0_typescript@5.8.3", - "npm:svelte@^5.36.8": "5.36.8_acorn@8.15.0", - "npm:tailwind-merge@^3.3.1": "3.3.1", - "npm:tailwindcss@^3.4.17": "3.4.17_postcss@8.5.6", - "npm:tslib@2.8": "2.8.1", - "npm:typescript@^5.8.3": "5.8.3" + "npm:tslib@2.8": "2.8.1" }, "npm": { - "@alloc/quick-lru@5.2.0": { - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==" - }, - "@ampproject/remapping@2.3.0": { - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dependencies": [ - "@jridgewell/gen-mapping", - "@jridgewell/trace-mapping" - ] - }, - "@asciidoctor/cli@4.0.0_@asciidoctor+core@3.0.4": { - "integrity": "sha512-x2T9gW42921Zd90juEagtbViPZHNP2MWf0+6rJEkOzW7E9m3TGJtz+Guye9J0gwrpZsTMGCpfYMQy1We3X7osg==", - "dependencies": [ - "@asciidoctor/core", - "yargs@17.3.1" - ], - "bin": true - }, - "@asciidoctor/core@3.0.4": { - "integrity": "sha512-41SDMi7iRRBViPe0L6VWFTe55bv6HEOJeRqMj5+E5wB1YPdUPuTucL4UAESPZM6OWmn4t/5qM5LusXomFUVwVQ==", - "dependencies": [ - "@asciidoctor/opal-runtime", - "unxhr" - ] - }, - "@asciidoctor/opal-runtime@3.0.1": { - "integrity": "sha512-iW7ACahOG0zZft4A/4CqDcc7JX+fWRNjV5tFAVkNCzwZD+EnFolPaUOPYt8jzadc0+Bgd80cQTtRMQnaaV1kkg==", - "dependencies": [ - "glob@8.1.0", - "unxhr" - ] - }, - "@babel/helper-string-parser@7.27.1": { - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" - }, - "@babel/helper-validator-identifier@7.27.1": { - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" - }, - "@babel/parser@7.28.0": { - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", - "dependencies": [ - "@babel/types" - ], - "bin": true - }, - "@babel/types@7.28.1": { - "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", - "dependencies": [ - "@babel/helper-string-parser", - "@babel/helper-validator-identifier" - ] - }, "@esbuild/aix-ppc64@0.25.7": { "integrity": "sha512-uD0kKFHh6ETr8TqEtaAcV+dn/2qnYbH/+8wGEdY70Qf7l1l/jmBUbrmQqwiPKAQE6cOQ7dTj6Xr0HzQDGHyceQ==", "os": ["aix"], @@ -230,203 +147,6 @@ "os": ["win32"], "cpu": ["x64"] }, - "@eslint-community/eslint-utils@4.7.0_eslint@9.31.0": { - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dependencies": [ - "eslint", - "eslint-visitor-keys@3.4.3" - ] - }, - "@eslint-community/regexpp@4.12.1": { - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==" - }, - "@eslint/config-array@0.21.0": { - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", - "dependencies": [ - "@eslint/object-schema", - "debug", - "minimatch@3.1.2" - ] - }, - "@eslint/config-helpers@0.3.0": { - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==" - }, - "@eslint/core@0.15.1": { - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dependencies": [ - "@types/json-schema" - ] - }, - "@eslint/eslintrc@3.3.1": { - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dependencies": [ - "ajv", - "debug", - "espree", - "globals@14.0.0", - "ignore", - "import-fresh", - "js-yaml", - "minimatch@3.1.2", - "strip-json-comments" - ] - }, - "@eslint/js@9.31.0": { - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==" - }, - "@eslint/object-schema@2.1.6": { - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==" - }, - "@eslint/plugin-kit@0.3.3": { - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", - "dependencies": [ - "@eslint/core", - "levn" - ] - }, - "@floating-ui/core@1.7.2": { - "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", - "dependencies": [ - "@floating-ui/utils" - ] - }, - "@floating-ui/dom@1.7.2": { - "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", - "dependencies": [ - "@floating-ui/core", - "@floating-ui/utils" - ] - }, - "@floating-ui/utils@0.2.10": { - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" - }, - "@humanfs/core@0.19.1": { - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==" - }, - "@humanfs/node@0.16.6": { - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dependencies": [ - "@humanfs/core", - "@humanwhocodes/retry@0.3.1" - ] - }, - "@humanwhocodes/module-importer@1.0.1": { - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" - }, - "@humanwhocodes/retry@0.3.1": { - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==" - }, - "@humanwhocodes/retry@0.4.3": { - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==" - }, - "@isaacs/cliui@8.0.2": { - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dependencies": [ - "string-width@5.1.2", - "string-width-cjs@npm:string-width@4.2.3", - "strip-ansi@7.1.0", - "strip-ansi-cjs@npm:strip-ansi@6.0.1", - "wrap-ansi@8.1.0", - "wrap-ansi-cjs@npm:wrap-ansi@7.0.0" - ] - }, - "@jridgewell/gen-mapping@0.3.12": { - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "dependencies": [ - "@jridgewell/sourcemap-codec", - "@jridgewell/trace-mapping" - ] - }, - "@jridgewell/resolve-uri@3.1.2": { - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" - }, - "@jridgewell/sourcemap-codec@1.5.4": { - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==" - }, - "@jridgewell/trace-mapping@0.3.29": { - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "dependencies": [ - "@jridgewell/resolve-uri", - "@jridgewell/sourcemap-codec" - ] - }, - "@noble/ciphers@0.5.3": { - "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==" - }, - "@noble/curves@1.1.0": { - "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", - "dependencies": [ - "@noble/hashes@1.3.1" - ] - }, - "@noble/curves@1.2.0": { - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", - "dependencies": [ - "@noble/hashes@1.3.2" - ] - }, - "@noble/curves@1.9.4": { - "integrity": "sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==", - "dependencies": [ - "@noble/hashes@1.8.0" - ] - }, - "@noble/hashes@1.3.1": { - "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==" - }, - "@noble/hashes@1.3.2": { - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" - }, - "@noble/hashes@1.8.0": { - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==" - }, - "@noble/secp256k1@2.3.0": { - "integrity": "sha512-0TQed2gcBbIrh7Ccyw+y/uZQvbJwm7Ao4scBUxqpBCcsOlZG0O4KGfjtNAy/li4W8n1xt3dxrwJ0beZ2h2G6Kw==" - }, - "@nodelib/fs.scandir@2.1.5": { - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dependencies": [ - "@nodelib/fs.stat", - "run-parallel" - ] - }, - "@nodelib/fs.stat@2.0.5": { - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" - }, - "@nodelib/fs.walk@1.2.8": { - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dependencies": [ - "@nodelib/fs.scandir", - "fastq" - ] - }, - "@nostr-dev-kit/ndk-cache-dexie@2.6.33_nostr-tools@2.15.1__typescript@5.8.3_typescript@5.8.3": { - "integrity": "sha512-JzUD5cuJbGQDUXYuW1530vy347Kk3AhdtvPO8tL6kFpV3KzGt/QPZ0SHxcjMhJdf7r6cAIpCEWj9oUlStr0gsg==", - "dependencies": [ - "@nostr-dev-kit/ndk", - "debug", - "dexie", - "nostr-tools", - "typescript-lru-cache" - ] - }, - "@nostr-dev-kit/ndk@2.14.32_nostr-tools@2.15.1__typescript@5.8.3_typescript@5.8.3": { - "integrity": "sha512-LUBO35RCB9/emBYsXNDece7m/WO2rGYR8j4SD0Crb3z8GcKTJq6P8OjpZ6+Kr+sLNo8N0uL07XxtAvEBnp2OqQ==", - "dependencies": [ - "@noble/curves@1.9.4", - "@noble/hashes@1.8.0", - "@noble/secp256k1", - "@scure/base@1.2.6", - "debug", - "light-bolt11-decoder", - "nostr-tools", - "tseep", - "typescript-lru-cache" - ] - }, - "@pkgjs/parseargs@0.11.0": { - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==" - }, "@playwright/test@1.54.1": { "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", "dependencies": [ @@ -434,206 +154,9 @@ ], "bin": true }, - "@popperjs/core@2.11.8": { - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" - }, - "@rollup/plugin-node-resolve@15.3.1": { - "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", - "dependencies": [ - "@rollup/pluginutils", - "@types/resolve", - "deepmerge", - "is-module", - "resolve" - ] - }, - "@rollup/pluginutils@5.2.0_rollup@4.45.1": { - "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", - "dependencies": [ - "@types/estree", - "estree-walker", - "picomatch@4.0.3", - "rollup" - ], - "optionalPeers": [ - "rollup" - ] - }, - "@rollup/rollup-android-arm-eabi@4.45.1": { - "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", - "os": ["android"], - "cpu": ["arm"] - }, - "@rollup/rollup-android-arm64@4.45.1": { - "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", - "os": ["android"], - "cpu": ["arm64"] - }, - "@rollup/rollup-darwin-arm64@4.45.1": { - "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", - "os": ["darwin"], - "cpu": ["arm64"] - }, - "@rollup/rollup-darwin-x64@4.45.1": { - "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", - "os": ["darwin"], - "cpu": ["x64"] - }, - "@rollup/rollup-freebsd-arm64@4.45.1": { - "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", - "os": ["freebsd"], - "cpu": ["arm64"] - }, - "@rollup/rollup-freebsd-x64@4.45.1": { - "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", - "os": ["freebsd"], - "cpu": ["x64"] - }, - "@rollup/rollup-linux-arm-gnueabihf@4.45.1": { - "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", - "os": ["linux"], - "cpu": ["arm"] - }, - "@rollup/rollup-linux-arm-musleabihf@4.45.1": { - "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", - "os": ["linux"], - "cpu": ["arm"] - }, - "@rollup/rollup-linux-arm64-gnu@4.45.1": { - "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", - "os": ["linux"], - "cpu": ["arm64"] - }, - "@rollup/rollup-linux-arm64-musl@4.45.1": { - "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", - "os": ["linux"], - "cpu": ["arm64"] - }, - "@rollup/rollup-linux-loongarch64-gnu@4.45.1": { - "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", - "os": ["linux"], - "cpu": ["loong64"] - }, - "@rollup/rollup-linux-powerpc64le-gnu@4.45.1": { - "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", - "os": ["linux"], - "cpu": ["ppc64"] - }, - "@rollup/rollup-linux-riscv64-gnu@4.45.1": { - "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", - "os": ["linux"], - "cpu": ["riscv64"] - }, - "@rollup/rollup-linux-riscv64-musl@4.45.1": { - "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", - "os": ["linux"], - "cpu": ["riscv64"] - }, - "@rollup/rollup-linux-s390x-gnu@4.45.1": { - "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", - "os": ["linux"], - "cpu": ["s390x"] - }, - "@rollup/rollup-linux-x64-gnu@4.45.1": { - "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", - "os": ["linux"], - "cpu": ["x64"] - }, - "@rollup/rollup-linux-x64-musl@4.45.1": { - "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", - "os": ["linux"], - "cpu": ["x64"] - }, - "@rollup/rollup-win32-arm64-msvc@4.45.1": { - "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", - "os": ["win32"], - "cpu": ["arm64"] - }, - "@rollup/rollup-win32-ia32-msvc@4.45.1": { - "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", - "os": ["win32"], - "cpu": ["ia32"] - }, - "@rollup/rollup-win32-x64-msvc@4.45.1": { - "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", - "os": ["win32"], - "cpu": ["x64"] - }, - "@scure/base@1.1.1": { - "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==" - }, - "@scure/base@1.2.6": { - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==" - }, - "@scure/bip32@1.3.1": { - "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", - "dependencies": [ - "@noble/curves@1.1.0", - "@noble/hashes@1.3.2", - "@scure/base@1.1.1" - ] - }, - "@scure/bip39@1.2.1": { - "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", - "dependencies": [ - "@noble/hashes@1.3.2", - "@scure/base@1.1.1" - ] - }, "@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" - ] - }, - "@svgdotjs/svg.draggable.js@3.0.6_@svgdotjs+svg.js@3.2.4": { - "integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==", - "dependencies": [ - "@svgdotjs/svg.js" - ] - }, - "@svgdotjs/svg.filter.js@3.0.9": { - "integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==", - "dependencies": [ - "@svgdotjs/svg.js" - ] - }, - "@svgdotjs/svg.js@3.2.4": { - "integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==" - }, - "@svgdotjs/svg.resize.js@2.0.5_@svgdotjs+svg.js@3.2.4_@svgdotjs+svg.select.js@4.0.3__@svgdotjs+svg.js@3.2.4": { - "integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==", - "dependencies": [ - "@svgdotjs/svg.js", - "@svgdotjs/svg.select.js" - ] - }, - "@svgdotjs/svg.select.js@4.0.3_@svgdotjs+svg.js@3.2.4": { - "integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==", - "dependencies": [ - "@svgdotjs/svg.js" - ] - }, - "@tailwindcss/forms@0.5.10_tailwindcss@3.4.17__postcss@8.5.6": { - "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", - "dependencies": [ - "mini-svg-data-uri", - "tailwindcss" - ] - }, - "@tailwindcss/typography@0.5.16_tailwindcss@3.4.17__postcss@8.5.6": { - "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", - "dependencies": [ - "lodash.castarray", - "lodash.isplainobject", - "lodash.merge", - "postcss-selector-parser@6.0.10", - "tailwindcss" - ] - }, "@types/d3-array@3.2.1": { "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" }, @@ -794,18 +317,12 @@ "@types/d3-zoom" ] }, - "@types/estree@1.0.8": { - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" - }, "@types/geojson@7946.0.16": { "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==" }, "@types/he@1.2.3": { "integrity": "sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==" }, - "@types/json-schema@7.0.15": { - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" - }, "@types/mathjax@0.0.40": { "integrity": "sha512-rHusx08LCg92WJxrsM3SPjvLTSvK5C+gealtSuhKbEOcUZfWlwigaFoPLf6Dfxhg4oryN5qP9Sj7zOQ4HYXINw==" }, @@ -827,1971 +344,61 @@ "@types/node@22.15.15" ] }, - "@types/resolve@1.20.2": { - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" - }, - "@yr/monotone-cubic-spline@1.0.3": { - "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==" + "bech32@2.0.0": { + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" }, - "a-sync-waterfall@1.0.1": { - "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==" + "char-regex@1.0.2": { + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==" }, - "acorn-jsx@5.3.2_acorn@8.15.0": { - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dependencies": [ - "acorn@8.15.0" - ] + "emojilib@2.4.0": { + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==" }, - "acorn@7.4.1": { - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "bin": true + "fsevents@2.3.2": { + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "os": ["darwin"], + "scripts": true }, - "acorn@8.15.0": { - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "he@1.2.0": { + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "bin": true }, - "ajv@6.12.6": { - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": [ - "fast-deep-equal", - "fast-json-stable-stringify", - "json-schema-traverse", - "uri-js" - ] - }, - "ansi-regex@5.0.1": { - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "ansi-regex@6.1.0": { - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==" - }, - "ansi-styles@4.3.0": { - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": [ - "color-convert" - ] - }, - "ansi-styles@6.2.1": { - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" - }, - "any-promise@1.3.0": { - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" - }, - "anymatch@3.1.3": { - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dependencies": [ - "normalize-path", - "picomatch@2.3.1" - ] - }, - "apexcharts@3.54.1": { - "integrity": "sha512-E4et0h/J1U3r3EwS/WlqJCQIbepKbp6wGUmaAwJOMjHUP4Ci0gxanLa7FR3okx6p9coi4st6J853/Cb1NP0vpA==", - "dependencies": [ - "@yr/monotone-cubic-spline", - "svg.draggable.js", - "svg.easing.js", - "svg.filter.js", - "svg.pathmorphing.js", - "svg.resize.js", - "svg.select.js@3.0.1" - ] + "highlight.js@11.11.1": { + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==" }, - "apexcharts@4.7.0_@svgdotjs+svg.js@3.2.4_@svgdotjs+svg.select.js@4.0.3__@svgdotjs+svg.js@3.2.4": { - "integrity": "sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==", + "node-emoji@2.2.0": { + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", "dependencies": [ - "@svgdotjs/svg.draggable.js", - "@svgdotjs/svg.filter.js", - "@svgdotjs/svg.js", - "@svgdotjs/svg.resize.js", - "@svgdotjs/svg.select.js", - "@yr/monotone-cubic-spline" + "@sindresorhus/is", + "char-regex", + "emojilib", + "skin-tone" ] }, - "arg@5.0.2": { - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" - }, - "argparse@2.0.1": { - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "aria-query@5.3.2": { - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==" - }, - "asap@2.0.6": { - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + "plantuml-encoder@1.4.0": { + "integrity": "sha512-sxMwpDw/ySY1WB2CE3+IdMuEcWibJ72DDOsXLkSmEaSzwEUaYBT6DWgOfBiHGCux4q433X6+OEFWjlVqp7gL6g==" }, - "asciidoctor@3.0.4_@asciidoctor+core@3.0.4": { - "integrity": "sha512-hIc0Bx73wePxtic+vWBHOIgMfKSNiCmRz7BBfkyykXATrw20YGd5a3CozCHvqEPH+Wxp5qKD4aBsgtokez8nEA==", - "dependencies": [ - "@asciidoctor/cli", - "@asciidoctor/core", - "ejs", - "handlebars", - "nunjucks", - "pug" - ], + "playwright-core@1.54.1": { + "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", "bin": true }, - "assert-never@1.4.0": { - "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==" - }, - "async@3.2.6": { - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" - }, - "autoprefixer@10.4.21_postcss@8.5.6": { - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "playwright@1.54.1": { + "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", "dependencies": [ - "browserslist", - "caniuse-lite", - "fraction.js", - "normalize-range", - "picocolors", - "postcss", - "postcss-value-parser" + "playwright-core" + ], + "optionalDependencies": [ + "fsevents" ], "bin": true }, - "axobject-query@4.1.0": { - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==" - }, - "babel-walk@3.0.0-canary-5": { - "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "skin-tone@2.0.0": { + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", "dependencies": [ - "@babel/types" + "unicode-emoji-modifier-base" ] }, - "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==" - }, - "brace-expansion@1.1.12": { - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dependencies": [ - "balanced-match", - "concat-map" - ] - }, - "brace-expansion@2.0.2": { - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dependencies": [ - "balanced-match" - ] - }, - "braces@3.0.3": { - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dependencies": [ - "fill-range" - ] - }, - "browserslist@4.25.1": { - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", - "dependencies": [ - "caniuse-lite", - "electron-to-chromium", - "node-releases", - "update-browserslist-db" - ], - "bin": true - }, - "call-bind-apply-helpers@1.0.2": { - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dependencies": [ - "es-errors", - "function-bind" - ] - }, - "call-bound@1.0.4": { - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dependencies": [ - "call-bind-apply-helpers", - "get-intrinsic" - ] - }, - "callsites@3.1.0": { - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" - }, - "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.30001727": { - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==" - }, - "chalk@4.1.2": { - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": [ - "ansi-styles@4.3.0", - "supports-color" - ] - }, - "char-regex@1.0.2": { - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==" - }, - "character-parser@2.2.0": { - "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", - "dependencies": [ - "is-regex" - ] - }, - "chokidar@3.6.0": { - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dependencies": [ - "anymatch", - "braces", - "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": { - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dependencies": [ - "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": [ - "string-width@4.2.3", - "strip-ansi@6.0.1", - "wrap-ansi@7.0.0" - ] - }, - "clsx@2.1.1": { - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" - }, - "color-convert@2.0.1": { - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": [ - "color-name" - ] - }, - "color-name@1.1.4": { - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "commander@4.1.1": { - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" - }, - "commander@5.1.0": { - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" - }, - "commander@7.2.0": { - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" - }, - "concat-map@0.0.1": { - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "constantinople@4.0.1": { - "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", - "dependencies": [ - "@babel/parser", - "@babel/types" - ] - }, - "cross-spawn@7.0.6": { - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dependencies": [ - "path-key", - "shebang-command", - "which" - ] - }, - "cssesc@3.0.0": { - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "bin": true - }, - "d3-array@3.2.4": { - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "dependencies": [ - "internmap" - ] - }, - "d3-axis@3.0.0": { - "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==" - }, - "d3-brush@3.0.0_d3-selection@3.0.0": { - "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", - "dependencies": [ - "d3-dispatch", - "d3-drag", - "d3-interpolate", - "d3-selection", - "d3-transition" - ] - }, - "d3-chord@3.0.1": { - "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", - "dependencies": [ - "d3-path" - ] - }, - "d3-color@3.1.0": { - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" - }, - "d3-contour@4.0.2": { - "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", - "dependencies": [ - "d3-array" - ] - }, - "d3-delaunay@6.0.4": { - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", - "dependencies": [ - "delaunator" - ] - }, - "d3-dispatch@3.0.1": { - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" - }, - "d3-drag@3.0.0": { - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "dependencies": [ - "d3-dispatch", - "d3-selection" - ] - }, - "d3-dsv@3.0.1": { - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", - "dependencies": [ - "commander@7.2.0", - "iconv-lite", - "rw" - ], - "bin": true - }, - "d3-ease@3.0.1": { - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" - }, - "d3-fetch@3.0.1": { - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", - "dependencies": [ - "d3-dsv" - ] - }, - "d3-force@3.0.0": { - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "dependencies": [ - "d3-dispatch", - "d3-quadtree", - "d3-timer" - ] - }, - "d3-format@3.1.0": { - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" - }, - "d3-geo@3.1.1": { - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", - "dependencies": [ - "d3-array" - ] - }, - "d3-hierarchy@3.1.2": { - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==" - }, - "d3-interpolate@3.0.1": { - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "dependencies": [ - "d3-color" - ] - }, - "d3-path@3.1.0": { - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==" - }, - "d3-polygon@3.0.1": { - "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==" - }, - "d3-quadtree@3.0.1": { - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==" - }, - "d3-random@3.0.1": { - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==" - }, - "d3-scale-chromatic@3.1.0": { - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "dependencies": [ - "d3-color", - "d3-interpolate" - ] - }, - "d3-scale@4.0.2": { - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "dependencies": [ - "d3-array", - "d3-format", - "d3-interpolate", - "d3-time", - "d3-time-format" - ] - }, - "d3-selection@3.0.0": { - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" - }, - "d3-shape@3.2.0": { - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "dependencies": [ - "d3-path" - ] - }, - "d3-time-format@4.1.0": { - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "dependencies": [ - "d3-time" - ] - }, - "d3-time@3.1.0": { - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "dependencies": [ - "d3-array" - ] - }, - "d3-timer@3.0.1": { - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" - }, - "d3-transition@3.0.1_d3-selection@3.0.0": { - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "dependencies": [ - "d3-color", - "d3-dispatch", - "d3-ease", - "d3-interpolate", - "d3-selection", - "d3-timer" - ] - }, - "d3-zoom@3.0.0_d3-selection@3.0.0": { - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "dependencies": [ - "d3-dispatch", - "d3-drag", - "d3-interpolate", - "d3-selection", - "d3-transition" - ] - }, - "d3@7.9.0_d3-selection@3.0.0": { - "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", - "dependencies": [ - "d3-array", - "d3-axis", - "d3-brush", - "d3-chord", - "d3-color", - "d3-contour", - "d3-delaunay", - "d3-dispatch", - "d3-drag", - "d3-dsv", - "d3-ease", - "d3-fetch", - "d3-force", - "d3-format", - "d3-geo", - "d3-hierarchy", - "d3-interpolate", - "d3-path", - "d3-polygon", - "d3-quadtree", - "d3-random", - "d3-scale", - "d3-scale-chromatic", - "d3-selection", - "d3-shape", - "d3-time", - "d3-time-format", - "d3-timer", - "d3-transition", - "d3-zoom" - ] - }, - "date-fns@4.1.0": { - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==" - }, - "debug@4.4.1": { - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dependencies": [ - "ms" - ] - }, - "decamelize@1.2.0": { - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" - }, - "deep-is@0.1.4": { - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" - }, - "deepmerge@4.3.1": { - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" - }, - "delaunator@5.0.1": { - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", - "dependencies": [ - "robust-predicates" - ] - }, - "dexie@4.0.11": { - "integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==" - }, - "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==" - }, - "doctypes@1.1.0": { - "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==" - }, - "dunder-proto@1.0.1": { - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dependencies": [ - "call-bind-apply-helpers", - "es-errors", - "gopd" - ] - }, - "eastasianwidth@0.2.0": { - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, - "ejs@3.1.10": { - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dependencies": [ - "jake" - ], - "bin": true - }, - "electron-to-chromium@1.5.187": { - "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==" - }, - "emoji-regex@8.0.0": { - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "emoji-regex@9.2.2": { - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "emojilib@2.4.0": { - "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==" - }, - "es-define-property@1.0.1": { - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" - }, - "es-errors@1.3.0": { - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" - }, - "es-object-atoms@1.1.1": { - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dependencies": [ - "es-errors" - ] - }, - "escalade@3.2.0": { - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" - }, - "escape-string-regexp@4.0.0": { - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" - }, - "eslint-plugin-svelte@3.11.0_eslint@9.31.0_svelte@5.36.8__acorn@8.15.0_postcss@8.5.6": { - "integrity": "sha512-KliWlkieHyEa65aQIkRwUFfHzT5Cn4u3BQQsu3KlkJOs7c1u7ryn84EWaOjEzilbKgttT4OfBURA8Uc4JBSQIw==", - "dependencies": [ - "@eslint-community/eslint-utils", - "@jridgewell/sourcemap-codec", - "eslint", - "esutils", - "globals@16.3.0", - "known-css-properties", - "postcss", - "postcss-load-config@3.1.4_postcss@8.5.6", - "postcss-safe-parser", - "semver", - "svelte", - "svelte-eslint-parser" - ], - "optionalPeers": [ - "svelte" - ] - }, - "eslint-scope@8.4.0": { - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dependencies": [ - "esrecurse", - "estraverse" - ] - }, - "eslint-visitor-keys@3.4.3": { - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" - }, - "eslint-visitor-keys@4.2.1": { - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==" - }, - "eslint@9.31.0": { - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", - "dependencies": [ - "@eslint-community/eslint-utils", - "@eslint-community/regexpp", - "@eslint/config-array", - "@eslint/config-helpers", - "@eslint/core", - "@eslint/eslintrc", - "@eslint/js", - "@eslint/plugin-kit", - "@humanfs/node", - "@humanwhocodes/module-importer", - "@humanwhocodes/retry@0.4.3", - "@types/estree", - "@types/json-schema", - "ajv", - "chalk", - "cross-spawn", - "debug", - "escape-string-regexp", - "eslint-scope", - "eslint-visitor-keys@4.2.1", - "espree", - "esquery", - "esutils", - "fast-deep-equal", - "file-entry-cache", - "find-up@5.0.0", - "glob-parent@6.0.2", - "ignore", - "imurmurhash", - "is-glob", - "json-stable-stringify-without-jsonify", - "lodash.merge", - "minimatch@3.1.2", - "natural-compare", - "optionator" - ], - "bin": true - }, - "esm-env@1.2.2": { - "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" - }, - "espree@10.4.0_acorn@8.15.0": { - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dependencies": [ - "acorn@8.15.0", - "acorn-jsx", - "eslint-visitor-keys@4.2.1" - ] - }, - "esquery@1.6.0": { - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dependencies": [ - "estraverse" - ] - }, - "esrap@2.1.0": { - "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", - "dependencies": [ - "@jridgewell/sourcemap-codec" - ] - }, - "esrecurse@4.3.0": { - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dependencies": [ - "estraverse" - ] - }, - "estraverse@5.3.0": { - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" - }, - "estree-walker@2.0.2": { - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "esutils@2.0.3": { - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "fast-deep-equal@3.1.3": { - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-glob@3.3.3": { - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dependencies": [ - "@nodelib/fs.stat", - "@nodelib/fs.walk", - "glob-parent@5.1.2", - "merge2", - "micromatch" - ] - }, - "fast-json-stable-stringify@2.1.0": { - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "fast-levenshtein@2.0.6": { - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" - }, - "fastq@1.19.1": { - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dependencies": [ - "reusify" - ] - }, - "fdir@6.4.6_picomatch@4.0.3": { - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dependencies": [ - "picomatch@4.0.3" - ], - "optionalPeers": [ - "picomatch@4.0.3" - ] - }, - "file-entry-cache@8.0.0": { - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dependencies": [ - "flat-cache" - ] - }, - "filelist@1.0.4": { - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dependencies": [ - "minimatch@5.1.6" - ] - }, - "fill-range@7.1.1": { - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dependencies": [ - "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@6.0.0", - "path-exists" - ] - }, - "flat-cache@4.0.1": { - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dependencies": [ - "flatted", - "keyv" - ] - }, - "flatted@3.3.3": { - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" - }, - "flowbite-datepicker@1.3.2": { - "integrity": "sha512-6Nfm0MCVX3mpaR7YSCjmEO2GO8CDt6CX8ZpQnGdeu03WUCWtEPQ/uy0PUiNtIJjJZWnX0Cm3H55MOhbD1g+E/g==", - "dependencies": [ - "@rollup/plugin-node-resolve", - "flowbite@2.5.2" - ] - }, - "flowbite-svelte-icons@2.1.1_svelte@5.36.8__acorn@8.15.0_tailwind-merge@3.3.1": { - "integrity": "sha512-VNNMcekjbM1bQEGgbdGsdYR9mRdTj/L0A5ba0P1tiFv5QB9GvbvJMABJoiD80eqpZUkfR2QVOmiZfgCwHicT/Q==", - "dependencies": [ - "svelte", - "tailwind-merge@3.3.1" - ] - }, - "flowbite-svelte-icons@2.2.1_svelte@5.36.8__acorn@8.15.0": { - "integrity": "sha512-SH59319zN4TFpmvFMD7+0ETyDxez4Wyw3mgz7hkjhvrx8HawNAS3Fp7au84pZEs1gniX4hvXIg54U+4YybV2rA==", - "dependencies": [ - "clsx", - "svelte", - "tailwind-merge@3.3.1" - ] - }, - "flowbite-svelte@0.48.6_svelte@5.36.8__acorn@8.15.0": { - "integrity": "sha512-/PmeR3ipHHvda8vVY9MZlymaRoJsk8VddEeoLzIygfYwJV68ey8gHuQPC1dq9J6NDCTE5+xOPtBiYUtVjCfvZw==", - "dependencies": [ - "@floating-ui/dom", - "apexcharts@3.54.1", - "flowbite@3.1.2", - "svelte", - "tailwind-merge@3.3.1" - ] - }, - "flowbite-svelte@1.10.10_svelte@5.36.8__acorn@8.15.0_tailwindcss@3.4.17__postcss@8.5.6": { - "integrity": "sha512-9YCB3EqQKlu7in9pxE46eeA+zt98vhUK1nb0eR2o5wpRfsWj60u9v43lMtfhpxSTsh2Jebh+wVLNYyyrYa0UGA==", - "dependencies": [ - "@floating-ui/dom", - "@floating-ui/utils", - "apexcharts@4.7.0_@svgdotjs+svg.js@3.2.4_@svgdotjs+svg.select.js@4.0.3__@svgdotjs+svg.js@3.2.4", - "clsx", - "date-fns", - "flowbite@3.1.2", - "svelte", - "tailwind-merge@3.3.1", - "tailwind-variants", - "tailwindcss" - ] - }, - "flowbite@2.5.2": { - "integrity": "sha512-kwFD3n8/YW4EG8GlY3Od9IoKND97kitO+/ejISHSqpn3vw2i5K/+ZI8Jm2V+KC4fGdnfi0XZ+TzYqQb4Q1LshA==", - "dependencies": [ - "@popperjs/core", - "flowbite-datepicker", - "mini-svg-data-uri" - ] - }, - "flowbite@3.1.2": { - "integrity": "sha512-MkwSgbbybCYgMC+go6Da5idEKUFfMqc/AmSjm/2ZbdmvoKf5frLPq/eIhXc9P+rC8t9boZtUXzHDgt5whZ6A/Q==", - "dependencies": [ - "@popperjs/core", - "flowbite-datepicker", - "mini-svg-data-uri", - "postcss" - ] - }, - "foreground-child@3.3.1": { - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dependencies": [ - "cross-spawn", - "signal-exit" - ] - }, - "fraction.js@4.3.7": { - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==" - }, - "fs.realpath@1.0.0": { - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "fsevents@2.3.2": { - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "os": ["darwin"], - "scripts": true - }, - "fsevents@2.3.3": { - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "os": ["darwin"], - "scripts": true - }, - "function-bind@1.1.2": { - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "get-caller-file@2.0.5": { - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-intrinsic@1.3.0": { - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dependencies": [ - "call-bind-apply-helpers", - "es-define-property", - "es-errors", - "es-object-atoms", - "function-bind", - "get-proto", - "gopd", - "has-symbols", - "hasown", - "math-intrinsics" - ] - }, - "get-proto@1.0.1": { - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dependencies": [ - "dunder-proto", - "es-object-atoms" - ] - }, - "glob-parent@5.1.2": { - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": [ - "is-glob" - ] - }, - "glob-parent@6.0.2": { - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dependencies": [ - "is-glob" - ] - }, - "glob@10.4.5": { - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dependencies": [ - "foreground-child", - "jackspeak", - "minimatch@9.0.5", - "minipass", - "package-json-from-dist", - "path-scurry" - ], - "bin": true - }, - "glob@8.1.0": { - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "dependencies": [ - "fs.realpath", - "inflight", - "inherits", - "minimatch@5.1.6", - "once" - ], - "deprecated": true - }, - "globals@14.0.0": { - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==" - }, - "globals@16.3.0": { - "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==" - }, - "gopd@1.2.0": { - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" - }, - "handlebars@4.7.8": { - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dependencies": [ - "minimist", - "neo-async", - "source-map", - "wordwrap" - ], - "optionalDependencies": [ - "uglify-js" - ], - "bin": true - }, - "has-flag@4.0.0": { - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "has-symbols@1.1.0": { - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" - }, - "has-tostringtag@1.0.2": { - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dependencies": [ - "has-symbols" - ] - }, - "hasown@2.0.2": { - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": [ - "function-bind" - ] - }, - "he@1.2.0": { - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "bin": true - }, - "highlight.js@11.11.1": { - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==" - }, - "iconv-lite@0.6.3": { - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": [ - "safer-buffer" - ] - }, - "ignore@5.3.2": { - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" - }, - "import-fresh@3.3.1": { - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dependencies": [ - "parent-module", - "resolve-from" - ] - }, - "imurmurhash@0.1.4": { - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" - }, - "inflight@1.0.6": { - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": [ - "once", - "wrappy" - ], - "deprecated": true - }, - "inherits@2.0.4": { - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "internmap@2.0.3": { - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" - }, - "is-binary-path@2.1.0": { - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": [ - "binary-extensions" - ] - }, - "is-core-module@2.16.1": { - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dependencies": [ - "hasown" - ] - }, - "is-expression@4.0.0": { - "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", - "dependencies": [ - "acorn@7.4.1", - "object-assign" - ] - }, - "is-extglob@2.1.1": { - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" - }, - "is-fullwidth-code-point@3.0.0": { - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "is-glob@4.0.3": { - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dependencies": [ - "is-extglob" - ] - }, - "is-module@1.0.0": { - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" - }, - "is-number@7.0.0": { - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "is-promise@2.2.2": { - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" - }, - "is-reference@3.0.3": { - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dependencies": [ - "@types/estree" - ] - }, - "is-regex@1.2.1": { - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dependencies": [ - "call-bound", - "gopd", - "has-tostringtag", - "hasown" - ] - }, - "isexe@2.0.0": { - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "jackspeak@3.4.3": { - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dependencies": [ - "@isaacs/cliui" - ], - "optionalDependencies": [ - "@pkgjs/parseargs" - ] - }, - "jake@10.9.2": { - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "dependencies": [ - "async", - "chalk", - "filelist", - "minimatch@3.1.2" - ], - "bin": true - }, - "jiti@1.21.7": { - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "bin": true - }, - "js-stringify@1.0.2": { - "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==" - }, - "js-yaml@4.1.0": { - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": [ - "argparse" - ], - "bin": true - }, - "json-buffer@3.0.1": { - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, - "json-schema-traverse@0.4.1": { - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stable-stringify-without-jsonify@1.0.1": { - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" - }, - "jstransformer@1.0.0": { - "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", - "dependencies": [ - "is-promise", - "promise" - ] - }, - "keyv@4.5.4": { - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dependencies": [ - "json-buffer" - ] - }, - "known-css-properties@0.37.0": { - "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==" - }, - "levn@0.4.1": { - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dependencies": [ - "prelude-ls", - "type-check" - ] - }, - "light-bolt11-decoder@3.2.0": { - "integrity": "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==", - "dependencies": [ - "@scure/base@1.1.1" - ] - }, - "lilconfig@2.1.0": { - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==" - }, - "lilconfig@3.1.3": { - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==" - }, - "lines-and-columns@1.2.4": { - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "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@5.0.0" - ] - }, - "lodash.castarray@4.4.0": { - "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==" - }, - "lodash.isplainobject@4.0.6": { - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "lodash.merge@4.6.2": { - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, - "lru-cache@10.4.3": { - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" - }, - "magic-string@0.30.17": { - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "dependencies": [ - "@jridgewell/sourcemap-codec" - ] - }, - "math-intrinsics@1.1.0": { - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" - }, - "merge2@1.4.1": { - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" - }, - "micromatch@4.0.8": { - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dependencies": [ - "braces", - "picomatch@2.3.1" - ] - }, - "mini-svg-data-uri@1.4.4": { - "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", - "bin": true - }, - "minimatch@3.1.2": { - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": [ - "brace-expansion@1.1.12" - ] - }, - "minimatch@5.1.6": { - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": [ - "brace-expansion@2.0.2" - ] - }, - "minimatch@9.0.5": { - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dependencies": [ - "brace-expansion@2.0.2" - ] - }, - "minimist@1.2.8": { - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" - }, - "minipass@7.1.2": { - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" - }, - "mri@1.2.0": { - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" - }, - "ms@2.1.3": { - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "mz@2.7.0": { - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dependencies": [ - "any-promise", - "object-assign", - "thenify-all" - ] - }, - "nanoid@3.3.11": { - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "bin": true - }, - "natural-compare@1.4.0": { - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" - }, - "neo-async@2.6.2": { - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "node-emoji@2.2.0": { - "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", - "dependencies": [ - "@sindresorhus/is", - "char-regex", - "emojilib", - "skin-tone" - ] - }, - "node-releases@2.0.19": { - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" - }, - "normalize-path@3.0.0": { - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "normalize-range@0.1.2": { - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" - }, - "nostr-tools@2.15.1_typescript@5.8.3": { - "integrity": "sha512-LpetHDR9ltnkpJDkva/SONgyKBbsoV+5yLB8DWc0/U3lCWGtoWJw6Nbc2vR2Ai67RIQYrBQeZLyMlhwVZRK/9A==", - "dependencies": [ - "@noble/ciphers", - "@noble/curves@1.2.0", - "@noble/hashes@1.3.1", - "@scure/base@1.1.1", - "@scure/bip32", - "@scure/bip39", - "nostr-wasm", - "typescript" - ], - "optionalPeers": [ - "typescript" - ] - }, - "nostr-wasm@0.1.0": { - "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==" - }, - "nunjucks@3.2.4": { - "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", - "dependencies": [ - "a-sync-waterfall", - "asap", - "commander@5.1.0" - ], - "bin": true - }, - "object-assign@4.1.1": { - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" - }, - "object-hash@3.0.0": { - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" - }, - "once@1.4.0": { - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": [ - "wrappy" - ] - }, - "optionator@0.9.4": { - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dependencies": [ - "deep-is", - "fast-levenshtein", - "levn", - "prelude-ls", - "type-check", - "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@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==" - }, - "parent-module@1.0.1": { - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dependencies": [ - "callsites" - ] - }, - "path-exists@4.0.0": { - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, - "path-key@3.1.1": { - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "path-parse@1.0.7": { - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "path-scurry@1.11.1": { - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dependencies": [ - "lru-cache", - "minipass" - ] - }, - "picocolors@1.1.1": { - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" - }, - "picomatch@2.3.1": { - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" - }, - "picomatch@4.0.3": { - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==" - }, - "pify@2.3.0": { - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" - }, - "pirates@4.0.7": { - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==" - }, - "plantuml-encoder@1.4.0": { - "integrity": "sha512-sxMwpDw/ySY1WB2CE3+IdMuEcWibJ72DDOsXLkSmEaSzwEUaYBT6DWgOfBiHGCux4q433X6+OEFWjlVqp7gL6g==" - }, - "playwright-core@1.54.1": { - "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", - "bin": true - }, - "playwright@1.54.1": { - "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", - "dependencies": [ - "playwright-core" - ], - "optionalDependencies": [ - "fsevents@2.3.2" - ], - "bin": true - }, - "pngjs@5.0.0": { - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" - }, - "postcss-import@15.1.0_postcss@8.5.6": { - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dependencies": [ - "postcss", - "postcss-value-parser", - "read-cache", - "resolve" - ] - }, - "postcss-js@4.0.1_postcss@8.5.6": { - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dependencies": [ - "camelcase-css", - "postcss" - ] - }, - "postcss-load-config@3.1.4_postcss@8.5.6": { - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dependencies": [ - "lilconfig@2.1.0", - "postcss", - "yaml@1.10.2" - ], - "optionalPeers": [ - "postcss" - ] - }, - "postcss-load-config@4.0.2_postcss@8.5.6": { - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dependencies": [ - "lilconfig@3.1.3", - "postcss", - "yaml@2.8.0" - ], - "optionalPeers": [ - "postcss" - ] - }, - "postcss-load-config@6.0.1_postcss@8.5.6": { - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dependencies": [ - "lilconfig@3.1.3", - "postcss" - ], - "optionalPeers": [ - "postcss" - ] - }, - "postcss-nested@6.2.0_postcss@8.5.6": { - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dependencies": [ - "postcss", - "postcss-selector-parser@6.1.2" - ] - }, - "postcss-safe-parser@7.0.1_postcss@8.5.6": { - "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", - "dependencies": [ - "postcss" - ] - }, - "postcss-scss@4.0.9_postcss@8.5.6": { - "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", - "dependencies": [ - "postcss" - ] - }, - "postcss-selector-parser@6.0.10": { - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "dependencies": [ - "cssesc", - "util-deprecate" - ] - }, - "postcss-selector-parser@6.1.2": { - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dependencies": [ - "cssesc", - "util-deprecate" - ] - }, - "postcss-selector-parser@7.1.0": { - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "dependencies": [ - "cssesc", - "util-deprecate" - ] - }, - "postcss-value-parser@4.2.0": { - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "postcss@8.5.6": { - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dependencies": [ - "nanoid", - "picocolors", - "source-map-js" - ] - }, - "prelude-ls@1.2.1": { - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" - }, - "prettier-plugin-svelte@3.4.0_prettier@3.6.2_svelte@5.36.8__acorn@8.15.0": { - "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", - "dependencies": [ - "prettier", - "svelte" - ] - }, - "prettier@3.6.2": { - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "bin": true - }, - "promise@7.3.1": { - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dependencies": [ - "asap" - ] - }, - "pug-attrs@3.0.0": { - "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", - "dependencies": [ - "constantinople", - "js-stringify", - "pug-runtime" - ] - }, - "pug-code-gen@3.0.3": { - "integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==", - "dependencies": [ - "constantinople", - "doctypes", - "js-stringify", - "pug-attrs", - "pug-error", - "pug-runtime", - "void-elements", - "with" - ] - }, - "pug-error@2.1.0": { - "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==" - }, - "pug-filters@4.0.0": { - "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", - "dependencies": [ - "constantinople", - "jstransformer", - "pug-error", - "pug-walk", - "resolve" - ] - }, - "pug-lexer@5.0.1": { - "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", - "dependencies": [ - "character-parser", - "is-expression", - "pug-error" - ] - }, - "pug-linker@4.0.0": { - "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", - "dependencies": [ - "pug-error", - "pug-walk" - ] - }, - "pug-load@3.0.0": { - "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", - "dependencies": [ - "object-assign", - "pug-walk" - ] - }, - "pug-parser@6.0.0": { - "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", - "dependencies": [ - "pug-error", - "token-stream" - ] - }, - "pug-runtime@3.0.1": { - "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==" - }, - "pug-strip-comments@2.0.0": { - "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", - "dependencies": [ - "pug-error" - ] - }, - "pug-walk@2.0.0": { - "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==" - }, - "pug@3.0.3": { - "integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==", - "dependencies": [ - "pug-code-gen", - "pug-filters", - "pug-lexer", - "pug-linker", - "pug-load", - "pug-parser", - "pug-runtime", - "pug-strip-comments" - ] - }, - "punycode@2.3.1": { - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" - }, - "qrcode@1.5.4": { - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", - "dependencies": [ - "dijkstrajs", - "pngjs", - "yargs@15.4.1" - ], - "bin": true - }, - "queue-microtask@1.2.3": { - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" - }, - "read-cache@1.0.0": { - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dependencies": [ - "pify" - ] - }, - "readdirp@3.6.0": { - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": [ - "picomatch@2.3.1" - ] - }, - "readdirp@4.1.2": { - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==" - }, - "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==" - }, - "resolve@1.22.10": { - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dependencies": [ - "is-core-module", - "path-parse", - "supports-preserve-symlinks-flag" - ], - "bin": true - }, - "reusify@1.1.0": { - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==" - }, - "robust-predicates@3.0.2": { - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" - }, - "rollup@4.45.1": { - "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", - "dependencies": [ - "@types/estree" - ], - "optionalDependencies": [ - "@rollup/rollup-android-arm-eabi", - "@rollup/rollup-android-arm64", - "@rollup/rollup-darwin-arm64", - "@rollup/rollup-darwin-x64", - "@rollup/rollup-freebsd-arm64", - "@rollup/rollup-freebsd-x64", - "@rollup/rollup-linux-arm-gnueabihf", - "@rollup/rollup-linux-arm-musleabihf", - "@rollup/rollup-linux-arm64-gnu", - "@rollup/rollup-linux-arm64-musl", - "@rollup/rollup-linux-loongarch64-gnu", - "@rollup/rollup-linux-powerpc64le-gnu", - "@rollup/rollup-linux-riscv64-gnu", - "@rollup/rollup-linux-riscv64-musl", - "@rollup/rollup-linux-s390x-gnu", - "@rollup/rollup-linux-x64-gnu", - "@rollup/rollup-linux-x64-musl", - "@rollup/rollup-win32-arm64-msvc", - "@rollup/rollup-win32-ia32-msvc", - "@rollup/rollup-win32-x64-msvc", - "fsevents@2.3.3" - ], - "bin": true - }, - "run-parallel@1.2.0": { - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dependencies": [ - "queue-microtask" - ] - }, - "rw@1.3.3": { - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" - }, - "sade@1.8.1": { - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dependencies": [ - "mri" - ] - }, - "safer-buffer@2.1.2": { - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "semver@7.7.2": { - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "bin": true - }, - "set-blocking@2.0.0": { - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, - "shebang-command@2.0.0": { - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": [ - "shebang-regex" - ] - }, - "shebang-regex@3.0.0": { - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "signal-exit@4.1.0": { - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" - }, - "skin-tone@2.0.0": { - "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", - "dependencies": [ - "unicode-emoji-modifier-base" - ] - }, - "source-map-js@1.2.1": { - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" - }, - "source-map@0.6.1": { - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "string-width@4.2.3": { - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": [ - "emoji-regex@8.0.0", - "is-fullwidth-code-point", - "strip-ansi@6.0.1" - ] - }, - "string-width@5.1.2": { - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dependencies": [ - "eastasianwidth", - "emoji-regex@9.2.2", - "strip-ansi@7.1.0" - ] - }, - "strip-ansi@6.0.1": { - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": [ - "ansi-regex@5.0.1" - ] - }, - "strip-ansi@7.1.0": { - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": [ - "ansi-regex@6.1.0" - ] - }, - "strip-json-comments@3.1.1": { - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" - }, - "sucrase@3.35.0": { - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dependencies": [ - "@jridgewell/gen-mapping", - "commander@4.1.1", - "glob@10.4.5", - "lines-and-columns", - "mz", - "pirates", - "ts-interface-checker" - ], - "bin": true - }, - "supports-color@7.2.0": { - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": [ - "has-flag" - ] - }, - "supports-preserve-symlinks-flag@1.0.0": { - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" - }, - "svelte-check@4.3.0_svelte@5.36.8__acorn@8.15.0_typescript@5.8.3": { - "integrity": "sha512-Iz8dFXzBNAM7XlEIsUjUGQhbEE+Pvv9odb9+0+ITTgFWZBGeJRRYqHUUglwe2EkLD5LIsQaAc4IUJyvtKuOO5w==", - "dependencies": [ - "@jridgewell/trace-mapping", - "chokidar@4.0.3", - "fdir", - "picocolors", - "sade", - "svelte", - "typescript" - ], - "bin": true - }, - "svelte-eslint-parser@1.3.0_svelte@5.36.8__acorn@8.15.0_postcss@8.5.6": { - "integrity": "sha512-VCgMHKV7UtOGcGLGNFSbmdm6kEKjtzo5nnpGU/mnx4OsFY6bZ7QwRF5DUx+Hokw5Lvdyo8dpk8B1m8mliomrNg==", - "dependencies": [ - "eslint-scope", - "eslint-visitor-keys@4.2.1", - "espree", - "postcss", - "postcss-scss", - "postcss-selector-parser@7.1.0", - "svelte" - ], - "optionalPeers": [ - "svelte" - ] - }, - "svelte@5.36.8_acorn@8.15.0": { - "integrity": "sha512-8JbZWQu96hMjH/oYQPxXW6taeC6Awl6muGHeZzJTxQx7NGRQ/J9wN1hkzRKLOlSDlbS2igiFg7p5xyTp5uXG3A==", - "dependencies": [ - "@ampproject/remapping", - "@jridgewell/sourcemap-codec", - "@sveltejs/acorn-typescript", - "@types/estree", - "acorn@8.15.0", - "aria-query", - "axobject-query", - "clsx", - "esm-env", - "esrap", - "is-reference", - "locate-character", - "magic-string", - "zimmerframe" - ] - }, - "svg.draggable.js@2.2.2": { - "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", - "dependencies": [ - "svg.js" - ] - }, - "svg.easing.js@2.0.0": { - "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", - "dependencies": [ - "svg.js" - ] - }, - "svg.filter.js@2.0.2": { - "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", - "dependencies": [ - "svg.js" - ] - }, - "svg.js@2.7.1": { - "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==" - }, - "svg.pathmorphing.js@0.1.3": { - "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", - "dependencies": [ - "svg.js" - ] - }, - "svg.resize.js@1.4.3": { - "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", - "dependencies": [ - "svg.js", - "svg.select.js@2.1.2" - ] - }, - "svg.select.js@2.1.2": { - "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", - "dependencies": [ - "svg.js" - ] - }, - "svg.select.js@3.0.1": { - "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", - "dependencies": [ - "svg.js" - ] - }, - "tailwind-merge@3.0.2": { - "integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==" - }, - "tailwind-merge@3.3.1": { - "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==" - }, - "tailwind-variants@1.0.0_tailwindcss@3.4.17__postcss@8.5.6": { - "integrity": "sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA==", - "dependencies": [ - "tailwind-merge@3.0.2", - "tailwindcss" - ] - }, - "tailwindcss@3.4.17_postcss@8.5.6": { - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "dependencies": [ - "@alloc/quick-lru", - "arg", - "chokidar@3.6.0", - "didyoumean", - "dlv", - "fast-glob", - "glob-parent@6.0.2", - "is-glob", - "jiti", - "lilconfig@3.1.3", - "micromatch", - "normalize-path", - "object-hash", - "picocolors", - "postcss", - "postcss-import", - "postcss-js", - "postcss-load-config@4.0.2_postcss@8.5.6", - "postcss-nested", - "postcss-selector-parser@6.1.2", - "resolve", - "sucrase" - ], - "bin": true - }, - "thenify-all@1.6.0": { - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dependencies": [ - "thenify" - ] - }, - "thenify@3.3.1": { - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dependencies": [ - "any-promise" - ] - }, - "to-regex-range@5.0.1": { - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": [ - "is-number" - ] - }, - "token-stream@1.0.0": { - "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==" - }, - "ts-interface-checker@0.1.13": { - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" - }, - "tseep@1.3.1": { - "integrity": "sha512-ZPtfk1tQnZVyr7BPtbJ93qaAh2lZuIOpTMjhrYa4XctT8xe7t4SAW9LIxrySDuYMsfNNayE51E/WNGrNVgVicQ==" - }, - "tslib@2.8.1": { - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "type-check@0.4.0": { - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dependencies": [ - "prelude-ls" - ] - }, - "typescript-lru-cache@2.0.0": { - "integrity": "sha512-Jp57Qyy8wXeMkdNuZiglE6v2Cypg13eDA1chHwDG6kq51X7gk4K7P7HaDdzZKCxkegXkVHNcPD0n5aW6OZH3aA==" - }, - "typescript@5.8.3": { - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "bin": true - }, - "uglify-js@3.19.3": { - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "bin": true + "tslib@2.8.1": { + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "undici-types@6.21.0": { "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" @@ -2801,139 +408,6 @@ }, "unicode-emoji-modifier-base@1.0.0": { "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==" - }, - "unxhr@1.2.0": { - "integrity": "sha512-6cGpm8NFXPD9QbSNx0cD2giy7teZ6xOkCUH3U89WKVkL9N9rBrWjlCwhR94Re18ZlAop4MOc3WU1M3Hv/bgpIw==" - }, - "update-browserslist-db@1.1.3_browserslist@4.25.1": { - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dependencies": [ - "browserslist", - "escalade", - "picocolors" - ], - "bin": true - }, - "uri-js@4.4.1": { - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": [ - "punycode" - ] - }, - "util-deprecate@1.0.2": { - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "void-elements@3.1.0": { - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" - }, - "which-module@2.0.1": { - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" - }, - "which@2.0.2": { - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": [ - "isexe" - ], - "bin": true - }, - "with@7.0.2": { - "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", - "dependencies": [ - "@babel/parser", - "@babel/types", - "assert-never", - "babel-walk" - ] - }, - "word-wrap@1.2.5": { - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" - }, - "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": [ - "ansi-styles@4.3.0", - "string-width@4.2.3", - "strip-ansi@6.0.1" - ] - }, - "wrap-ansi@8.1.0": { - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dependencies": [ - "ansi-styles@6.2.1", - "string-width@5.1.2", - "strip-ansi@7.1.0" - ] - }, - "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==" - }, - "yaml@1.10.2": { - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" - }, - "yaml@2.8.0": { - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", - "bin": true - }, - "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@7.0.4", - "escalade", - "get-caller-file", - "require-directory", - "string-width@4.2.3", - "y18n@5.0.8", - "yargs-parser@21.1.1" - ] - }, - "yocto-queue@0.1.0": { - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" - }, - "zimmerframe@1.1.2": { - "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==" } }, "redirects": { @@ -2945,18 +419,25 @@ }, "workspace": { "dependencies": [ - "npm:@nostr-dev-kit/ndk-cache-dexie@^2.6.33", + "npm:@noble/curves@^1.9.4", + "npm:@noble/hashes@^1.8.0", + "npm:@nostr-dev-kit/ndk-cache-dexie@2.6", "npm:@nostr-dev-kit/ndk@^2.14.32", "npm:@popperjs/core@2.11", "npm:@tailwindcss/forms@0.5", "npm:@tailwindcss/typography@0.5", "npm:asciidoctor@3.0", - "npm:d3@7.9", - "npm:flowbite-svelte-icons@^2.2.1", - "npm:flowbite-svelte@^1.10.10", - "npm:flowbite@^3.1.2", + "npm:bech32@2", + "npm:d3@^7.9.0", + "npm:flowbite-svelte-icons@2.1", + "npm:flowbite-svelte@0.48", + "npm:flowbite@2", "npm:he@1.2", - "npm:nostr-tools@^2.15.1", + "npm:highlight.js@^11.11.1", + "npm:node-emoji@^2.2.0", + "npm:nostr-tools@2.15", + "npm:plantuml-encoder@^1.4.0", + "npm:qrcode@^1.5.4", "npm:svelte@^5.36.8", "npm:tailwind-merge@^3.3.1" ], diff --git a/playwright.config.ts b/playwright.config.ts index 5779001..bd4b2c4 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -27,7 +27,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://localhost:5173', + baseURL: "http://localhost:5173", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", @@ -49,7 +49,6 @@ export default defineConfig({ name: "webkit", use: { ...devices["Desktop Safari"] }, }, - /* Test against mobile viewports. */ // { // name: 'Mobile Chrome', @@ -73,8 +72,8 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: 'npm run dev', - url: 'http://localhost:5173', + command: "npm run dev", + url: "http://localhost:5173", reuseExistingServer: !process.env.CI, }, diff --git a/src/app.css b/src/app.css index 2ca3c92..31b91cf 100644 --- a/src/app.css +++ b/src/app.css @@ -28,7 +28,9 @@ } div[role="tooltip"] button.btn-leather { - @apply hover:text-primary-600 dark:hover:text-primary-400 hover:border-primary-600 dark:hover:border-primary-400 hover:bg-gray-200 dark:hover:bg-gray-700; + @apply hover:text-primary-600 dark:hover:text-primary-400 + hover:border-primary-600 dark:hover:border-primary-400 hover:bg-gray-200 + dark:hover:bg-gray-700; } .image-border { @@ -36,8 +38,10 @@ } div.card-leather { - @apply shadow-none text-primary-1000 border-s-4 bg-highlight border-primary-200 has-[:hover]:border-primary-700; - @apply dark:bg-primary-1000 dark:border-primary-800 dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500; + @apply shadow-none text-primary-1000 border-s-4 bg-highlight + border-primary-200 has-[:hover]:border-primary-700; + @apply dark:bg-primary-1000 dark:border-primary-800 + dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500; } div.card-leather h1, @@ -46,11 +50,13 @@ div.card-leather h4, div.card-leather h5, div.card-leather h6 { - @apply text-gray-900 hover:text-primary-600 dark:text-gray-100 dark:hover:text-primary-400; + @apply text-gray-900 hover:text-primary-600 dark:text-gray-100 + dark:hover:text-primary-400; } div.card-leather .font-thin { - @apply text-gray-900 hover:text-primary-700 dark:text-gray-100 dark:hover:text-primary-300; + @apply text-gray-900 hover:text-primary-700 dark:text-gray-100 + dark:hover:text-primary-300; } main { @@ -74,7 +80,8 @@ div.note-leather, p.note-leather, section.note-leather { - @apply bg-primary-0 dark:bg-primary-1000 text-gray-900 dark:text-gray-100 p-2 rounded; + @apply bg-primary-0 dark:bg-primary-1000 text-gray-900 dark:text-gray-100 + p-2 rounded; } .edit div.note-leather:hover:not(:has(.note-leather:hover)), @@ -117,7 +124,8 @@ } div.modal-leather > div { - @apply bg-primary-0 dark:bg-primary-950 border-b-[1px] border-primary-100 dark:border-primary-600; + @apply bg-primary-0 dark:bg-primary-950 border-b-[1px] border-primary-100 + dark:border-primary-600; } div.modal-leather > div > h1, @@ -126,11 +134,14 @@ div.modal-leather > div > h4, div.modal-leather > div > h5, div.modal-leather > div > h6 { - @apply text-gray-900 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-100; + @apply text-gray-900 hover:text-gray-900 dark:text-gray-100 + dark:hover:text-gray-100; } div.modal-leather button { - @apply bg-primary-0 hover:bg-primary-0 dark:bg-primary-950 dark:hover:bg-primary-950 text-gray-900 hover:text-primary-600 dark:text-gray-100 dark:hover:text-primary-400; + @apply bg-primary-0 hover:bg-primary-0 dark:bg-primary-950 + dark:hover:bg-primary-950 text-gray-900 hover:text-primary-600 + dark:text-gray-100 dark:hover:text-primary-400; } /* Navbar */ @@ -143,7 +154,8 @@ } nav.navbar-leather svg { - @apply fill-gray-900 hover:fill-primary-600 dark:fill-gray-100 dark:hover:fill-primary-400; + @apply fill-gray-900 hover:fill-primary-600 dark:fill-gray-100 + dark:hover:fill-primary-400; } nav.navbar-leather h1, @@ -152,7 +164,8 @@ nav.navbar-leather h4, nav.navbar-leather h5, nav.navbar-leather h6 { - @apply text-gray-900 hover:text-primary-600 dark:text-gray-100 dark:hover:text-primary-400; + @apply text-gray-900 hover:text-primary-600 dark:text-gray-100 + dark:hover:text-primary-400; } div.skeleton-leather div { @@ -201,16 +214,16 @@ .network-node-content { @apply fill-primary-100; } - + /* Person link colors */ .person-link-signed { @apply stroke-green-500; } - + .person-link-referenced { @apply stroke-blue-400; } - + /* Person anchor node */ .person-anchor-node { @apply fill-green-400 stroke-green-600; @@ -272,11 +285,13 @@ /* Lists */ .ol-leather li a, .ul-leather li a { - @apply text-gray-900 hover:text-primary-600 dark:text-gray-100 dark:hover:text-primary-400; + @apply text-gray-900 hover:text-primary-600 dark:text-gray-100 + dark:hover:text-primary-400; } .link { - @apply underline cursor-pointer hover:text-primary-600 dark:hover:text-primary-400; + @apply underline cursor-pointer hover:text-primary-600 + dark:hover:text-primary-400; } /* Card with transition */ @@ -290,11 +305,14 @@ } .tags span { - @apply bg-primary-50 text-primary-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded-sm dark:bg-primary-900 dark:text-primary-200; + @apply bg-primary-50 text-primary-800 text-sm font-medium me-2 px-2.5 py-0.5 + rounded-sm dark:bg-primary-900 dark:text-primary-200; } .npub-badge { - @apply inline-flex space-x-1 items-center text-primary-600 dark:text-primary-500 hover:underline me-2 px-2 py-0.5 rounded-sm border border-primary-600 dark:border-primary-500; + @apply inline-flex space-x-1 items-center text-primary-600 + dark:text-primary-500 hover:underline me-2 px-2 py-0.5 rounded-sm border + border-primary-600 dark:border-primary-500; svg { @apply fill-primary-600 dark:fill-primary-500; @@ -305,14 +323,19 @@ @layer components { /* Legend */ .leather-legend { - @apply relative m-4 sm:m-0 sm:absolute sm:top-1 sm:left-1 flex-shrink-0 p-2 rounded; - @apply shadow-none text-primary-1000 border border-s-4 bg-highlight border-primary-200 has-[:hover]:border-primary-700; - @apply dark:bg-primary-1000 dark:border-primary-800 dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500; + @apply relative m-4 sm:m-0 sm:absolute sm:top-1 sm:left-1 flex-shrink-0 p-2 + rounded; + @apply shadow-none text-primary-1000 border border-s-4 bg-highlight + border-primary-200 has-[:hover]:border-primary-700; + @apply dark:bg-primary-1000 dark:border-primary-800 + dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500; } /* Tooltip */ .tooltip-leather { - @apply fixed p-4 rounded shadow-lg bg-primary-0 dark:bg-primary-1000 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700 transition-colors duration-200; + @apply fixed p-4 rounded shadow-lg bg-primary-0 dark:bg-primary-1000 + text-gray-900 dark:text-gray-100 border border-gray-200 + dark:border-gray-700 transition-colors duration-200; max-width: 400px; z-index: 1000; } @@ -536,13 +559,15 @@ input[type="tel"], input[type="url"], textarea { - @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 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-200 dark:bg-primary-700 border-l-4 border-primary-600 dark:border-primary-400 font-medium; + @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; } @@ -551,14 +576,8 @@ } /* Override prose first-line bold styling */ - .prose p:first-line { - font-weight: normal !important; - } - - .prose-sm p:first-line { - font-weight: normal !important; - } - + .prose p:first-line, + .prose-sm p:first-line, .prose-invert p:first-line { font-weight: normal !important; } diff --git a/src/app.d.ts b/src/app.d.ts index 25c13d3..1e997cc 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -23,7 +23,9 @@ declare global { var MathJax: any; var nostr: NDKNip07Signer & { - getRelays: () => Promise>>; + getRelays: () => Promise< + Record> + >; // deno-lint-ignore no-explicit-any signEvent: (event: any) => Promise; }; diff --git a/src/app.html b/src/app.html index 97127be..345607e 100644 --- a/src/app.html +++ b/src/app.html @@ -1,4 +1,4 @@ - + @@ -26,14 +26,18 @@ }, }; - + - + %sveltekit.head% diff --git a/src/lib/components/CommentViewer.svelte b/src/lib/components/CommentViewer.svelte index 6ed9b4c..ec5a069 100644 --- a/src/lib/components/CommentViewer.svelte +++ b/src/lib/components/CommentViewer.svelte @@ -6,9 +6,7 @@ import { goto } from "$app/navigation"; import { onMount } from "svelte"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; - import { userBadge } from "$lib/snippets/UserSnippets.svelte"; - import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; - import { parseRepostContent, parseContent as parseNotificationContent } from "$lib/utils/notification_utils"; + import EmbeddedEvent from "./EmbeddedEvent.svelte"; const { event } = $props<{ event: NDKEvent }>(); @@ -654,19 +652,6 @@ return `${actualLevel * 16}px`; } - async function parseContent(content: string, eventKind?: number): Promise { - if (!content) return ""; - - // Use parseRepostContent for kind 6 and 16 events (reposts) - if (eventKind === 6 || eventKind === 16) { - return await parseRepostContent(content); - } else { - return await parseNotificationContent(content); - } - } - - - // AI-NOTE: 2025-01-24 - Get highlight source information function getHighlightSource(highlightEvent: NDKEvent): { type: string; value: string; url?: string } | null { // Check for e-tags (nostr events) @@ -785,11 +770,7 @@
    Comment:
    - {#await parseContent(node.event.getMatchingTags("comment")[0]?.[1] || "") then parsedContent} - {@html parsedContent} - {:catch} - {@html node.event.getMatchingTags("comment")[0]?.[1] || ""} - {/await} +
    {:else} @@ -829,11 +810,7 @@
    {:else} - {#await parseContent(node.event.content || "", node.event.kind) then parsedContent} - {@html parsedContent} - {:catch} - {@html node.event.content || ""} - {/await} + {/if}
    diff --git a/src/lib/components/EmbeddedEvent.svelte b/src/lib/components/EmbeddedEvent.svelte index f94d68b..54d4633 100644 --- a/src/lib/components/EmbeddedEvent.svelte +++ b/src/lib/components/EmbeddedEvent.svelte @@ -4,16 +4,14 @@ import { fetchEventWithFallback } from "$lib/utils/nostrUtils"; import { getUserMetadata, toNpub } from "$lib/utils/nostrUtils"; import { userBadge } from "$lib/snippets/UserSnippets.svelte"; - import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; - import { parseEmbeddedMarkup } from "$lib/utils/markup/embeddedMarkupParser"; - import { parseRepostContent } from "$lib/utils/notification_utils"; - import EmbeddedEventRenderer from "./EmbeddedEventRenderer.svelte"; - import { neventEncode, naddrEncode } from "$lib/utils"; + import { parsedContent } from "$lib/components/util/Notifications.svelte"; + import { naddrEncode } from "$lib/utils"; import { activeInboxRelays, ndkInstance } from "$lib/ndk"; import { goto } from "$app/navigation"; import { getEventType } from "$lib/utils/mime"; import { nip19 } from "nostr-tools"; import { get } from "svelte/store"; + import { repostKinds } from "$lib/consts"; const { nostrIdentifier, @@ -36,7 +34,6 @@ } | null>(null); let loading = $state(true); let error = $state(null); - let parsedContent = $state(""); let authorDisplayName = $state(undefined); // Maximum nesting level allowed @@ -120,16 +117,6 @@ } } - // Parse content if available - if (event?.content) { - if (event.kind === 6 || event.kind === 16) { - parsedContent = await parseRepostContent(event.content); - } else { - // Use embedded markup parser for nested events - parsedContent = await parseEmbeddedMarkup(event.content, nestingLevel + 1); - } - } - // Parse profile if it's a profile event if (event?.kind === 0) { try { @@ -196,10 +183,6 @@ } } - function getNeventUrl(event: NDKEvent): string { - return neventEncode(event, $activeInboxRelays); - } - function getNaddrUrl(event: NDKEvent): string { return naddrEncode(event, $activeInboxRelays); } @@ -303,17 +286,15 @@ {/if} - {#if event.kind === 1 && parsedContent} + {#if event.kind === 1 || repostKinds.includes(event.kind)}
    - - {#if parsedContent.length > 300} + {@render parsedContent(event.content.slice(0, 300))} + {#if event.content.length > 300} ... {/if}
    - {/if} - - {#if event.kind === 0 && profile} + {:else if event.kind === 0 && profile}
    {#if profile.picture} - import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; - import { parseEmbeddedMarkup } from "$lib/utils/markup/embeddedMarkupParser"; - import EmbeddedEventRenderer from "./EmbeddedEventRenderer.svelte"; import { getMimeTags } from "$lib/utils/mime"; import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import { toNpub } from "$lib/utils/nostrUtils"; import { neventEncode, naddrEncode, nprofileEncode } from "$lib/utils"; - import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; - import { searchRelays } from "$lib/consts"; + import { activeInboxRelays } from "$lib/ndk"; import type { NDKEvent } from "$lib/utils/nostrUtils"; import { getMatchingTags } from "$lib/utils/nostrUtils"; import ProfileHeader from "$components/cards/ProfileHeader.svelte"; @@ -18,13 +14,11 @@ import { navigateToEvent } from "$lib/utils/nostrEventService"; import ContainingIndexes from "$lib/components/util/ContainingIndexes.svelte"; import Notifications from "$lib/components/Notifications.svelte"; - import { parseRepostContent } from "$lib/utils/notification_utils"; - import RelayActions from "$lib/components/RelayActions.svelte"; + import EmbeddedEvent from "./EmbeddedEvent.svelte"; const { event, profile = null, - searchValue = null, } = $props<{ event: NDKEvent; profile?: { @@ -37,20 +31,11 @@ lud16?: string; nip05?: string; } | null; - searchValue?: string | null; }>(); - let showFullContent = $state(false); - let parsedContent = $state(""); - let contentProcessing = $state(false); let authorDisplayName = $state(undefined); - - // Determine if content should be truncated - let shouldTruncate = $state(false); - - $effect(() => { - shouldTruncate = event.content.length > 250 && !showFullContent; - }); + let showFullContent = $state(false); + let shouldTruncate = $derived(event.content.length > 250 && !showFullContent); function getEventTitle(event: NDKEvent): string { // First try to get title from title tag @@ -92,109 +77,11 @@ return getMatchingTags(event, "summary")[0]?.[1] || ""; } - function getEventHashtags(event: NDKEvent): string[] { - return getMatchingTags(event, "t").map((tag: string[]) => tag[1]); - } - function getEventTypeDisplay(event: NDKEvent): string { const [mTag, MTag] = getMimeTags(event.kind || 0); return MTag[1].split("/")[1] || `Event Kind ${event.kind}`; } - function renderTag(tag: string[]): string { - if (tag[0] === "a" && tag.length > 1) { - const parts = tag[1].split(":"); - if (parts.length >= 3) { - const [kind, pubkey, d] = parts; - // Validate that pubkey is a valid hex string - if (pubkey && /^[0-9a-fA-F]{64}$/.test(pubkey)) { - try { - const mockEvent = { - kind: +kind, - pubkey, - tags: [["d", d]], - content: "", - id: "", - sig: "", - } as any; - const naddr = naddrEncode(mockEvent, $activeInboxRelays); - return `a:${tag[1]}`; - } catch (error) { - console.warn( - "Failed to encode naddr for a tag in renderTag:", - tag[1], - error, - ); - return `a:${tag[1]}`; - } - } else { - console.warn("Invalid pubkey in a tag in renderTag:", pubkey); - return `a:${tag[1]}`; - } - } else { - console.warn("Invalid a tag format in renderTag:", tag[1]); - return `a:${tag[1]}`; - } - } else if (tag[0] === "e" && tag.length > 1) { - // Validate that event ID is a valid hex string - if (/^[0-9a-fA-F]{64}$/.test(tag[1])) { - try { - const mockEvent = { - id: tag[1], - kind: 1, - content: "", - tags: [], - pubkey: "", - sig: "", - } as any; - const nevent = neventEncode(mockEvent, $activeInboxRelays); - return `e:${tag[1]}`; - } catch (error) { - console.warn( - "Failed to encode nevent for e tag in renderTag:", - tag[1], - error, - ); - return `e:${tag[1]}`; - } - } else { - console.warn("Invalid event ID in e tag in renderTag:", tag[1]); - return `e:${tag[1]}`; - } - } else if (tag[0] === "note" && tag.length > 1) { - // 'note' tags are the same as 'e' tags but with different prefix - if (/^[0-9a-fA-F]{64}$/.test(tag[1])) { - try { - const mockEvent = { - id: tag[1], - kind: 1, - content: "", - tags: [], - pubkey: "", - sig: "", - } as any; - const nevent = neventEncode(mockEvent, $activeInboxRelays); - return `note:${tag[1]}`; - } catch (error) { - console.warn( - "Failed to encode nevent for note tag in renderTag:", - tag[1], - error, - ); - return `note:${tag[1]}`; - } - } else { - console.warn("Invalid event ID in note tag in renderTag:", tag[1]); - return `note:${tag[1]}`; - } - } else if (tag[0] === "d" && tag.length > 1) { - // 'd' tags are used for identifiers in addressable events - return `d:${tag[1]}`; - } else { - return `${tag[0]}:${tag[1]}`; - } - } - function getTagButtonInfo(tag: string[]): { text: string; gotoValue?: string; @@ -303,52 +190,12 @@ return { text: `${tag[0]}:${tag[1]}` }; } - function getNeventUrl(event: NDKEvent): string { - return neventEncode(event, $activeInboxRelays); - } - - function getNaddrUrl(event: NDKEvent): string { - return naddrEncode(event, $activeInboxRelays); - } - - function getNprofileUrl(pubkey: string): string { - return nprofileEncode(pubkey, $activeInboxRelays); - } - - $effect(() => { - if (event && event.kind !== 0 && event.content) { - contentProcessing = true; - - // Use parseRepostContent for kind 6 and 16 events (reposts) - if (event.kind === 6 || event.kind === 16) { - parseRepostContent(event.content).then((html) => { - parsedContent = html; - contentProcessing = false; - }).catch((error) => { - console.error('Error parsing repost content:', error); - contentProcessing = false; - }); - } else { - // Use embedded markup parser for better Nostr event support - parseEmbeddedMarkup(event.content, 0).then((html) => { - parsedContent = html; - contentProcessing = false; - }).catch((error) => { - console.error('Error parsing embedded markup:', error); - contentProcessing = false; - }); - } - } else { - contentProcessing = false; - parsedContent = ""; - } - }); - $effect(() => { if (!event?.pubkey) { authorDisplayName = undefined; return; } + getUserMetadata(toNpub(event.pubkey) as string).then((profile) => { authorDisplayName = profile.displayName || @@ -403,13 +250,6 @@ return ids; } - function isCurrentSearch(value: string): boolean { - if (!searchValue) return false; - // Compare ignoring case and possible nostr: prefix - const norm = (s: string) => s.replace(/^nostr:/, "").toLowerCase(); - return norm(value) === norm(searchValue); - } - onMount(() => { function handleInternalLinkClick(event: MouseEvent) { const target = event.target as HTMLElement; @@ -468,8 +308,6 @@
    {/if} - - @@ -479,19 +317,15 @@
    Content:
    - {#if contentProcessing} -
    Processing content...
    - {:else} -
    - -
    - {#if shouldTruncate} - - {/if} - {/if} +
    + +
    + {#if shouldTruncate} + + {/if}
    diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 805ea0e..f66baba 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -1,37 +1,28 @@ + +{#snippet parsedContent(content: string)} + {#await parseEmbeddedMarkup(content, 0) then parsed} + {@html parsed} + {/await} +{/snippet} + +{#snippet repostContent(content: string)} + {@const originalEvent = (() => { + try { + return JSON.parse(content); + } catch { + return null; + } + })()} + + {#if originalEvent} + {@const originalContent = originalEvent.content || ""} + {@const originalAuthor = originalEvent.pubkey || ""} + {@const originalCreatedAt = originalEvent.created_at || 0} + {@const originalKind = originalEvent.kind || 1} + {@const formattedDate = originalCreatedAt ? new Date(originalCreatedAt * 1000).toLocaleDateString() : "Unknown date"} + {@const shortAuthor = originalAuthor ? `${originalAuthor.slice(0, 8)}...${originalAuthor.slice(-4)}` : "Unknown"} + +
    + +
    +
    + + Kind {originalKind} + + + (repost) + + + Author: + + {shortAuthor} + + + + {formattedDate} + +
    + +
    + + +
    + {#await parseEmbeddedMarkup(originalContent, 0) then parsedOriginalContent} + {@html parsedOriginalContent} + {/await} +
    +
    + {:else} + {#await parseEmbeddedMarkup(content, 0) then parsedContent} + {@html parsedContent} + {/await} + {/if} +{/snippet} + +{#snippet quotedContent(message: NDKEvent, publicMessages: NDKEvent[])} + {@const qTags = message.getMatchingTags("q")} + {#if qTags.length > 0} + {@const qTag = qTags[0]} + {@const eventId = qTag[1]} + + {#if eventId} + {#await findQuotedMessage(eventId, publicMessages) then quotedMessage} + {#if quotedMessage} + {@const quotedContent = quotedMessage.content ? quotedMessage.content.slice(0, 200) : "No content"} + {#await parseEmbeddedMarkup(quotedContent, 0) then parsedContent} + + {/await} + {:else} + {@const isValidEventId = /^[a-fA-F0-9]{64}$/.test(eventId)} + {#if isValidEventId} + {@const nevent = (() => { + try { + return nip19.neventEncode({ id: eventId }); + } catch (error) { + console.warn(`[quotedContent] Failed to encode nevent for ${eventId}:`, error); + return null; + } + })()} + {#if nevent} + + {:else} +
    + Quoted message not found. Event ID: {eventId.slice(0, 8)}... +
    + {/if} + {:else} +
    + Invalid quoted message reference +
    + {/if} + {/if} + {/await} + {/if} + {/if} +{/snippet} diff --git a/src/lib/consts.ts b/src/lib/consts.ts index 4f78a56..e0224b1 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -3,6 +3,7 @@ export const wikiKind = 30818; export const indexKind = 30040; export const zettelKinds = [30041, 30818, 30023]; +export const repostKinds = [6, 16]; export const communityRelays = [ "wss://theforest.nostr1.com", @@ -16,7 +17,7 @@ export const searchRelays = [ "wss://nostr.wine", "wss://relay.damus.io", "wss://relay.nostr.band", - "wss://freelay.sovbit.host" + "wss://freelay.sovbit.host", ]; export const secondaryRelays = [ @@ -32,7 +33,7 @@ export const secondaryRelays = [ export const anonymousRelays = [ "wss://freelay.sovbit.host", - "wss://thecitadel.nostr1.com" + "wss://thecitadel.nostr1.com", ]; export const lowbandwidthRelays = [ @@ -44,7 +45,7 @@ export const lowbandwidthRelays = [ export const localRelays: string[] = [ "ws://localhost:8080", "ws://localhost:4869", - "ws://localhost:3334" + "ws://localhost:3334", ]; export enum FeedType { diff --git a/src/lib/data_structures/docs/relay_selector_design.md b/src/lib/data_structures/docs/relay_selector_design.md index 0fb1616..c4acf16 100644 --- a/src/lib/data_structures/docs/relay_selector_design.md +++ b/src/lib/data_structures/docs/relay_selector_design.md @@ -1,6 +1,11 @@ # Relay Selector Class Design -The relay selector will be a singleton that tracks, rates, and ranks Nostr relays to help the application determine which relay should be used to handle each request. It will weight relays based on observed characteristics, then use these weights to implement a weighted round robin algorithm for selecting relays, with some additional modifications to account for domain-specific features of Nostr. +The relay selector will be a singleton that tracks, rates, and ranks Nostr +relays to help the application determine which relay should be used to handle +each request. It will weight relays based on observed characteristics, then use +these weights to implement a weighted round robin algorithm for selecting +relays, with some additional modifications to account for domain-specific +features of Nostr. ## Relay Weights @@ -9,63 +14,92 @@ The relay selector will be a singleton that tracks, rates, and ranks Nostr relay Relays are broadly divided into three categories: 1. **Public**: no authorization is required -2. **Private Write**: authorization is required to write to this relay, but not to read -3. **Private Read and Write**: authorization is required to use any features of this relay +2. **Private Write**: authorization is required to write to this relay, but not + to read +3. **Private Read and Write**: authorization is required to use any features of + this relay The broadest level of relay selection is based on these categories. - For users that are not logged in, public relays are used exclusively. -- For logged-in users, public and private read relays are initially rated equally for read operations. -- For logged-in users, private write relays are preferred above public relays for write operations. +- For logged-in users, public and private read relays are initially rated + equally for read operations. +- For logged-in users, private write relays are preferred above public relays + for write operations. ### User Preferences -The relay selector will respect user relay preferences while still attempting to optimize for responsiveness and success rate. - -- User inbox relays will be stored in a separate list from general-purpose relays, and weighted and sorted separately using the same algorithm as the general-purpose relay list. -- Local relays (beginning with `wss://localhost` or `ws://localhost`) will be stored _unranked_ in a separate list, and used when the relay selector is operating on a web browser (as opposed to a server). -- When a caller requests relays from the relay selector, the selector will return: +The relay selector will respect user relay preferences while still attempting to +optimize for responsiveness and success rate. + +- User inbox relays will be stored in a separate list from general-purpose + relays, and weighted and sorted separately using the same algorithm as the + general-purpose relay list. +- Local relays (beginning with `wss://localhost` or `ws://localhost`) will be + stored _unranked_ in a separate list, and used when the relay selector is + operating on a web browser (as opposed to a server). +- When a caller requests relays from the relay selector, the selector will + return: - The highest-ranked general-purpose relay - The highest-ranked user inbox relay - (If on browser) any local relays ### Weighted Metrics -Several weighted metrics are used to compute a relay's score. The score is used to rank relays to determine which to prefer when fetching events. +Several weighted metrics are used to compute a relay's score. The score is used +to rank relays to determine which to prefer when fetching events. #### Response Time -The response time weight of each relay is computed according to the logarithmic function $`r(t) = -log(t) + 1`$, where $`t`$ is the median response time in seconds. This function has a few features which make it useful: +The response time weight of each relay is computed according to the logarithmic +function $`r(t) = -log(t) + 1`$, where $`t`$ is the median response time in +seconds. This function has a few features which make it useful: -- $`r(1) = 1`$, making a response time of 1s the netural point. This causes the algorithm to prefer relays that respond in under 1s. -- $`r(0.3) \approx 1.5`$ and $`r(3) \approx 0.5`$. This clusters the 0.5 to 1.5 weight range in the 300ms to 3s response time range, which is a sufficiently rapid response time to keep user's from switching context. -- The function has a long tail, so it doesn't discount slower response times too heavily, too quickly. +- $`r(1) = 1`$, making a response time of 1s the netural point. This causes the + algorithm to prefer relays that respond in under 1s. +- $`r(0.3) \approx 1.5`$ and $`r(3) \approx 0.5`$. This clusters the 0.5 to 1.5 + weight range in the 300ms to 3s response time range, which is a sufficiently + rapid response time to keep user's from switching context. +- The function has a long tail, so it doesn't discount slower response times too + heavily, too quickly. #### Success Rate -The success rate $`s(x)`$ is computed as the fraction of total requests sent to the relay that returned at least one event in response. The optimal score is 1, meaning the relay successfully responds to 100% of requests. +The success rate $`s(x)`$ is computed as the fraction of total requests sent to +the relay that returned at least one event in response. The optimal score is 1, +meaning the relay successfully responds to 100% of requests. #### Trust Level -Certain relays may be assigned a constant "trust level" score $`T`$. This modifier is a number in the range $`[-0.5, 0.5]`$ that indicates how much a relay is trusted by the GitCitadel organization. +Certain relays may be assigned a constant "trust level" score $`T`$. This +modifier is a number in the range $`[-0.5, 0.5]`$ that indicates how much a +relay is trusted by the GitCitadel organization. A few factors contribute to a higher trust rating: - Effective filtering of spam and abusive content. - Good data transparency, including such policies as honoring deletion requests. -- Event aggregation policies that aim at synchronization with the broader relay network. +- Event aggregation policies that aim at synchronization with the broader relay + network. #### Preferred Vendors -Certain relays may be assigned a constant "preferred vendor" score $`V`$. This modifier is a number in the range $`[0, 0.5]`$. It is used to increase the priority of GitCitadel's preferred relay vendors. +Certain relays may be assigned a constant "preferred vendor" score $`V`$. This +modifier is a number in the range $`[0, 0.5]`$. It is used to increase the +priority of GitCitadel's preferred relay vendors. ### Overall Weight -The overall weight of a relay is calculated as $`w(t, x) = r(t) \times s(x) + T + V`$. The `RelaySelector` class maintains a list of relays sorted by their overall weights. The weights may be updated at runtime when $`t`$ or $`x`$ change. On update, the relay list is re-sorted to account for the new weights. +The overall weight of a relay is calculated as +$`w(t, x) = r(t) \times s(x) + T + V`$. The `RelaySelector` class maintains a +list of relays sorted by their overall weights. The weights may be updated at +runtime when $`t`$ or $`x`$ change. On update, the relay list is re-sorted to +account for the new weights. ## Algorithm -The relay weights contribute to a weighted round robin (WRR) algorithm for relay selection. Pseudocode for the algorithm is given below: +The relay weights contribute to a weighted round robin (WRR) algorithm for relay +selection. Pseudocode for the algorithm is given below: ```pseudocode Constants and Variables: @@ -86,11 +120,13 @@ Function getRelay: ## Class Methods -The `RelaySelector` class should expose the following methods to support updates to relay weights. Pseudocode for each method is given below. +The `RelaySelector` class should expose the following methods to support updates +to relay weights. Pseudocode for each method is given below. ### Add Response Time Datum -This function updates the class state by side effect. Locking should be used in concurrent use cases. +This function updates the class state by side effect. Locking should be used in +concurrent use cases. ```pseudocode Constants and Variables: @@ -123,7 +159,8 @@ Function addResponseTimeDatum: ### Add Success Rate Datum -This function updates the class state by side effect. Locking should be used in concurrent use cases. +This function updates the class state by side effect. Locking should be used in +concurrent use cases. ```pseudocode Constants and Variables: diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index c507b9f..a64489b 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -2,7 +2,10 @@ import { Lazy } from "./lazy.ts"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type NDK from "@nostr-dev-kit/ndk"; import { fetchEventById } from "../utils/websocket_utils.ts"; -import { fetchEventWithFallback, NDKRelaySetFromNDK } from "../utils/nostrUtils.ts"; +import { + fetchEventWithFallback, + NDKRelaySetFromNDK, +} from "../utils/nostrUtils.ts"; import { get } from "svelte/store"; import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts"; import { searchRelays, secondaryRelays } from "../consts.ts"; @@ -50,7 +53,7 @@ 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. */ @@ -486,7 +489,10 @@ export class PublicationTree implements AsyncIterable { continue; } - if (this.#cursor.target && this.#cursor.target.status === PublicationTreeNodeStatus.Error) { + if ( + this.#cursor.target && + this.#cursor.target.status === PublicationTreeNodeStatus.Error + ) { return { done: false, value: null }; } @@ -494,7 +500,10 @@ export class PublicationTree implements AsyncIterable { } } while (this.#cursor.tryMoveToParent()); - if (this.#cursor.target && this.#cursor.target.status === PublicationTreeNodeStatus.Error) { + if ( + this.#cursor.target && + this.#cursor.target.status === PublicationTreeNodeStatus.Error + ) { return { done: false, value: null }; } @@ -533,7 +542,10 @@ export class PublicationTree implements AsyncIterable { } } while (this.#cursor.tryMoveToParent()); - if (this.#cursor.target && this.#cursor.target.status === PublicationTreeNodeStatus.Error) { + if ( + this.#cursor.target && + this.#cursor.target.status === PublicationTreeNodeStatus.Error + ) { return { done: false, value: null }; } @@ -588,47 +600,84 @@ export class PublicationTree implements AsyncIterable { .filter((tag) => tag[0] === "a") .map((tag) => tag[1]); - console.debug(`[PublicationTree] Current event ${currentEvent.id} has ${currentEvent.tags.length} tags:`, currentEvent.tags); - console.debug(`[PublicationTree] Found ${currentChildAddresses.length} a-tags in current event:`, currentChildAddresses); + console.debug( + `[PublicationTree] Current event ${currentEvent.id} has ${currentEvent.tags.length} tags:`, + currentEvent.tags, + ); + console.debug( + `[PublicationTree] Found ${currentChildAddresses.length} a-tags in current event:`, + currentChildAddresses, + ); // If no a-tags found, try e-tags as fallback if (currentChildAddresses.length === 0) { const eTags = currentEvent.tags - .filter((tag) => tag[0] === "e" && tag[1] && /^[0-9a-fA-F]{64}$/.test(tag[1])); - - console.debug(`[PublicationTree] Found ${eTags.length} e-tags for current event ${currentEvent.id}:`, eTags.map(tag => tag[1])); - + .filter((tag) => + tag[0] === "e" && tag[1] && /^[0-9a-fA-F]{64}$/.test(tag[1]) + ); + + console.debug( + `[PublicationTree] Found ${eTags.length} e-tags for current event ${currentEvent.id}:`, + eTags.map((tag) => tag[1]), + ); + // For e-tags with hex IDs, fetch the referenced events to get their addresses const eTagPromises = eTags.map(async (tag) => { try { - console.debug(`[PublicationTree] Fetching event for e-tag ${tag[1]} in depthFirstRetrieve`); + console.debug( + `[PublicationTree] Fetching event for e-tag ${ + tag[1] + } in depthFirstRetrieve`, + ); const referencedEvent = await fetchEventById(tag[1]); - + if (referencedEvent) { // Construct the proper address format from the referenced event - const dTag = referencedEvent.tags.find(tag => tag[0] === "d")?.[1]; + const dTag = referencedEvent.tags.find((tag) => tag[0] === "d") + ?.[1]; if (dTag) { - const address = `${referencedEvent.kind}:${referencedEvent.pubkey}:${dTag}`; - console.debug(`[PublicationTree] Constructed address from e-tag in depthFirstRetrieve: ${address}`); + const address = + `${referencedEvent.kind}:${referencedEvent.pubkey}:${dTag}`; + console.debug( + `[PublicationTree] Constructed address from e-tag in depthFirstRetrieve: ${address}`, + ); return address; } else { - console.debug(`[PublicationTree] Referenced event ${tag[1]} has no d-tag in depthFirstRetrieve`); + console.debug( + `[PublicationTree] Referenced event ${ + tag[1] + } has no d-tag in depthFirstRetrieve`, + ); } } else { - console.debug(`[PublicationTree] Failed to fetch event for e-tag ${tag[1]} in depthFirstRetrieve - event not found`); + console.debug( + `[PublicationTree] Failed to fetch event for e-tag ${ + tag[1] + } in depthFirstRetrieve - event not found`, + ); } return null; } catch (error) { - console.warn(`[PublicationTree] Failed to fetch event for e-tag ${tag[1]} in depthFirstRetrieve:`, error); + console.warn( + `[PublicationTree] Failed to fetch event for e-tag ${ + tag[1] + } in depthFirstRetrieve:`, + error, + ); return null; } }); - + const resolvedAddresses = await Promise.all(eTagPromises); - const validAddresses = resolvedAddresses.filter(addr => addr !== null) as string[]; - - console.debug(`[PublicationTree] Resolved ${validAddresses.length} valid addresses from e-tags in depthFirstRetrieve:`, validAddresses); - + const validAddresses = resolvedAddresses.filter((addr) => + addr !== null + ) as string[]; + + console.debug( + `[PublicationTree] Resolved ${validAddresses.length} valid addresses from e-tags in depthFirstRetrieve:`, + validAddresses, + ); + if (validAddresses.length > 0) { currentChildAddresses.push(...validAddresses); } @@ -646,9 +695,9 @@ export class PublicationTree implements AsyncIterable { // Augment the tree with the children of the current event. const childPromises = currentChildAddresses - .filter(childAddress => !this.#nodes.has(childAddress)) - .map(childAddress => this.#addNode(childAddress, currentNode!)); - + .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. @@ -663,7 +712,7 @@ export class PublicationTree implements AsyncIterable { #addNode(address: string, parentNode: PublicationTreeNode) { const lazyNode = new Lazy(() => - this.#resolveNode(address, parentNode), + this.#resolveNode(address, parentNode) ); parentNode.children!.push(lazyNode); this.#nodes.set(address, lazyNode); @@ -686,10 +735,10 @@ export class PublicationTree implements AsyncIterable { ): Promise { // Check cache first let event = this.#eventCache.get(address); - + if (!event) { const [kind, pubkey, dTag] = address.split(":"); - + // AI-NOTE: 2025-01-24 - Enhanced event fetching with comprehensive fallback // First try to fetch using the enhanced fetchEventWithFallback function // which includes search relay fallback logic @@ -698,33 +747,50 @@ export class PublicationTree implements AsyncIterable { authors: [pubkey], "#d": [dTag], }, 5000) // 5 second timeout for publication events - .then(fetchedEvent => { + .then((fetchedEvent) => { if (fetchedEvent) { // Cache the event if found this.#eventCache.set(address, fetchedEvent); event = fetchedEvent; } - + if (!event) { console.warn( `[PublicationTree] Event with address ${address} not found on primary relays, trying search relays.`, ); - - // If still not found, try a more aggressive search using search relays - return this.#trySearchRelayFallback(address, kind, pubkey, dTag, parentNode); + + // If still not found, try a more aggressive search using search relays + return this.#trySearchRelayFallback( + address, + kind, + pubkey, + dTag, + parentNode, + ); } - + return this.#buildNodeFromEvent(event, address, parentNode); }) - .catch(error => { - console.warn(`[PublicationTree] Error fetching event for address ${address}:`, error); - - // Try search relay fallback even on error - return this.#trySearchRelayFallback(address, kind, pubkey, dTag, parentNode); + .catch((error) => { + console.warn( + `[PublicationTree] Error fetching event for address ${address}:`, + error, + ); + + // Try search relay fallback even on error + return this.#trySearchRelayFallback( + address, + kind, + pubkey, + dTag, + parentNode, + ); }); } - return Promise.resolve(this.#buildNodeFromEvent(event, address, parentNode)); + return Promise.resolve( + this.#buildNodeFromEvent(event, address, parentNode), + ); } /** @@ -732,54 +798,75 @@ export class PublicationTree implements AsyncIterable { * This method tries to find events on search relays when they're not found on primary relays */ async #trySearchRelayFallback( - address: string, - kind: string, - pubkey: string, + address: string, + kind: string, + pubkey: string, dTag: string, - parentNode: PublicationTreeNode + parentNode: PublicationTreeNode, ): Promise { try { - console.log(`[PublicationTree] Trying search relay fallback for address: ${address}`); - + console.log( + `[PublicationTree] Trying search relay fallback for address: ${address}`, + ); + // Get current relay configuration const inboxRelays = get(activeInboxRelays); const outboxRelays = get(activeOutboxRelays); - + // Create a comprehensive relay set including search relays - const allRelays = [...inboxRelays, ...outboxRelays, ...searchRelays, ...secondaryRelays]; + const allRelays = [ + ...inboxRelays, + ...outboxRelays, + ...searchRelays, + ...secondaryRelays, + ]; const uniqueRelays = [...new Set(allRelays)]; // Remove duplicates - - console.log(`[PublicationTree] Trying ${uniqueRelays.length} relays for fallback search:`, uniqueRelays); - + + console.log( + `[PublicationTree] Trying ${uniqueRelays.length} relays for fallback search:`, + uniqueRelays, + ); + // Try each relay individually with a shorter timeout for (const relay of uniqueRelays) { try { - const relaySet = NDKRelaySetFromNDK.fromRelayUrls([relay], this.#ndk); - - const fetchedEvent = await this.#ndk.fetchEvent({ - kinds: [parseInt(kind)], - authors: [pubkey], - "#d": [dTag], - }, undefined, relaySet).withTimeout(3000); // 3 second timeout per relay - + const relaySet = NDKRelaySetFromNDK.fromRelayUrls([relay], this.#ndk); + + const fetchedEvent = await this.#ndk.fetchEvent( + { + kinds: [parseInt(kind)], + authors: [pubkey], + "#d": [dTag], + }, + undefined, + relaySet, + ).withTimeout(3000); // 3 second timeout per relay + if (fetchedEvent) { - console.log(`[PublicationTree] Found event ${fetchedEvent.id} on search relay: ${relay}`); - + console.log( + `[PublicationTree] Found event ${fetchedEvent.id} on search relay: ${relay}`, + ); + // Cache the event this.#eventCache.set(address, fetchedEvent); this.#events.set(address, fetchedEvent); - + return this.#buildNodeFromEvent(fetchedEvent, address, parentNode); } } catch (error) { - console.debug(`[PublicationTree] Failed to fetch from relay ${relay}:`, error); + console.debug( + `[PublicationTree] Failed to fetch from relay ${relay}:`, + error, + ); continue; // Try next relay } } - + // If we get here, the event was not found on any relay - console.warn(`[PublicationTree] Event with address ${address} not found on any relay after fallback search.`); - + console.warn( + `[PublicationTree] Event with address ${address} not found on any relay after fallback search.`, + ); + return { type: PublicationTreeNodeType.Leaf, status: PublicationTreeNodeStatus.Error, @@ -787,10 +874,12 @@ export class PublicationTree implements AsyncIterable { parent: parentNode, children: [], }; - } catch (error) { - console.error(`[PublicationTree] Error in search relay fallback for ${address}:`, error); - + console.error( + `[PublicationTree] Error in search relay fallback for ${address}:`, + error, + ); + return { type: PublicationTreeNodeType.Leaf, status: PublicationTreeNodeStatus.Error, @@ -806,9 +895,9 @@ export class PublicationTree implements AsyncIterable { * This extracts the common logic for building nodes from events */ #buildNodeFromEvent( - event: NDKEvent, - address: string, - parentNode: PublicationTreeNode + event: NDKEvent, + address: string, + parentNode: PublicationTreeNode, ): PublicationTreeNode { this.#events.set(address, event); @@ -816,46 +905,68 @@ export class PublicationTree implements AsyncIterable { .filter((tag) => tag[0] === "a") .map((tag) => tag[1]); - console.debug(`[PublicationTree] Event ${event.id} has ${event.tags.length} tags:`, event.tags); - console.debug(`[PublicationTree] Found ${childAddresses.length} a-tags:`, childAddresses); + console.debug( + `[PublicationTree] Event ${event.id} has ${event.tags.length} tags:`, + event.tags, + ); + console.debug( + `[PublicationTree] Found ${childAddresses.length} a-tags:`, + childAddresses, + ); // If no a-tags found, try e-tags as fallback if (childAddresses.length === 0) { const eTags = event.tags - .filter((tag) => tag[0] === "e" && tag[1] && /^[0-9a-fA-F]{64}$/.test(tag[1])); - - console.debug(`[PublicationTree] Found ${eTags.length} e-tags for event ${event.id}:`, eTags.map(tag => tag[1])); - + .filter((tag) => + tag[0] === "e" && tag[1] && /^[0-9a-fA-F]{64}$/.test(tag[1]) + ); + + console.debug( + `[PublicationTree] Found ${eTags.length} e-tags for event ${event.id}:`, + eTags.map((tag) => tag[1]), + ); + // For e-tags with hex IDs, fetch the referenced events to get their addresses const eTagPromises = eTags.map(async (tag) => { try { console.debug(`[PublicationTree] Fetching event for e-tag ${tag[1]}`); const referencedEvent = await fetchEventById(tag[1]); - + if (referencedEvent) { // Construct the proper address format from the referenced event - const dTag = referencedEvent.tags.find(tag => tag[0] === "d")?.[1]; + const dTag = referencedEvent.tags.find((tag) => tag[0] === "d") + ?.[1]; if (dTag) { - const address = `${referencedEvent.kind}:${referencedEvent.pubkey}:${dTag}`; - console.debug(`[PublicationTree] Constructed address from e-tag: ${address}`); + const address = + `${referencedEvent.kind}:${referencedEvent.pubkey}:${dTag}`; + console.debug( + `[PublicationTree] Constructed address from e-tag: ${address}`, + ); return address; } else { - console.debug(`[PublicationTree] Referenced event ${tag[1]} has no d-tag`); + console.debug( + `[PublicationTree] Referenced event ${tag[1]} has no d-tag`, + ); } } else { - console.debug(`[PublicationTree] Failed to fetch event for e-tag ${tag[1]}`); + console.debug( + `[PublicationTree] Failed to fetch event for e-tag ${tag[1]}`, + ); } return null; } catch (error) { - console.warn(`[PublicationTree] Failed to fetch event for e-tag ${tag[1]}:`, error); + console.warn( + `[PublicationTree] Failed to fetch event for e-tag ${tag[1]}:`, + error, + ); return null; } }); - + // Note: We can't await here since this is a synchronous method // The e-tag resolution will happen when the children are processed // For now, we'll add the e-tags as potential child addresses - const eTagAddresses = eTags.map(tag => tag[1]); + const eTagAddresses = eTags.map((tag) => tag[1]); childAddresses.push(...eTagAddresses); } @@ -868,11 +979,14 @@ export class PublicationTree implements AsyncIterable { }; // Add children asynchronously - const childPromises = childAddresses.map(address => + const childPromises = childAddresses.map((address) => this.addEventByAddress(address, event) ); - Promise.all(childPromises).catch(error => { - console.warn(`[PublicationTree] Error adding children for ${address}:`, error); + Promise.all(childPromises).catch((error) => { + console.warn( + `[PublicationTree] Error adding children for ${address}:`, + error, + ); }); this.#nodeResolvedObservers.forEach((observer) => observer(address)); @@ -881,10 +995,14 @@ export class PublicationTree implements AsyncIterable { } #getNodeType(event: NDKEvent): PublicationTreeNodeType { - if (event.kind === 30040 && ( - event.tags.some((tag) => tag[0] === "a") || - event.tags.some((tag) => tag[0] === "e" && tag[1] && /^[0-9a-fA-F]{64}$/.test(tag[1])) - )) { + if ( + event.kind === 30040 && ( + event.tags.some((tag) => tag[0] === "a") || + event.tags.some((tag) => + tag[0] === "e" && tag[1] && /^[0-9a-fA-F]{64}$/.test(tag[1]) + ) + ) + ) { return PublicationTreeNodeType.Branch; } diff --git a/src/lib/data_structures/websocket_pool.ts b/src/lib/data_structures/websocket_pool.ts index 5eda81a..e1c1c02 100644 --- a/src/lib/data_structures/websocket_pool.ts +++ b/src/lib/data_structures/websocket_pool.ts @@ -42,7 +42,10 @@ export class WebSocketPool { * @param maxConnections - The maximum number of simultaneous WebSocket connections. Defaults to * 16. */ - private constructor(idleTimeoutMs: number = 60000, maxConnections: number = 16) { + private constructor( + idleTimeoutMs: number = 60000, + maxConnections: number = 16, + ) { this.#idleTimeoutMs = idleTimeoutMs; this.#maxConnections = maxConnections; } @@ -71,15 +74,17 @@ export class WebSocketPool { } if (limit == null || isNaN(limit)) { - throw new Error('[WebSocketPool] Connection limit must be a number.'); + throw new Error("[WebSocketPool] Connection limit must be a number."); } if (limit <= 0) { - throw new Error('[WebSocketPool] Connection limit must be greater than 0.'); + throw new Error( + "[WebSocketPool] Connection limit must be greater than 0.", + ); } if (!Number.isInteger(limit)) { - throw new Error('[WebSocketPool] Connection limit must be an integer.'); + throw new Error("[WebSocketPool] Connection limit must be an integer."); } this.#maxConnections = limit; @@ -106,15 +111,15 @@ export class WebSocketPool { } if (timeoutMs == null || isNaN(timeoutMs)) { - throw new Error('[WebSocketPool] Idle timeout must be a number.'); + throw new Error("[WebSocketPool] Idle timeout must be a number."); } if (timeoutMs <= 0) { - throw new Error('[WebSocketPool] Idle timeout must be greater than 0.'); + throw new Error("[WebSocketPool] Idle timeout must be greater than 0."); } if (!Number.isInteger(timeoutMs)) { - throw new Error('[WebSocketPool] Idle timeout must be an integer.'); + throw new Error("[WebSocketPool] Idle timeout must be an integer."); } this.#idleTimeoutMs = timeoutMs; @@ -151,9 +156,9 @@ export class WebSocketPool { if (this.#pool.size >= this.#maxConnections) { return new Promise((resolve, reject) => { - this.#waitingQueue.push({ - url: normalizedUrl, - resolve: (handle) => resolve(handle.ws), + this.#waitingQueue.push({ + url: normalizedUrl, + resolve: (handle) => resolve(handle.ws), reject, }); }); @@ -163,7 +168,7 @@ export class WebSocketPool { return newHandle.ws; } catch (error) { throw new Error( - `[WebSocketPool] Failed to acquire connection for ${normalizedUrl}: ${error}` + `[WebSocketPool] Failed to acquire connection for ${normalizedUrl}: ${error}`, ); } } @@ -179,7 +184,9 @@ export class WebSocketPool { const normalizedUrl = this.#normalizeUrl(ws.url); const handle = this.#pool.get(normalizedUrl); if (!handle) { - throw new Error('[WebSocketPool] Attempted to release an unmanaged WebSocket connection.'); + throw new Error( + "[WebSocketPool] Attempted to release an unmanaged WebSocket connection.", + ); } if (--handle.refCount === 0) { @@ -191,8 +198,10 @@ export class WebSocketPool { * Closes all WebSocket connections and "drains" the pool. */ public drain(): void { - console.debug(`[WebSocketPool] Draining pool with ${this.#pool.size} connections and ${this.#waitingQueue.length} waiting requests`); - + console.debug( + `[WebSocketPool] Draining pool with ${this.#pool.size} connections and ${this.#waitingQueue.length} waiting requests`, + ); + // Clear all idle timers first for (const handle of this.#pool.values()) { this.#clearIdleTimer(handle); @@ -200,7 +209,7 @@ export class WebSocketPool { // Reject all waiting requests for (const { reject } of this.#waitingQueue) { - reject(new Error('[WebSocketPool] Draining pool.')); + reject(new Error("[WebSocketPool] Draining pool.")); } this.#waitingQueue = []; @@ -211,8 +220,8 @@ export class WebSocketPool { } } this.#pool.clear(); - - console.debug('[WebSocketPool] Pool drained successfully'); + + console.debug("[WebSocketPool] Pool drained successfully"); } // #endregion @@ -239,7 +248,9 @@ export class WebSocketPool { this.#removeSocket(handle); this.#processWaitingQueue(); reject( - new Error(`[WebSocketPool] WebSocket connection failed for ${url}: ${event.type}`) + new Error( + `[WebSocketPool] WebSocket connection failed for ${url}: ${event.type}`, + ), ); }; } catch (error) { @@ -251,7 +262,7 @@ export class WebSocketPool { #removeSocket(handle: WebSocketHandle): void { this.#clearIdleTimer(handle); - + // Clean up event listeners to prevent memory leaks // AI-NOTE: Code that checks out connections should clean up its own listener callbacks before // releasing the connection to the pool. @@ -261,11 +272,13 @@ export class WebSocketPool { handle.ws.onclose = null; handle.ws.onmessage = null; } - + const url = this.#normalizeUrl(handle.ws.url); this.#pool.delete(url); - console.debug(`[WebSocketPool] Removed socket for ${url}, pool size: ${this.#pool.size}`); + console.debug( + `[WebSocketPool] Removed socket for ${url}, pool size: ${this.#pool.size}`, + ); this.#processWaitingQueue(); } @@ -283,7 +296,9 @@ export class WebSocketPool { handle.idleTimer = setTimeout(() => { const refCount = handle.refCount; if (refCount === 0 && handle.ws.readyState === WebSocket.OPEN) { - console.debug(`[WebSocketPool] Closing idle connection to ${handle.ws.url}`); + console.debug( + `[WebSocketPool] Closing idle connection to ${handle.ws.url}`, + ); handle.ws.close(); this.#removeSocket(handle); } @@ -331,7 +346,7 @@ export class WebSocketPool { #checkOut(handle: WebSocketHandle): void { if (handle.refCount == null) { - throw new Error('[WebSocketPool] Handle refCount unexpectedly null.'); + throw new Error("[WebSocketPool] Handle refCount unexpectedly null."); } ++handle.refCount; @@ -346,10 +361,10 @@ export class WebSocketPool { // The logic to remove a trailing slash for connection coalescing can be kept, // but should be done on the normalized string. - if (urlObj.pathname !== '/' && normalized.endsWith('/')) { + if (urlObj.pathname !== "/" && normalized.endsWith("/")) { normalized = normalized.slice(0, -1); } - + return normalized; } catch { // If URL is invalid, return it as-is and let WebSocket constructor handle the error. diff --git a/src/lib/navigator/EventNetwork/types.ts b/src/lib/navigator/EventNetwork/types.ts index 67fe49f..bdf7cac 100644 --- a/src/lib/navigator/EventNetwork/types.ts +++ b/src/lib/navigator/EventNetwork/types.ts @@ -53,7 +53,7 @@ export interface NetworkNode extends SimulationNodeDatum { tagType?: string; // Type of tag (t, p, e, etc.) tagValue?: string; // The tag value connectedNodes?: string[]; // IDs of nodes that have this tag - + // Person anchor specific fields isPersonAnchor?: boolean; // Whether this is a person anchor node pubkey?: string; // The person's public key diff --git a/src/lib/navigator/EventNetwork/utils/common.ts b/src/lib/navigator/EventNetwork/utils/common.ts index f8c0bef..63b1c8a 100644 --- a/src/lib/navigator/EventNetwork/utils/common.ts +++ b/src/lib/navigator/EventNetwork/utils/common.ts @@ -38,4 +38,4 @@ export function createDebugFunction(prefix: string) { console.log(`[${prefix}]`, ...args); } }; -} \ No newline at end of file +} diff --git a/src/lib/navigator/EventNetwork/utils/forceSimulation.ts b/src/lib/navigator/EventNetwork/utils/forceSimulation.ts index d74ba1d..7d02fd3 100644 --- a/src/lib/navigator/EventNetwork/utils/forceSimulation.ts +++ b/src/lib/navigator/EventNetwork/utils/forceSimulation.ts @@ -1,11 +1,11 @@ /** * D3 Force Simulation Utilities - * + * * This module provides utilities for creating and managing D3 force-directed * graph simulations for the event network visualization. */ -import type { NetworkNode, NetworkLink } from "../types"; +import type { NetworkLink, NetworkNode } from "../types"; import * as d3 from "d3"; import { createDebugFunction } from "./common"; @@ -21,18 +21,18 @@ const debug = createDebugFunction("ForceSimulation"); * Provides type safety for simulation operations */ export interface Simulation { - nodes(): NodeType[]; - nodes(nodes: NodeType[]): this; - alpha(): number; - alpha(alpha: number): this; - alphaTarget(): number; - alphaTarget(target: number): this; - restart(): this; - stop(): this; - tick(): this; - on(type: string, listener: (this: this) => void): this; - force(name: string): any; - force(name: string, force: any): this; + nodes(): NodeType[]; + nodes(nodes: NodeType[]): this; + alpha(): number; + alpha(alpha: number): this; + alphaTarget(): number; + alphaTarget(target: number): this; + restart(): this; + stop(): this; + tick(): this; + on(type: string, listener: (this: this) => void): this; + force(name: string): any; + force(name: string, force: any): this; } /** @@ -40,175 +40,192 @@ export interface Simulation { * Provides type safety for drag operations */ export interface D3DragEvent { - active: number; - sourceEvent: any; - subject: Subject; - x: number; - y: number; - dx: number; - dy: number; - identifier: string | number; + active: number; + sourceEvent: any; + subject: Subject; + x: number; + y: number; + dx: number; + dy: number; + identifier: string | number; } /** * Updates a node's velocity by applying a force - * + * * @param node - The node to update * @param deltaVx - Change in x velocity * @param deltaVy - Change in y velocity */ export function updateNodeVelocity( - node: NetworkNode, - deltaVx: number, - deltaVy: number + node: NetworkNode, + deltaVx: number, + deltaVy: number, ) { - debug("Updating node velocity", { - nodeId: node.id, - currentVx: node.vx, - currentVy: node.vy, - deltaVx, - deltaVy - }); - - if (typeof node.vx === "number" && typeof node.vy === "number") { - node.vx = node.vx - deltaVx; - node.vy = node.vy - deltaVy; - debug("New velocity", { nodeId: node.id, vx: node.vx, vy: node.vy }); - } else { - debug("Node velocity not defined", { nodeId: node.id }); - } + debug("Updating node velocity", { + nodeId: node.id, + currentVx: node.vx, + currentVy: node.vy, + deltaVx, + deltaVy, + }); + + if (typeof node.vx === "number" && typeof node.vy === "number") { + node.vx = node.vx - deltaVx; + node.vy = node.vy - deltaVy; + debug("New velocity", { nodeId: node.id, vx: node.vx, vy: node.vy }); + } else { + debug("Node velocity not defined", { nodeId: node.id }); + } } /** * Applies a logarithmic gravity force pulling the node toward the center - * + * * The logarithmic scale ensures that nodes far from the center experience * stronger gravity, preventing them from drifting too far away. - * + * * @param node - The node to apply gravity to * @param centerX - X coordinate of the center * @param centerY - Y coordinate of the center * @param alpha - Current simulation alpha (cooling factor) */ export function applyGlobalLogGravity( - node: NetworkNode, - centerX: number, - centerY: number, - alpha: number, + node: NetworkNode, + centerX: number, + centerY: number, + alpha: number, ) { - // Tag anchors and person anchors should not be affected by gravity - if (node.isTagAnchor || node.isPersonAnchor) return; - - const dx = (node.x ?? 0) - centerX; - const dy = (node.y ?? 0) - centerY; - const distance = Math.sqrt(dx * dx + dy * dy); + // Tag anchors and person anchors should not be affected by gravity + if (node.isTagAnchor || node.isPersonAnchor) return; + + const dx = (node.x ?? 0) - centerX; + const dy = (node.y ?? 0) - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); - if (distance === 0) return; + if (distance === 0) return; - const force = Math.log(distance + 1) * GRAVITY_STRENGTH * alpha; - updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force); + const force = Math.log(distance + 1) * GRAVITY_STRENGTH * alpha; + updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force); } /** * Applies gravity between connected nodes - * + * * This creates a cohesive force that pulls connected nodes toward their * collective center of gravity, creating more meaningful clusters. - * + * * @param node - The node to apply connected gravity to * @param links - All links in the network * @param alpha - Current simulation alpha (cooling factor) */ export function applyConnectedGravity( - node: NetworkNode, - links: NetworkLink[], - alpha: number, + node: NetworkNode, + links: NetworkLink[], + alpha: number, ) { - // Tag anchors and person anchors should not be affected by connected gravity - if (node.isTagAnchor || node.isPersonAnchor) return; - - // Find all nodes connected to this node (excluding tag anchors and person anchors) - const connectedNodes = links - .filter(link => link.source.id === node.id || link.target.id === node.id) - .map(link => link.source.id === node.id ? link.target : link.source) - .filter(n => !n.isTagAnchor && !n.isPersonAnchor); + // Tag anchors and person anchors should not be affected by connected gravity + if (node.isTagAnchor || node.isPersonAnchor) return; - if (connectedNodes.length === 0) return; + // Find all nodes connected to this node (excluding tag anchors and person anchors) + const connectedNodes = links + .filter((link) => link.source.id === node.id || link.target.id === node.id) + .map((link) => link.source.id === node.id ? link.target : link.source) + .filter((n) => !n.isTagAnchor && !n.isPersonAnchor); - // Calculate center of gravity of connected nodes - const cogX = d3.mean(connectedNodes, (n: NetworkNode) => n.x); - const cogY = d3.mean(connectedNodes, (n: NetworkNode) => n.y); + if (connectedNodes.length === 0) return; - if (cogX === undefined || cogY === undefined) return; + // Calculate center of gravity of connected nodes + const cogX = d3.mean(connectedNodes, (n: NetworkNode) => n.x); + const cogY = d3.mean(connectedNodes, (n: NetworkNode) => n.y); - // Calculate force direction and magnitude - const dx = (node.x ?? 0) - cogX; - const dy = (node.y ?? 0) - cogY; - const distance = Math.sqrt(dx * dx + dy * dy); + if (cogX === undefined || cogY === undefined) return; - if (distance === 0) return; + // Calculate force direction and magnitude + const dx = (node.x ?? 0) - cogX; + const dy = (node.y ?? 0) - cogY; + const distance = Math.sqrt(dx * dx + dy * dy); - // Apply force proportional to distance - const force = distance * CONNECTED_GRAVITY_STRENGTH * alpha; - updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force); + if (distance === 0) return; + + // Apply force proportional to distance + const force = distance * CONNECTED_GRAVITY_STRENGTH * alpha; + updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force); } /** * Sets up drag behavior for nodes - * + * * This enables interactive dragging of nodes in the visualization. - * + * * @param simulation - The D3 force simulation * @param warmupClickEnergy - Alpha target when dragging starts (0-1) * @returns D3 drag behavior configured for the simulation */ export function setupDragHandlers( - simulation: Simulation, - warmupClickEnergy: number = 0.9 + simulation: Simulation, + warmupClickEnergy: number = 0.9, ) { - return d3 - .drag() - .on("start", (event: D3DragEvent, d: NetworkNode) => { - // Tag anchors and person anchors retain their anchor behavior - if (d.isTagAnchor || d.isPersonAnchor) { - // Still allow dragging but maintain anchor status - d.fx = d.x; - d.fy = d.y; - return; - } - - // Warm up simulation if it's cooled down - if (!event.active) { - simulation.alphaTarget(warmupClickEnergy).restart(); - } - // Fix node position at current location - d.fx = d.x; - d.fy = d.y; - }) - .on("drag", (event: D3DragEvent, d: NetworkNode) => { - // Update position for all nodes including anchors - - // Update fixed position to mouse position - d.fx = event.x; - d.fy = event.y; - }) - .on("end", (event: D3DragEvent, d: NetworkNode) => { - - // Cool down simulation when drag ends - if (!event.active) { - simulation.alphaTarget(0); - } - - // Keep all nodes fixed after dragging - // This allows users to manually position any node type - d.fx = d.x; - d.fy = d.y; - }); + return d3 + .drag() + .on( + "start", + ( + event: D3DragEvent, + d: NetworkNode, + ) => { + // Tag anchors and person anchors retain their anchor behavior + if (d.isTagAnchor || d.isPersonAnchor) { + // Still allow dragging but maintain anchor status + d.fx = d.x; + d.fy = d.y; + return; + } + + // Warm up simulation if it's cooled down + if (!event.active) { + simulation.alphaTarget(warmupClickEnergy).restart(); + } + // Fix node position at current location + d.fx = d.x; + d.fy = d.y; + }, + ) + .on( + "drag", + ( + event: D3DragEvent, + d: NetworkNode, + ) => { + // Update position for all nodes including anchors + + // Update fixed position to mouse position + d.fx = event.x; + d.fy = event.y; + }, + ) + .on( + "end", + ( + event: D3DragEvent, + d: NetworkNode, + ) => { + // Cool down simulation when drag ends + if (!event.active) { + simulation.alphaTarget(0); + } + + // Keep all nodes fixed after dragging + // This allows users to manually position any node type + d.fx = d.x; + d.fy = d.y; + }, + ); } /** * Creates a D3 force simulation for the network - * + * * @param nodes - Array of network nodes * @param links - Array of network links * @param nodeRadius - Radius of node circles @@ -216,34 +233,34 @@ export function setupDragHandlers( * @returns Configured D3 force simulation */ export function createSimulation( - nodes: NetworkNode[], - links: NetworkLink[], - nodeRadius: number, - linkDistance: number + nodes: NetworkNode[], + links: NetworkLink[], + nodeRadius: number, + linkDistance: number, ): Simulation { - debug("Creating simulation", { - nodeCount: nodes.length, - linkCount: links.length, - nodeRadius, - linkDistance - }); - - try { - // Create the simulation with nodes - const simulation = d3 - .forceSimulation(nodes) - .force( - "link", - d3.forceLink(links) - .id((d: NetworkNode) => d.id) - .distance(linkDistance * 0.1) - ) - .force("collide", d3.forceCollide().radius(nodeRadius * 4)); - - debug("Simulation created successfully"); - return simulation; - } catch (error) { - console.error("Error creating simulation:", error); - throw error; - } + debug("Creating simulation", { + nodeCount: nodes.length, + linkCount: links.length, + nodeRadius, + linkDistance, + }); + + try { + // Create the simulation with nodes + const simulation = d3 + .forceSimulation(nodes) + .force( + "link", + d3.forceLink(links) + .id((d: NetworkNode) => d.id) + .distance(linkDistance * 0.1), + ) + .force("collide", d3.forceCollide().radius(nodeRadius * 4)); + + debug("Simulation created successfully"); + return simulation; + } catch (error) { + console.error("Error creating simulation:", error); + throw error; + } } diff --git a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts index 3ba3abd..4e133b3 100644 --- a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts +++ b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts @@ -1,16 +1,16 @@ /** * Network Builder Utilities - * + * * This module provides utilities for building a network graph from Nostr events. * It handles the creation of nodes and links, and the processing of event relationships. */ import type { NDKEvent } from "@nostr-dev-kit/ndk"; -import type { NetworkNode, NetworkLink, GraphData, GraphState } from "../types"; +import type { GraphData, GraphState, NetworkLink, NetworkNode } from "../types"; import { nip19 } from "nostr-tools"; import { communityRelays } from "$lib/consts"; -import { getMatchingTags } from '$lib/utils/nostrUtils'; -import { getDisplayNameSync } from '$lib/utils/profileCache'; +import { getMatchingTags } from "$lib/utils/nostrUtils"; +import { getDisplayNameSync } from "$lib/utils/profileCache"; import { createDebugFunction } from "./common"; // Configuration @@ -22,165 +22,173 @@ const debug = createDebugFunction("NetworkBuilder"); /** * Creates a NetworkNode from an NDKEvent - * + * * Extracts relevant information from the event and creates a node representation * for the visualization. - * + * * @param event - The Nostr event to convert to a node * @param level - The hierarchy level of the node (default: 0) * @returns A NetworkNode object representing the event */ export function createNetworkNode( - event: NDKEvent, - level: number = 0 + event: NDKEvent, + level: number = 0, ): NetworkNode { - debug("Creating network node", { eventId: event.id, kind: event.kind, level }); - - const isContainer = event.kind === INDEX_EVENT_KIND; - const nodeType = isContainer ? "Index" : event.kind === CONTENT_EVENT_KIND || event.kind === 30818 ? "Content" : `Kind ${event.kind}`; + debug("Creating network node", { + eventId: event.id, + kind: event.kind, + level, + }); - // Create the base node with essential properties - const node: NetworkNode = { + const isContainer = event.kind === INDEX_EVENT_KIND; + const nodeType = isContainer + ? "Index" + : event.kind === CONTENT_EVENT_KIND || event.kind === 30818 + ? "Content" + : `Kind ${event.kind}`; + + // Create the base node with essential properties + const node: NetworkNode = { + id: event.id, + event, + isContainer, + level, + title: event.getMatchingTags("title")?.[0]?.[1] || "Untitled", + content: event.content || "", + author: event.pubkey ? getDisplayNameSync(event.pubkey) : "", + kind: event.kind !== undefined ? event.kind : CONTENT_EVENT_KIND, // Default to content event kind only if truly undefined + type: nodeType as "Index" | "Content" | "TagAnchor", + }; + + // Add NIP-19 identifiers if possible + if (event.kind && event.pubkey) { + try { + const dTag = event.getMatchingTags("d")?.[0]?.[1] || ""; + + // Create naddr (NIP-19 address) for the event + node.naddr = nip19.naddrEncode({ + pubkey: event.pubkey, + identifier: dTag, + kind: event.kind, + relays: communityRelays, + }); + + // Create nevent (NIP-19 event reference) for the event + node.nevent = nip19.neventEncode({ id: event.id, - event, - isContainer, - level, - title: event.getMatchingTags("title")?.[0]?.[1] || "Untitled", - content: event.content || "", - author: event.pubkey ? getDisplayNameSync(event.pubkey) : "", - kind: event.kind !== undefined ? event.kind : CONTENT_EVENT_KIND, // Default to content event kind only if truly undefined - type: nodeType as "Index" | "Content" | "TagAnchor", - }; - - // Add NIP-19 identifiers if possible - if (event.kind && event.pubkey) { - try { - const dTag = event.getMatchingTags("d")?.[0]?.[1] || ""; - - // Create naddr (NIP-19 address) for the event - node.naddr = nip19.naddrEncode({ - pubkey: event.pubkey, - identifier: dTag, - kind: event.kind, - relays: communityRelays, - }); - - // Create nevent (NIP-19 event reference) for the event - node.nevent = nip19.neventEncode({ - id: event.id, - relays: communityRelays, - kind: event.kind, - }); - } catch (error) { - console.warn("Failed to generate identifiers for node:", error); - } + relays: communityRelays, + kind: event.kind, + }); + } catch (error) { + console.warn("Failed to generate identifiers for node:", error); } + } - return node; + return node; } /** * Creates a map of event IDs to events for quick lookup - * + * * @param events - Array of Nostr events * @returns Map of event IDs to events */ export function createEventMap(events: NDKEvent[]): Map { - debug("Creating event map", { eventCount: events.length }); - - const eventMap = new Map(); - events.forEach((event) => { - if (event.id) { - eventMap.set(event.id, event); - } - }); - - debug("Event map created", { mapSize: eventMap.size }); - return eventMap; + debug("Creating event map", { eventCount: events.length }); + + const eventMap = new Map(); + events.forEach((event) => { + if (event.id) { + eventMap.set(event.id, event); + } + }); + + debug("Event map created", { mapSize: eventMap.size }); + return eventMap; } /** * Extracts an event ID from an 'a' tag - * + * * @param tag - The tag array from a Nostr event * @returns The event ID or null if not found */ export function extractEventIdFromATag(tag: string[]): string | null { - return tag[3] || null; + return tag[3] || null; } /** * Generates a deterministic color for an event based on its ID - * + * * This creates visually distinct colors for different index events * while ensuring the same event always gets the same color. - * + * * @param eventId - The event ID to generate a color for * @returns An HSL color string */ export function getEventColor(eventId: string): string { - // Use first 4 characters of event ID as a hex number - const num = parseInt(eventId.slice(0, 4), 16); - // Convert to a hue value (0-359) - const hue = num % 360; - // Use fixed saturation and lightness for pastel colors - const saturation = 70; - const lightness = 75; - return `hsl(${hue}, ${saturation}%, ${lightness}%)`; + // Use first 4 characters of event ID as a hex number + const num = parseInt(eventId.slice(0, 4), 16); + // Convert to a hue value (0-359) + const hue = num % 360; + // Use fixed saturation and lightness for pastel colors + const saturation = 70; + const lightness = 75; + return `hsl(${hue}, ${saturation}%, ${lightness}%)`; } /** * Initializes the graph state from a set of events - * + * * Creates nodes for all events and identifies referenced events. - * + * * @param events - Array of Nostr events * @returns Initial graph state */ export function initializeGraphState(events: NDKEvent[]): GraphState { - debug("Initializing graph state", { eventCount: events.length }); - - const nodeMap = new Map(); - const eventMap = createEventMap(events); - - // Create initial nodes for all events - events.forEach((event) => { - if (!event.id) return; - const node = createNetworkNode(event); - nodeMap.set(event.id, node); + debug("Initializing graph state", { eventCount: events.length }); + + const nodeMap = new Map(); + const eventMap = createEventMap(events); + + // Create initial nodes for all events + events.forEach((event) => { + if (!event.id) return; + const node = createNetworkNode(event); + nodeMap.set(event.id, node); + }); + debug("Node map created", { nodeCount: nodeMap.size }); + + // Build set of referenced event IDs to identify root events + const referencedIds = new Set(); + events.forEach((event) => { + const aTags = getMatchingTags(event, "a"); + debug("Processing a-tags for event", { + eventId: event.id, + aTagCount: aTags.length, }); - debug("Node map created", { nodeCount: nodeMap.size }); - - // Build set of referenced event IDs to identify root events - const referencedIds = new Set(); - events.forEach((event) => { - const aTags = getMatchingTags(event, "a"); - debug("Processing a-tags for event", { - eventId: event.id, - aTagCount: aTags.length - }); - - aTags.forEach((tag) => { - const id = extractEventIdFromATag(tag); - if (id) referencedIds.add(id); - }); + + aTags.forEach((tag) => { + const id = extractEventIdFromATag(tag); + if (id) referencedIds.add(id); }); - debug("Referenced IDs set created", { referencedCount: referencedIds.size }); - - return { - nodeMap, - links: [], - eventMap, - referencedIds, - }; + }); + debug("Referenced IDs set created", { referencedCount: referencedIds.size }); + + return { + nodeMap, + links: [], + eventMap, + referencedIds, + }; } /** * Processes a sequence of nodes referenced by an index event - * + * * Creates links between the index and its content, and between sequential content nodes. * Also processes nested indices recursively up to the maximum level. - * + * * @param sequence - Array of nodes in the sequence * @param indexEvent - The index event referencing the sequence * @param level - Current hierarchy level @@ -188,156 +196,157 @@ export function initializeGraphState(events: NDKEvent[]): GraphState { * @param maxLevel - Maximum hierarchy level to process */ export function processSequence( - sequence: NetworkNode[], - indexEvent: NDKEvent, - level: number, - state: GraphState, - maxLevel: number, + sequence: NetworkNode[], + indexEvent: NDKEvent, + level: number, + state: GraphState, + maxLevel: number, ): void { - // Stop if we've reached max level or have no nodes - if (level >= maxLevel || sequence.length === 0) return; + // Stop if we've reached max level or have no nodes + if (level >= maxLevel || sequence.length === 0) return; + + // Set levels for all nodes in the sequence + sequence.forEach((node) => { + node.level = level + 1; + }); - // Set levels for all nodes in the sequence - sequence.forEach((node) => { - node.level = level + 1; + // Create link from index to first content node + const indexNode = state.nodeMap.get(indexEvent.id); + if (indexNode && sequence[0]) { + state.links.push({ + source: indexNode, + target: sequence[0], + isSequential: true, }); + } - // Create link from index to first content node - const indexNode = state.nodeMap.get(indexEvent.id); - if (indexNode && sequence[0]) { - state.links.push({ - source: indexNode, - target: sequence[0], - isSequential: true, - }); - } + // Create sequential links between content nodes + for (let i = 0; i < sequence.length - 1; i++) { + const currentNode = sequence[i]; + const nextNode = sequence[i + 1]; - // Create sequential links between content nodes - for (let i = 0; i < sequence.length - 1; i++) { - const currentNode = sequence[i]; - const nextNode = sequence[i + 1]; - - state.links.push({ - source: currentNode, - target: nextNode, - isSequential: true, - }); - - // Process nested indices recursively - if (currentNode.isContainer) { - processNestedIndex(currentNode, level + 1, state, maxLevel); - } - } + state.links.push({ + source: currentNode, + target: nextNode, + isSequential: true, + }); - // Process the last node if it's an index - const lastNode = sequence[sequence.length - 1]; - if (lastNode?.isContainer) { - processNestedIndex(lastNode, level + 1, state, maxLevel); + // Process nested indices recursively + if (currentNode.isContainer) { + processNestedIndex(currentNode, level + 1, state, maxLevel); } + } + + // Process the last node if it's an index + const lastNode = sequence[sequence.length - 1]; + if (lastNode?.isContainer) { + processNestedIndex(lastNode, level + 1, state, maxLevel); + } } /** * Processes a nested index node - * + * * @param node - The index node to process * @param level - Current hierarchy level * @param state - Current graph state * @param maxLevel - Maximum hierarchy level to process */ export function processNestedIndex( - node: NetworkNode, - level: number, - state: GraphState, - maxLevel: number, + node: NetworkNode, + level: number, + state: GraphState, + maxLevel: number, ): void { - if (!node.isContainer || level >= maxLevel) return; + if (!node.isContainer || level >= maxLevel) return; - const nestedEvent = state.eventMap.get(node.id); - if (nestedEvent) { - processIndexEvent(nestedEvent, level, state, maxLevel); - } + const nestedEvent = state.eventMap.get(node.id); + if (nestedEvent) { + processIndexEvent(nestedEvent, level, state, maxLevel); + } } /** * Processes an index event and its referenced content - * + * * @param indexEvent - The index event to process * @param level - Current hierarchy level * @param state - Current graph state * @param maxLevel - Maximum hierarchy level to process */ export function processIndexEvent( - indexEvent: NDKEvent, - level: number, - state: GraphState, - maxLevel: number, + indexEvent: NDKEvent, + level: number, + state: GraphState, + maxLevel: number, ): void { - if (level >= maxLevel) return; + if (level >= maxLevel) return; - // Extract the sequence of nodes referenced by this index - const sequence = getMatchingTags(indexEvent, "a") - .map((tag) => extractEventIdFromATag(tag)) - .filter((id): id is string => id !== null) - .map((id) => state.nodeMap.get(id)) - .filter((node): node is NetworkNode => node !== undefined); + // Extract the sequence of nodes referenced by this index + const sequence = getMatchingTags(indexEvent, "a") + .map((tag) => extractEventIdFromATag(tag)) + .filter((id): id is string => id !== null) + .map((id) => state.nodeMap.get(id)) + .filter((node): node is NetworkNode => node !== undefined); - processSequence(sequence, indexEvent, level, state, maxLevel); + processSequence(sequence, indexEvent, level, state, maxLevel); } /** * Generates a complete graph from a set of events - * + * * This is the main entry point for building the network visualization. - * + * * @param events - Array of Nostr events * @param maxLevel - Maximum hierarchy level to process * @returns Complete graph data for visualization */ export function generateGraph( - events: NDKEvent[], - maxLevel: number + events: NDKEvent[], + maxLevel: number, ): GraphData { - debug("Generating graph", { eventCount: events.length, maxLevel }); - - // Initialize the graph state - const state = initializeGraphState(events); - - // Find root events (index events not referenced by others, and all non-publication events) - const publicationKinds = [30040, 30041, 30818]; - const rootEvents = events.filter( - (e) => e.id && ( - // Index events not referenced by others - (e.kind === INDEX_EVENT_KIND && !state.referencedIds.has(e.id)) || - // All non-publication events are treated as roots - (e.kind !== undefined && !publicationKinds.includes(e.kind)) - ) - ); - - debug("Found root events", { - rootCount: rootEvents.length, - rootIds: rootEvents.map(e => e.id) - }); - - // Process each root event - rootEvents.forEach((rootEvent) => { - debug("Processing root event", { - rootId: rootEvent.id, - kind: rootEvent.kind, - aTags: getMatchingTags(rootEvent, "a").length - }); - processIndexEvent(rootEvent, 0, state, maxLevel); - }); + debug("Generating graph", { eventCount: events.length, maxLevel }); - // Create the final graph data - const result = { - nodes: Array.from(state.nodeMap.values()), - links: state.links, - }; - - debug("Graph generation complete", { - nodeCount: result.nodes.length, - linkCount: result.links.length + // Initialize the graph state + const state = initializeGraphState(events); + + // Find root events (index events not referenced by others, and all non-publication events) + const publicationKinds = [30040, 30041, 30818]; + const rootEvents = events.filter( + (e) => + e.id && ( + // Index events not referenced by others + (e.kind === INDEX_EVENT_KIND && !state.referencedIds.has(e.id)) || + // All non-publication events are treated as roots + (e.kind !== undefined && !publicationKinds.includes(e.kind)) + ), + ); + + debug("Found root events", { + rootCount: rootEvents.length, + rootIds: rootEvents.map((e) => e.id), + }); + + // Process each root event + rootEvents.forEach((rootEvent) => { + debug("Processing root event", { + rootId: rootEvent.id, + kind: rootEvent.kind, + aTags: getMatchingTags(rootEvent, "a").length, }); - - return result; + processIndexEvent(rootEvent, 0, state, maxLevel); + }); + + // Create the final graph data + const result = { + nodes: Array.from(state.nodeMap.values()), + links: state.links, + }; + + debug("Graph generation complete", { + nodeCount: result.nodes.length, + linkCount: result.links.length, + }); + + return result; } diff --git a/src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts b/src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts index aaafa00..426442e 100644 --- a/src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts +++ b/src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts @@ -5,9 +5,9 @@ */ import type { NDKEvent } from "@nostr-dev-kit/ndk"; -import type { NetworkNode, NetworkLink } from "../types"; +import type { NetworkLink, NetworkNode } from "../types"; import { getDisplayNameSync } from "$lib/utils/profileCache"; -import { SeededRandom, createDebugFunction } from "./common"; +import { createDebugFunction, SeededRandom } from "./common"; const PERSON_ANCHOR_RADIUS = 15; const PERSON_ANCHOR_PLACEMENT_RADIUS = 1000; @@ -16,7 +16,6 @@ const MAX_PERSON_NODES = 20; // Default limit for person nodes // Debug function const debug = createDebugFunction("PersonNetworkBuilder"); - /** * Creates a deterministic seed from a string */ @@ -42,13 +41,16 @@ export interface PersonConnection { */ export function extractUniquePersons( events: NDKEvent[], - followListEvents?: NDKEvent[] + followListEvents?: NDKEvent[], ): Map { // Map of pubkey -> PersonConnection const personMap = new Map(); - - debug("Extracting unique persons", { eventCount: events.length, followListCount: followListEvents?.length || 0 }); - + + debug("Extracting unique persons", { + eventCount: events.length, + followListCount: followListEvents?.length || 0, + }); + // First collect pubkeys from follow list events const followListPubkeys = new Set(); if (followListEvents && followListEvents.length > 0) { @@ -60,10 +62,10 @@ export function extractUniquePersons( // People in follow lists (p tags) if (event.tags) { event.tags - .filter(tag => { - tag[0] === 'p' + .filter((tag) => { + tag[0] === "p"; }) - .forEach(tag => { + .forEach((tag) => { followListPubkeys.add(tag[1]); }); } @@ -79,7 +81,7 @@ export function extractUniquePersons( personMap.set(event.pubkey, { signedByEventIds: new Set(), referencedInEventIds: new Set(), - isFromFollowList: followListPubkeys.has(event.pubkey) + isFromFollowList: followListPubkeys.has(event.pubkey), }); } personMap.get(event.pubkey)!.signedByEventIds.add(event.id); @@ -87,14 +89,14 @@ export function extractUniquePersons( // Track referenced connections from "p" tags if (event.tags) { - event.tags.forEach(tag => { + event.tags.forEach((tag) => { if (tag[0] === "p" && tag[1]) { const referencedPubkey = tag[1]; if (!personMap.has(referencedPubkey)) { personMap.set(referencedPubkey, { signedByEventIds: new Set(), referencedInEventIds: new Set(), - isFromFollowList: followListPubkeys.has(referencedPubkey) + isFromFollowList: followListPubkeys.has(referencedPubkey), }); } personMap.get(referencedPubkey)!.referencedInEventIds.add(event.id); @@ -102,7 +104,7 @@ export function extractUniquePersons( }); } }); - + debug("Extracted persons", { personCount: personMap.size }); return personMap; @@ -115,7 +117,7 @@ function buildEligiblePerson( pubkey: string, connection: PersonConnection, showSignedBy: boolean, - showReferenced: boolean + showReferenced: boolean, ): { pubkey: string; connection: PersonConnection; @@ -125,11 +127,11 @@ function buildEligiblePerson( const connectedEventIds = new Set(); if (showSignedBy) { - connection.signedByEventIds.forEach(id => connectedEventIds.add(id)); + connection.signedByEventIds.forEach((id) => connectedEventIds.add(id)); } if (showReferenced) { - connection.referencedInEventIds.forEach(id => connectedEventIds.add(id)); + connection.referencedInEventIds.forEach((id) => connectedEventIds.add(id)); } if (connectedEventIds.size === 0) { @@ -140,7 +142,7 @@ function buildEligiblePerson( pubkey, connection, connectedEventIds, - totalConnections: connectedEventIds.size + totalConnections: connectedEventIds.size, }; } @@ -155,7 +157,7 @@ function getEligiblePersons( personMap: Map, showSignedBy: boolean, showReferenced: boolean, - limit: number + limit: number, ): EligiblePerson[] { // Build eligible persons and keep only top N using a min-heap or partial sort const eligible: EligiblePerson[] = []; @@ -163,16 +165,20 @@ function getEligiblePersons( for (const [pubkey, connection] of personMap) { let totalConnections = 0; if (showSignedBy) totalConnections += connection.signedByEventIds.size; - if (showReferenced) totalConnections += connection.referencedInEventIds.size; + if (showReferenced) { + totalConnections += connection.referencedInEventIds.size; + } if (totalConnections === 0) continue; // Only build the set if this person is eligible const connectedEventIds = new Set(); if (showSignedBy) { - connection.signedByEventIds.forEach(id => connectedEventIds.add(id)); + connection.signedByEventIds.forEach((id) => connectedEventIds.add(id)); } if (showReferenced) { - connection.referencedInEventIds.forEach(id => connectedEventIds.add(id)); + connection.referencedInEventIds.forEach((id) => + connectedEventIds.add(id) + ); } eligible.push({ pubkey, connection, totalConnections, connectedEventIds }); @@ -192,22 +198,27 @@ export function createPersonAnchorNodes( height: number, showSignedBy: boolean, showReferenced: boolean, - limit: number = MAX_PERSON_NODES -): { nodes: NetworkNode[], totalCount: number } { + limit: number = MAX_PERSON_NODES, +): { nodes: NetworkNode[]; totalCount: number } { const anchorNodes: NetworkNode[] = []; const centerX = width / 2; const centerY = height / 2; // Calculate eligible persons and their connection counts - const eligiblePersons = getEligiblePersons(personMap, showSignedBy, showReferenced, limit); + const eligiblePersons = getEligiblePersons( + personMap, + showSignedBy, + showReferenced, + limit, + ); // Create nodes for the limited set - debug("Creating person anchor nodes", { - eligibleCount: eligiblePersons.length, + debug("Creating person anchor nodes", { + eligibleCount: eligiblePersons.length, limitedCount: eligiblePersons.length, showSignedBy, - showReferenced + showReferenced, }); eligiblePersons.forEach(({ pubkey, connection, connectedEventIds }) => { @@ -226,7 +237,8 @@ export function createPersonAnchorNodes( const anchorNode: NetworkNode = { id: `person-anchor-${pubkey}`, title: displayName, - content: `${connection.signedByEventIds.size} signed, ${connection.referencedInEventIds.size} referenced`, + content: + `${connection.signedByEventIds.size} signed, ${connection.referencedInEventIds.size} referenced`, author: "", kind: 0, // Special kind for anchors type: "PersonAnchor", @@ -245,11 +257,14 @@ export function createPersonAnchorNodes( anchorNodes.push(anchorNode); }); - debug("Created person anchor nodes", { count: anchorNodes.length, totalEligible: eligiblePersons.length }); + debug("Created person anchor nodes", { + count: anchorNodes.length, + totalEligible: eligiblePersons.length, + }); return { nodes: anchorNodes, - totalCount: eligiblePersons.length + totalCount: eligiblePersons.length, }; } @@ -264,10 +279,13 @@ export interface PersonLink extends NetworkLink { export function createPersonLinks( personAnchors: NetworkNode[], nodes: NetworkNode[], - personMap: Map + personMap: Map, ): PersonLink[] { - debug("Creating person links", { anchorCount: personAnchors.length, nodeCount: nodes.length }); - + debug("Creating person links", { + anchorCount: personAnchors.length, + nodeCount: nodes.length, + }); + const nodeMap = new Map(nodes.map((n) => [n.id, n])); const links: PersonLink[] = personAnchors.flatMap((anchor) => { @@ -286,11 +304,11 @@ export function createPersonLinks( return undefined; } - let connectionType: 'signed-by' | 'referenced' | undefined; + let connectionType: "signed-by" | "referenced" | undefined; if (connection.signedByEventIds.has(nodeId)) { - connectionType = 'signed-by'; + connectionType = "signed-by"; } else if (connection.referencedInEventIds.has(nodeId)) { - connectionType = 'referenced'; + connectionType = "referenced"; } const link: PersonLink = { @@ -299,7 +317,7 @@ export function createPersonLinks( isSequential: false, connectionType, }; - + return link; }).filter((link): link is PersonLink => link !== undefined); // Remove undefineds and type guard }); @@ -324,9 +342,9 @@ export interface PersonAnchorInfo { */ export function extractPersonAnchorInfo( personAnchors: NetworkNode[], - personMap: Map + personMap: Map, ): PersonAnchorInfo[] { - return personAnchors.map(anchor => { + return personAnchors.map((anchor) => { const connection = personMap.get(anchor.pubkey || ""); return { pubkey: anchor.pubkey || "", @@ -336,4 +354,4 @@ export function extractPersonAnchorInfo( isFromFollowList: connection?.isFromFollowList || false, }; }); -} \ No newline at end of file +} diff --git a/src/lib/navigator/EventNetwork/utils/starForceSimulation.ts b/src/lib/navigator/EventNetwork/utils/starForceSimulation.ts index c22ac1d..0c6b76e 100644 --- a/src/lib/navigator/EventNetwork/utils/starForceSimulation.ts +++ b/src/lib/navigator/EventNetwork/utils/starForceSimulation.ts @@ -1,25 +1,25 @@ /** * Star Network Force Simulation - * + * * Custom force simulation optimized for star network layouts. * Provides stronger connections between star centers and their content nodes, * with specialized forces to maintain hierarchical structure. */ import * as d3 from "d3"; -import type { NetworkNode, NetworkLink } from "../types"; +import type { NetworkLink, NetworkNode } from "../types"; import type { Simulation } from "./forceSimulation"; import { createTagGravityForce } from "./tagNetworkBuilder"; // Configuration for star network forces -const STAR_CENTER_CHARGE = -300; // Stronger repulsion between star centers -const CONTENT_NODE_CHARGE = -50; // Weaker repulsion for content nodes -const STAR_LINK_STRENGTH = 0.5; // Moderate connection to star center +const STAR_CENTER_CHARGE = -300; // Stronger repulsion between star centers +const CONTENT_NODE_CHARGE = -50; // Weaker repulsion for content nodes +const STAR_LINK_STRENGTH = 0.5; // Moderate connection to star center const INTER_STAR_LINK_STRENGTH = 0.2; // Weaker connection between stars -const STAR_LINK_DISTANCE = 80; // Fixed distance from center to content -const INTER_STAR_DISTANCE = 200; // Distance between star centers -const CENTER_GRAVITY = 0.02; // Gentle pull toward canvas center -const STAR_CENTER_WEIGHT = 10; // Weight multiplier for star centers +const STAR_LINK_DISTANCE = 80; // Fixed distance from center to content +const INTER_STAR_DISTANCE = 200; // Distance between star centers +const CENTER_GRAVITY = 0.02; // Gentle pull toward canvas center +const STAR_CENTER_WEIGHT = 10; // Weight multiplier for star centers /** * Creates a custom force simulation for star networks @@ -28,15 +28,18 @@ export function createStarSimulation( nodes: NetworkNode[], links: NetworkLink[], width: number, - height: number + height: number, ): Simulation { // Create the simulation - const simulation = d3.forceSimulation(nodes) as any + const simulation = d3.forceSimulation(nodes) as any; simulation - .force("center", d3.forceCenter(width / 2, height / 2).strength(CENTER_GRAVITY)) + .force( + "center", + d3.forceCenter(width / 2, height / 2).strength(CENTER_GRAVITY), + ) .velocityDecay(0.2) // Lower decay for more responsive simulation - .alphaDecay(0.0001) // Much slower alpha decay to prevent freezing - .alphaMin(0.001); // Keep minimum energy to prevent complete freeze + .alphaDecay(0.0001) // Much slower alpha decay to prevent freezing + .alphaMin(0.001); // Keep minimum energy to prevent complete freeze // Custom charge force that varies by node type const chargeForce = d3.forceManyBody() @@ -91,9 +94,9 @@ export function createStarSimulation( // Custom radial force to keep content nodes around their star center simulation.force("radial", createRadialForce(nodes, links)); - + // Add tag gravity force if there are tag anchors - const hasTagAnchors = nodes.some(n => n.isTagAnchor); + const hasTagAnchors = nodes.some((n) => n.isTagAnchor); if (hasTagAnchors) { simulation.force("tagGravity", createTagGravityForce(nodes, links)); } @@ -122,9 +125,9 @@ function applyRadialForce( nodes: NetworkNode[], nodeToCenter: Map, targetDistance: number, - alpha: number + alpha: number, ): void { - nodes.forEach(node => { + nodes.forEach((node) => { if (node.kind === 30041) { const center = nodeToCenter.get(node.id); if ( @@ -157,7 +160,7 @@ function createRadialForce(nodes: NetworkNode[], links: NetworkLink[]): any { // Build a map of content nodes to their star centers const nodeToCenter = new Map(); - links.forEach(link => { + links.forEach((link) => { const source = link.source as NetworkNode; const target = link.target as NetworkNode; if (source.kind === 30040 && target.kind === 30041) { @@ -169,7 +172,7 @@ function createRadialForce(nodes: NetworkNode[], links: NetworkLink[]): any { applyRadialForce(nodes, nodeToCenter, STAR_LINK_DISTANCE, alpha); } - force.initialize = function(_: NetworkNode[]) { + force.initialize = function (_: NetworkNode[]) { nodes = _; }; @@ -183,14 +186,14 @@ export function applyInitialStarPositions( nodes: NetworkNode[], links: NetworkLink[], width: number, - height: number + height: number, ): void { // Group nodes by their star centers const starGroups = new Map(); const starCenters: NetworkNode[] = []; - + // Identify star centers - nodes.forEach(node => { + nodes.forEach((node) => { if (node.isContainer && node.kind === 30040) { starCenters.push(node); starGroups.set(node.id, []); @@ -198,7 +201,7 @@ export function applyInitialStarPositions( }); // Assign content nodes to their star centers - links.forEach(link => { + links.forEach((link) => { const source = link.source as NetworkNode; const target = link.target as NetworkNode; if (source.kind === 30040 && target.kind === 30041) { @@ -222,7 +225,7 @@ export function applyInitialStarPositions( const centerY = height / 2; const radius = Math.min(width, height) * 0.3; const angleStep = (2 * Math.PI) / starCenters.length; - + starCenters.forEach((center, i) => { const angle = i * angleStep; center.x = centerX + radius * Math.cos(angle); @@ -233,9 +236,9 @@ export function applyInitialStarPositions( // Position content nodes around their star centers starGroups.forEach((contentNodes, centerId) => { - const center = nodes.find(n => n.id === centerId); + const center = nodes.find((n) => n.id === centerId); if (!center) return; - + const angleStep = (2 * Math.PI) / Math.max(contentNodes.length, 1); contentNodes.forEach((node, i) => { const angle = i * angleStep; @@ -252,7 +255,11 @@ export function applyInitialStarPositions( * @param d - The node being dragged * @param simulation - The d3 force simulation instance */ -function dragstarted(event: any, d: NetworkNode, simulation: Simulation) { +function dragstarted( + event: any, + d: NetworkNode, + simulation: Simulation, +) { // If no other drag is active, set a low alpha target to keep the simulation running smoothly if (!event.active) { simulation.alphaTarget(0.1).restart(); @@ -281,7 +288,11 @@ function dragged(event: any, d: NetworkNode) { * @param d - The node being dragged * @param simulation - The d3 force simulation instance */ -function dragended(event: any, d: NetworkNode, simulation: Simulation) { +function dragended( + event: any, + d: NetworkNode, + simulation: Simulation, +) { // If no other drag is active, lower the alpha target to let the simulation cool down if (!event.active) { simulation.alphaTarget(0); @@ -297,12 +308,16 @@ function dragended(event: any, d: NetworkNode, simulation: Simulation + simulation: Simulation, ): any { // These handlers are now top-level functions, so we use closures to pass simulation to them. // This is a common pattern in JavaScript/TypeScript when you need to pass extra arguments to event handlers. return d3.drag() - .on('start', function(event: any, d: NetworkNode) { dragstarted(event, d, simulation); }) - .on('drag', dragged) - .on('end', function(event: any, d: NetworkNode) { dragended(event, d, simulation); }); -} \ No newline at end of file + .on("start", function (event: any, d: NetworkNode) { + dragstarted(event, d, simulation); + }) + .on("drag", dragged) + .on("end", function (event: any, d: NetworkNode) { + dragended(event, d, simulation); + }); +} diff --git a/src/lib/navigator/EventNetwork/utils/starNetworkBuilder.ts b/src/lib/navigator/EventNetwork/utils/starNetworkBuilder.ts index 9f41031..cbcbc70 100644 --- a/src/lib/navigator/EventNetwork/utils/starNetworkBuilder.ts +++ b/src/lib/navigator/EventNetwork/utils/starNetworkBuilder.ts @@ -1,19 +1,23 @@ /** * Star Network Builder for NKBIP-01 Events - * + * * This module provides utilities for building star network visualizations specifically * for NKBIP-01 events (kinds 30040 and 30041). Unlike the sequential network builder, - * this creates star formations where index events (30040) are central nodes with + * this creates star formations where index events (30040) are central nodes with * content events (30041) arranged around them. */ import type { NDKEvent } from "@nostr-dev-kit/ndk"; -import type { NetworkNode, NetworkLink, GraphData, GraphState } from "../types"; -import { getMatchingTags } from '$lib/utils/nostrUtils'; -import { createNetworkNode, createEventMap, extractEventIdFromATag, getEventColor } from './networkBuilder'; -import { createDebugFunction } from './common'; -import { wikiKind, indexKind, zettelKinds } from '$lib/consts'; - +import type { GraphData, GraphState, NetworkLink, NetworkNode } from "../types"; +import { getMatchingTags } from "$lib/utils/nostrUtils"; +import { + createEventMap, + createNetworkNode, + extractEventIdFromATag, + getEventColor, +} from "./networkBuilder"; +import { createDebugFunction } from "./common"; +import { indexKind, wikiKind, zettelKinds } from "$lib/consts"; // Debug function const debug = createDebugFunction("StarNetworkBuilder"); @@ -22,14 +26,14 @@ const debug = createDebugFunction("StarNetworkBuilder"); * Represents a star network with a central index node and peripheral content nodes */ export interface StarNetwork { - center: NetworkNode; // Central index node (30040) + center: NetworkNode; // Central index node (30040) peripheralNodes: NetworkNode[]; // Content nodes (30041) and connected indices (30040) - links: NetworkLink[]; // Links within this star + links: NetworkLink[]; // Links within this star } /** * Creates a star network from an index event and its references - * + * * @param indexEvent - The central index event (30040) * @param state - Current graph state * @param level - Hierarchy level for this star @@ -38,10 +42,10 @@ export interface StarNetwork { export function createStarNetwork( indexEvent: NDKEvent, state: GraphState, - level: number = 0 + level: number = 0, ): StarNetwork | null { debug("Creating star network", { indexId: indexEvent.id, level }); - + const centerNode = state.nodeMap.get(indexEvent.id); if (!centerNode) { debug("Center node not found for index event", indexEvent.id); @@ -50,32 +54,35 @@ export function createStarNetwork( // Set the center node level centerNode.level = level; - + // Extract referenced event IDs from 'a' tags const referencedIds = getMatchingTags(indexEvent, "a") - .map(tag => extractEventIdFromATag(tag)) + .map((tag) => extractEventIdFromATag(tag)) .filter((id): id is string => id !== null); - debug("Found referenced IDs", { count: referencedIds.length, ids: referencedIds }); + debug("Found referenced IDs", { + count: referencedIds.length, + ids: referencedIds, + }); // Get peripheral nodes (both content and nested indices) const peripheralNodes: NetworkNode[] = []; const links: NetworkLink[] = []; - referencedIds.forEach(id => { + referencedIds.forEach((id) => { const node = state.nodeMap.get(id); if (node) { // Set the peripheral node level node.level += 1; peripheralNodes.push(node); - + // Create link from center to peripheral node links.push({ source: centerNode, target: node, - isSequential: false // Star links are not sequential + isSequential: false, // Star links are not sequential }); - + debug("Added peripheral node", { nodeId: id, nodeType: node.type }); } }); @@ -83,13 +90,13 @@ export function createStarNetwork( return { center: centerNode, peripheralNodes, - links + links, }; } /** * Processes all index events to create star networks - * + * * @param events - Array of all events * @param maxLevel - Maximum nesting level to process * @returns Array of star networks @@ -97,17 +104,17 @@ export function createStarNetwork( export function createStarNetworks( events: NDKEvent[], maxLevel: number, - existingNodeMap?: Map + existingNodeMap?: Map, ): StarNetwork[] { debug("Creating star networks", { eventCount: events.length, maxLevel }); - + // Use existing node map or create new one const nodeMap = existingNodeMap || new Map(); const eventMap = createEventMap(events); // Create nodes for all events if not using existing map if (!existingNodeMap) { - events.forEach(event => { + events.forEach((event) => { if (!event.id) return; const node = createNetworkNode(event); nodeMap.set(event.id, node); @@ -118,16 +125,16 @@ export function createStarNetworks( nodeMap, links: [], eventMap, - referencedIds: new Set() + referencedIds: new Set(), }; // Find all index events and non-publication events const publicationKinds = [wikiKind, indexKind, ...zettelKinds]; - const indexEvents = events.filter(event => event.kind === indexKind); - const nonPublicationEvents = events.filter(event => + const indexEvents = events.filter((event) => event.kind === indexKind); + const nonPublicationEvents = events.filter((event) => event.kind !== undefined && !publicationKinds.includes(event.kind) ); - + debug("Found index events", { count: indexEvents.length }); debug("Found non-publication events", { count: nonPublicationEvents.length }); @@ -135,34 +142,34 @@ export function createStarNetworks( const processedIndices = new Set(); // Process all index events regardless of level - indexEvents.forEach(indexEvent => { + indexEvents.forEach((indexEvent) => { if (!indexEvent.id || processedIndices.has(indexEvent.id)) return; const star = createStarNetwork(indexEvent, state, 0); if (star && star.peripheralNodes.length > 0) { starNetworks.push(star); processedIndices.add(indexEvent.id); - debug("Created star network", { - centerId: star.center.id, - peripheralCount: star.peripheralNodes.length + debug("Created star network", { + centerId: star.center.id, + peripheralCount: star.peripheralNodes.length, }); } }); - + // Add non-publication events as standalone nodes (stars with no peripherals) - nonPublicationEvents.forEach(event => { + nonPublicationEvents.forEach((event) => { if (!event.id || !nodeMap.has(event.id)) return; - + const node = nodeMap.get(event.id)!; const star: StarNetwork = { center: node, peripheralNodes: [], - links: [] + links: [], }; starNetworks.push(star); - debug("Created standalone star for non-publication event", { + debug("Created standalone star for non-publication event", { eventId: event.id, - kind: event.kind + kind: event.kind, }); }); @@ -171,36 +178,40 @@ export function createStarNetworks( /** * Creates inter-star connections between star networks - * + * * @param starNetworks - Array of star networks * @returns Additional links connecting different star networks */ -export function createInterStarConnections(starNetworks: StarNetwork[]): NetworkLink[] { +export function createInterStarConnections( + starNetworks: StarNetwork[], +): NetworkLink[] { debug("Creating inter-star connections", { starCount: starNetworks.length }); - + const interStarLinks: NetworkLink[] = []; - + // Create a map of center nodes for quick lookup const centerNodeMap = new Map(); - starNetworks.forEach(star => { + starNetworks.forEach((star) => { centerNodeMap.set(star.center.id, star.center); }); // For each star, check if any of its peripheral nodes are centers of other stars - starNetworks.forEach(star => { - star.peripheralNodes.forEach(peripheralNode => { + starNetworks.forEach((star) => { + star.peripheralNodes.forEach((peripheralNode) => { // If this peripheral node is the center of another star, create an inter-star link if (peripheralNode.isContainer && centerNodeMap.has(peripheralNode.id)) { - const targetStar = starNetworks.find(s => s.center.id === peripheralNode.id); + const targetStar = starNetworks.find((s) => + s.center.id === peripheralNode.id + ); if (targetStar) { interStarLinks.push({ source: star.center, target: targetStar.center, - isSequential: false + isSequential: false, }); - debug("Created inter-star connection", { - from: star.center.id, - to: targetStar.center.id + debug("Created inter-star connection", { + from: star.center.id, + to: targetStar.center.id, }); } } @@ -212,7 +223,7 @@ export function createInterStarConnections(starNetworks: StarNetwork[]): Network /** * Applies star-specific positioning to nodes using a radial layout - * + * * @param starNetworks - Array of star networks * @param width - Canvas width * @param height - Canvas height @@ -220,61 +231,62 @@ export function createInterStarConnections(starNetworks: StarNetwork[]): Network export function applyStarLayout( starNetworks: StarNetwork[], width: number, - height: number + height: number, ): void { - debug("Applying star layout", { - starCount: starNetworks.length, - dimensions: { width, height } + debug("Applying star layout", { + starCount: starNetworks.length, + dimensions: { width, height }, }); const centerX = width / 2; const centerY = height / 2; - + // If only one star, center it if (starNetworks.length === 1) { const star = starNetworks[0]; - + // Position center node star.center.x = centerX; star.center.y = centerY; star.center.fx = centerX; // Fix center position star.center.fy = centerY; - + // Position peripheral nodes in a circle around center const radius = Math.min(width, height) * 0.25; const angleStep = (2 * Math.PI) / star.peripheralNodes.length; - + star.peripheralNodes.forEach((node, index) => { const angle = index * angleStep; node.x = centerX + radius * Math.cos(angle); node.y = centerY + radius * Math.sin(angle); }); - + return; } // For multiple stars, arrange them in a grid or circle const starsPerRow = Math.ceil(Math.sqrt(starNetworks.length)); const starSpacingX = width / (starsPerRow + 1); - const starSpacingY = height / (Math.ceil(starNetworks.length / starsPerRow) + 1); + const starSpacingY = height / + (Math.ceil(starNetworks.length / starsPerRow) + 1); starNetworks.forEach((star, index) => { const row = Math.floor(index / starsPerRow); const col = index % starsPerRow; - + const starCenterX = (col + 1) * starSpacingX; const starCenterY = (row + 1) * starSpacingY; - + // Position center node star.center.x = starCenterX; star.center.y = starCenterY; star.center.fx = starCenterX; // Fix center position star.center.fy = starCenterY; - + // Position peripheral nodes around this star's center const radius = Math.min(starSpacingX, starSpacingY) * 0.3; const angleStep = (2 * Math.PI) / Math.max(star.peripheralNodes.length, 1); - + star.peripheralNodes.forEach((node, nodeIndex) => { const angle = nodeIndex * angleStep; node.x = starCenterX + radius * Math.cos(angle); @@ -285,69 +297,69 @@ export function applyStarLayout( /** * Generates a complete star network graph from events - * + * * @param events - Array of Nostr events * @param maxLevel - Maximum hierarchy level to process * @returns Complete graph data with star network layout */ export function generateStarGraph( events: NDKEvent[], - maxLevel: number + maxLevel: number, ): GraphData { debug("Generating star graph", { eventCount: events.length, maxLevel }); - + // Guard against empty events if (!events || events.length === 0) { return { nodes: [], links: [] }; } - + // Initialize all nodes first const nodeMap = new Map(); - events.forEach(event => { + events.forEach((event) => { if (!event.id) return; const node = createNetworkNode(event); nodeMap.set(event.id, node); }); - + // Create star networks with the existing node map const starNetworks = createStarNetworks(events, maxLevel, nodeMap); - + // Create inter-star connections const interStarLinks = createInterStarConnections(starNetworks); - + // Collect nodes that are part of stars const nodesInStars = new Set(); const allLinks: NetworkLink[] = []; - + // Add nodes and links from all stars - starNetworks.forEach(star => { + starNetworks.forEach((star) => { nodesInStars.add(star.center.id); - star.peripheralNodes.forEach(node => { + star.peripheralNodes.forEach((node) => { nodesInStars.add(node.id); }); allLinks.push(...star.links); }); - + // Add inter-star links allLinks.push(...interStarLinks); - + // Include orphaned nodes (those not in any star) const allNodes: NetworkNode[] = []; nodeMap.forEach((node, id) => { allNodes.push(node); }); - + const result = { nodes: allNodes, - links: allLinks + links: allLinks, }; - - debug("Star graph generation complete", { - nodeCount: result.nodes.length, + + debug("Star graph generation complete", { + nodeCount: result.nodes.length, linkCount: result.links.length, starCount: starNetworks.length, - orphanedNodes: allNodes.length - nodesInStars.size + orphanedNodes: allNodes.length - nodesInStars.size, }); - + return result; -} \ No newline at end of file +} diff --git a/src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts b/src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts index d4e28c4..e0eb13b 100644 --- a/src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts +++ b/src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts @@ -6,9 +6,9 @@ */ import type { NDKEvent } from "@nostr-dev-kit/ndk"; -import type { NetworkNode, NetworkLink, GraphData } from "../types"; +import type { GraphData, NetworkLink, NetworkNode } from "../types"; import { getDisplayNameSync } from "$lib/utils/profileCache"; -import { SeededRandom, createDebugFunction } from "./common"; +import { createDebugFunction, SeededRandom } from "./common"; // Configuration const TAG_ANCHOR_RADIUS = 15; @@ -18,7 +18,6 @@ const TAG_ANCHOR_PLACEMENT_RADIUS = 1250; // Radius from center within which to // Debug function const debug = createDebugFunction("TagNetworkBuilder"); - /** * Creates a deterministic seed from a string */ @@ -63,7 +62,10 @@ export function extractUniqueTagsForType( ): Map> { // Map of tagValue -> Set of event IDs const tagMap = new Map>(); - debug("Extracting unique tags for type", { tagType, eventCount: events.length }); + debug("Extracting unique tags for type", { + tagType, + eventCount: events.length, + }); events.forEach((event) => { if (!event.tags || !event.id) return; @@ -83,7 +85,7 @@ export function extractUniqueTagsForType( tagMap.get(tagValue)!.add(event.id); }); }); - + debug("Extracted tags", { tagCount: tagMap.size }); return tagMap; @@ -110,7 +112,7 @@ export function createTagAnchorNodes( ); if (validTags.length === 0) return []; - + // Sort all tags by number of connections (events) descending validTags.sort((a, b) => b[1].size - a[1].size); @@ -172,8 +174,11 @@ export function createTagLinks( tagAnchors: NetworkNode[], nodes: NetworkNode[], ): NetworkLink[] { - debug("Creating tag links", { anchorCount: tagAnchors.length, nodeCount: nodes.length }); - + debug("Creating tag links", { + anchorCount: tagAnchors.length, + nodeCount: nodes.length, + }); + const links: NetworkLink[] = []; const nodeMap = new Map(nodes.map((n) => [n.id, n])); @@ -208,13 +213,13 @@ export function enhanceGraphWithTags( displayLimit?: number, ): GraphData { debug("Enhancing graph with tags", { tagType, displayLimit }); - + // Extract unique tags for the specified type const tagMap = extractUniqueTagsForType(events, tagType); // Create tag anchor nodes let tagAnchors = createTagAnchorNodes(tagMap, tagType, width, height); - + // Apply display limit if provided if (displayLimit && displayLimit > 0 && tagAnchors.length > displayLimit) { // Sort by connection count (already done in createTagAnchorNodes) @@ -242,7 +247,7 @@ export function enhanceGraphWithTags( export function applyTagGravity( nodes: NetworkNode[], nodeToAnchors: Map, - alpha: number + alpha: number, ): void { nodes.forEach((node) => { if (node.isTagAnchor) return; // Tag anchors don't move @@ -301,7 +306,7 @@ export function createTagGravityForce( }); debug("Creating tag gravity force"); - + function force(alpha: number) { applyTagGravity(nodes, nodeToAnchors, alpha); } diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index fed11c6..2ac9bd3 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -1,27 +1,27 @@ import NDK, { + NDKEvent, NDKNip07Signer, NDKRelay, NDKRelayAuthPolicies, NDKRelaySet, NDKUser, - NDKEvent, } from "@nostr-dev-kit/ndk"; -import { writable, get, type Writable } from "svelte/store"; -import { - loginStorageKey, - anonymousRelays, -} from "./consts.ts"; +import { get, type Writable, writable } from "svelte/store"; +import { anonymousRelays, loginStorageKey } from "./consts.ts"; import { buildCompleteRelaySet, - testRelayConnection, deduplicateRelayUrls, + testRelayConnection, } from "./utils/relay_management.ts"; // Re-export testRelayConnection for components that need it export { testRelayConnection }; import { userStore } from "./stores/userStore.ts"; import { userPubkey } from "./stores/authStore.Svelte.ts"; -import { startNetworkStatusMonitoring, stopNetworkStatusMonitoring } from "./stores/networkStore.ts"; +import { + startNetworkStatusMonitoring, + stopNetworkStatusMonitoring, +} from "./stores/networkStore.ts"; import { WebSocketPool } from "./data_structures/websocket_pool.ts"; export const ndkInstance: Writable = writable(); @@ -35,34 +35,39 @@ export const activeInboxRelays = writable([]); export const activeOutboxRelays = writable([]); // AI-NOTE: 2025-01-08 - Persistent relay storage to avoid recalculation -let persistentRelaySet: { inboxRelays: string[]; outboxRelays: string[] } | null = null; +let persistentRelaySet: + | { inboxRelays: string[]; outboxRelays: string[] } + | null = null; let relaySetLastUpdated: number = 0; const RELAY_SET_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes -const RELAY_SET_STORAGE_KEY = 'alexandria/relay_set_cache'; +const RELAY_SET_STORAGE_KEY = "alexandria/relay_set_cache"; /** * Load persistent relay set from localStorage */ -function loadPersistentRelaySet(): { relaySet: { inboxRelays: string[]; outboxRelays: string[] } | null; lastUpdated: number } { +function loadPersistentRelaySet(): { + relaySet: { inboxRelays: string[]; outboxRelays: string[] } | null; + lastUpdated: number; +} { // Only load from localStorage on client-side - if (typeof window === 'undefined') return { relaySet: null, lastUpdated: 0 }; - + if (typeof window === "undefined") return { relaySet: null, lastUpdated: 0 }; + try { const stored = localStorage.getItem(RELAY_SET_STORAGE_KEY); if (!stored) return { relaySet: null, lastUpdated: 0 }; - + const data = JSON.parse(stored); const now = Date.now(); - + // Check if cache is expired if (now - data.timestamp > RELAY_SET_CACHE_DURATION) { localStorage.removeItem(RELAY_SET_STORAGE_KEY); return { relaySet: null, lastUpdated: 0 }; } - + return { relaySet: data.relaySet, lastUpdated: data.timestamp }; } catch (error) { - console.warn('[NDK.ts] Failed to load persistent relay set:', error); + console.warn("[NDK.ts] Failed to load persistent relay set:", error); localStorage.removeItem(RELAY_SET_STORAGE_KEY); return { relaySet: null, lastUpdated: 0 }; } @@ -71,18 +76,20 @@ function loadPersistentRelaySet(): { relaySet: { inboxRelays: string[]; outboxRe /** * Save persistent relay set to localStorage */ -function savePersistentRelaySet(relaySet: { inboxRelays: string[]; outboxRelays: string[] }): void { +function savePersistentRelaySet( + relaySet: { inboxRelays: string[]; outboxRelays: string[] }, +): void { // Only save to localStorage on client-side - if (typeof window === 'undefined') return; - + if (typeof window === "undefined") return; + try { const data = { relaySet, - timestamp: Date.now() + timestamp: Date.now(), }; localStorage.setItem(RELAY_SET_STORAGE_KEY, JSON.stringify(data)); } catch (error) { - console.warn('[NDK.ts] Failed to save persistent relay set:', error); + console.warn("[NDK.ts] Failed to save persistent relay set:", error); } } @@ -91,12 +98,12 @@ function savePersistentRelaySet(relaySet: { inboxRelays: string[]; outboxRelays: */ function clearPersistentRelaySet(): void { // Only clear from localStorage on client-side - if (typeof window === 'undefined') return; - + if (typeof window === "undefined") return; + try { localStorage.removeItem(RELAY_SET_STORAGE_KEY); } catch (error) { - console.warn('[NDK.ts] Failed to clear persistent relay set:', error); + console.warn("[NDK.ts] Failed to clear persistent relay set:", error); } } @@ -230,8 +237,7 @@ class CustomRelayAuthPolicy { export function checkEnvironmentForWebSocketDowngrade(): void { console.debug("[NDK.ts] Environment Check for WebSocket Protocol:"); - const isLocalhost = - globalThis.location.hostname === "localhost" || + const isLocalhost = globalThis.location.hostname === "localhost" || globalThis.location.hostname === "127.0.0.1"; const isHttp = globalThis.location.protocol === "http:"; const isHttps = globalThis.location.protocol === "https:"; @@ -281,8 +287,6 @@ export function checkWebSocketSupport(): void { } } - - /** * Gets the user's pubkey from local storage, if it exists. * @returns The user's pubkey, or null if there is no logged-in user. @@ -291,8 +295,8 @@ export function checkWebSocketSupport(): void { */ export function getPersistedLogin(): string | null { // Only access localStorage on client-side - if (typeof window === 'undefined') return null; - + if (typeof window === "undefined") return null; + const pubkey = localStorage.getItem(loginStorageKey); return pubkey; } @@ -305,8 +309,8 @@ export function getPersistedLogin(): string | null { */ export function persistLogin(user: NDKUser): void { // Only access localStorage on client-side - if (typeof window === 'undefined') return; - + if (typeof window === "undefined") return; + localStorage.setItem(loginStorageKey, user.pubkey); } @@ -316,8 +320,8 @@ export function persistLogin(user: NDKUser): void { */ export function clearLogin(): void { // Only access localStorage on client-side - if (typeof window === 'undefined') return; - + if (typeof window === "undefined") return; + localStorage.removeItem(loginStorageKey); } @@ -333,8 +337,8 @@ function getRelayStorageKey(user: NDKUser, type: "inbox" | "outbox"): string { export function clearPersistedRelays(user: NDKUser): void { // Only access localStorage on client-side - if (typeof window === 'undefined') return; - + if (typeof window === "undefined") return; + localStorage.removeItem(getRelayStorageKey(user, "inbox")); localStorage.removeItem(getRelayStorageKey(user, "outbox")); } @@ -346,11 +350,11 @@ export function clearPersistedRelays(user: NDKUser): void { */ function ensureSecureWebSocket(url: string): string { // For localhost, always use ws:// (never wss://) - if (url.includes('localhost') || url.includes('127.0.0.1')) { + 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://"); @@ -369,7 +373,7 @@ function ensureSecureWebSocket(url: string): string { function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { try { // Reduce verbosity in development - only log relay creation if debug mode is enabled - if (process.env.NODE_ENV === 'development' && process.env.DEBUG_RELAYS) { + if (process.env.NODE_ENV === "development" && process.env.DEBUG_RELAYS) { console.debug(`[NDK.ts] Creating relay with URL: ${url}`); } @@ -387,7 +391,9 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { const connectionTimeout = setTimeout(() => { try { // Only log connection timeouts if debug mode is enabled - if (process.env.NODE_ENV === 'development' && process.env.DEBUG_RELAYS) { + if ( + process.env.NODE_ENV === "development" && process.env.DEBUG_RELAYS + ) { console.debug(`[NDK.ts] Connection timeout for ${secureUrl}`); } relay.disconnect(); @@ -402,7 +408,9 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { relay.on("connect", () => { try { // Only log successful connections if debug mode is enabled - if (process.env.NODE_ENV === 'development' && process.env.DEBUG_RELAYS) { + if ( + process.env.NODE_ENV === "development" && process.env.DEBUG_RELAYS + ) { console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); } clearTimeout(connectionTimeout); @@ -415,7 +423,9 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { relay.on("connect", () => { try { // Only log successful connections if debug mode is enabled - if (process.env.NODE_ENV === 'development' && process.env.DEBUG_RELAYS) { + if ( + process.env.NODE_ENV === "development" && process.env.DEBUG_RELAYS + ) { console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); } clearTimeout(connectionTimeout); @@ -438,46 +448,66 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { return relay; } catch (error) { // If relay creation fails, try to use an anonymous relay as fallback - console.debug(`[NDK.ts] Failed to create relay for ${url}, trying anonymous relay fallback`); - + console.debug( + `[NDK.ts] Failed to create relay for ${url}, trying anonymous relay fallback`, + ); + // Find an anonymous relay that's not the same as the failed URL - const fallbackUrl = anonymousRelays.find(relay => relay !== url) || anonymousRelays[0]; - + const fallbackUrl = anonymousRelays.find((relay) => relay !== url) || + anonymousRelays[0]; + if (fallbackUrl) { - console.debug(`[NDK.ts] Using anonymous relay as fallback: ${fallbackUrl}`); + console.debug( + `[NDK.ts] Using anonymous relay as fallback: ${fallbackUrl}`, + ); try { - const fallbackRelay = new NDKRelay(fallbackUrl, NDKRelayAuthPolicies.signIn({ ndk }), ndk); + const fallbackRelay = new NDKRelay( + fallbackUrl, + NDKRelayAuthPolicies.signIn({ ndk }), + ndk, + ); return fallbackRelay; } catch (fallbackError) { - console.debug(`[NDK.ts] Fallback relay creation also failed: ${fallbackError}`); + console.debug( + `[NDK.ts] Fallback relay creation also failed: ${fallbackError}`, + ); } } - + // If all else fails, create a minimal relay that will fail gracefully - console.debug(`[NDK.ts] All fallback attempts failed, creating minimal relay for ${url}`); + console.debug( + `[NDK.ts] All fallback attempts failed, creating minimal relay for ${url}`, + ); const minimalRelay = new NDKRelay(url, undefined, ndk); return minimalRelay; } } - - - - /** * 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[] }> { +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 }); - + 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); + 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'); + console.debug( + "[NDK.ts] getActiveRelaySet: Building relay set for anonymous user", + ); return await buildCompleteRelaySet(ndk, null); } } @@ -487,61 +517,88 @@ export async function getActiveRelaySet(ndk: NDK): Promise<{ inboxRelays: string * @param ndk NDK instance * @param forceUpdate Force update even if cached (default: false) */ -export async function updateActiveRelayStores(ndk: NDK, forceUpdate: boolean = false): Promise { +export async function updateActiveRelayStores( + ndk: NDK, + forceUpdate: boolean = false, +): Promise { try { // AI-NOTE: 2025-01-08 - Use persistent relay set to avoid recalculation const now = Date.now(); const cacheExpired = now - relaySetLastUpdated > RELAY_SET_CACHE_DURATION; - + // Load from persistent storage if not already loaded if (!persistentRelaySet) { const loaded = loadPersistentRelaySet(); persistentRelaySet = loaded.relaySet; relaySetLastUpdated = loaded.lastUpdated; } - + if (!forceUpdate && persistentRelaySet && !cacheExpired) { - console.debug('[NDK.ts] updateActiveRelayStores: Using cached relay set'); + console.debug("[NDK.ts] updateActiveRelayStores: Using cached relay set"); activeInboxRelays.set(persistentRelaySet.inboxRelays); activeOutboxRelays.set(persistentRelaySet.outboxRelays); return; } - - console.debug('[NDK.ts] updateActiveRelayStores: Starting relay store update'); - + + 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); - + console.debug("[NDK.ts] updateActiveRelayStores: Got relay set:", relaySet); + // Cache the relay set persistentRelaySet = relaySet; relaySetLastUpdated = now; savePersistentRelaySet(relaySet); // Save to persistent storage - + // 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); - + 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]); + const allRelayUrls = deduplicateRelayUrls([ + ...relaySet.inboxRelays, + ...relaySet.outboxRelays, + ]); // Reduce verbosity in development - only log relay addition if debug mode is enabled - if (process.env.NODE_ENV === 'development' && process.env.DEBUG_RELAYS) { - console.debug('[NDK.ts] updateActiveRelayStores: Adding', allRelayUrls.length, 'relays to NDK pool'); + if (process.env.NODE_ENV === "development" && process.env.DEBUG_RELAYS) { + 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) { - console.debug('[NDK.ts] updateActiveRelayStores: Failed to add relay', url, ':', error); + console.debug( + "[NDK.ts] updateActiveRelayStores: Failed to add relay", + url, + ":", + error, + ); } } - - console.debug('[NDK.ts] updateActiveRelayStores: Relay store update completed'); + + console.debug( + "[NDK.ts] updateActiveRelayStores: Relay store update completed", + ); } catch (error) { - console.warn('[NDK.ts] updateActiveRelayStores: Error updating relay stores:', error); + console.warn( + "[NDK.ts] updateActiveRelayStores: Error updating relay stores:", + error, + ); } } @@ -551,23 +608,25 @@ export async function updateActiveRelayStores(ndk: NDK, forceUpdate: boolean = f 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`); + + console.log("🔌 Current Relay Configuration:"); + console.log("📥 Inbox Relays:", inboxRelays); + console.log("📤 Outbox Relays:", outboxRelays); + console.log( + `📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`, + ); } /** * Clears the relay set cache to force a rebuild */ export function clearRelaySetCache(): void { - console.debug('[NDK.ts] Clearing relay set cache'); + console.debug("[NDK.ts] Clearing relay set cache"); persistentRelaySet = null; relaySetLastUpdated = 0; // Clear from localStorage as well (client-side only) - if (typeof window !== 'undefined') { - localStorage.removeItem('alexandria/relay_set_cache'); + if (typeof window !== "undefined") { + localStorage.removeItem("alexandria/relay_set_cache"); } } @@ -576,7 +635,7 @@ export function clearRelaySetCache(): void { * @param ndk NDK instance */ export async function refreshRelayStores(ndk: NDK): Promise { - console.debug('[NDK.ts] Refreshing relay stores due to user state change'); + console.debug("[NDK.ts] Refreshing relay stores due to user state change"); clearRelaySetCache(); // Clear cache when user state changes await updateActiveRelayStores(ndk, true); // Force update } @@ -585,8 +644,12 @@ export async function refreshRelayStores(ndk: NDK): Promise { * 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'); +export async function refreshRelayStoresOnNetworkChange( + ndk: NDK, +): Promise { + console.debug( + "[NDK.ts] Refreshing relay stores due to network condition change", + ); await updateActiveRelayStores(ndk); } @@ -606,10 +669,10 @@ export function startNetworkMonitoringForRelays(): void { * @returns NDKRelaySet */ function createRelaySetFromUrls(relayUrls: string[], ndk: NDK): NDKRelaySet { - const relays = relayUrls.map(url => + const relays = relayUrls.map((url) => new NDKRelay(url, NDKRelayAuthPolicies.signIn({ ndk }), ndk) ); - + return new NDKRelaySet(new Set(relays), ndk); } @@ -621,11 +684,11 @@ function createRelaySetFromUrls(relayUrls: string[], ndk: NDK): NDKRelaySet { */ export async function getActiveRelaySetAsNDKRelaySet( ndk: NDK, - useInbox: boolean = true + useInbox: boolean = true, ): Promise { const relaySet = await getActiveRelaySet(ndk); const urls = useInbox ? relaySet.inboxRelays : relaySet.outboxRelays; - + return createRelaySetFromUrls(urls, ndk); } @@ -650,11 +713,11 @@ export function initNdk(): NDK { const attemptConnection = async () => { // Only attempt connection on client-side - if (typeof window === 'undefined') { + if (typeof window === "undefined") { console.debug("[NDK.ts] Skipping NDK connection during SSR"); return; } - + try { await ndk.connect(); console.debug("[NDK.ts] NDK connected successfully"); @@ -664,17 +727,21 @@ export function initNdk(): NDK { startNetworkMonitoringForRelays(); } 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})...`); + console.debug( + `[NDK.ts] Attempting to reconnect (${retryCount}/${maxRetries})...`, + ); // Use a more reasonable retry delay and prevent memory leaks setTimeout(() => { attemptConnection(); }, 2000 * retryCount); // Exponential backoff } else { - console.warn("[NDK.ts] Max retries reached, continuing with limited functionality"); + 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); @@ -687,21 +754,24 @@ export function initNdk(): NDK { }; // Only attempt connection on client-side - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { attemptConnection(); } // AI-NOTE: Set up userStore subscription after NDK initialization to prevent initialization errors userStore.subscribe(async (userState) => { ndkSignedIn.set(userState.signedIn); - + // Refresh relay stores when user state changes const ndk = get(ndkInstance); if (ndk) { try { await refreshRelayStores(ndk); } catch (error) { - console.warn('[NDK.ts] Failed to refresh relay stores on user state change:', error); + console.warn( + "[NDK.ts] Failed to refresh relay stores on user state change:", + error, + ); } } }); @@ -715,7 +785,7 @@ export function initNdk(): NDK { */ export function cleanupNdk(): void { console.debug("[NDK.ts] Cleaning up NDK resources"); - + const ndk = get(ndkInstance); if (ndk) { try { @@ -725,13 +795,13 @@ export function cleanupNdk(): void { relay.disconnect(); } } - + // Drain the WebSocket pool WebSocketPool.instance.drain(); - + // Stop network monitoring stopNetworkStatusMonitoring(); - + console.debug("[NDK.ts] NDK cleanup completed"); } catch (error) { console.warn("[NDK.ts] Error during NDK cleanup:", error); @@ -761,7 +831,7 @@ export async function loginWithExtension( userPubkey.set(signerUser.pubkey); const user = ndk.getUser({ pubkey: signerUser.pubkey }); - + // Update relay stores with the new system await updateActiveRelayStores(ndk); @@ -787,22 +857,20 @@ export function logout(user: NDKUser): void { activePubkey.set(null); userPubkey.set(null); ndkSignedIn.set(false); - + // Clear relay stores activeInboxRelays.set([]); activeOutboxRelays.set([]); - + // AI-NOTE: 2025-01-08 - Clear persistent relay set on logout persistentRelaySet = null; relaySetLastUpdated = 0; clearPersistentRelaySet(); // Clear persistent storage - + // Stop network monitoring stopNetworkStatusMonitoring(); - + // Re-initialize with anonymous instance const newNdk = initNdk(); ndkInstance.set(newNdk); } - - diff --git a/src/lib/parser.ts b/src/lib/parser.ts index 38c7b36..fc539de 100644 --- a/src/lib/parser.ts +++ b/src/lib/parser.ts @@ -7,11 +7,11 @@ import type { Block, Document, Extensions, - Section, ProcessorOptions, + Section, } from "asciidoctor"; import he from "he"; -import { writable, type Writable } from "svelte/store"; +import { type Writable, writable } from "svelte/store"; import { zettelKinds } from "./consts.ts"; import { getMatchingTags } from "./utils/nostrUtils.ts"; @@ -906,13 +906,13 @@ export default class Pharos { ["#d", nodeId], ...this.extractAndNormalizeWikilinks(content!), ]; - + // Extract image from content if present const imageUrl = this.extractImageFromContent(content!); if (imageUrl) { event.tags.push(["image", imageUrl]); } - + event.created_at = Date.now(); event.pubkey = pubkey; diff --git a/src/lib/services/event_search_service.ts b/src/lib/services/event_search_service.ts index 76ee6ca..649b694 100644 --- a/src/lib/services/event_search_service.ts +++ b/src/lib/services/event_search_service.ts @@ -8,33 +8,37 @@ export class EventSearchService { */ getSearchType(query: string): { type: string; term: string } | null { const lowerQuery = query.toLowerCase(); - + if (lowerQuery.startsWith("d:")) { const dTag = query.slice(2).trim().toLowerCase(); return dTag ? { type: "d", term: dTag } : null; } - + if (lowerQuery.startsWith("t:")) { const searchTerm = query.slice(2).trim(); return searchTerm ? { type: "t", term: searchTerm } : null; } - + if (lowerQuery.startsWith("n:")) { const searchTerm = query.slice(2).trim(); return searchTerm ? { type: "n", term: searchTerm } : null; } - + if (query.includes("@")) { return { type: "nip05", term: query }; } - + return null; } /** * Checks if a search value matches the current event */ - isCurrentEventMatch(searchValue: string, event: any, relays: string[]): boolean { + isCurrentEventMatch( + searchValue: string, + event: any, + relays: string[], + ): boolean { const currentEventId = event.id; let currentNaddr = null; let currentNevent = null; @@ -42,21 +46,23 @@ export class EventSearchService { let currentNprofile = null; try { - const { neventEncode, naddrEncode, nprofileEncode } = require("$lib/utils"); + const { neventEncode, naddrEncode, nprofileEncode } = require( + "$lib/utils", + ); const { getMatchingTags, toNpub } = require("$lib/utils/nostrUtils"); - + currentNevent = neventEncode(event, relays); } catch {} - + try { const { naddrEncode } = require("$lib/utils"); const { getMatchingTags } = require("$lib/utils/nostrUtils"); - + currentNaddr = getMatchingTags(event, "d")[0]?.[1] ? naddrEncode(event, relays) : null; } catch {} - + try { const { toNpub } = require("$lib/utils/nostrUtils"); currentNpub = event.kind === 0 ? toNpub(event.pubkey) : null; diff --git a/src/lib/services/publisher.ts b/src/lib/services/publisher.ts index 98b63f4..296ac22 100644 --- a/src/lib/services/publisher.ts +++ b/src/lib/services/publisher.ts @@ -1,8 +1,11 @@ import { get } from "svelte/store"; import { ndkInstance } from "../ndk.ts"; import { getMimeTags } from "../utils/mime.ts"; -import { parseAsciiDocWithMetadata, metadataToTags } from "../utils/asciidoc_metadata.ts"; -import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk"; +import { + metadataToTags, + parseAsciiDocWithMetadata, +} from "../utils/asciidoc_metadata.ts"; +import { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk"; import { nip19 } from "nostr-tools"; export interface PublishResult { @@ -97,8 +100,9 @@ export async function publishZettel( throw new Error("Failed to publish to any relays"); } } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; + const errorMessage = error instanceof Error + ? error.message + : "Unknown error"; onError?.(errorMessage); return { success: false, error: errorMessage }; } @@ -115,14 +119,14 @@ export async function publishMultipleZettels( const { content, kind = 30041, onError } = options; if (!content.trim()) { - const error = 'Please enter some content'; + const error = "Please enter some content"; onError?.(error); return [{ success: false, error }]; } const ndk = get(ndkInstance); if (!ndk?.activeUser) { - const error = 'Please log in first'; + const error = "Please log in first"; onError?.(error); return [{ success: false, error }]; } @@ -130,12 +134,14 @@ export async function publishMultipleZettels( try { const parsed = parseAsciiDocWithMetadata(content); if (parsed.sections.length === 0) { - throw new Error('No valid sections found in content'); + throw new Error("No valid sections found in content"); } - const allRelayUrls = Array.from(ndk.pool?.relays.values() || []).map((r) => r.url); + const allRelayUrls = Array.from(ndk.pool?.relays.values() || []).map((r) => + r.url + ); if (allRelayUrls.length === 0) { - throw new Error('No relays available in NDK pool'); + throw new Error("No relays available in NDK pool"); } const relaySet = NDKRelaySet.fromRelayUrls(allRelayUrls, ndk); @@ -164,31 +170,42 @@ export async function publishMultipleZettels( results.push({ success: true, eventId: ndkEvent.id }); publishedEvents.push(ndkEvent); } else { - results.push({ success: false, error: 'Failed to publish to any relays' }); + results.push({ + success: false, + error: "Failed to publish to any relays", + }); } } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Unknown error'; + const errorMessage = err instanceof Error + ? err.message + : "Unknown error"; results.push({ success: false, error: errorMessage }); } } // Debug: extract and log 'e' and 'a' tags from all published events - publishedEvents.forEach(ev => { + publishedEvents.forEach((ev) => { // Extract d-tag from tags - const dTagEntry = ev.tags.find(t => t[0] === 'd'); - const dTag = dTagEntry ? dTagEntry[1] : ''; + const dTagEntry = ev.tags.find((t) => t[0] === "d"); + const dTag = dTagEntry ? dTagEntry[1] : ""; const aTag = `${ev.kind}:${ev.pubkey}:${dTag}`; console.log(`Event ${ev.id} tags:`); - console.log(' e:', ev.id); - console.log(' a:', aTag); + console.log(" e:", ev.id); + console.log(" a:", aTag); // Print nevent and naddr using nip19 const nevent = nip19.neventEncode({ id: ev.id }); - const naddr = nip19.naddrEncode({ kind: ev.kind, pubkey: ev.pubkey, identifier: dTag }); - console.log(' nevent:', nevent); - console.log(' naddr:', naddr); + const naddr = nip19.naddrEncode({ + kind: ev.kind, + pubkey: ev.pubkey, + identifier: dTag, + }); + console.log(" nevent:", nevent); + console.log(" naddr:", naddr); }); return results; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + const errorMessage = error instanceof Error + ? error.message + : "Unknown error"; onError?.(errorMessage); return [{ success: false, error: errorMessage }]; } diff --git a/src/lib/services/search_state_manager.ts b/src/lib/services/search_state_manager.ts index d673b9d..10e2556 100644 --- a/src/lib/services/search_state_manager.ts +++ b/src/lib/services/search_state_manager.ts @@ -13,7 +13,7 @@ export class SearchStateManager { searchResultCount: number | null; searchResultType: string | null; }, - onLoadingChange?: (loading: boolean) => void + onLoadingChange?: (loading: boolean) => void, ): void { if (onLoadingChange) { onLoadingChange(state.searching); @@ -25,10 +25,16 @@ export class SearchStateManager { */ resetSearchState( callbacks: { - onSearchResults: (events: any[], secondOrder: any[], tTagEvents: any[], eventIds: Set, addresses: Set) => void; + onSearchResults: ( + events: any[], + secondOrder: any[], + tTagEvents: any[], + eventIds: Set, + addresses: Set, + ) => void; cleanupSearch: () => void; clearTimeout: () => void; - } + }, ): void { callbacks.cleanupSearch(); callbacks.onSearchResults([], [], [], new Set(), new Set()); @@ -46,16 +52,18 @@ export class SearchStateManager { cleanupSearch: () => void; updateSearchState: (state: any) => void; resetProcessingFlags: () => void; - } + }, ): void { - const errorMessage = error instanceof Error ? error.message : defaultMessage; + const errorMessage = error instanceof Error + ? error.message + : defaultMessage; callbacks.setLocalError(errorMessage); callbacks.cleanupSearch(); callbacks.updateSearchState({ searching: false, searchCompleted: false, searchResultCount: null, - searchResultType: null + searchResultType: null, }); callbacks.resetProcessingFlags(); } diff --git a/src/lib/state.ts b/src/lib/state.ts index ba4f8b4..37c5bbc 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -1,5 +1,5 @@ import { browser } from "$app/environment"; -import { writable, type Writable } from "svelte/store"; +import { type Writable, writable } from "svelte/store"; import type { Tab } from "./types.ts"; export const pathLoaded: Writable = writable(false); diff --git a/src/lib/stores/authStore.Svelte.ts b/src/lib/stores/authStore.Svelte.ts index 2ee771a..a29917b 100644 --- a/src/lib/stores/authStore.Svelte.ts +++ b/src/lib/stores/authStore.Svelte.ts @@ -1,4 +1,4 @@ -import { writable, derived } from "svelte/store"; +import { derived, writable } from "svelte/store"; /** * Stores the user's public key if logged in, or null otherwise. diff --git a/src/lib/stores/networkStore.ts b/src/lib/stores/networkStore.ts index 1c81a08..477a093 100644 --- a/src/lib/stores/networkStore.ts +++ b/src/lib/stores/networkStore.ts @@ -1,8 +1,14 @@ import { writable } from "svelte/store"; -import { detectNetworkCondition, NetworkCondition, startNetworkMonitoring } from '../utils/network_detection.ts'; +import { + detectNetworkCondition, + NetworkCondition, + startNetworkMonitoring, +} from "../utils/network_detection.ts"; // Network status store -export const networkCondition = writable(NetworkCondition.ONLINE); +export const networkCondition = writable( + NetworkCondition.ONLINE, +); export const isNetworkChecking = writable(false); // Network monitoring state @@ -16,14 +22,16 @@ export function startNetworkStatusMonitoring(): void { return; // Already monitoring } - console.debug('[networkStore.ts] Starting network status monitoring'); - + console.debug("[networkStore.ts] Starting network status monitoring"); + stopNetworkMonitoring = startNetworkMonitoring( (condition: NetworkCondition) => { - console.debug(`[networkStore.ts] Network condition changed to: ${condition}`); + console.debug( + `[networkStore.ts] Network condition changed to: ${condition}`, + ); networkCondition.set(condition); }, - 60000 // Check every 60 seconds to reduce spam + 60000, // Check every 60 seconds to reduce spam ); } @@ -32,7 +40,7 @@ export function startNetworkStatusMonitoring(): void { */ export function stopNetworkStatusMonitoring(): void { if (stopNetworkMonitoring) { - console.debug('[networkStore.ts] Stopping network status monitoring'); + console.debug("[networkStore.ts] Stopping network status monitoring"); stopNetworkMonitoring(); stopNetworkMonitoring = null; } @@ -47,9 +55,9 @@ export async function checkNetworkStatus(): Promise { const condition = await detectNetworkCondition(); networkCondition.set(condition); } catch (error) { - console.warn('[networkStore.ts] Failed to check network status:', 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/userStore.ts b/src/lib/stores/userStore.ts index 0b96dd0..8a7a405 100644 --- a/src/lib/stores/userStore.ts +++ b/src/lib/stores/userStore.ts @@ -1,14 +1,19 @@ -import { writable, get } from "svelte/store"; +import { get, writable } from "svelte/store"; import type { NostrProfile } from "../utils/nostrUtils.ts"; -import type { NDKUser, NDKSigner } from "@nostr-dev-kit/ndk"; +import type { NDKSigner, NDKUser } from "@nostr-dev-kit/ndk"; import NDK, { NDKNip07Signer, + NDKRelay, NDKRelayAuthPolicies, NDKRelaySet, - NDKRelay, } from "@nostr-dev-kit/ndk"; import { getUserMetadata } from "../utils/nostrUtils.ts"; -import { ndkInstance, activeInboxRelays, activeOutboxRelays, updateActiveRelayStores } from "../ndk.ts"; +import { + activeInboxRelays, + activeOutboxRelays, + ndkInstance, + updateActiveRelayStores, +} from "../ndk.ts"; import { loginStorageKey } from "../consts.ts"; import { nip19 } from "nostr-tools"; import { userPubkey } from "../stores/authStore.Svelte.ts"; @@ -46,8 +51,8 @@ function persistRelays( outboxes: Set, ): void { // Only access localStorage on client-side - if (typeof window === 'undefined') return; - + if (typeof window === "undefined") return; + localStorage.setItem( getRelayStorageKey(user, "inbox"), JSON.stringify(Array.from(inboxes).map((relay) => relay.url)), @@ -60,10 +65,10 @@ function persistRelays( function getPersistedRelays(user: NDKUser): [Set, Set] { // Only access localStorage on client-side - if (typeof window === 'undefined') { + if (typeof window === "undefined") { return [new Set(), new Set()]; } - + const inboxes = new Set( JSON.parse(localStorage.getItem(getRelayStorageKey(user, "inbox")) ?? "[]"), ); @@ -79,7 +84,10 @@ function getPersistedRelays(user: NDKUser): [Set, Set] { async function getUserPreferredRelays( ndk: NDK, user: NDKUser, - fallbacks: readonly string[] = [...get(activeInboxRelays), ...get(activeOutboxRelays)], + fallbacks: readonly string[] = [ + ...get(activeInboxRelays), + ...get(activeOutboxRelays), + ], ): Promise<[Set, Set]> { const relayList = await ndk.fetchEvent( { @@ -144,8 +152,8 @@ export const loginMethodStorageKey = "alexandria/login/method"; function persistLogin(user: NDKUser, method: "extension" | "amber" | "npub") { // Only access localStorage on client-side - if (typeof window === 'undefined') return; - + if (typeof window === "undefined") return; + localStorage.setItem(loginStorageKey, user.pubkey); localStorage.setItem(loginMethodStorageKey, method); } @@ -165,9 +173,9 @@ export async function loginWithExtension() { const signer = new NDKNip07Signer(); 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 { @@ -183,7 +191,7 @@ export async function loginWithExtension() { }; console.log("Login with extension - using fallback profile:", profile); } - + // Fetch user's preferred relays const [persistedInboxes, persistedOutboxes] = getPersistedRelays(user); for (const relay of persistedInboxes) { @@ -193,7 +201,7 @@ export async function loginWithExtension() { persistRelays(user, inboxes, outboxes); ndk.signer = signer; ndk.activeUser = user; - + const userState = { pubkey: user.pubkey, npub, @@ -209,22 +217,27 @@ export async function loginWithExtension() { signer, signedIn: true, }; - + 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 { - console.debug('[userStore.ts] loginWithExtension: Updating relay stores for authenticated user'); + console.debug( + "[userStore.ts] loginWithExtension: Updating relay stores for authenticated user", + ); await updateActiveRelayStores(ndk, true); // Force update to rebuild relay set for authenticated user } catch (error) { - console.warn('[userStore.ts] loginWithExtension: Failed to update relay stores:', error); + console.warn( + "[userStore.ts] loginWithExtension: Failed to update relay stores:", + error, + ); } - + clearLogin(); // Only access localStorage on client-side - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { localStorage.removeItem("alexandria/logout/flag"); } persistLogin(user, "extension"); @@ -238,9 +251,9 @@ 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; - + console.log("Login with Amber - fetching profile for npub:", npub); - + let profile: NostrProfile | null = null; try { profile = await getUserMetadata(npub, true); // Force fresh fetch @@ -254,7 +267,7 @@ export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser) { }; console.log("Login with Amber - using fallback profile:", profile); } - + const [persistedInboxes, persistedOutboxes] = getPersistedRelays(user); for (const relay of persistedInboxes) { ndk.addExplicitRelay(relay); @@ -263,7 +276,7 @@ export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser) { persistRelays(user, inboxes, outboxes); ndk.signer = amberSigner; ndk.activeUser = user; - + const userState = { pubkey: user.pubkey, npub, @@ -279,22 +292,27 @@ export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser) { signer: amberSigner, signedIn: true, }; - + 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'); + console.debug( + "[userStore.ts] loginWithAmber: Updating relay stores for authenticated user", + ); await updateActiveRelayStores(ndk, true); // Force update to rebuild relay set for authenticated user } catch (error) { - console.warn('[userStore.ts] loginWithAmber: Failed to update relay stores:', error); + console.warn( + "[userStore.ts] loginWithAmber: Failed to update relay stores:", + error, + ); } - + clearLogin(); // Only access localStorage on client-side - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { localStorage.removeItem("alexandria/logout/flag"); } persistLogin(user, "amber"); @@ -331,23 +349,28 @@ 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 }); let profile: NostrProfile | null = null; - + // First, update relay stores to ensure we have relays available try { - console.debug('[userStore.ts] loginWithNpub: Updating relay stores for authenticated user'); + 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); + console.warn( + "[userStore.ts] loginWithNpub: Failed to update relay stores:", + error, + ); } - + // Wait a moment for relay stores to be properly initialized - await new Promise(resolve => setTimeout(resolve, 500)); - + await new Promise((resolve) => setTimeout(resolve, 500)); + try { profile = await getUserMetadata(npub, true); // Force fresh fetch console.log("Login with npub - fetched profile:", profile); @@ -360,10 +383,10 @@ export async function loginWithNpub(pubkeyOrNpub: string) { }; console.log("Login with npub - using fallback profile:", profile); } - + ndk.signer = undefined; ndk.activeUser = user; - + const userState = { pubkey: user.pubkey, npub, @@ -374,14 +397,14 @@ export async function loginWithNpub(pubkeyOrNpub: string) { signer: null, signedIn: true, }; - + console.log("Login with npub - setting userStore with:", userState); userStore.set(userState); userPubkey.set(user.pubkey); - + clearLogin(); // Only access localStorage on client-side - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { localStorage.removeItem("alexandria/logout/flag"); } persistLogin(user, "npub"); @@ -393,13 +416,15 @@ export async function loginWithNpub(pubkeyOrNpub: string) { export function logoutUser() { console.log("Logging out user..."); const currentUser = get(userStore); - + // Only access localStorage on client-side - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { if (currentUser.ndkUser) { // Clear persisted relays for the user localStorage.removeItem(getRelayStorageKey(currentUser.ndkUser, "inbox")); - localStorage.removeItem(getRelayStorageKey(currentUser.ndkUser, "outbox")); + localStorage.removeItem( + getRelayStorageKey(currentUser.ndkUser, "outbox"), + ); } // Clear all possible login states from localStorage diff --git a/src/lib/stores/visualizationConfig.ts b/src/lib/stores/visualizationConfig.ts index a17c052..a5500ee 100644 --- a/src/lib/stores/visualizationConfig.ts +++ b/src/lib/stores/visualizationConfig.ts @@ -1,4 +1,4 @@ -import { writable, derived, get } from "svelte/store"; +import { derived, get, writable } from "svelte/store"; export interface EventKindConfig { kind: number; @@ -39,8 +39,10 @@ function createVisualizationConfig() { eventConfigs: DEFAULT_EVENT_CONFIGS, searchThroughFetched: true, }; - - const { subscribe, set, update } = writable(initialConfig); + + const { subscribe, set, update } = writable( + initialConfig, + ); function reset() { set(initialConfig); @@ -52,19 +54,19 @@ function createVisualizationConfig() { if (config.eventConfigs.some((ec) => ec.kind === kind)) { return config; } - + const newConfig: EventKindConfig = { kind, limit, enabled: true }; - + // Add nestedLevels for 30040 if (kind === 30040) { newConfig.nestedLevels = 1; } - + // Add depth for kind 3 if (kind === 3) { newConfig.depth = 0; } - + return { ...config, eventConfigs: [...config.eventConfigs, newConfig], @@ -83,7 +85,7 @@ function createVisualizationConfig() { update((config) => ({ ...config, eventConfigs: config.eventConfigs.map((ec) => - ec.kind === kind ? { ...ec, limit } : ec, + ec.kind === kind ? { ...ec, limit } : ec ), })); } @@ -92,7 +94,7 @@ function createVisualizationConfig() { update((config) => ({ ...config, eventConfigs: config.eventConfigs.map((ec) => - ec.kind === 30040 ? { ...ec, nestedLevels: levels } : ec, + ec.kind === 30040 ? { ...ec, nestedLevels: levels } : ec ), })); } @@ -101,7 +103,7 @@ function createVisualizationConfig() { update((config) => ({ ...config, eventConfigs: config.eventConfigs.map((ec) => - ec.kind === 3 ? { ...ec, depth: depth } : ec, + ec.kind === 3 ? { ...ec, depth: depth } : ec ), })); } @@ -110,7 +112,7 @@ function createVisualizationConfig() { update((config) => ({ ...config, eventConfigs: config.eventConfigs.map((ec) => - ec.kind === kind ? { ...ec, showAll: !ec.showAll } : ec, + ec.kind === kind ? { ...ec, showAll: !ec.showAll } : ec ), })); } @@ -134,7 +136,7 @@ function createVisualizationConfig() { update((config) => ({ ...config, eventConfigs: config.eventConfigs.map((ec) => - ec.kind === kind ? { ...ec, enabled: !ec.enabled } : ec, + ec.kind === kind ? { ...ec, enabled: !ec.enabled } : ec ), })); } @@ -158,10 +160,12 @@ function createVisualizationConfig() { export const visualizationConfig = createVisualizationConfig(); // Helper to get all enabled event kinds -export const enabledEventKinds = derived(visualizationConfig, ($config) => - $config.eventConfigs - .filter((ec) => ec.enabled !== false) - .map((ec) => ec.kind), +export const enabledEventKinds = derived( + visualizationConfig, + ($config) => + $config.eventConfigs + .filter((ec) => ec.enabled !== false) + .map((ec) => ec.kind), ); /** @@ -169,7 +173,10 @@ export const enabledEventKinds = derived(visualizationConfig, ($config) => * @param config - The VisualizationConfig object. * @param kind - The event kind number to check. */ -export function isKindEnabledFn(config: VisualizationConfig, kind: number): boolean { +export function isKindEnabledFn( + config: VisualizationConfig, + kind: number, +): boolean { const eventConfig = config.eventConfigs.find((ec) => ec.kind === kind); // If not found, return false. Otherwise, return true unless explicitly disabled. return !!eventConfig && eventConfig.enabled !== false; @@ -178,5 +185,5 @@ export function isKindEnabledFn(config: VisualizationConfig, kind: number): bool // Derived store: returns a function that checks if a kind is enabled in the current config. export const isKindEnabledStore = derived( visualizationConfig, - ($config) => (kind: number) => isKindEnabledFn($config, kind) + ($config) => (kind: number) => isKindEnabledFn($config, kind), ); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 60237f8..bda1cca 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -26,7 +26,7 @@ export function neventEncode(event: NDKEvent, relays: string[]) { relays, author: event.pubkey, }); - + return nevent; } catch (error) { console.error(`[neventEncode] Error encoding nevent:`, error); @@ -54,7 +54,10 @@ export function naddrEncode(event: NDKEvent, relays: string[]) { * @param relays Optional relay list for the address * @returns A tag address string */ -export function createTagAddress(event: NostrEvent, relays: string[] = []): string { +export function createTagAddress( + event: NostrEvent, + relays: string[] = [], +): string { const dTag = event.tags.find((tag: string[]) => tag[0] === "d")?.[1]; if (!dTag) { throw new Error("Event does not have a d tag"); @@ -144,10 +147,9 @@ export function next(): number { export function scrollTabIntoView(el: string | HTMLElement, wait: boolean) { function scrollTab() { - const element = - typeof el === "string" - ? document.querySelector(`[id^="wikitab-v0-${el}"]`) - : el; + const element = typeof el === "string" + ? document.querySelector(`[id^="wikitab-v0-${el}"]`) + : el; if (!element) return; element.scrollIntoView({ @@ -166,10 +168,9 @@ export function scrollTabIntoView(el: string | HTMLElement, wait: boolean) { } export function isElementInViewport(el: string | HTMLElement) { - const element = - typeof el === "string" - ? document.querySelector(`[id^="wikitab-v0-${el}"]`) - : el; + const element = typeof el === "string" + ? document.querySelector(`[id^="wikitab-v0-${el}"]`) + : el; if (!element) return; const rect = element.getBoundingClientRect(); @@ -179,7 +180,8 @@ export function isElementInViewport(el: string | HTMLElement) { rect.left >= 0 && rect.bottom <= (globalThis.innerHeight || document.documentElement.clientHeight) && - rect.right <= (globalThis.innerWidth || document.documentElement.clientWidth) + rect.right <= + (globalThis.innerWidth || document.documentElement.clientWidth) ); } diff --git a/src/lib/utils/ZettelParser.ts b/src/lib/utils/ZettelParser.ts index 2796d47..c016e5a 100644 --- a/src/lib/utils/ZettelParser.ts +++ b/src/lib/utils/ZettelParser.ts @@ -41,7 +41,7 @@ export function parseZettelSection(section: string): ZettelSection { const trimmed = line.trim(); if (trimmed.startsWith("==")) { title = trimmed.replace(/^==+/, "").trim(); - + // Process header metadata (everything after title until blank line) let j = i + 1; while (j < lines.length && lines[j].trim() !== "") { @@ -54,12 +54,12 @@ export function parseZettelSection(section: string): ZettelSection { j++; } } - + // Skip the blank line if (j < lines.length && lines[j].trim() === "") { j++; } - + // Everything after the blank line is content for (let k = j; k < lines.length; k++) { contentLines.push(lines[k]); @@ -100,13 +100,13 @@ export function extractTags(content: string): string[][] { for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); - + if (trimmed.startsWith("==")) { // Process header metadata (everything after title until blank line) let j = i + 1; while (j < lines.length && lines[j].trim() !== "") { const headerLine = lines[j].trim(); - + if (headerLine.startsWith(":")) { // Parse AsciiDoc attribute format: :tagname: value const match = headerLine.match(/^:([^:]+):\s*(.*)$/); diff --git a/src/lib/utils/asciidoc_metadata.ts b/src/lib/utils/asciidoc_metadata.ts index 6d6754c..8367469 100644 --- a/src/lib/utils/asciidoc_metadata.ts +++ b/src/lib/utils/asciidoc_metadata.ts @@ -1,6 +1,6 @@ /** * AsciiDoc Metadata Extraction Service using Asciidoctor - * + * * Thin wrapper around Asciidoctor's built-in metadata extraction capabilities. * Leverages the existing Pharos parser to avoid duplication. */ @@ -23,7 +23,7 @@ export interface AsciiDocMetadata { source?: string; publishedBy?: string; type?: string; - autoUpdate?: 'yes' | 'ask' | 'no'; + autoUpdate?: "yes" | "ask" | "no"; } export type SectionMetadata = AsciiDocMetadata; @@ -41,29 +41,29 @@ export interface ParsedAsciiDoc { // Shared attribute mapping based on Asciidoctor standard attributes const ATTRIBUTE_MAP: Record = { // Standard Asciidoctor attributes - 'author': 'authors', - 'description': 'summary', - 'keywords': 'tags', - 'revnumber': 'version', - 'revdate': 'publicationDate', - 'revremark': 'edition', - 'title': 'title', - + "author": "authors", + "description": "summary", + "keywords": "tags", + "revnumber": "version", + "revdate": "publicationDate", + "revremark": "edition", + "title": "title", + // Custom attributes for Alexandria - 'published_by': 'publishedBy', - 'publisher': 'publisher', - 'summary': 'summary', - 'image': 'coverImage', - 'cover': 'coverImage', - 'isbn': 'isbn', - 'source': 'source', - 'type': 'type', - 'auto-update': 'autoUpdate', - 'version': 'version', - 'edition': 'edition', - 'published_on': 'publicationDate', - 'date': 'publicationDate', - 'version-label': 'version', + "published_by": "publishedBy", + "publisher": "publisher", + "summary": "summary", + "image": "coverImage", + "cover": "coverImage", + "isbn": "isbn", + "source": "source", + "type": "type", + "auto-update": "autoUpdate", + "version": "version", + "edition": "edition", + "published_on": "publicationDate", + "date": "publicationDate", + "version-label": "version", }; /** @@ -78,37 +78,41 @@ function createProcessor() { */ function extractTagsFromAttributes(attributes: Record): string[] { const tags: string[] = []; - const attrTags = attributes['tags']; - const attrKeywords = attributes['keywords']; - - if (attrTags && typeof attrTags === 'string') { - tags.push(...attrTags.split(',').map(tag => tag.trim())); + const attrTags = attributes["tags"]; + const attrKeywords = attributes["keywords"]; + + if (attrTags && typeof attrTags === "string") { + tags.push(...attrTags.split(",").map((tag) => tag.trim())); } - - if (attrKeywords && typeof attrKeywords === 'string') { - tags.push(...attrKeywords.split(',').map(tag => tag.trim())); + + if (attrKeywords && typeof attrKeywords === "string") { + tags.push(...attrKeywords.split(",").map((tag) => tag.trim())); } - + return [...new Set(tags)]; // Remove duplicates } /** * Maps attributes to metadata with special handling for authors and tags */ -function mapAttributesToMetadata(attributes: Record, metadata: AsciiDocMetadata, isDocument: boolean = false): void { +function mapAttributesToMetadata( + attributes: Record, + metadata: AsciiDocMetadata, + isDocument: boolean = false, +): void { for (const [key, value] of Object.entries(attributes)) { const metadataKey = ATTRIBUTE_MAP[key.toLowerCase()]; - if (metadataKey && value && typeof value === 'string') { - if (metadataKey === 'authors' && isDocument) { + if (metadataKey && value && typeof value === "string") { + if (metadataKey === "authors" && isDocument) { // Skip author mapping for documents since it's handled manually continue; - } else if (metadataKey === 'authors' && !isDocument) { + } else if (metadataKey === "authors" && !isDocument) { // For sections, append author to existing authors array if (!metadata.authors) { metadata.authors = []; } metadata.authors.push(value); - } else if (metadataKey === 'tags') { + } else if (metadataKey === "tags") { // Skip tags mapping since it's handled by extractTagsFromAttributes continue; } else { @@ -121,11 +125,14 @@ function mapAttributesToMetadata(attributes: Record, metadata: Asci /** * Extracts authors from header line (document or section) */ -function extractAuthorsFromHeader(sourceContent: string, isSection: boolean = false): string[] { +function extractAuthorsFromHeader( + sourceContent: string, + isSection: boolean = false, +): string[] { const authors: string[] = []; const lines = sourceContent.split(/\r?\n/); const headerPattern = isSection ? /^==\s+/ : /^=\s+/; - + for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.match(headerPattern)) { @@ -133,51 +140,60 @@ function extractAuthorsFromHeader(sourceContent: string, isSection: boolean = fa let j = i + 1; while (j < lines.length) { const authorLine = lines[j]; - + // Stop if we hit a blank line or content that's not an author - if (authorLine.trim() === '') { + if (authorLine.trim() === "") { break; } - - if (authorLine.includes('<') && !authorLine.startsWith(':')) { + + if (authorLine.includes("<") && !authorLine.startsWith(":")) { // This is an author line like "John Doe " - const authorName = authorLine.split('<')[0].trim(); + const authorName = authorLine.split("<")[0].trim(); if (authorName) { authors.push(authorName); } - } else if (isSection && authorLine.match(/^[A-Za-z\s]+$/) && authorLine.trim() !== '' && authorLine.trim().split(/\s+/).length <= 2) { + } else if ( + isSection && authorLine.match(/^[A-Za-z\s]+$/) && + authorLine.trim() !== "" && authorLine.trim().split(/\s+/).length <= 2 + ) { // This is a simple author name without email (for sections) authors.push(authorLine.trim()); - } else if (authorLine.startsWith(':')) { + } else if (authorLine.startsWith(":")) { // This is an attribute line, skip it - attributes are handled by mapAttributesToMetadata // Don't break here, continue to next line } else { // Not an author line, stop looking break; } - + j++; } break; } } - + return authors; } /** * Strips header and attribute lines from content */ -function stripHeaderAndAttributes(content: string, isSection: boolean = false): string { +function stripHeaderAndAttributes( + content: string, + isSection: boolean = false, +): string { const lines = content.split(/\r?\n/); let contentStart = 0; const headerPattern = isSection ? /^==\s+/ : /^=\s+/; - + for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Skip title line, author line, revision line, and attribute lines - if (!line.match(headerPattern) && !line.includes('<') && !line.match(/^.+,\s*.+:\s*.+$/) && - !line.match(/^:[^:]+:\s*.+$/) && line.trim() !== '') { + if ( + !line.match(headerPattern) && !line.includes("<") && + !line.match(/^.+,\s*.+:\s*.+$/) && + !line.match(/^:[^:]+:\s*.+$/) && line.trim() !== "" + ) { contentStart = i; break; } @@ -185,20 +201,26 @@ function stripHeaderAndAttributes(content: string, isSection: boolean = false): // Filter out all attribute lines and author lines from the content const contentLines = lines.slice(contentStart); - const filteredLines = contentLines.filter(line => { + const filteredLines = contentLines.filter((line) => { // Skip attribute lines if (line.match(/^:[^:]+:\s*.+$/)) { return false; } // Skip author lines (simple names without email) - if (isSection && line.match(/^[A-Za-z\s]+$/) && line.trim() !== '' && line.trim().split(/\s+/).length <= 2) { + if ( + isSection && line.match(/^[A-Za-z\s]+$/) && line.trim() !== "" && + line.trim().split(/\s+/).length <= 2 + ) { return false; } return true; }); - + // Remove extra blank lines and normalize newlines - return filteredLines.join('\n').replace(/\n\s*\n\s*\n/g, '\n\n').replace(/\n\s*\n/g, '\n').trim(); + return filteredLines.join("\n").replace(/\n\s*\n\s*\n/g, "\n\n").replace( + /\n\s*\n/g, + "\n", + ).trim(); } /** @@ -207,7 +229,7 @@ function stripHeaderAndAttributes(content: string, isSection: boolean = false): function parseSectionAttributes(sectionContent: string): Record { const attributes: Record = {}; const lines = sectionContent.split(/\r?\n/); - + for (const line of lines) { const match = line.match(/^:([^:]+):\s*(.+)$/); if (match) { @@ -215,14 +237,10 @@ function parseSectionAttributes(sectionContent: string): Record { attributes[key.trim()] = value.trim(); } } - + return attributes; } - - - - /** * Extracts metadata from AsciiDoc document using Asciidoctor */ @@ -231,7 +249,9 @@ export function extractDocumentMetadata(inputContent: string): { content: string; } { const asciidoctor = createProcessor(); - const document = asciidoctor.load(inputContent, { standalone: false }) as Document; + const document = asciidoctor.load(inputContent, { + standalone: false, + }) as Document; const metadata: AsciiDocMetadata = {}; const attributes = document.getAttributes(); @@ -242,13 +262,16 @@ export function extractDocumentMetadata(inputContent: string): { // Handle multiple authors - combine header line and attributes const authors = extractAuthorsFromHeader(document.getSource()); - + // Get authors from attributes (but avoid duplicates) - const attrAuthor = attributes['author']; - if (attrAuthor && typeof attrAuthor === 'string' && !authors.includes(attrAuthor)) { + const attrAuthor = attributes["author"]; + if ( + attrAuthor && typeof attrAuthor === "string" && + !authors.includes(attrAuthor) + ) { authors.push(attrAuthor); } - + if (authors.length > 0) { metadata.authors = [...new Set(authors)]; // Remove duplicates } @@ -265,12 +288,12 @@ export function extractDocumentMetadata(inputContent: string): { // Map attributes to metadata (but skip version and publishedBy if we already have them from revision) mapAttributesToMetadata(attributes, metadata, true); - + // If we got version from revision, don't override it with attribute if (revisionNumber) { metadata.version = revisionNumber; } - + // If we got publishedBy from revision, don't override it with attribute if (revisionRemark) { metadata.publishedBy = revisionRemark; @@ -295,17 +318,19 @@ export function extractSectionMetadata(inputSectionContent: string): { title: string; } { const asciidoctor = createProcessor(); - const document = asciidoctor.load(`= Temp\n\n${inputSectionContent}`, { standalone: false }) as Document; + const document = asciidoctor.load(`= Temp\n\n${inputSectionContent}`, { + standalone: false, + }) as Document; const sections = document.getSections(); - + if (sections.length === 0) { - return { metadata: {}, content: inputSectionContent, title: '' }; + return { metadata: {}, content: inputSectionContent, title: "" }; } const section = sections[0]; - const title = section.getTitle() || ''; + const title = section.getTitle() || ""; const metadata: SectionMetadata = { title }; - + // Parse attributes from the section content const attributes = parseSectionAttributes(inputSectionContent); @@ -335,7 +360,7 @@ export function parseAsciiDocWithMetadata(content: string): ParsedAsciiDoc { const asciidoctor = createProcessor(); const document = asciidoctor.load(content, { standalone: false }) as Document; const { metadata: docMetadata } = extractDocumentMetadata(content); - + // Parse the original content to find section attributes const lines = content.split(/\r?\n/); const sectionsWithMetadata: Array<{ @@ -345,15 +370,15 @@ export function parseAsciiDocWithMetadata(content: string): ParsedAsciiDoc { }> = []; let currentSection: string | null = null; let currentSectionContent: string[] = []; - + for (const line of lines) { if (line.match(/^==\s+/)) { // Save previous section if exists if (currentSection) { - const sectionContent = currentSectionContent.join('\n'); + const sectionContent = currentSectionContent.join("\n"); sectionsWithMetadata.push(extractSectionMetadata(sectionContent)); } - + // Start new section currentSection = line; currentSectionContent = [line]; @@ -361,42 +386,46 @@ export function parseAsciiDocWithMetadata(content: string): ParsedAsciiDoc { currentSectionContent.push(line); } } - + // Save the last section if (currentSection) { - const sectionContent = currentSectionContent.join('\n'); + const sectionContent = currentSectionContent.join("\n"); sectionsWithMetadata.push(extractSectionMetadata(sectionContent)); } return { metadata: docMetadata, content: document.getSource(), - sections: sectionsWithMetadata + sections: sectionsWithMetadata, }; } /** * Converts metadata to Nostr event tags */ -export function metadataToTags(metadata: AsciiDocMetadata | SectionMetadata): [string, string][] { +export function metadataToTags( + metadata: AsciiDocMetadata | SectionMetadata, +): [string, string][] { const tags: [string, string][] = []; - if (metadata.title) tags.push(['title', metadata.title]); + if (metadata.title) tags.push(["title", metadata.title]); if (metadata.authors?.length) { - metadata.authors.forEach(author => tags.push(['author', author])); + metadata.authors.forEach((author) => tags.push(["author", author])); + } + if (metadata.version) tags.push(["version", metadata.version]); + if (metadata.edition) tags.push(["edition", metadata.edition]); + if (metadata.publicationDate) { + tags.push(["published_on", metadata.publicationDate]); } - if (metadata.version) tags.push(['version', metadata.version]); - if (metadata.edition) tags.push(['edition', metadata.edition]); - if (metadata.publicationDate) tags.push(['published_on', metadata.publicationDate]); - if (metadata.publishedBy) tags.push(['published_by', metadata.publishedBy]); - if (metadata.summary) tags.push(['summary', metadata.summary]); - if (metadata.coverImage) tags.push(['image', metadata.coverImage]); - if (metadata.isbn) tags.push(['i', metadata.isbn]); - if (metadata.source) tags.push(['source', metadata.source]); - if (metadata.type) tags.push(['type', metadata.type]); - if (metadata.autoUpdate) tags.push(['auto-update', metadata.autoUpdate]); + if (metadata.publishedBy) tags.push(["published_by", metadata.publishedBy]); + if (metadata.summary) tags.push(["summary", metadata.summary]); + if (metadata.coverImage) tags.push(["image", metadata.coverImage]); + if (metadata.isbn) tags.push(["i", metadata.isbn]); + if (metadata.source) tags.push(["source", metadata.source]); + if (metadata.type) tags.push(["type", metadata.type]); + if (metadata.autoUpdate) tags.push(["auto-update", metadata.autoUpdate]); if (metadata.tags?.length) { - metadata.tags.forEach(tag => tags.push(['t', tag])); + metadata.tags.forEach((tag) => tags.push(["t", tag])); } return tags; @@ -408,7 +437,7 @@ export function metadataToTags(metadata: AsciiDocMetadata | SectionMetadata): [s export function removeMetadataFromContent(content: string): string { const { content: cleanedContent } = extractDocumentMetadata(content); return cleanedContent; -} +} /** * Extracts metadata from content that only contains sections (no document header) @@ -424,19 +453,19 @@ export function extractMetadataFromSectionsOnly(content: string): { content: string; title: string; }> = []; - + let currentSection: string | null = null; let currentSectionContent: string[] = []; - + // Parse sections from the content for (const line of lines) { if (line.match(/^==\s+/)) { // Save previous section if exists if (currentSection) { - const sectionContent = currentSectionContent.join('\n'); + const sectionContent = currentSectionContent.join("\n"); sections.push(extractSectionMetadata(sectionContent)); } - + // Start new section currentSection = line; currentSectionContent = [line]; @@ -444,20 +473,20 @@ export function extractMetadataFromSectionsOnly(content: string): { currentSectionContent.push(line); } } - + // Save the last section if (currentSection) { - const sectionContent = currentSectionContent.join('\n'); + const sectionContent = currentSectionContent.join("\n"); sections.push(extractSectionMetadata(sectionContent)); } - + // For section-only content, we don't have document metadata // Return the first section's title as the document title if available const metadata: AsciiDocMetadata = {}; if (sections.length > 0 && sections[0].title) { metadata.title = sections[0].title; } - + return { metadata, content }; } @@ -470,31 +499,31 @@ export function extractSmartMetadata(content: string): { } { // Check if content has a document header const hasDocumentHeader = content.match(/^=\s+/m); - + if (hasDocumentHeader) { // Check if it's a minimal document header (just title, no other metadata) const lines = content.split(/\r?\n/); - const titleLine = lines.find(line => line.match(/^=\s+/)); - const hasOtherMetadata = lines.some(line => - line.includes('<') || // author line + const titleLine = lines.find((line) => line.match(/^=\s+/)); + const hasOtherMetadata = lines.some((line) => + line.includes("<") || // author line line.match(/^.+,\s*.+:\s*.+$/) // revision line ); - + if (hasOtherMetadata) { // Full document with metadata - use standard extraction return extractDocumentMetadata(content); - } else { - // Minimal document header (just title) - preserve the title line for 30040 events - const title = titleLine?.replace(/^=\s+/, '').trim(); - const metadata: AsciiDocMetadata = {}; - if (title) { - metadata.title = title; - } - - // Keep the title line in content for 30040 events - return { metadata, content }; - } + } else { + // Minimal document header (just title) - preserve the title line for 30040 events + const title = titleLine?.replace(/^=\s+/, "").trim(); + const metadata: AsciiDocMetadata = {}; + if (title) { + metadata.title = title; + } + + // Keep the title line in content for 30040 events + return { metadata, content }; + } } else { return extractMetadataFromSectionsOnly(content); } -} \ No newline at end of file +} diff --git a/src/lib/utils/community_checker.ts b/src/lib/utils/community_checker.ts index 1d19d91..5971864 100644 --- a/src/lib/utils/community_checker.ts +++ b/src/lib/utils/community_checker.ts @@ -43,7 +43,7 @@ export async function checkCommunity(pubkey: string): Promise { } }; }); - + if (result) { return true; } @@ -52,7 +52,7 @@ export async function checkCommunity(pubkey: string): Promise { continue; } } - + // If we get here, no relay found the user communityCache.set(pubkey, false); return false; diff --git a/src/lib/utils/displayLimits.ts b/src/lib/utils/displayLimits.ts index 029ec25..552521b 100644 --- a/src/lib/utils/displayLimits.ts +++ b/src/lib/utils/displayLimits.ts @@ -1,7 +1,7 @@ -import type { NDKEvent } from '@nostr-dev-kit/ndk'; -import type { VisualizationConfig } from '$lib/stores/visualizationConfig'; -import { isEventId, isCoordinate, parseCoordinate } from './nostr_identifiers'; -import type { NostrEventId } from './nostr_identifiers'; +import type { NDKEvent } from "@nostr-dev-kit/ndk"; +import type { VisualizationConfig } from "$lib/stores/visualizationConfig"; +import { isCoordinate, isEventId, parseCoordinate } from "./nostr_identifiers"; +import type { NostrEventId } from "./nostr_identifiers"; /** * Filters events based on visualization configuration @@ -9,7 +9,10 @@ import type { NostrEventId } from './nostr_identifiers'; * @param config - Visualization configuration * @returns Filtered events that should be displayed */ -export function filterByDisplayLimits(events: NDKEvent[], config: VisualizationConfig): NDKEvent[] { +export function filterByDisplayLimits( + events: NDKEvent[], + config: VisualizationConfig, +): NDKEvent[] { const result: NDKEvent[] = []; const kindCounts = new Map(); @@ -18,13 +21,13 @@ export function filterByDisplayLimits(events: NDKEvent[], config: VisualizationC if (kind === undefined) continue; // Get the config for this event kind - const eventConfig = config.eventConfigs.find(ec => ec.kind === kind); - + const eventConfig = config.eventConfigs.find((ec) => ec.kind === kind); + // Skip if the kind is disabled if (eventConfig && eventConfig.enabled === false) { continue; } - + const limit = eventConfig?.limit; // Special handling for content kinds (30041, 30818) with showAll option @@ -58,48 +61,48 @@ export function filterByDisplayLimits(events: NDKEvent[], config: VisualizationC * @returns Set of missing event identifiers */ export function detectMissingEvents( - events: NDKEvent[], + events: NDKEvent[], existingIds: Set, - existingCoordinates?: Map + existingCoordinates?: Map, ): Set { const missing = new Set(); for (const event of events) { // Check 'e' tags for direct event references (hex IDs) - const eTags = event.getMatchingTags('e'); + const eTags = event.getMatchingTags("e"); for (const eTag of eTags) { if (eTag.length < 2) continue; - + const eventId = eTag[1]; - + // Type check: ensure it's a valid hex event ID if (!isEventId(eventId)) { - console.warn('Invalid event ID in e tag:', eventId); + console.warn("Invalid event ID in e tag:", eventId); continue; } - + if (!existingIds.has(eventId)) { missing.add(eventId); } } // Check 'a' tags for NIP-33 references (kind:pubkey:d-tag) - const aTags = event.getMatchingTags('a'); + const aTags = event.getMatchingTags("a"); for (const aTag of aTags) { if (aTag.length < 2) continue; - + const identifier = aTag[1]; - + // Type check: ensure it's a valid coordinate if (!isCoordinate(identifier)) { - console.warn('Invalid coordinate in a tag:', identifier); + console.warn("Invalid coordinate in a tag:", identifier); continue; } - + // Parse the coordinate const parsed = parseCoordinate(identifier); if (!parsed) continue; - + // If we have existing coordinates, check if this one exists if (existingCoordinates) { if (!existingCoordinates.has(identifier)) { @@ -108,7 +111,10 @@ export function detectMissingEvents( } else { // Without coordinate map, we can't detect missing NIP-33 events // This is a limitation when we only have hex IDs - console.debug('Cannot detect missing NIP-33 events without coordinate map:', identifier); + console.debug( + "Cannot detect missing NIP-33 events without coordinate map:", + identifier, + ); } } } @@ -123,20 +129,19 @@ export function detectMissingEvents( */ export function buildCoordinateMap(events: NDKEvent[]): Map { const coordinateMap = new Map(); - + for (const event of events) { // Only process replaceable events (kinds 30000-39999) if (event.kind && event.kind >= 30000 && event.kind < 40000) { - const dTag = event.tagValue('d'); + const dTag = event.tagValue("d"); const author = event.pubkey; - + if (dTag && author) { const coordinate = `${event.kind}:${author}:${dTag}`; coordinateMap.set(coordinate, event); } } } - + return coordinateMap; } - diff --git a/src/lib/utils/eventColors.ts b/src/lib/utils/eventColors.ts index e123c7b..4479666 100644 --- a/src/lib/utils/eventColors.ts +++ b/src/lib/utils/eventColors.ts @@ -13,11 +13,11 @@ const GOLDEN_RATIO = (1 + Math.sqrt(5)) / 2; export function getEventKindColor(kind: number): string { // Use golden ratio for better distribution const hue = (kind * GOLDEN_RATIO * 360) % 360; - + // Use different saturation/lightness for better visibility const saturation = 65 + (kind % 20); // 65-85% const lightness = 55 + ((kind * 3) % 15); // 55-70% - + return `hsl(${Math.round(hue)}, ${saturation}%, ${lightness}%)`; } @@ -28,55 +28,54 @@ export function getEventKindColor(kind: number): string { */ export function getEventKindName(kind: number): string { const kindNames: Record = { - 0: 'Metadata', - 1: 'Text Note', - 2: 'Recommend Relay', - 3: 'Contact List', - 4: 'Encrypted DM', - 5: 'Event Deletion', - 6: 'Repost', - 7: 'Reaction', - 8: 'Badge Award', - 16: 'Generic Repost', - 40: 'Channel Creation', - 41: 'Channel Metadata', - 42: 'Channel Message', - 43: 'Channel Hide Message', - 44: 'Channel Mute User', - 1984: 'Reporting', - 9734: 'Zap Request', - 9735: 'Zap', - 10000: 'Mute List', - 10001: 'Pin List', - 10002: 'Relay List', - 22242: 'Client Authentication', - 24133: 'Nostr Connect', - 27235: 'HTTP Auth', - 30000: 'Categorized People List', - 30001: 'Categorized Bookmark List', - 30008: 'Profile Badges', - 30009: 'Badge Definition', - 30017: 'Create or update a stall', - 30018: 'Create or update a product', - 30023: 'Long-form Content', - 30024: 'Draft Long-form Content', - 30040: 'Publication Index', - 30041: 'Publication Content', - 30078: 'Application-specific Data', - 30311: 'Live Event', - 30402: 'Classified Listing', - 30403: 'Draft Classified Listing', - 30617: 'Repository', - 30818: 'Wiki Page', - 31922: 'Date-Based Calendar Event', - 31923: 'Time-Based Calendar Event', - 31924: 'Calendar', - 31925: 'Calendar Event RSVP', - 31989: 'Handler recommendation', - 31990: 'Handler information', - 34550: 'Community Definition', + 0: "Metadata", + 1: "Text Note", + 2: "Recommend Relay", + 3: "Contact List", + 4: "Encrypted DM", + 5: "Event Deletion", + 6: "Repost", + 7: "Reaction", + 8: "Badge Award", + 16: "Generic Repost", + 40: "Channel Creation", + 41: "Channel Metadata", + 42: "Channel Message", + 43: "Channel Hide Message", + 44: "Channel Mute User", + 1984: "Reporting", + 9734: "Zap Request", + 9735: "Zap", + 10000: "Mute List", + 10001: "Pin List", + 10002: "Relay List", + 22242: "Client Authentication", + 24133: "Nostr Connect", + 27235: "HTTP Auth", + 30000: "Categorized People List", + 30001: "Categorized Bookmark List", + 30008: "Profile Badges", + 30009: "Badge Definition", + 30017: "Create or update a stall", + 30018: "Create or update a product", + 30023: "Long-form Content", + 30024: "Draft Long-form Content", + 30040: "Publication Index", + 30041: "Publication Content", + 30078: "Application-specific Data", + 30311: "Live Event", + 30402: "Classified Listing", + 30403: "Draft Classified Listing", + 30617: "Repository", + 30818: "Wiki Page", + 31922: "Date-Based Calendar Event", + 31923: "Time-Based Calendar Event", + 31924: "Calendar", + 31925: "Calendar Event RSVP", + 31989: "Handler recommendation", + 31990: "Handler information", + 34550: "Community Definition", }; - + return kindNames[kind] || `Kind ${kind}`; } - diff --git a/src/lib/utils/eventDeduplication.ts b/src/lib/utils/eventDeduplication.ts index 8c52e64..259f808 100644 --- a/src/lib/utils/eventDeduplication.ts +++ b/src/lib/utils/eventDeduplication.ts @@ -1,69 +1,88 @@ -import type { NDKEvent } from '@nostr-dev-kit/ndk'; +import type { NDKEvent } from "@nostr-dev-kit/ndk"; /** * Deduplicate content events by keeping only the most recent version * @param contentEventSets Array of event sets from different sources * @returns Map of coordinate to most recent event */ -export function deduplicateContentEvents(contentEventSets: Set[]): Map { +export function deduplicateContentEvents( + contentEventSets: Set[], +): Map { const eventsByCoordinate = new Map(); - + // Track statistics for debugging let totalEvents = 0; let duplicateCoordinates = 0; - const duplicateDetails: Array<{ coordinate: string; count: number; events: string[] }> = []; - + const duplicateDetails: Array< + { coordinate: string; count: number; events: string[] } + > = []; + contentEventSets.forEach((eventSet) => { - eventSet.forEach(event => { + eventSet.forEach((event) => { totalEvents++; const dTag = event.tagValue("d"); const author = event.pubkey; const kind = event.kind; - + if (dTag && author && kind) { const coordinate = `${kind}:${author}:${dTag}`; const existing = eventsByCoordinate.get(coordinate); - + if (existing) { // We found a duplicate coordinate duplicateCoordinates++; - + // Track details for the first few duplicates if (duplicateDetails.length < 5) { - const existingDetails = duplicateDetails.find(d => d.coordinate === coordinate); + const existingDetails = duplicateDetails.find((d) => + d.coordinate === coordinate + ); if (existingDetails) { existingDetails.count++; - existingDetails.events.push(`${event.id} (created_at: ${event.created_at})`); + existingDetails.events.push( + `${event.id} (created_at: ${event.created_at})`, + ); } else { duplicateDetails.push({ coordinate, count: 2, // existing + current events: [ `${existing.id} (created_at: ${existing.created_at})`, - `${event.id} (created_at: ${event.created_at})` - ] + `${event.id} (created_at: ${event.created_at})`, + ], }); } } } - + // Keep the most recent event (highest created_at) - if (!existing || (event.created_at !== undefined && existing.created_at !== undefined && event.created_at > existing.created_at)) { + if ( + !existing || + (event.created_at !== undefined && + existing.created_at !== undefined && + event.created_at > existing.created_at) + ) { eventsByCoordinate.set(coordinate, event); } } }); }); - + // Log deduplication results if any duplicates were found if (duplicateCoordinates > 0) { - console.log(`[eventDeduplication] Found ${duplicateCoordinates} duplicate events out of ${totalEvents} total events`); - console.log(`[eventDeduplication] Reduced to ${eventsByCoordinate.size} unique coordinates`); + console.log( + `[eventDeduplication] Found ${duplicateCoordinates} duplicate events out of ${totalEvents} total events`, + ); + console.log( + `[eventDeduplication] Reduced to ${eventsByCoordinate.size} unique coordinates`, + ); console.log(`[eventDeduplication] Duplicate details:`, duplicateDetails); } else if (totalEvents > 0) { - console.log(`[eventDeduplication] No duplicates found in ${totalEvents} events`); + console.log( + `[eventDeduplication] No duplicates found in ${totalEvents} events`, + ); } - + return eventsByCoordinate; } @@ -77,83 +96,95 @@ export function deduplicateContentEvents(contentEventSets: Set[]): Map export function deduplicateAndCombineEvents( nonPublicationEvents: NDKEvent[], validIndexEvents: Set, - contentEvents: Set + contentEvents: Set, ): NDKEvent[] { // Track statistics for debugging - const initialCount = nonPublicationEvents.length + validIndexEvents.size + contentEvents.size; + const initialCount = nonPublicationEvents.length + validIndexEvents.size + + contentEvents.size; let replaceableEventsProcessed = 0; let duplicateCoordinatesFound = 0; - const duplicateDetails: Array<{ coordinate: string; count: number; events: string[] }> = []; - + const duplicateDetails: Array< + { coordinate: string; count: number; events: string[] } + > = []; + // First, build coordinate map for replaceable events const coordinateMap = new Map(); const allEventsToProcess = [ ...nonPublicationEvents, // Non-publication events fetched earlier - ...Array.from(validIndexEvents), - ...Array.from(contentEvents) + ...Array.from(validIndexEvents), + ...Array.from(contentEvents), ]; - + // First pass: identify the most recent version of each replaceable event - allEventsToProcess.forEach(event => { + allEventsToProcess.forEach((event) => { if (!event.id) return; - + // For replaceable events (30000-39999), track by coordinate if (event.kind && event.kind >= 30000 && event.kind < 40000) { replaceableEventsProcessed++; const dTag = event.tagValue("d"); const author = event.pubkey; - + if (dTag && author) { const coordinate = `${event.kind}:${author}:${dTag}`; const existing = coordinateMap.get(coordinate); - + if (existing) { // We found a duplicate coordinate duplicateCoordinatesFound++; - + // Track details for the first few duplicates if (duplicateDetails.length < 5) { - const existingDetails = duplicateDetails.find(d => d.coordinate === coordinate); + const existingDetails = duplicateDetails.find((d) => + d.coordinate === coordinate + ); if (existingDetails) { existingDetails.count++; - existingDetails.events.push(`${event.id} (created_at: ${event.created_at})`); + existingDetails.events.push( + `${event.id} (created_at: ${event.created_at})`, + ); } else { duplicateDetails.push({ coordinate, count: 2, // existing + current events: [ `${existing.id} (created_at: ${existing.created_at})`, - `${event.id} (created_at: ${event.created_at})` - ] + `${event.id} (created_at: ${event.created_at})`, + ], }); } } } - + // Keep the most recent version - if (!existing || (event.created_at !== undefined && existing.created_at !== undefined && event.created_at > existing.created_at)) { + if ( + !existing || + (event.created_at !== undefined && + existing.created_at !== undefined && + event.created_at > existing.created_at) + ) { coordinateMap.set(coordinate, event); } } } }); - + // Second pass: build final event map const finalEventMap = new Map(); const seenCoordinates = new Set(); - - allEventsToProcess.forEach(event => { + + allEventsToProcess.forEach((event) => { if (!event.id) return; - + // For replaceable events, only add if it's the chosen version if (event.kind && event.kind >= 30000 && event.kind < 40000) { const dTag = event.tagValue("d"); const author = event.pubkey; - + if (dTag && author) { const coordinate = `${event.kind}:${author}:${dTag}`; const chosenEvent = coordinateMap.get(coordinate); - + // Only add this event if it's the chosen one for this coordinate if (chosenEvent && chosenEvent.id === event.id) { if (!seenCoordinates.has(coordinate)) { @@ -164,23 +195,32 @@ export function deduplicateAndCombineEvents( return; } } - + // Non-replaceable events are added directly finalEventMap.set(event.id, event); }); - + const finalCount = finalEventMap.size; const reduction = initialCount - finalCount; - + // Log deduplication results if any duplicates were found if (duplicateCoordinatesFound > 0) { - console.log(`[eventDeduplication] deduplicateAndCombineEvents: Found ${duplicateCoordinatesFound} duplicate coordinates out of ${replaceableEventsProcessed} replaceable events`); - console.log(`[eventDeduplication] deduplicateAndCombineEvents: Reduced from ${initialCount} to ${finalCount} events (${reduction} removed)`); - console.log(`[eventDeduplication] deduplicateAndCombineEvents: Duplicate details:`, duplicateDetails); + console.log( + `[eventDeduplication] deduplicateAndCombineEvents: Found ${duplicateCoordinatesFound} duplicate coordinates out of ${replaceableEventsProcessed} replaceable events`, + ); + console.log( + `[eventDeduplication] deduplicateAndCombineEvents: Reduced from ${initialCount} to ${finalCount} events (${reduction} removed)`, + ); + console.log( + `[eventDeduplication] deduplicateAndCombineEvents: Duplicate details:`, + duplicateDetails, + ); } else if (replaceableEventsProcessed > 0) { - console.log(`[eventDeduplication] deduplicateAndCombineEvents: No duplicates found in ${replaceableEventsProcessed} replaceable events`); + console.log( + `[eventDeduplication] deduplicateAndCombineEvents: No duplicates found in ${replaceableEventsProcessed} replaceable events`, + ); } - + return Array.from(finalEventMap.values()); } @@ -202,13 +242,13 @@ export function getEventCoordinate(event: NDKEvent): string | null { if (!isReplaceableEvent(event)) { return null; } - + const dTag = event.tagValue("d"); const author = event.pubkey; - + if (!dTag || !author) { return null; } - + return `${event.kind}:${author}:${dTag}`; -} \ No newline at end of file +} diff --git a/src/lib/utils/event_input_utils.ts b/src/lib/utils/event_input_utils.ts index bdee225..ae878df 100644 --- a/src/lib/utils/event_input_utils.ts +++ b/src/lib/utils/event_input_utils.ts @@ -3,12 +3,12 @@ import { get } from "svelte/store"; import { ndkInstance } from "../ndk.ts"; import { NDKEvent as NDKEventClass } from "@nostr-dev-kit/ndk"; import { EVENT_KINDS } from "./search_constants"; -import { - extractDocumentMetadata, - extractSectionMetadata, - parseAsciiDocWithMetadata, +import { + extractDocumentMetadata, + extractSectionMetadata, metadataToTags, - removeMetadataFromContent + parseAsciiDocWithMetadata, + removeMetadataFromContent, } from "./asciidoc_metadata"; // ========================= @@ -92,12 +92,14 @@ export function validate30040EventSet(content: string): { const lines = content.split(/\r?\n/); const { metadata } = extractDocumentMetadata(content); const documentTitle = metadata.title; - const nonEmptyLines = lines.filter(line => line.trim() !== "").map(line => line.trim()); - const isIndexCardFormat = documentTitle && - nonEmptyLines.length === 2 && - nonEmptyLines[0].startsWith("=") && + const nonEmptyLines = lines.filter((line) => line.trim() !== "").map((line) => + line.trim() + ); + const isIndexCardFormat = documentTitle && + nonEmptyLines.length === 2 && + nonEmptyLines[0].startsWith("=") && nonEmptyLines[1].toLowerCase() === "index card"; - + if (isIndexCardFormat) { return { valid: true }; } @@ -125,18 +127,20 @@ export function validate30040EventSet(content: string): { if (documentHeaderMatches && documentHeaderMatches.length > 1) { return { valid: false, - reason: '30040 events must have exactly one document title ("="). Found multiple document headers.', + reason: + '30040 events must have exactly one document title ("="). Found multiple document headers.', }; } // Parse the content to check sections const parsed = parseAsciiDocWithMetadata(content); const hasSections = parsed.sections.length > 0; - + if (!hasSections) { return { valid: true, - warning: "No section headers (==) found. This will create a 30040 index event and a single 30041 preamble section. Continue?", + warning: + "No section headers (==) found. This will create a 30040 index event and a single 30041 preamble section. Continue?", }; } @@ -147,7 +151,9 @@ export function validate30040EventSet(content: string): { } // Check for empty sections - const emptySections = parsed.sections.filter(section => section.content.trim() === ""); + const emptySections = parsed.sections.filter((section) => + section.content.trim() === "" + ); if (emptySections.length > 0) { return { valid: true, @@ -226,21 +232,23 @@ export function build30040EventSet( // Check if this is an "index card" format (no sections, just title + "index card") const lines = content.split(/\r?\n/); const documentTitle = parsed.metadata.title; - + // For index card format, the content should be exactly: title + "index card" - const nonEmptyLines = lines.filter(line => line.trim() !== "").map(line => line.trim()); - const isIndexCardFormat = documentTitle && - nonEmptyLines.length === 2 && - nonEmptyLines[0].startsWith("=") && + const nonEmptyLines = lines.filter((line) => line.trim() !== "").map((line) => + line.trim() + ); + const isIndexCardFormat = documentTitle && + nonEmptyLines.length === 2 && + nonEmptyLines[0].startsWith("=") && nonEmptyLines[1].toLowerCase() === "index card"; - + if (isIndexCardFormat) { console.log("Creating index card format (no sections)"); const indexDTag = normalizeDTagValue(documentTitle); - + // Convert document metadata to tags const metadataTags = metadataToTags(parsed.metadata); - + const indexEvent: NDKEvent = new NDKEventClass(ndk, { kind: 30040, content: "", @@ -253,7 +261,7 @@ export function build30040EventSet( pubkey: baseEvent.pubkey, created_at: baseEvent.created_at, }); - + console.log("Final index event (index card):", indexEvent); console.log("=== build30040EventSet completed (index card) ==="); return { indexEvent, sectionEvents: [] }; @@ -266,24 +274,24 @@ export function build30040EventSet( // Create section events with their metadata const sectionEvents: NDKEvent[] = parsed.sections.map((section, i) => { const sectionDTag = `${indexDTag}-${normalizeDTagValue(section.title)}`; - console.log(`Creating section ${i}:`, { - title: section.title, - dTag: sectionDTag, + console.log(`Creating section ${i}:`, { + title: section.title, + dTag: sectionDTag, content: section.content, - metadata: section.metadata + metadata: section.metadata, }); - + // Convert section metadata to tags const sectionMetadataTags = metadataToTags(section.metadata); - + return new NDKEventClass(ndk, { kind: 30041, content: section.content, tags: [ ...tags, ...sectionMetadataTags, - ["d", sectionDTag], - ["title", section.title] + ["d", sectionDTag], + ["title", section.title], ], pubkey: baseEvent.pubkey, created_at: baseEvent.created_at, @@ -291,7 +299,7 @@ export function build30040EventSet( }); // Create proper a tags with format: kind:pubkey:d-tag - const aTags = sectionEvents.map(event => { + const aTags = sectionEvents.map((event) => { const dTag = event.tags.find(([k]) => k === "d")?.[1]; return ["a", `30041:${baseEvent.pubkey}:${dTag}`] as [string, string]; }); diff --git a/src/lib/utils/event_kind_utils.ts b/src/lib/utils/event_kind_utils.ts index 7d40715..39a2a56 100644 --- a/src/lib/utils/event_kind_utils.ts +++ b/src/lib/utils/event_kind_utils.ts @@ -1,4 +1,4 @@ -import type { EventKindConfig } from '$lib/stores/visualizationConfig'; +import type { EventKindConfig } from "$lib/stores/visualizationConfig"; /** * Validates an event kind input value. @@ -7,29 +7,29 @@ import type { EventKindConfig } from '$lib/stores/visualizationConfig'; * @returns The validated kind number, or null if validation fails. */ export function validateEventKind( - value: string | number, - existingKinds: number[] + value: string | number, + existingKinds: number[], ): { kind: number | null; error: string } { // Convert to string for consistent handling const strValue = String(value); - if (strValue === null || strValue === undefined || strValue.trim() === '') { - return { kind: null, error: '' }; + if (strValue === null || strValue === undefined || strValue.trim() === "") { + return { kind: null, error: "" }; } - + const kind = parseInt(strValue.trim()); if (isNaN(kind)) { - return { kind: null, error: 'Must be a number' }; + return { kind: null, error: "Must be a number" }; } - + if (kind < 0) { - return { kind: null, error: 'Must be non-negative' }; + return { kind: null, error: "Must be non-negative" }; } - + if (existingKinds.includes(kind)) { - return { kind: null, error: 'Already added' }; + return { kind: null, error: "Already added" }; } - - return { kind, error: '' }; + + return { kind, error: "" }; } /** @@ -44,20 +44,20 @@ export function handleAddEventKind( newKind: string, existingKinds: number[], addKindFunction: (kind: number) => void, - resetStateFunction: () => void + resetStateFunction: () => void, ): { success: boolean; error: string } { - console.log('[handleAddEventKind] called with:', newKind); - + console.log("[handleAddEventKind] called with:", newKind); + const validation = validateEventKind(newKind, existingKinds); - console.log('[handleAddEventKind] Validation result:', validation); - + console.log("[handleAddEventKind] Validation result:", validation); + if (validation.kind !== null) { - console.log('[handleAddEventKind] Adding event kind:', validation.kind); + console.log("[handleAddEventKind] Adding event kind:", validation.kind); addKindFunction(validation.kind); resetStateFunction(); - return { success: true, error: '' }; + return { success: true, error: "" }; } else { - console.log('[handleAddEventKind] Validation failed:', validation.error); + console.log("[handleAddEventKind] Validation failed:", validation.error); return { success: false, error: validation.error }; } } @@ -71,11 +71,11 @@ export function handleAddEventKind( export function handleEventKindKeydown( e: KeyboardEvent, onEnter: () => void, - onEscape: () => void + onEscape: () => void, ): void { - if (e.key === 'Enter') { + if (e.key === "Enter") { onEnter(); - } else if (e.key === 'Escape') { + } else if (e.key === "Escape") { onEscape(); } } @@ -87,12 +87,19 @@ export function handleEventKindKeydown( */ export function getEventKindDisplayName(kind: number): string { switch (kind) { - case 30040: return 'Publication Index'; - case 30041: return 'Publication Content'; - case 30818: return 'Wiki'; - case 1: return 'Text Note'; - case 0: return 'Metadata'; - case 3: return 'Follow List'; - default: return `Kind ${kind}`; + case 30040: + return "Publication Index"; + case 30041: + return "Publication Content"; + case 30818: + return "Wiki"; + case 1: + return "Text Note"; + case 0: + return "Metadata"; + case 3: + return "Follow List"; + default: + return `Kind ${kind}`; } -} \ No newline at end of file +} diff --git a/src/lib/utils/event_search.ts b/src/lib/utils/event_search.ts index 5407be4..fc3b372 100644 --- a/src/lib/utils/event_search.ts +++ b/src/lib/utils/event_search.ts @@ -4,7 +4,7 @@ import { nip19 } from "nostr-tools"; import { NDKEvent } from "@nostr-dev-kit/ndk"; import type { Filter } from "./search_types.ts"; import { get } from "svelte/store"; -import { wellKnownUrl, isValidNip05Address } from "./search_utils.ts"; +import { isValidNip05Address, wellKnownUrl } from "./search_utils.ts"; import { TIMEOUTS, VALIDATION } from "./search_constants.ts"; import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts"; @@ -22,31 +22,39 @@ export async function searchEvent(query: string): Promise { // This ensures searches can proceed even if some relay types are not available let attempts = 0; const maxAttempts = 5; // Reduced since we'll use fallback relays - + while (attempts < maxAttempts) { // Check if we have any relays in the pool if (ndk.pool.relays.size > 0) { console.log(`[Search] Found ${ndk.pool.relays.size} relays in NDK pool`); break; } - + // Also check if we have any active relays const inboxRelays = get(activeInboxRelays); const outboxRelays = get(activeOutboxRelays); if (inboxRelays.length > 0 || outboxRelays.length > 0) { - console.log(`[Search] Found active relays - inbox: ${inboxRelays.length}, outbox: ${outboxRelays.length}`); + console.log( + `[Search] Found active relays - inbox: ${inboxRelays.length}, outbox: ${outboxRelays.length}`, + ); break; } - - console.log(`[Search] Waiting for relays to be available (attempt ${attempts + 1}/${maxAttempts})`); - await new Promise(resolve => setTimeout(resolve, 500)); + + console.log( + `[Search] Waiting for relays to be available (attempt ${ + attempts + 1 + }/${maxAttempts})`, + ); + await new Promise((resolve) => setTimeout(resolve, 500)); attempts++; } // AI-NOTE: 2025-01-24 - Don't fail if no relays are available, let fetchEventWithFallback handle fallbacks // The fetchEventWithFallback function will use all available relays including fallback relays if (ndk.pool.relays.size === 0) { - console.warn("[Search] No relays in pool, but proceeding with search - fallback relays will be used"); + console.warn( + "[Search] No relays in pool, but proceeding with search - fallback relays will be used", + ); } // Clean the query and normalize to lowercase @@ -89,50 +97,70 @@ export async function searchEvent(query: string): Promise { try { const decoded = nip19.decode(cleanedQuery); if (!decoded) throw new Error("Invalid identifier"); - + console.log(`[Search] Decoded identifier:`, { type: decoded.type, data: decoded.data, - query: cleanedQuery + query: cleanedQuery, }); - + switch (decoded.type) { case "nevent": console.log(`[Search] Processing nevent:`, { id: decoded.data.id, kind: decoded.data.kind, - relays: decoded.data.relays + relays: decoded.data.relays, }); - + // Use the relays from the nevent if available if (decoded.data.relays && decoded.data.relays.length > 0) { - console.log(`[Search] Using relays from nevent:`, decoded.data.relays); - + console.log( + `[Search] Using relays from nevent:`, + decoded.data.relays, + ); + // Try to fetch the event using the nevent's relays try { // Create a temporary relay set for this search - const neventRelaySet = NDKRelaySetFromNDK.fromRelayUrls(decoded.data.relays, ndk); - + const neventRelaySet = NDKRelaySetFromNDK.fromRelayUrls( + decoded.data.relays, + ndk, + ); + if (neventRelaySet.relays.size > 0) { - console.log(`[Search] Created relay set with ${neventRelaySet.relays.size} relays from nevent`); - + console.log( + `[Search] Created relay set with ${neventRelaySet.relays.size} relays from nevent`, + ); + // Try to fetch the event using the nevent's relays const event = await ndk - .fetchEvent({ ids: [decoded.data.id] }, undefined, neventRelaySet) + .fetchEvent( + { ids: [decoded.data.id] }, + undefined, + neventRelaySet, + ) .withTimeout(TIMEOUTS.EVENT_FETCH); - + if (event) { - console.log(`[Search] Found event using nevent relays:`, event.id); + console.log( + `[Search] Found event using nevent relays:`, + event.id, + ); return event; } else { - console.log(`[Search] Event not found on nevent relays, trying default relays`); + console.log( + `[Search] Event not found on nevent relays, trying default relays`, + ); } } } catch (error) { - console.warn(`[Search] Error fetching from nevent relays:`, error); + console.warn( + `[Search] Error fetching from nevent relays:`, + error, + ); } } - + filterOrId = decoded.data.id; break; case "note": diff --git a/src/lib/utils/image_utils.ts b/src/lib/utils/image_utils.ts index 4922995..031416e 100644 --- a/src/lib/utils/image_utils.ts +++ b/src/lib/utils/image_utils.ts @@ -11,14 +11,16 @@ export function generateDarkPastelColor(seed: string): string { 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 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')}`; + + return `#${r.toString(16).padStart(2, "0")}${ + g.toString(16).padStart(2, "0") + }${b.toString(16).padStart(2, "0")}`; } /** @@ -28,4 +30,4 @@ export function generateDarkPastelColor(seed: string): string { */ export function testColorGeneration(eventId: string): string { return generateDarkPastelColor(eventId); -} \ No newline at end of file +} diff --git a/src/lib/utils/kind24_utils.ts b/src/lib/utils/kind24_utils.ts index 2dada04..04188c8 100644 --- a/src/lib/utils/kind24_utils.ts +++ b/src/lib/utils/kind24_utils.ts @@ -18,7 +18,7 @@ import { buildCompleteRelaySet } from "./relay_management"; */ export async function getKind24RelaySet( senderPubkey: string, - recipientPubkey: string + recipientPubkey: string, ): Promise { const ndk = get(ndkInstance); if (!ndk) { @@ -27,14 +27,16 @@ export async function getKind24RelaySet( const senderPrefix = senderPubkey.slice(0, 8); const recipientPrefix = recipientPubkey.slice(0, 8); - - console.log(`[getKind24RelaySet] Getting relays for ${senderPrefix} -> ${recipientPrefix}`); + + console.log( + `[getKind24RelaySet] Getting relays for ${senderPrefix} -> ${recipientPrefix}`, + ); try { // Fetch both users' complete relay sets using existing utilities const [senderRelaySet, recipientRelaySet] = await Promise.all([ buildCompleteRelaySet(ndk, ndk.getUser({ pubkey: senderPubkey })), - buildCompleteRelaySet(ndk, ndk.getUser({ pubkey: recipientPubkey })) + buildCompleteRelaySet(ndk, ndk.getUser({ pubkey: recipientPubkey })), ]); // Use sender's outbox relays and recipient's inbox relays @@ -42,24 +44,33 @@ export async function getKind24RelaySet( const recipientInboxRelays = recipientRelaySet.inboxRelays; // Prioritize common relays for better privacy - const commonRelays = senderOutboxRelays.filter(relay => + const commonRelays = senderOutboxRelays.filter((relay) => recipientInboxRelays.includes(relay) ); - const senderOnlyRelays = senderOutboxRelays.filter(relay => + const senderOnlyRelays = senderOutboxRelays.filter((relay) => !recipientInboxRelays.includes(relay) ); - const recipientOnlyRelays = recipientInboxRelays.filter(relay => + const recipientOnlyRelays = recipientInboxRelays.filter((relay) => !senderOutboxRelays.includes(relay) ); // Prioritize: common relays first, then sender outbox, then recipient inbox - const finalRelays = [...commonRelays, ...senderOnlyRelays, ...recipientOnlyRelays]; - - console.log(`[getKind24RelaySet] ${senderPrefix}->${recipientPrefix} - Common: ${commonRelays.length}, Sender-only: ${senderOnlyRelays.length}, Recipient-only: ${recipientOnlyRelays.length}, Total: ${finalRelays.length}`); - + const finalRelays = [ + ...commonRelays, + ...senderOnlyRelays, + ...recipientOnlyRelays, + ]; + + console.log( + `[getKind24RelaySet] ${senderPrefix}->${recipientPrefix} - Common: ${commonRelays.length}, Sender-only: ${senderOnlyRelays.length}, Recipient-only: ${recipientOnlyRelays.length}, Total: ${finalRelays.length}`, + ); + return finalRelays; } catch (error) { - console.error(`[getKind24RelaySet] Error getting relay set for ${senderPrefix}->${recipientPrefix}:`, error); + console.error( + `[getKind24RelaySet] Error getting relay set for ${senderPrefix}->${recipientPrefix}:`, + error, + ); throw error; } } @@ -74,8 +85,10 @@ export async function getKind24RelaySet( export async function createKind24Reply( content: string, recipientPubkey: string, - originalEvent?: NDKEvent -): Promise<{ success: boolean; eventId?: string; error?: string; relays?: string[] }> { + originalEvent?: NDKEvent, +): Promise< + { success: boolean; eventId?: string; error?: string; relays?: string[] } +> { const ndk = get(ndkInstance); if (!ndk?.activeUser) { return { success: false, error: "Not logged in" }; @@ -87,49 +100,56 @@ export async function createKind24Reply( try { // Get optimal relay set for this sender-recipient pair - const targetRelays = await getKind24RelaySet(ndk.activeUser.pubkey, recipientPubkey); - + const targetRelays = await getKind24RelaySet( + ndk.activeUser.pubkey, + recipientPubkey, + ); + if (targetRelays.length === 0) { return { success: false, error: "No relays available for publishing" }; } // Build tags for the kind 24 event const tags: string[][] = [ - ["p", recipientPubkey, targetRelays[0]] // Use first relay as primary + ["p", recipientPubkey, targetRelays[0]], // Use first relay as primary ]; - + // Add q tag if replying to an original event if (originalEvent) { tags.push(["q", originalEvent.id, targetRelays[0] || anonymousRelays[0]]); } - + // Create and sign the event const { event: signedEventData } = await createSignedEvent( content, ndk.activeUser.pubkey, 24, - tags + tags, ); - + // Create NDKEvent and publish const event = new NDKEvent(ndk, signedEventData); const relaySet = NDKRelaySet.fromRelayUrls(targetRelays, ndk); const publishedToRelays = await event.publish(relaySet); if (publishedToRelays.size > 0) { - console.log(`[createKind24Reply] Successfully published to ${publishedToRelays.size} relays`); + console.log( + `[createKind24Reply] Successfully published to ${publishedToRelays.size} relays`, + ); return { success: true, eventId: event.id, relays: targetRelays }; } else { console.warn(`[createKind24Reply] Failed to publish to any relays`); - return { success: false, error: "Failed to publish to any relays", relays: targetRelays }; + return { + success: false, + error: "Failed to publish to any relays", + relays: targetRelays, + }; } } catch (error) { console.error("[createKind24Reply] Error creating kind 24 reply:", error); - return { - success: false, - error: error instanceof Error ? error.message : "Unknown error" + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", }; } } - - diff --git a/src/lib/utils/markup/MarkupInfo.md b/src/lib/utils/markup/MarkupInfo.md index 38d78e6..0ee3e56 100644 --- a/src/lib/utils/markup/MarkupInfo.md +++ b/src/lib/utils/markup/MarkupInfo.md @@ -1,10 +1,14 @@ # Markup Support in Alexandria -Alexandria supports multiple markup formats for different use cases. Below is a summary of the supported tags and features for each parser, as well as the formats used for publications and wikis. +Alexandria supports multiple markup formats for different use cases. Below is a +summary of the supported tags and features for each parser, as well as the +formats used for publications and wikis. ## Basic Markup Parser -The **basic markup parser** follows the [Nostr best-practice guidelines](https://github.com/nostrability/nostrability/issues/146) and supports: +The **basic markup parser** follows the +[Nostr best-practice guidelines](https://github.com/nostrability/nostrability/issues/146) +and supports: - **Headers:** - ATX-style: `# H1` through `###### H6` @@ -18,7 +22,8 @@ The **basic markup parser** follows the [Nostr best-practice guidelines](https:/ - **Links:** `[text](url)` - **Images:** `![alt](url)` - **Hashtags:** `#hashtag` -- **Nostr identifiers:** npub, nprofile, nevent, naddr, note, with or without `nostr:` prefix (note is deprecated) +- **Nostr identifiers:** npub, nprofile, nevent, naddr, note, with or without + `nostr:` prefix (note is deprecated) - **Emoji shortcodes:** `:smile:` will render as 😄 ## Advanced Markup Parser @@ -26,17 +31,25 @@ The **basic markup parser** follows the [Nostr best-practice guidelines](https:/ The **advanced markup parser** includes all features of the basic parser, plus: - **Inline code:** `` `code` `` -- **Syntax highlighting:** for code blocks in many programming languages (from [highlight.js](https://highlightjs.org/)) +- **Syntax highlighting:** for code blocks in many programming languages (from + [highlight.js](https://highlightjs.org/)) - **Tables:** Pipe-delimited tables with or without headers -- **Footnotes:** `[^1]` or `[^Smith]`, which should appear where the footnote shall be placed, and will be displayed as unique, consecutive numbers -- **Footnote References:** `[^1]: footnote text` or `[^Smith]: Smith, Adam. 1984 "The Wiggle Mysteries`, which will be listed in order, at the bottom of the event, with back-reference links to the footnote, and text footnote labels appended -- **Wikilinks:** `[[NIP-54]]` will render as a hyperlink and goes to [NIP-54](./events?d=nip-54) +- **Footnotes:** `[^1]` or `[^Smith]`, which should appear where the footnote + shall be placed, and will be displayed as unique, consecutive numbers +- **Footnote References:** `[^1]: footnote text` or + `[^Smith]: Smith, Adam. 1984 "The Wiggle Mysteries`, which will be listed in + order, at the bottom of the event, with back-reference links to the footnote, + and text footnote labels appended +- **Wikilinks:** `[[NIP-54]]` will render as a hyperlink and goes to + [NIP-54](./events?d=nip-54) ## Publications and Wikis -**Publications** and **wikis** in Alexandria use **AsciiDoc** as their primary markup language, not Markdown. +**Publications** and **wikis** in Alexandria use **AsciiDoc** as their primary +markup language, not Markdown. -AsciiDoc supports a much broader set of formatting, semantic, and structural features, including: +AsciiDoc supports a much broader set of formatting, semantic, and structural +features, including: - Section and document structure - Advanced tables, callouts, admonitions @@ -48,7 +61,8 @@ AsciiDoc supports a much broader set of formatting, semantic, and structural fea ### Advanced Content Types -Alexandria supports rendering of advanced content types commonly used in academic, technical, and business documents: +Alexandria supports rendering of advanced content types commonly used in +academic, technical, and business documents: #### Math Rendering @@ -113,18 +127,26 @@ TikZ diagrams for mathematical illustrations: ### Rendering Features -- **Automatic Detection**: Content types are automatically detected based on syntax -- **Fallback Display**: If rendering fails, the original source code is displayed +- **Automatic Detection**: Content types are automatically detected based on + syntax +- **Fallback Display**: If rendering fails, the original source code is + displayed - **Source Code**: Click "Show source" to view the original code -- **Responsive Design**: All rendered content is responsive and works on mobile devices +- **Responsive Design**: All rendered content is responsive and works on mobile + devices -For more information on AsciiDoc, see the [AsciiDoc documentation](https://asciidoc.org/). +For more information on AsciiDoc, see the +[AsciiDoc documentation](https://asciidoc.org/). --- **Note:** -- The markdown parsers are primarily used for comments, issues, and other user-generated content. -- Publications and wikis are rendered using AsciiDoc for maximum expressiveness and compatibility. -- All URLs are sanitized to remove tracking parameters, and YouTube links are presented in a clean, privacy-friendly format. -- [Here is a test markup file](/tests/integration/markupTestfile.md) that you can use to test out the parser and see how things should be formatted. +- The markdown parsers are primarily used for comments, issues, and other + user-generated content. +- Publications and wikis are rendered using AsciiDoc for maximum expressiveness + and compatibility. +- All URLs are sanitized to remove tracking parameters, and YouTube links are + presented in a clean, privacy-friendly format. +- [Here is a test markup file](/tests/integration/markupTestfile.md) that you + can use to test out the parser and see how things should be formatted. diff --git a/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts b/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts index 41e4df9..2cde13e 100644 --- a/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts +++ b/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts @@ -188,7 +188,8 @@ function processPlantUMLBlocks(html: string): string { try { const rawContent = decodeHTMLEntities(content); const encoded = plantumlEncoder.encode(rawContent); - const plantUMLUrl = `https://www.plantuml.com/plantuml/svg/${encoded}`; + const plantUMLUrl = + `https://www.plantuml.com/plantuml/svg/${encoded}`; return `
    PlantUML diagram 1 && rows[1].trim().match(/^\|[-\s|]+\|$/); + const hasHeader = rows.length > 1 && + rows[1].trim().match(/^\|[-\s|]+\|$/); // Extract header and body rows let headerCells: string[] = []; @@ -124,7 +125,8 @@ function processTables(content: string): string { if (hasHeader) { html += "\n\n"; headerCells.forEach((cell) => { - html += `${cell}\n`; + html += + `${cell}\n`; }); html += "\n\n"; } @@ -135,7 +137,8 @@ function processTables(content: string): string { const cells = processCells(row); html += "\n"; cells.forEach((cell) => { - html += `${cell}\n`; + html += + `${cell}\n`; }); html += "\n"; }); @@ -197,7 +200,9 @@ function processFootnotes(content: string): string { if (!referenceMap.has(id)) referenceMap.set(id, []); referenceMap.get(id)!.push(refNum); referenceOrder.push({ id, refNum, label: id }); - return `[${refNum}]`; + return `[${refNum}]`; }, ); @@ -216,12 +221,15 @@ function processFootnotes(content: string): string { const backrefs = refs .map( (num, i) => - `↩${num}`, + `↩${num}`, ) .join(" "); // If label is not a number, show it after all backrefs const labelSuffix = isNaN(Number(label)) ? ` ${label}` : ""; - processedContent += `
  • ${text} ${backrefs}${labelSuffix}
  • \n`; + processedContent += + `
  • ${text} ${backrefs}${labelSuffix}
  • \n`; } processedContent += ""; } @@ -233,8 +241,6 @@ function processFootnotes(content: string): string { } } - - /** * Process code blocks by finding consecutive code lines and preserving their content */ @@ -357,13 +363,17 @@ function restoreCodeBlocks(text: string, blocks: Map): string { language, ignoreIllegals: true, }).value; - html = `
    ${highlighted}
    `; + html = + `
    ${highlighted}
    `; } catch (e: unknown) { console.warn("Failed to highlight code block:", e); - html = `
    ${code}
    `; + html = `
    ${code}
    `; } } else { - html = `
    ${code}
    `; + html = + `
    ${code}
    `; } result = result.replace(id, html); @@ -672,8 +682,6 @@ function isLaTeXContent(content: string): boolean { return latexPatterns.some((pattern) => pattern.test(trimmed)); } - - /** * Parse markup text with advanced formatting */ @@ -711,6 +719,8 @@ export async function parseAdvancedmarkup(text: string): Promise { return processedText; } catch (e: unknown) { console.error("Error in parseAdvancedmarkup:", e); - return `
    Error processing markup: ${(e as Error)?.message ?? "Unknown error"}
    `; + return `
    Error processing markup: ${ + (e as Error)?.message ?? "Unknown error" + }
    `; } } diff --git a/src/lib/utils/markup/asciidoctorPostProcessor.ts b/src/lib/utils/markup/asciidoctorPostProcessor.ts index bd6fff1..92b799c 100644 --- a/src/lib/utils/markup/asciidoctorPostProcessor.ts +++ b/src/lib/utils/markup/asciidoctorPostProcessor.ts @@ -1,6 +1,9 @@ -import { processImageWithReveal, processNostrIdentifiersInText, processWikilinks, processAsciiDocAnchors } from "./markupServices"; - - +import { + processAsciiDocAnchors, + processImageWithReveal, + processNostrIdentifiersInText, + processWikilinks, +} from "./markupServices"; /** * Processes nostr addresses in HTML content, but skips addresses that are @@ -41,8 +44,7 @@ async function processNostrAddresses(html: string): Promise { const processedMatch = await processNostrIdentifiersInText(fullMatch); // Replace the match in the HTML - processedHtml = - processedHtml.slice(0, matchIndex) + + processedHtml = processedHtml.slice(0, matchIndex) + processedMatch + processedHtml.slice(matchIndex + fullMatch.length); } @@ -61,18 +63,18 @@ function processImageBlocks(html: string): string { // Extract src and alt from img attributes const srcMatch = imgAttributes.match(/src="([^"]+)"/); const altMatch = imgAttributes.match(/alt="([^"]*)"/); - const src = srcMatch ? srcMatch[1] : ''; - const alt = altMatch ? altMatch[1] : ''; - - const titleHtml = title ? `
    ${title}
    ` : ''; - + const src = srcMatch ? srcMatch[1] : ""; + const alt = altMatch ? altMatch[1] : ""; + + const titleHtml = title ? `
    ${title}
    ` : ""; + return `
    ${processImageWithReveal(src, alt)}
    ${titleHtml}
    `; - } + }, ); } diff --git a/src/lib/utils/markup/basicMarkupParser.ts b/src/lib/utils/markup/basicMarkupParser.ts index dddd31d..221c391 100644 --- a/src/lib/utils/markup/basicMarkupParser.ts +++ b/src/lib/utils/markup/basicMarkupParser.ts @@ -1,16 +1,16 @@ import * as emoji from "node-emoji"; import { nip19 } from "nostr-tools"; -import { - processImageWithReveal, - processMediaUrl, - processNostrIdentifiersInText, - processEmojiShortcodes, - processWebSocketUrls, - processHashtags, +import { processBasicTextFormatting, processBlockquotes, + processEmojiShortcodes, + processHashtags, + processImageWithReveal, + processMediaUrl, + processNostrIdentifiersInText, + processWebSocketUrls, processWikilinks, - stripTrackingParams + stripTrackingParams, } from "./markupServices"; /* Regex constants for basic markup parsing */ @@ -21,8 +21,6 @@ const MARKUP_IMAGE = /!\[([^\]]*)\]\(([^)]+)\)/g; // AI-NOTE: 2025-01-24 - Added negative lookbehind (?"]+)(?!["'])/g; - - // Add this helper function near the top: function replaceAlexandriaNostrLinks(text: string): string { // Regex for Alexandria/localhost URLs @@ -82,12 +80,6 @@ function replaceAlexandriaNostrLinks(text: string): string { return text; } - - - - - - function renderListGroup(lines: string[], typeHint?: "ol" | "ul"): string { function parseList( start: number, @@ -96,7 +88,9 @@ function renderListGroup(lines: string[], typeHint?: "ol" | "ul"): string { ): [string, number] { let html = ""; let i = start; - html += `<${type} class="${type === "ol" ? "list-decimal" : "list-disc"} ml-6 mb-2">`; + html += `<${type} class="${ + type === "ol" ? "list-decimal" : "list-disc" + } ml-6 mb-2">`; while (i < lines.length) { const line = lines[i]; const match = line.match(/^([ \t]*)([*+-]|\d+\.)[ \t]+(.*)$/); @@ -168,7 +162,9 @@ function processBasicFormatting(content: string): string { processedText = processedText.replace( MARKUP_LINK, (_match, text, url) => - `${text}`, + `${text}`, ); // Process WebSocket URLs using shared services @@ -181,7 +177,7 @@ function processBasicFormatting(content: string): string { // Process text formatting using shared services processedText = processBasicTextFormatting(processedText); - + // Process hashtags using shared services processedText = processHashtags(processedText); @@ -220,12 +216,6 @@ function processBasicFormatting(content: string): string { return processedText; } - - - - - - export async function parseBasicmarkup(text: string): Promise { if (!text) return ""; @@ -249,9 +239,10 @@ export async function parseBasicmarkup(text: string): Promise { // AI-NOTE: 2025-01-24 - Added img tag to skip wrapping to prevent image rendering issues // Skip wrapping if para already contains block-level elements, math blocks, or images if ( - /(]*class=["'][^"']*math-block[^"']*["'])|<(div|h[1-6]|blockquote|table|pre|ul|ol|hr|img)/i.test( - para, - ) + /(]*class=["'][^"']*math-block[^"']*["'])|<(div|h[1-6]|blockquote|table|pre|ul|ol|hr|img)/i + .test( + para, + ) ) { return para; } @@ -268,6 +259,8 @@ export async function parseBasicmarkup(text: string): Promise { return processedText; } catch (e: unknown) { console.error("Error in parseBasicmarkup:", e); - return `
    Error processing markup: ${(e as Error)?.message ?? "Unknown error"}
    `; + return `
    Error processing markup: ${ + (e as Error)?.message ?? "Unknown error" + }
    `; } } diff --git a/src/lib/utils/markup/embeddedMarkupParser.ts b/src/lib/utils/markup/embeddedMarkupParser.ts index ab34ddf..a7ccab5 100644 --- a/src/lib/utils/markup/embeddedMarkupParser.ts +++ b/src/lib/utils/markup/embeddedMarkupParser.ts @@ -1,18 +1,18 @@ import * as emoji from "node-emoji"; import { nip19 } from "nostr-tools"; -import { - processImageWithReveal, - processMediaUrl, - processNostrIdentifiersInText, - processEmojiShortcodes, - processWebSocketUrls, - processHashtags, +import { processBasicTextFormatting, processBlockquotes, - processWikilinks, + processEmojiShortcodes, + processHashtags, + processImageWithReveal, + processMediaUrl, + processNostrIdentifiersInText, processNostrIdentifiersWithEmbeddedEvents, - stripTrackingParams -} from "./markupServices"; + processWebSocketUrls, + processWikilinks, + stripTrackingParams, +} from "./markupServices.ts"; /* Regex constants for basic markup parsing */ @@ -89,7 +89,9 @@ function renderListGroup(lines: string[], typeHint?: "ol" | "ul"): string { ): [string, number] { let html = ""; let i = start; - html += `<${type} class="${type === "ol" ? "list-decimal" : "list-disc"} ml-6 mb-2">`; + html += `<${type} class="${ + type === "ol" ? "list-decimal" : "list-disc" + } ml-6 mb-2">`; while (i < lines.length) { const line = lines[i]; const match = line.match(/^([ \t]*)([*+-]|\d+\.)[ \t]+(.*)$/); @@ -161,7 +163,9 @@ function processBasicFormatting(content: string): string { processedText = processedText.replace( MARKUP_LINK, (_match, text, url) => - `${text}`, + `${text}`, ); // Process WebSocket URLs using shared services @@ -174,7 +178,7 @@ function processBasicFormatting(content: string): string { // Process text formatting using shared services processedText = processBasicTextFormatting(processedText); - + // Process hashtags using shared services processedText = processHashtags(processedText); @@ -218,7 +222,10 @@ function processBasicFormatting(content: string): string { * AI-NOTE: 2025-01-24 - Enhanced markup parser that supports nested Nostr event embedding * Up to 3 levels of nesting are supported, after which events are shown as links */ -export async function parseEmbeddedMarkup(text: string, nestingLevel: number = 0): Promise { +export async function parseEmbeddedMarkup( + text: string, + nestingLevel: number = 0, +): Promise { if (!text) return ""; try { @@ -233,29 +240,30 @@ export async function parseEmbeddedMarkup(text: string, nestingLevel: number = 0 // Process paragraphs - split by double newlines and wrap in p tags // Skip wrapping if content already contains block-level elements + const blockLevelEls = + /(]*class=["'][^"']*math-block[^"']*["'])|<(div|h[1-6]|blockquote|table|pre|ul|ol|hr|img)/i; processedText = processedText .split(/\n\n+/) .map((para) => para.trim()) .filter((para) => para.length > 0) .map((para) => { - // AI-NOTE: 2025-01-24 - Added img tag to skip wrapping to prevent image rendering issues // Skip wrapping if para already contains block-level elements, math blocks, or images - if ( - /(]*class=["'][^"']*math-block[^"']*["'])|<(div|h[1-6]|blockquote|table|pre|ul|ol|hr|img)/i.test( - para, - ) - ) { + if (blockLevelEls.test(para)) { return para; } + return `

    ${para}

    `; }) .join("\n"); // Process profile identifiers (npub, nprofile) first using the regular processor processedText = await processNostrIdentifiersInText(processedText); - + // Then process event identifiers with embedded events (only event-related identifiers) - processedText = processNostrIdentifiersWithEmbeddedEvents(processedText, nestingLevel); + processedText = processNostrIdentifiersWithEmbeddedEvents( + processedText, + nestingLevel, + ); // Replace wikilinks processedText = processWikilinks(processedText); @@ -263,6 +271,8 @@ export async function parseEmbeddedMarkup(text: string, nestingLevel: number = 0 return processedText; } catch (e: unknown) { console.error("Error in parseEmbeddedMarkup:", e); - return `
    Error processing markup: ${(e as Error)?.message ?? "Unknown error"}
    `; + return `
    Error processing markup: ${ + (e as Error)?.message ?? "Unknown error" + }
    `; } } diff --git a/src/lib/utils/markup/markupServices.ts b/src/lib/utils/markup/markupServices.ts index f4ce0a5..6377675 100644 --- a/src/lib/utils/markup/markupServices.ts +++ b/src/lib/utils/markup/markupServices.ts @@ -1,18 +1,25 @@ -import { processNostrIdentifiers, NOSTR_PROFILE_REGEX } from "../nostrUtils.ts"; +import { + createProfileLink, + getUserMetadata, + NOSTR_PROFILE_REGEX, +} from "../nostrUtils.ts"; + import * as emoji from "node-emoji"; // Media URL patterns const IMAGE_EXTENSIONS = /\.(jpg|jpeg|gif|png|webp|svg)$/i; const VIDEO_URL_REGEX = /https?:\/\/[^\s<]+\.(?:mp4|webm|mov|avi)(?:[^\s<]*)?/i; const AUDIO_URL_REGEX = /https?:\/\/[^\s<]+\.(?:mp3|wav|ogg|m4a)(?:[^\s<]*)?/i; -const YOUTUBE_URL_REGEX = /https?:\/\/(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})(?:[^\s<]*)?/; - - +const YOUTUBE_URL_REGEX = + /https?:\/\/(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})(?:[^\s<]*)?/; /** * Shared service for processing images with expand functionality */ -export function processImageWithReveal(src: string, alt: string = "Image"): string { +export function processImageWithReveal( + src: string, + alt: string = "Image", +): string { if (!src || !IMAGE_EXTENSIONS.test(src.split("?")[0])) { return `${alt}`; } @@ -43,26 +50,32 @@ export function processImageWithReveal(src: string, alt: string = "Image"): stri */ export function processMediaUrl(url: string, alt?: string): string { const clean = stripTrackingParams(url); - + if (YOUTUBE_URL_REGEX.test(clean)) { const videoId = extractYouTubeVideoId(clean); if (videoId) { - return ``; + return ``; } } - + if (VIDEO_URL_REGEX.test(clean)) { - return ``; + return ``; } - + if (AUDIO_URL_REGEX.test(clean)) { - return ``; + return ``; } - + if (IMAGE_EXTENSIONS.test(clean.split("?")[0])) { return processImageWithReveal(clean, alt || "Embedded media"); } - + // Default to clickable link return `${clean}`; } @@ -70,40 +83,45 @@ export function processMediaUrl(url: string, alt?: string): string { /** * Shared service for processing nostr identifiers */ -export async function processNostrIdentifiersInText(text: string): Promise { +export async function processNostrIdentifiersInText( + text: string, +): Promise { let processedText = text; - + // Find all profile-related nostr addresses (only npub and nprofile) const matches = Array.from(processedText.matchAll(NOSTR_PROFILE_REGEX)); - + // Process them in reverse order to avoid index shifting issues for (let i = matches.length - 1; i >= 0; i--) { const match = matches[i]; const [fullMatch] = match; const matchIndex = match.index ?? 0; - + // Skip if part of a URL - const before = processedText.slice(Math.max(0, matchIndex - 12), matchIndex); + const before = processedText.slice( + Math.max(0, matchIndex - 12), + matchIndex, + ); if (/https?:\/\/$|www\.$/i.test(before)) { continue; } - + // Process the nostr identifier directly let identifier = fullMatch; if (!identifier.startsWith("nostr:")) { identifier = "nostr:" + identifier; } - + // Get user metadata and create link - const { getUserMetadata, createProfileLink } = await import("../nostrUtils.ts"); const metadata = await getUserMetadata(identifier); const displayText = metadata.displayName || metadata.name; const link = createProfileLink(identifier, displayText); - + // Replace the match in the text - processedText = processedText.slice(0, matchIndex) + link + processedText.slice(matchIndex + fullMatch.length); + processedText = processedText.slice(0, matchIndex) + link + + processedText.slice(matchIndex + fullMatch.length); } - + return processedText; } @@ -112,37 +130,45 @@ export async function processNostrIdentifiersInText(text: string): Promise= 0; i--) { const match = matches[i]; const [fullMatch] = match; const matchIndex = match.index ?? 0; - + let replacement: string; - + if (nestingLevel >= MAX_NESTING_LEVEL) { // At max nesting level, just show the link - replacement = `${fullMatch}`; + replacement = + `${fullMatch}`; } else { // Create a placeholder for embedded event - const componentId = `embedded-event-${Math.random().toString(36).substr(2, 9)}`; - replacement = `
    `; + const componentId = `embedded-event-${ + Math.random().toString(36).substr(2, 9) + }`; + replacement = + `
    `; } - + // Replace the match in the text - processedText = processedText.slice(0, matchIndex) + replacement + processedText.slice(matchIndex + fullMatch.length); + processedText = processedText.slice(0, matchIndex) + replacement + + processedText.slice(matchIndex + fullMatch.length); } - + return processedText; } @@ -169,7 +195,10 @@ export function processWebSocketUrls(text: string): string { */ export function processHashtags(text: string): string { const hashtagRegex = /(?#$1'); + return text.replace( + hashtagRegex, + '', + ); } /** @@ -177,20 +206,26 @@ export function processHashtags(text: string): string { */ export function processBasicTextFormatting(text: string): string { // Bold: **text** or *text* - text = text.replace(/(\*\*|[*])((?:[^*\n]|\*(?!\*))+)\1/g, "$2"); - + text = text.replace( + /(\*\*|[*])((?:[^*\n]|\*(?!\*))+)\1/g, + "$2", + ); + // Italic: _text_ or __text__ text = text.replace(/\b(_[^_\n]+_|\b__[^_\n]+__)\b/g, (match) => { const text = match.replace(/^_+|_+$/g, ""); return `${text}`; }); - + // Strikethrough: ~~text~~ or ~text~ - text = text.replace(/~~([^~\n]+)~~|~([^~\n]+)~/g, (_match, doubleText, singleText) => { - const text = doubleText || singleText; - return `${text}`; - }); - + text = text.replace( + /~~([^~\n]+)~~|~([^~\n]+)~/g, + (_match, doubleText, singleText) => { + const text = doubleText || singleText; + return `${text}`; + }, + ); + return text; } @@ -203,7 +238,9 @@ export function processBlockquotes(text: string): string { const lines = match.split("\n").map((line) => { return line.replace(/^[ \t]*>[ \t]?/, "").trim(); }); - return `
    ${lines.join("\n")}
    `; + return `
    ${ + lines.join("\n") + }
    `; }); } @@ -212,8 +249,16 @@ export function stripTrackingParams(url: string): string { try { const urlObj = new URL(url); // Remove common tracking parameters - const trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid']; - trackingParams.forEach(param => urlObj.searchParams.delete(param)); + const trackingParams = [ + "utm_source", + "utm_medium", + "utm_campaign", + "utm_term", + "utm_content", + "fbclid", + "gclid", + ]; + trackingParams.forEach((param) => urlObj.searchParams.delete(param)); return urlObj.toString(); } catch { return url; @@ -221,7 +266,9 @@ export function stripTrackingParams(url: string): string { } function extractYouTubeVideoId(url: string): string | null { - const match = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})/); + const match = url.match( + /(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})/, + ); return match ? match[1] : null; } @@ -263,4 +310,4 @@ export function processAsciiDocAnchors(text: string): string { const url = `/events?d=${normalized}`; return `${id}`; }); -} \ No newline at end of file +} diff --git a/src/lib/utils/markup/tikzRenderer.ts b/src/lib/utils/markup/tikzRenderer.ts index 3e194b6..3be3932 100644 --- a/src/lib/utils/markup/tikzRenderer.ts +++ b/src/lib/utils/markup/tikzRenderer.ts @@ -44,7 +44,9 @@ function createBasicSVG(tikzCode: string): string {
    -
    ${escapeHtml(tikzCode)}
    +
    ${
    +    escapeHtml(tikzCode)
    +  }
    `; diff --git a/src/lib/utils/mime.ts b/src/lib/utils/mime.ts index b4326db..a8714c3 100644 --- a/src/lib/utils/mime.ts +++ b/src/lib/utils/mime.ts @@ -104,7 +104,7 @@ export function getMimeTags(kind: number): [string, string][] { MTag = ["M", `article/long-form/${replaceability}`]; break; - // Add more cases as needed... + // Add more cases as needed... } return [mTag, MTag]; diff --git a/src/lib/utils/network_detection.ts b/src/lib/utils/network_detection.ts index b7a7315..c1821b8 100644 --- a/src/lib/utils/network_detection.ts +++ b/src/lib/utils/network_detection.ts @@ -4,18 +4,18 @@ import { deduplicateRelayUrls } from "./relay_management.ts"; * Network conditions for relay selection */ export enum NetworkCondition { - ONLINE = 'online', - SLOW = 'slow', - OFFLINE = 'offline' + 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' + "https://www.google.com/favicon.ico", + "https://httpbin.org/status/200", + "https://api.github.com/zen", ]; /** @@ -27,20 +27,23 @@ export async function isNetworkOnline(): Promise { try { // Use a simple fetch without HEAD method to avoid CORS issues await fetch(endpoint, { - method: 'GET', - cache: 'no-cache', + method: "GET", + cache: "no-cache", signal: AbortSignal.timeout(3000), - mode: 'no-cors' // Use no-cors mode to avoid CORS issues + 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); + console.debug( + `[network_detection.ts] Failed to reach ${endpoint}:`, + error, + ); continue; } } - - console.debug('[network_detection.ts] All network endpoints failed'); + + console.debug("[network_detection.ts] All network endpoints failed"); return false; } @@ -50,25 +53,30 @@ export async function isNetworkOnline(): Promise { */ export async function testNetworkSpeed(): Promise { const startTime = performance.now(); - + for (const endpoint of NETWORK_ENDPOINTS) { try { await fetch(endpoint, { - method: 'GET', - cache: 'no-cache', + method: "GET", + cache: "no-cache", signal: AbortSignal.timeout(5000), - mode: 'no-cors' // Use no-cors mode to avoid CORS issues + 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); + console.debug( + `[network_detection.ts] Speed test failed for ${endpoint}:`, + error, + ); continue; } } - - console.debug('[network_detection.ts] Network speed test failed for all endpoints'); + + console.debug( + "[network_detection.ts] Network speed test failed for all endpoints", + ); return Infinity; // Very slow if it fails } @@ -78,21 +86,25 @@ export async function testNetworkSpeed(): Promise { */ export async function detectNetworkCondition(): Promise { const isOnline = await isNetworkOnline(); - + if (!isOnline) { - console.debug('[network_detection.ts] Network condition: OFFLINE'); + 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)`); + 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)`); + + console.debug( + `[network_detection.ts] Network condition: ONLINE (${speed.toFixed(0)}ms)`, + ); return NetworkCondition.ONLINE; } @@ -108,39 +120,49 @@ export function getRelaySetForNetworkCondition( networkCondition: NetworkCondition, discoveredLocalRelays: string[], lowbandwidthRelays: string[], - fullRelaySet: { inboxRelays: string[]; outboxRelays: 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)'); + console.debug("[network_detection.ts] Using local relays (offline)"); return { inboxRelays: discoveredLocalRelays, - outboxRelays: discoveredLocalRelays + outboxRelays: discoveredLocalRelays, }; } else { - console.debug('[network_detection.ts] No local relays available, will rely on cache (offline)'); + console.debug( + "[network_detection.ts] No local relays available, will rely on cache (offline)", + ); return { inboxRelays: [], - outboxRelays: [] + 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]); + 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 + outboxRelays: slowOutboxRelays, }; } case NetworkCondition.ONLINE: default: // Full relay set when online - console.debug('[network_detection.ts] Using full relay set (online)'); + console.debug("[network_detection.ts] Using full relay set (online)"); return fullRelaySet; } } @@ -161,14 +183,16 @@ export function startNetworkMonitoring( const checkNetwork = async () => { try { const currentCondition = await detectNetworkCondition(); - + if (currentCondition !== lastCondition) { - console.debug(`[network_detection.ts] Network condition changed: ${lastCondition} -> ${currentCondition}`); + 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); + console.warn("[network_detection.ts] Network monitoring error:", error); } }; @@ -185,4 +209,4 @@ export function startNetworkMonitoring( intervalId = null; } }; -} \ No newline at end of file +} diff --git a/src/lib/utils/nostrEventService.ts b/src/lib/utils/nostrEventService.ts index 459275c..745bef4 100644 --- a/src/lib/utils/nostrEventService.ts +++ b/src/lib/utils/nostrEventService.ts @@ -1,11 +1,11 @@ import { nip19 } from "nostr-tools"; -import { getEventHash, signEvent, prefixNostrAddresses } from "./nostrUtils.ts"; +import { getEventHash, prefixNostrAddresses, signEvent } from "./nostrUtils.ts"; import { get } from "svelte/store"; import { goto } from "$app/navigation"; import { EVENT_KINDS, TIME_CONSTANTS } from "./search_constants.ts"; import { EXPIRATION_DURATION } from "../consts.ts"; import { ndkInstance } from "../ndk.ts"; -import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk"; +import { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk"; export interface RootEventInfo { rootId: string; @@ -96,21 +96,21 @@ export function extractRootEventInfo(parent: NDKEvent): RootEventInfo { rootInfo.rootId = rootE[1]; rootInfo.rootRelay = getRelayString(rootE[2]); rootInfo.rootPubkey = getPubkeyString(rootE[3] || rootInfo.rootPubkey); - rootInfo.rootKind = - Number(getTagValue(parent.tags, "K")) || rootInfo.rootKind; + rootInfo.rootKind = Number(getTagValue(parent.tags, "K")) || + rootInfo.rootKind; } else if (rootA) { rootInfo.rootAddress = rootA[1]; rootInfo.rootRelay = getRelayString(rootA[2]); rootInfo.rootPubkey = getPubkeyString( getTagValue(parent.tags, "P") || rootInfo.rootPubkey, ); - rootInfo.rootKind = - Number(getTagValue(parent.tags, "K")) || rootInfo.rootKind; + rootInfo.rootKind = Number(getTagValue(parent.tags, "K")) || + rootInfo.rootKind; } else if (rootI) { rootInfo.rootIValue = rootI[1]; rootInfo.rootIRelay = getRelayString(rootI[2]); - rootInfo.rootKind = - Number(getTagValue(parent.tags, "K")) || rootInfo.rootKind; + rootInfo.rootKind = Number(getTagValue(parent.tags, "K")) || + rootInfo.rootKind; } return rootInfo; @@ -224,7 +224,8 @@ export function buildReplyTags( if (isParentReplaceable) { const dTag = getTagValue(parent.tags || [], "d"); if (dTag) { - const parentAddress = `${parentInfo.parentKind}:${parentInfo.parentPubkey}:${dTag}`; + const parentAddress = + `${parentInfo.parentKind}:${parentInfo.parentPubkey}:${dTag}`; addTags(tags, createTag("a", parentAddress, "", "root")); } } @@ -233,7 +234,8 @@ export function buildReplyTags( if (isParentReplaceable) { const dTag = getTagValue(parent.tags || [], "d"); if (dTag) { - const parentAddress = `${parentInfo.parentKind}:${parentInfo.parentPubkey}:${dTag}`; + const parentAddress = + `${parentInfo.parentKind}:${parentInfo.parentPubkey}:${dTag}`; if (isReplyToComment) { // Root scope (uppercase) - use the original article @@ -317,14 +319,16 @@ export async function createSignedEvent( pubkey: string, kind: number, tags: string[][], -// deno-lint-ignore no-explicit-any + // deno-lint-ignore no-explicit-any ): Promise<{ id: string; sig: string; event: any }> { const prefixedContent = prefixNostrAddresses(content); // Add expiration tag for kind 24 events (NIP-40) const finalTags = [...tags]; if (kind === 24) { - const expirationTimestamp = Math.floor(Date.now() / TIME_CONSTANTS.UNIX_TIMESTAMP_FACTOR) + EXPIRATION_DURATION; + const expirationTimestamp = + Math.floor(Date.now() / TIME_CONSTANTS.UNIX_TIMESTAMP_FACTOR) + + EXPIRATION_DURATION; finalTags.push(["expiration", String(expirationTimestamp)]); } @@ -344,7 +348,10 @@ export async function createSignedEvent( }; let sig, id; - if (typeof window !== "undefined" && globalThis.nostr && globalThis.nostr.signEvent) { + if ( + typeof window !== "undefined" && globalThis.nostr && + globalThis.nostr.signEvent + ) { const signed = await globalThis.nostr.signEvent(eventToSign); sig = signed.sig as string; id = "id" in signed ? (signed.id as string) : getEventHash(eventToSign); @@ -387,7 +394,7 @@ export async function publishEvent( try { // If event is a plain object, create an NDKEvent from it let ndkEvent: NDKEvent; - if (event.publish && typeof event.publish === 'function') { + if (event.publish && typeof event.publish === "function") { // It's already an NDKEvent ndkEvent = event; } else { @@ -397,15 +404,15 @@ export async function publishEvent( // Publish with timeout 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: ndkEvent.id, relayCount: relayUrls.length, - successfulRelays + successfulRelays, }); } catch (error) { console.error("[nostrEventService] Failed to publish event:", error); diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index 8d5a2dc..33263e4 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -5,7 +5,12 @@ import { npubCache } from "./npubCache.ts"; import NDK, { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; import type { NDKKind, NostrEvent } from "@nostr-dev-kit/ndk"; import type { Filter } from "./search_types.ts"; -import { communityRelays, secondaryRelays, searchRelays, anonymousRelays } from "../consts.ts"; +import { + anonymousRelays, + communityRelays, + searchRelays, + secondaryRelays, +} from "../consts.ts"; import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts"; import { NDKRelaySet as NDKRelaySetFromNDK } from "@nostr-dev-kit/ndk"; import { sha256 } from "@noble/hashes/sha2.js"; @@ -55,7 +60,7 @@ function escapeHtml(text: string): string { * Escape regex special characters */ function escapeRegExp(string: string): string { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } /** @@ -68,7 +73,12 @@ export async function getUserMetadata( // Remove nostr: prefix if present const cleanId = identifier.replace(/^nostr:/, ""); - console.log("getUserMetadata called with identifier:", identifier, "force:", force); + console.log( + "getUserMetadata called with identifier:", + identifier, + "force:", + force, + ); if (!force && npubCache.has(cleanId)) { const cached = npubCache.get(cleanId)!; @@ -100,7 +110,10 @@ export async function getUserMetadata( } else if (decoded.type === "nprofile") { pubkey = decoded.data.pubkey; } else { - console.warn("getUserMetadata: Unsupported identifier type:", decoded.type); + console.warn( + "getUserMetadata: Unsupported identifier type:", + decoded.type, + ); npubCache.set(cleanId, fallback); return fallback; } @@ -111,13 +124,12 @@ export async function getUserMetadata( kinds: [0], authors: [pubkey], }); - + console.log("getUserMetadata: Profile event found:", profileEvent); - - const profile = - profileEvent && profileEvent.content - ? JSON.parse(profileEvent.content) - : null; + + const profile = profileEvent && profileEvent.content + ? JSON.parse(profileEvent.content) + : null; console.log("getUserMetadata: Parsed profile:", profile); @@ -199,7 +211,7 @@ export async function createProfileLinkWithVerification( }; const allRelays = [ - ...searchRelays, // Include search relays for profile searches + ...searchRelays, // Include search relays for profile searches ...communityRelays, ...userRelays, ...secondaryRelays, @@ -223,8 +235,7 @@ export async function createProfileLinkWithVerification( const defaultText = `${cleanId.slice(0, 8)}...${cleanId.slice(-4)}`; const escapedText = escapeHtml(displayText || defaultText); - const displayIdentifier = - profile?.displayName ?? + const displayIdentifier = profile?.displayName ?? profile?.display_name ?? profile?.name ?? escapedText; @@ -287,7 +298,10 @@ export async function processNostrIdentifiers( const displayText = metadata.displayName || metadata.name; const link = createProfileLink(identifier, displayText); // Replace all occurrences of this exact match - processedContent = processedContent.replace(new RegExp(escapeRegExp(fullMatch), 'g'), link); + processedContent = processedContent.replace( + new RegExp(escapeRegExp(fullMatch), "g"), + link, + ); } // Process notes (nevent, note, naddr) @@ -304,7 +318,10 @@ export async function processNostrIdentifiers( } const link = createNoteLink(identifier); // Replace all occurrences of this exact match - processedContent = processedContent.replace(new RegExp(escapeRegExp(fullMatch), 'g'), link); + processedContent = processedContent.replace( + new RegExp(escapeRegExp(fullMatch), "g"), + link, + ); } return processedContent; @@ -409,7 +426,7 @@ export function withTimeout( return Promise.race([ promise, new Promise((_, reject) => - setTimeout(() => reject(new Error("Timeout")), timeoutMs), + setTimeout(() => reject(new Error("Timeout")), timeoutMs) ), ]); } @@ -420,7 +437,7 @@ export function withTimeout( return Promise.race([ promise, new Promise((_, reject) => - setTimeout(() => reject(new Error("Timeout")), timeoutMs), + setTimeout(() => reject(new Error("Timeout")), timeoutMs) ), ]); } @@ -455,40 +472,54 @@ export async function fetchEventWithFallback( ): Promise { // AI-NOTE: 2025-01-24 - Use ALL available relays for comprehensive event discovery // This ensures we don't miss events that might be on any available relay - + // Get all relays from NDK pool first (most comprehensive) - const poolRelays = Array.from(ndk.pool.relays.values()).map((r: any) => r.url); + const poolRelays = Array.from(ndk.pool.relays.values()).map((r: any) => + r.url + ); const inboxRelays = get(activeInboxRelays); const outboxRelays = get(activeOutboxRelays); - + // Combine all available relays, prioritizing pool relays - let allRelays = [...new Set([...poolRelays, ...inboxRelays, ...outboxRelays])]; - + let allRelays = [ + ...new Set([...poolRelays, ...inboxRelays, ...outboxRelays]), + ]; + console.log("fetchEventWithFallback: Using pool relays:", poolRelays); console.log("fetchEventWithFallback: Using inbox relays:", inboxRelays); console.log("fetchEventWithFallback: Using outbox relays:", outboxRelays); console.log("fetchEventWithFallback: Total unique relays:", allRelays.length); - + // Check if we have any relays available if (allRelays.length === 0) { - console.warn("fetchEventWithFallback: No relays available for event fetch, using fallback relays"); + console.warn( + "fetchEventWithFallback: No relays available for event fetch, using fallback relays", + ); // Use fallback relays when no relays are available allRelays = [...secondaryRelays, ...searchRelays, ...anonymousRelays]; console.log("fetchEventWithFallback: Using fallback relays:", allRelays); } - + // Create relay set from all available relays const relaySet = NDKRelaySetFromNDK.fromRelayUrls(allRelays, ndk); try { if (relaySet.relays.size === 0) { - console.warn("fetchEventWithFallback: 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: Relay set size:", + relaySet.relays.size, + ); console.log("fetchEventWithFallback: Filter:", filterOrId); - console.log("fetchEventWithFallback: Relay URLs:", Array.from(relaySet.relays).map((r) => r.url)); + console.log( + "fetchEventWithFallback: Relay URLs:", + Array.from(relaySet.relays).map((r) => r.url), + ); let found: NDKEvent | null = null; @@ -500,8 +531,9 @@ export async function fetchEventWithFallback( .fetchEvent({ ids: [filterOrId] }, undefined, relaySet) .withTimeout(timeoutMs); } else { - const filter = - typeof filterOrId === "string" ? { ids: [filterOrId] } : filterOrId; + const filter = typeof filterOrId === "string" + ? { ids: [filterOrId] } + : filterOrId; const results = await ndk .fetchEvents(filter, undefined, relaySet) .withTimeout(timeoutMs); @@ -512,7 +544,9 @@ export async function fetchEventWithFallback( if (!found) { const timeoutSeconds = timeoutMs / 1000; - const relayUrls = Array.from(relaySet.relays).map((r) => r.url).join(", "); + const relayUrls = Array.from(relaySet.relays).map((r) => r.url).join( + ", ", + ); console.warn( `fetchEventWithFallback: Event not found after ${timeoutSeconds}s timeout. Tried inbox relays: ${relayUrls}. Some relays may be offline or slow.`, ); @@ -523,14 +557,19 @@ export async function fetchEventWithFallback( // Always wrap as NDKEvent return found instanceof NDKEvent ? found : new NDKEvent(ndk, found); } catch (err) { - if (err instanceof Error && err.message === 'Timeout') { + if (err instanceof Error && err.message === "Timeout") { const timeoutSeconds = timeoutMs / 1000; - const relayUrls = Array.from(relaySet.relays).map((r) => r.url).join(", "); + const relayUrls = Array.from(relaySet.relays).map((r) => r.url).join( + ", ", + ); console.warn( `fetchEventWithFallback: Event fetch timed out after ${timeoutSeconds}s. Tried inbox relays: ${relayUrls}. Some relays may be offline or slow.`, ); } else { - console.error("fetchEventWithFallback: Error in fetchEventWithFallback:", err); + console.error( + "fetchEventWithFallback: Error in fetchEventWithFallback:", + err, + ); } return null; } @@ -545,20 +584,22 @@ export function toNpub(pubkey: string | undefined): string | null { try { // If it's already an npub, return it if (pubkey.startsWith("npub")) return pubkey; - + // If it's a hex pubkey, convert to npub if (new RegExp(`^[a-f0-9]{${VALIDATION.HEX_LENGTH}}$`, "i").test(pubkey)) { return nip19.npubEncode(pubkey); } - + // If it's an nprofile, decode and extract npub if (pubkey.startsWith("nprofile")) { const decoded = nip19.decode(pubkey); - if (decoded.type === 'nprofile') { - return decoded.data.pubkey ? nip19.npubEncode(decoded.data.pubkey) : null; + if (decoded.type === "nprofile") { + return decoded.data.pubkey + ? nip19.npubEncode(decoded.data.pubkey) + : null; } } - + return null; } catch { return null; @@ -573,7 +614,10 @@ export function createRelaySetFromUrls(relayUrls: string[], ndk: NDK) { return NDKRelaySetFromNDK.fromRelayUrls(relayUrls, ndk); } -export function createNDKEvent(ndk: NDK, rawEvent: NDKEvent | NostrEvent | undefined) { +export function createNDKEvent( + ndk: NDK, + rawEvent: NDKEvent | NostrEvent | undefined, +) { return new NDKEvent(ndk, rawEvent); } diff --git a/src/lib/utils/nostr_identifiers.ts b/src/lib/utils/nostr_identifiers.ts index 8e789d7..78d1a3d 100644 --- a/src/lib/utils/nostr_identifiers.ts +++ b/src/lib/utils/nostr_identifiers.ts @@ -1,4 +1,4 @@ -import { VALIDATION } from './search_constants'; +import { VALIDATION } from "./search_constants"; /** * Nostr identifier types @@ -22,7 +22,7 @@ export interface ParsedCoordinate { * @returns True if it's a valid hex event ID */ export function isEventId(id: string): id is NostrEventId { - return new RegExp(`^[a-f0-9]{${VALIDATION.HEX_LENGTH}}$`, 'i').test(id); + return new RegExp(`^[a-f0-9]{${VALIDATION.HEX_LENGTH}}$`, "i").test(id); } /** @@ -30,22 +30,24 @@ export function isEventId(id: string): id is NostrEventId { * @param coordinate The string to check * @returns True if it's a valid coordinate */ -export function isCoordinate(coordinate: string): coordinate is NostrCoordinate { - const parts = coordinate.split(':'); +export function isCoordinate( + coordinate: string, +): coordinate is NostrCoordinate { + const parts = coordinate.split(":"); if (parts.length < 3) return false; - + const [kindStr, pubkey, ...dTagParts] = parts; - + // Check if kind is a valid number const kind = parseInt(kindStr, 10); if (isNaN(kind) || kind < 0) return false; - + // Check if pubkey is a valid hex string if (!isEventId(pubkey)) return false; - + // Check if d-tag exists (can contain colons) if (dTagParts.length === 0) return false; - + return true; } @@ -56,14 +58,14 @@ export function isCoordinate(coordinate: string): coordinate is NostrCoordinate */ export function parseCoordinate(coordinate: string): ParsedCoordinate | null { if (!isCoordinate(coordinate)) return null; - - const parts = coordinate.split(':'); + + const parts = coordinate.split(":"); const [kindStr, pubkey, ...dTagParts] = parts; - + return { kind: parseInt(kindStr, 10), pubkey, - dTag: dTagParts.join(':') // Rejoin in case d-tag contains colons + dTag: dTagParts.join(":"), // Rejoin in case d-tag contains colons }; } @@ -74,7 +76,11 @@ export function parseCoordinate(coordinate: string): ParsedCoordinate | null { * @param dTag The d-tag value * @returns The coordinate string */ -export function createCoordinate(kind: number, pubkey: string, dTag: string): NostrCoordinate { +export function createCoordinate( + kind: number, + pubkey: string, + dTag: string, +): NostrCoordinate { return `${kind}:${pubkey}:${dTag}`; } @@ -83,6 +89,8 @@ export function createCoordinate(kind: number, pubkey: string, dTag: string): No * @param identifier The string to check * @returns True if it's a valid Nostr identifier */ -export function isNostrIdentifier(identifier: string): identifier is NostrIdentifier { +export function isNostrIdentifier( + identifier: string, +): identifier is NostrIdentifier { return isEventId(identifier) || isCoordinate(identifier); -} \ No newline at end of file +} diff --git a/src/lib/utils/notification_utils.ts b/src/lib/utils/notification_utils.ts deleted file mode 100644 index a9f70a4..0000000 --- a/src/lib/utils/notification_utils.ts +++ /dev/null @@ -1,306 +0,0 @@ -import type { NDKEvent } from "$lib/utils/nostrUtils"; -import { getUserMetadata, NDKRelaySetFromNDK, toNpub } from "$lib/utils/nostrUtils"; -import { get } from "svelte/store"; -import { ndkInstance } from "$lib/ndk"; -import { searchRelays } from "$lib/consts"; -import { userStore, type UserState } from "$lib/stores/userStore"; -import { buildCompleteRelaySet } from "$lib/utils/relay_management"; -import { neventEncode } from "$lib/utils"; -import { nip19 } from "nostr-tools"; -import type NDK from "@nostr-dev-kit/ndk"; -import { parseEmbeddedMarkup } from "./markup/embeddedMarkupParser"; - -// AI-NOTE: Notification-specific utility functions that don't exist elsewhere - -/** - * Truncates content to a specified length - */ -export function truncateContent(content: string, maxLength: number = 300): string { - if (content.length <= maxLength) return content; - return content.slice(0, maxLength) + "..."; -} - -/** - * Truncates rendered HTML content while preserving quote boxes - */ -export function truncateRenderedContent(renderedHtml: string, maxLength: number = 300): string { - if (renderedHtml.length <= maxLength) return renderedHtml; - - const hasQuoteBoxes = renderedHtml.includes('jump-to-message'); - - if (hasQuoteBoxes) { - const quoteBoxPattern = /
    ]*>[^<]*<\/div>/g; - const quoteBoxes = renderedHtml.match(quoteBoxPattern) || []; - - let textOnly = renderedHtml.replace(quoteBoxPattern, '|||QUOTEBOX|||'); - - if (textOnly.length > maxLength) { - const availableLength = maxLength - (quoteBoxes.join('').length); - if (availableLength > 50) { - textOnly = textOnly.slice(0, availableLength) + "..."; - } else { - textOnly = textOnly.slice(0, 50) + "..."; - } - } - - let result = textOnly; - quoteBoxes.forEach(box => { - result = result.replace('|||QUOTEBOX|||', box); - }); - - return result; - } else { - if (renderedHtml.includes('<')) { - const truncated = renderedHtml.slice(0, maxLength); - const lastTagStart = truncated.lastIndexOf('<'); - const lastTagEnd = truncated.lastIndexOf('>'); - - if (lastTagStart > lastTagEnd) { - return renderedHtml.slice(0, lastTagStart) + "..."; - } - return truncated + "..."; - } else { - return renderedHtml.slice(0, maxLength) + "..."; - } - } -} - -/** - * Parses content with support for embedded events - */ -export async function parseContent(content: string): Promise { - if (!content) return ""; - return await parseEmbeddedMarkup(content, 0); -} - -/** - * Parses repost content and renders it as an embedded event - */ -export async function parseRepostContent(content: string): Promise { - if (!content) return ""; - - try { - // Try to parse the content as JSON (repost events contain the original event as JSON) - const originalEvent = JSON.parse(content); - - // Extract the original event's content - const originalContent = originalEvent.content || ""; - const originalAuthor = originalEvent.pubkey || ""; - const originalCreatedAt = originalEvent.created_at || 0; - const originalKind = originalEvent.kind || 1; - - // Parse the original content with embedded markup support - const parsedOriginalContent = await parseEmbeddedMarkup(originalContent, 0); - - // Create an embedded event display with proper structure - const formattedDate = originalCreatedAt ? new Date(originalCreatedAt * 1000).toLocaleDateString() : "Unknown date"; - const shortAuthor = originalAuthor ? `${originalAuthor.slice(0, 8)}...${originalAuthor.slice(-4)}` : "Unknown"; - - return ` -
    - -
    -
    - - Kind ${originalKind} - - - (repost) - - - Author: - - ${shortAuthor} - - - - ${formattedDate} - -
    - -
    - - -
    - ${parsedOriginalContent} -
    -
    - `; - } catch (error) { - // If JSON parsing fails, fall back to embedded markup - console.warn("Failed to parse repost content as JSON, falling back to embedded markup:", error); - return await parseEmbeddedMarkup(content, 0); - } -} - -/** - * Renders quoted content for a message - */ -export async function renderQuotedContent(message: NDKEvent, publicMessages: NDKEvent[]): Promise { - const qTags = message.getMatchingTags("q"); - if (qTags.length === 0) return ""; - - const qTag = qTags[0]; - const eventId = qTag[1]; - - if (eventId) { - // Validate eventId format (should be 64 character hex string) - const isValidEventId = /^[a-fA-F0-9]{64}$/.test(eventId); - - // First try to find in local messages - let quotedMessage = publicMessages.find(msg => msg.id === eventId); - - // If not found locally, fetch from relays - if (!quotedMessage) { - try { - const ndk: NDK | undefined = get(ndkInstance); - if (ndk) { - const userStoreValue: UserState = get(userStore); - const user = userStoreValue.signedIn && userStoreValue.pubkey ? ndk.getUser({ pubkey: userStoreValue.pubkey }) : null; - const relaySet = await buildCompleteRelaySet(ndk, user); - const allRelays = [...relaySet.inboxRelays, ...relaySet.outboxRelays, ...searchRelays]; - - if (allRelays.length > 0) { - const ndkRelaySet = NDKRelaySetFromNDK.fromRelayUrls(allRelays, ndk); - const fetchedEvent = await ndk.fetchEvent({ ids: [eventId], limit: 1 }, undefined, ndkRelaySet); - quotedMessage = fetchedEvent || undefined; - } - } - } catch (error) { - console.warn(`[renderQuotedContent] Failed to fetch quoted event ${eventId}:`, error); - } - } - - if (quotedMessage) { - const quotedContent = quotedMessage.content ? quotedMessage.content.slice(0, 200) : "No content"; - const parsedContent = await parseEmbeddedMarkup(quotedContent, 0); - return `
    ${parsedContent}
    `; - } else { - // Fallback to nevent link - only if eventId is valid - if (isValidEventId) { - try { - const nevent = nip19.neventEncode({ id: eventId }); - return `
    Quoted message not found. Click to view event ${eventId.slice(0, 8)}...
    `; - } catch (error) { - console.warn(`[renderQuotedContent] Failed to encode nevent for ${eventId}:`, error); - // Fall back to just showing the event ID without a link - return `
    Quoted message not found. Event ID: ${eventId.slice(0, 8)}...
    `; - } - } else { - // Invalid event ID format - return `
    Invalid quoted message reference
    `; - } - } - } - - return ""; -} - -/** - * Gets notification type based on event kind - */ -export function getNotificationType(event: NDKEvent): string { - switch (event.kind) { - case 1: return "Reply"; - case 1111: return "Custom Reply"; - case 9802: return "Highlight"; - case 6: return "Repost"; - case 16: return "Generic Repost"; - case 24: return "Public Message"; - default: return `Kind ${event.kind}`; - } -} - -/** - * Fetches author profiles for a list of events - */ -export async function fetchAuthorProfiles(events: NDKEvent[]): Promise> { - const authorProfiles = new Map(); - const uniquePubkeys = new Set(); - - events.forEach(event => { - if (event.pubkey) uniquePubkeys.add(event.pubkey); - }); - - const profilePromises = Array.from(uniquePubkeys).map(async (pubkey) => { - try { - const npub = toNpub(pubkey); - if (!npub) return; - - // Try cache first - let profile = await getUserMetadata(npub, false); - if (profile && (profile.name || profile.displayName || profile.picture)) { - authorProfiles.set(pubkey, profile); - return; - } - - // Try search relays - for (const relay of searchRelays) { - try { - const ndk: NDK | undefined = get(ndkInstance); - if (!ndk) break; - - const relaySet = NDKRelaySetFromNDK.fromRelayUrls([relay], ndk); - const profileEvent = await ndk.fetchEvent( - { kinds: [0], authors: [pubkey] }, - undefined, - relaySet - ); - - if (profileEvent) { - const profileData = JSON.parse(profileEvent.content); - authorProfiles.set(pubkey, { - name: profileData.name, - displayName: profileData.display_name || profileData.displayName, - picture: profileData.picture || profileData.image - }); - return; - } - } catch (error) { - console.warn(`[fetchAuthorProfiles] Failed to fetch profile from ${relay}:`, error); - } - } - - // Try all available relays as fallback - try { - const ndk: NDK | undefined = get(ndkInstance); - if (!ndk) return; - - const userStoreValue: UserState = get(userStore); - const user = userStoreValue.signedIn && userStoreValue.pubkey ? ndk.getUser({ pubkey: userStoreValue.pubkey }) : null; - const relaySet = await buildCompleteRelaySet(ndk, user); - const allRelays = [...relaySet.inboxRelays, ...relaySet.outboxRelays]; - - if (allRelays.length > 0) { - const ndkRelaySet = NDKRelaySetFromNDK.fromRelayUrls(allRelays, ndk); - const profileEvent = await ndk.fetchEvent( - { kinds: [0], authors: [pubkey] }, - undefined, - ndkRelaySet - ); - - if (profileEvent) { - const profileData = JSON.parse(profileEvent.content); - authorProfiles.set(pubkey, { - name: profileData.name, - displayName: profileData.display_name || profileData.displayName, - picture: profileData.picture || profileData.image - }); - } - } - } catch (error) { - console.warn(`[fetchAuthorProfiles] Failed to fetch profile from all relays:`, error); - } - } catch (error) { - console.warn(`[fetchAuthorProfiles] Error processing profile for ${pubkey}:`, error); - } - }); - - await Promise.all(profilePromises); - return authorProfiles; -} diff --git a/src/lib/utils/npubCache.ts b/src/lib/utils/npubCache.ts index 8c1c36f..4cd49c8 100644 --- a/src/lib/utils/npubCache.ts +++ b/src/lib/utils/npubCache.ts @@ -4,7 +4,7 @@ export type NpubMetadata = NostrProfile; class NpubCache { private cache: Record = {}; - private readonly storageKey = 'alexandria_npub_cache'; + private readonly storageKey = "alexandria_npub_cache"; private readonly maxAge = 24 * 60 * 60 * 1000; // 24 hours in milliseconds constructor() { @@ -13,12 +13,15 @@ class NpubCache { private loadFromStorage(): void { try { - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { const stored = localStorage.getItem(this.storageKey); if (stored) { - const data = JSON.parse(stored) as Record; + const data = JSON.parse(stored) as Record< + string, + { profile: NpubMetadata; timestamp: number } + >; const now = Date.now(); - + // Filter out expired entries for (const [key, entry] of Object.entries(data)) { if (entry.timestamp && (now - entry.timestamp) < this.maxAge) { @@ -28,21 +31,24 @@ class NpubCache { } } } catch (error) { - console.warn('Failed to load npub cache from storage:', error); + console.warn("Failed to load npub cache from storage:", error); } } private saveToStorage(): void { try { - if (typeof window !== 'undefined') { - const data: Record = {}; + if (typeof window !== "undefined") { + const data: Record< + string, + { profile: NpubMetadata; timestamp: number } + > = {}; for (const [key, profile] of Object.entries(this.cache)) { data[key] = { profile, timestamp: Date.now() }; } localStorage.setItem(this.storageKey, JSON.stringify(data)); } } catch (error) { - console.warn('Failed to save npub cache to storage:', error); + console.warn("Failed to save npub cache to storage:", error); } } diff --git a/src/lib/utils/profileCache.ts b/src/lib/utils/profileCache.ts index 2a93a45..8034dd4 100644 --- a/src/lib/utils/profileCache.ts +++ b/src/lib/utils/profileCache.ts @@ -24,7 +24,7 @@ async function fetchProfile(pubkey: string): Promise { const profileEvents = await ndk.fetchEvents({ kinds: [0], authors: [pubkey], - limit: 1 + limit: 1, }); if (profileEvents.size === 0) { @@ -33,7 +33,7 @@ async function fetchProfile(pubkey: string): Promise { // Get the most recent profile event const profileEvent = Array.from(profileEvents)[0]; - + try { const content = JSON.parse(profileEvent.content); return content as ProfileData; @@ -77,14 +77,14 @@ export async function getDisplayName(pubkey: string): Promise { * @returns Array of profile events */ export async function batchFetchProfiles( - pubkeys: string[], - onProgress?: (fetched: number, total: number) => void + pubkeys: string[], + onProgress?: (fetched: number, total: number) => void, ): Promise { const allProfileEvents: NDKEvent[] = []; - + // Filter out already cached pubkeys - const uncachedPubkeys = pubkeys.filter(pk => !profileCache.has(pk)); - + const uncachedPubkeys = pubkeys.filter((pk) => !profileCache.has(pk)); + if (uncachedPubkeys.length === 0) { if (onProgress) onProgress(pubkeys.length, pubkeys.length); return allProfileEvents; @@ -92,21 +92,24 @@ export async function batchFetchProfiles( try { const ndk = get(ndkInstance); - + // Report initial progress const cachedCount = pubkeys.length - uncachedPubkeys.length; if (onProgress) onProgress(cachedCount, pubkeys.length); - + // Batch fetch in chunks to avoid overwhelming relays const CHUNK_SIZE = 50; let fetchedCount = cachedCount; - + for (let i = 0; i < uncachedPubkeys.length; i += CHUNK_SIZE) { - const chunk = uncachedPubkeys.slice(i, Math.min(i + CHUNK_SIZE, uncachedPubkeys.length)); - + const chunk = uncachedPubkeys.slice( + i, + Math.min(i + CHUNK_SIZE, uncachedPubkeys.length), + ); + const profileEvents = await ndk.fetchEvents({ kinds: [0], - authors: chunk + authors: chunk, }); // Process each profile event @@ -120,19 +123,19 @@ export async function batchFetchProfiles( console.error("Failed to parse profile content:", e); } }); - + // Update progress if (onProgress) { onProgress(fetchedCount, pubkeys.length); } } - + // Final progress update if (onProgress) onProgress(pubkeys.length, pubkeys.length); } catch (e) { console.error("Failed to batch fetch profiles:", e); } - + return allProfileEvents; } @@ -173,29 +176,29 @@ export function clearProfileCache(): void { */ export function extractPubkeysFromEvents(events: NDKEvent[]): Set { const pubkeys = new Set(); - - events.forEach(event => { + + events.forEach((event) => { // Add author pubkey if (event.pubkey) { pubkeys.add(event.pubkey); } - + // Add pubkeys from p tags const pTags = event.getMatchingTags("p"); - pTags.forEach(tag => { + pTags.forEach((tag) => { if (tag[1]) { pubkeys.add(tag[1]); } }); - + // Extract pubkeys from content (nostr:npub1... format) const npubPattern = /nostr:npub1[a-z0-9]{58}/g; const matches = event.content?.match(npubPattern) || []; - matches.forEach(match => { + matches.forEach((match) => { try { - const npub = match.replace('nostr:', ''); + const npub = match.replace("nostr:", ""); const decoded = nip19.decode(npub); - if (decoded.type === 'npub') { + if (decoded.type === "npub") { pubkeys.add(decoded.data as string); } } catch (e) { @@ -203,7 +206,7 @@ export function extractPubkeysFromEvents(events: NDKEvent[]): Set { } }); }); - + return pubkeys; } @@ -214,17 +217,17 @@ export function extractPubkeysFromEvents(events: NDKEvent[]): Set { */ export function replaceContentPubkeys(content: string): string { if (!content) return content; - + // Replace nostr:npub1... references const npubPattern = /nostr:npub[a-z0-9]{58}/g; let result = content; - + const matches = content.match(npubPattern) || []; - matches.forEach(match => { + matches.forEach((match) => { try { - const npub = match.replace('nostr:', ''); + const npub = match.replace("nostr:", ""); const decoded = nip19.decode(npub); - if (decoded.type === 'npub') { + if (decoded.type === "npub") { const pubkey = decoded.data as string; const displayName = getDisplayNameSync(pubkey); result = result.replace(match, `@${displayName}`); @@ -233,7 +236,7 @@ export function replaceContentPubkeys(content: string): string { // Invalid npub, leave as is } }); - + return result; } @@ -245,8 +248,8 @@ export function replaceContentPubkeys(content: string): string { export function replacePubkeysWithDisplayNames(text: string): string { // Match hex pubkeys (64 characters) const pubkeyRegex = /\b[0-9a-fA-F]{64}\b/g; - + return text.replace(pubkeyRegex, (match) => { return getDisplayNameSync(match); }); -} \ No newline at end of file +} diff --git a/src/lib/utils/profile_search.ts b/src/lib/utils/profile_search.ts index ecf43ec..55bb582 100644 --- a/src/lib/utils/profile_search.ts +++ b/src/lib/utils/profile_search.ts @@ -1,15 +1,15 @@ -import { ndkInstance, activeInboxRelays } from "../ndk.ts"; -import { getUserMetadata, getNpubFromNip05 } from "./nostrUtils.ts"; -import NDK, { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk"; +import { activeInboxRelays, ndkInstance } from "../ndk.ts"; +import { getNpubFromNip05, getUserMetadata } from "./nostrUtils.ts"; +import NDK, { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk"; import { searchCache } from "./searchCache.ts"; -import { searchRelays, communityRelays, secondaryRelays } from "../consts.ts"; +import { communityRelays, searchRelays, secondaryRelays } from "../consts.ts"; import { get } from "svelte/store"; import type { NostrProfile, ProfileSearchResult } from "./search_types.ts"; import { + createProfileFromEvent, fieldMatches, nip05Matches, normalizeSearchTerm, - createProfileFromEvent, } from "./search_utils.ts"; /** @@ -267,12 +267,12 @@ async function quickRelaySearch( // Use search relays (optimized for profiles) + user's inbox relays + community relays const userInboxRelays = get(activeInboxRelays); const quickRelayUrls = [ - ...searchRelays, // Dedicated profile search relays - ...userInboxRelays, // User's personal inbox relays - ...communityRelays, // Community relays - ...secondaryRelays // Secondary relays as fallback + ...searchRelays, // Dedicated profile search relays + ...userInboxRelays, // User's personal inbox relays + ...communityRelays, // Community relays + ...secondaryRelays, // Secondary relays as fallback ]; - + // Deduplicate relay URLs const uniqueRelayUrls = [...new Set(quickRelayUrls)]; console.log("Using relays for profile search:", uniqueRelayUrls); @@ -312,8 +312,8 @@ async function quickRelaySearch( try { if (!event.content) return; const profileData = JSON.parse(event.content); - const displayName = - profileData.displayName || profileData.display_name || ""; + const displayName = profileData.displayName || + profileData.display_name || ""; const display_name = profileData.display_name || ""; const name = profileData.name || ""; const nip05 = profileData.nip05 || ""; @@ -363,7 +363,9 @@ async function quickRelaySearch( sub.on("eose", () => { console.log( - `Relay ${index + 1} (${uniqueRelayUrls[index]}) search completed, processed ${eventCount} events, found ${foundInRelay.length} matches`, + `Relay ${index + 1} (${ + uniqueRelayUrls[index] + }) search completed, processed ${eventCount} events, found ${foundInRelay.length} matches`, ); resolve(foundInRelay); }); @@ -371,7 +373,9 @@ async function quickRelaySearch( // Short timeout for quick search setTimeout(() => { console.log( - `Relay ${index + 1} (${uniqueRelayUrls[index]}) search timed out after 1.5s, processed ${eventCount} events, found ${foundInRelay.length} matches`, + `Relay ${index + 1} (${ + uniqueRelayUrls[index] + }) search timed out after 1.5s, processed ${eventCount} events, found ${foundInRelay.length} matches`, ); sub.stop(); resolve(foundInRelay); diff --git a/src/lib/utils/relayDiagnostics.ts b/src/lib/utils/relayDiagnostics.ts index 6be37c4..2e650e3 100644 --- a/src/lib/utils/relayDiagnostics.ts +++ b/src/lib/utils/relayDiagnostics.ts @@ -42,9 +42,8 @@ export async function testRelay(url: string): Promise { responseTime: Date.now() - startTime, }); } - } + }; }); - } /** @@ -93,7 +92,9 @@ export function logRelayDiagnostics(diagnostics: RelayDiagnostic[]): void { console.log(`✅ Working relays (${working.length}):`); working.forEach((d) => { console.log( - ` - ${d.url}${d.requiresAuth ? " (requires auth)" : ""}${d.responseTime ? ` (${d.responseTime}ms)` : ""}`, + ` - ${d.url}${d.requiresAuth ? " (requires auth)" : ""}${ + d.responseTime ? ` (${d.responseTime}ms)` : "" + }`, ); }); diff --git a/src/lib/utils/relay_info_service.ts b/src/lib/utils/relay_info_service.ts index 8b978a0..2b83c71 100644 --- a/src/lib/utils/relay_info_service.ts +++ b/src/lib/utils/relay_info_service.ts @@ -6,7 +6,7 @@ function simplifyUrl(url: string): string { try { const urlObj = new URL(url); - return urlObj.hostname + (urlObj.port ? `:${urlObj.port}` : ''); + return urlObj.hostname + (urlObj.port ? `:${urlObj.port}` : ""); } catch { // If URL parsing fails, return the original string return url; @@ -42,18 +42,23 @@ export interface RelayInfoWithMetadata extends RelayInfo { * @param url The relay URL to fetch info for * @returns Promise resolving to relay info or undefined if failed */ -export async function fetchRelayInfo(url: string): Promise { +export async function fetchRelayInfo( + url: string, +): Promise { try { // Convert WebSocket URL to HTTP URL for NIP-11 - const httpUrl = url.replace('ws://', 'http://').replace('wss://', 'https://'); - + const httpUrl = url.replace("ws://", "http://").replace( + "wss://", + "https://", + ); + const response = await fetch(httpUrl, { - headers: { - 'Accept': 'application/nostr+json', - 'User-Agent': 'Alexandria/1.0' + headers: { + "Accept": "application/nostr+json", + "User-Agent": "Alexandria/1.0", }, // Add timeout to prevent hanging - signal: AbortSignal.timeout(5000) + signal: AbortSignal.timeout(5000), }); if (!response.ok) { @@ -62,18 +67,18 @@ export async function fetchRelayInfo(url: string): Promise 0, - triedNip11: true + triedNip11: true, }; } catch (error) { console.warn(`[RelayInfo] Failed to fetch info for ${url}:`, error); @@ -81,7 +86,7 @@ export async function fetchRelayInfo(url: string): Promise { +export async function fetchRelayInfos( + urls: string[], +): Promise { if (urls.length === 0) { return []; } - const promises = urls.map(url => fetchRelayInfo(url)); + const promises = urls.map((url) => fetchRelayInfo(url)); const results = await Promise.allSettled(promises); - + return results - .map(result => result.status === 'fulfilled' ? result.value : undefined) + .map((result) => result.status === "fulfilled" ? result.value : undefined) .filter((info): info is RelayInfoWithMetadata => info !== undefined); } @@ -110,34 +117,42 @@ export async function fetchRelayInfos(urls: string[]): Promise { // Only test connections on client-side - if (typeof window === 'undefined') { + if (typeof window === "undefined") { return Promise.resolve({ connected: false, requiresAuth: false, @@ -66,7 +72,7 @@ export function testLocalRelayConnection( actualUrl: relayUrl, }); } - + return new Promise((resolve) => { try { // Ensure the URL is using ws:// protocol for local relays @@ -193,7 +199,7 @@ export function testRemoteRelayConnection( actualUrl?: string; }> { // Only test connections on client-side - if (typeof window === 'undefined') { + if (typeof window === "undefined") { return Promise.resolve({ connected: false, requiresAuth: false, @@ -201,12 +207,14 @@ export function testRemoteRelayConnection( actualUrl: relayUrl, }); } - + return new Promise((resolve) => { // Ensure the URL is using wss:// protocol for remote relays const secureUrl = relayUrl.replace(/^ws:\/\//, "wss://"); - - console.debug(`[relay_management.ts] Testing remote relay connection: ${secureUrl}`); + + console.debug( + `[relay_management.ts] Testing remote relay connection: ${secureUrl}`, + ); // Use the existing NDK instance instead of creating a new one const relay = new NDKRelay(secureUrl, undefined, ndk); @@ -216,7 +224,9 @@ export function testRemoteRelayConnection( let actualUrl: string | undefined; const timeout = setTimeout(() => { - console.debug(`[relay_management.ts] Relay ${secureUrl} connection timeout`); + console.debug( + `[relay_management.ts] Relay ${secureUrl} connection timeout`, + ); relay.disconnect(); resolve({ connected: false, @@ -227,7 +237,9 @@ export function testRemoteRelayConnection( }, 3000); relay.on("connect", () => { - console.debug(`[relay_management.ts] Relay ${secureUrl} connected successfully`); + console.debug( + `[relay_management.ts] Relay ${secureUrl} connected successfully`, + ); connected = true; actualUrl = secureUrl; clearTimeout(timeout); @@ -248,7 +260,9 @@ export function testRemoteRelayConnection( relay.on("disconnect", () => { if (!connected) { - console.debug(`[relay_management.ts] Relay ${secureUrl} disconnected without connecting`); + console.debug( + `[relay_management.ts] Relay ${secureUrl} disconnected without connecting`, + ); error = "Connection failed"; clearTimeout(timeout); resolve({ @@ -280,14 +294,12 @@ export function testRelayConnection( actualUrl?: string; }> { // Determine if this is a local or remote relay - if (relayUrl.includes('localhost') || relayUrl.includes('127.0.0.1')) { + if (relayUrl.includes("localhost") || relayUrl.includes("127.0.0.1")) { return testLocalRelayConnection(relayUrl, ndk); } else { return testRemoteRelayConnection(relayUrl, ndk); } } - - /** * Tests connection to local relays @@ -295,14 +307,17 @@ export function testRelayConnection( * @param ndk NDK instance * @returns Promise that resolves to array of working local relay URLs */ -async function testLocalRelays(localRelayUrls: string[], ndk: NDK): Promise { +async function testLocalRelays( + localRelayUrls: string[], + ndk: NDK, +): Promise { try { const workingRelays: string[] = []; - + if (localRelayUrls.length === 0) { return workingRelays; } - + // Test local relays quietly, without logging failures await Promise.all( localRelayUrls.map(async (url) => { @@ -310,17 +325,21 @@ async function testLocalRelays(localRelayUrls: string[], ndk: NDK): Promise 0) { - console.info(`[relay_management.ts] Found ${workingRelays.length} working local relays`); + console.info( + `[relay_management.ts] Found ${workingRelays.length} working local relays`, + ); } return workingRelays; } catch { @@ -339,17 +358,17 @@ export async function discoverLocalRelays(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'); + 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: string) => - url.replace(/^wss:\/\//, 'ws://') + const localRelayUrls = localRelays.map((url: string) => + 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; @@ -365,7 +384,10 @@ export async function discoverLocalRelays(ndk: NDK): Promise { * @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 { +export async function getUserLocalRelays( + ndk: NDK, + user: NDKUser, +): Promise { try { const localRelayEvent = await ndk.fetchEvent( { @@ -376,7 +398,7 @@ export async function getUserLocalRelays(ndk: NDK, user: NDKUser): Promise { - if (tag[0] === 'r' && tag[1]) { + 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); + console.info( + "[relay_management.ts] Error fetching user local relays:", + error, + ); return []; } } @@ -403,7 +428,10 @@ export async function getUserLocalRelays(ndk: NDK, user: NDKUser): Promise { +export async function getUserBlockedRelays( + ndk: NDK, + user: NDKUser, +): Promise { try { const blockedRelayEvent = await ndk.fetchEvent( { @@ -414,7 +442,7 @@ export async function getUserBlockedRelays(ndk: NDK, user: NDKUser): Promise { - if (tag[0] === 'r' && tag[1]) { + 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); + console.info( + "[relay_management.ts] Error fetching user blocked relays:", + error, + ); return []; } } @@ -441,9 +472,15 @@ export async function getUserBlockedRelays(ndk: NDK, user: NDKUser): Promise { +export async function getUserOutboxRelays( + ndk: NDK, + user: NDKUser, +): Promise { try { - console.debug('[relay_management.ts] Fetching outbox relays for user:', user.pubkey); + console.debug( + "[relay_management.ts] Fetching outbox relays for user:", + user.pubkey, + ); const relayList = await ndk.fetchEvent( { kinds: [10002], @@ -453,36 +490,47 @@ export async function getUserOutboxRelays(ndk: NDK, user: NDKUser): Promise { - console.debug('[relay_management.ts] Processing tag:', tag); - if (tag[0] === 'w' && tag[1]) { + 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]) { + 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]); + 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] Skipping tag:", + tag[0], + "value:", + tag[1], + ); } }); - console.debug('[relay_management.ts] Final outbox relays:', outboxRelays); + console.debug("[relay_management.ts] Final outbox relays:", outboxRelays); return outboxRelays; } catch (error) { - console.info('[relay_management.ts] Error fetching user outbox relays:', error); + console.info( + "[relay_management.ts] Error fetching user outbox relays:", + error, + ); return []; } } @@ -494,45 +542,65 @@ 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' || !globalThis.nostr) { - console.debug('[relay_management.ts] No globalThis.nostr available'); + if (typeof window === "undefined" || !globalThis.nostr) { + console.debug("[relay_management.ts] No globalThis.nostr available"); return []; } - console.debug('[relay_management.ts] Extension available, checking for getRelays()'); + 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 (globalThis.nostr.getRelays) { - console.debug('[relay_management.ts] getRelays() method found, calling it...'); + console.debug( + "[relay_management.ts] getRelays() method found, calling it...", + ); try { const relays = await globalThis.nostr.getRelays(); - console.debug('[relay_management.ts] getRelays() returned:', relays); - if (relays && typeof relays === 'object') { + 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); + console.debug( + "[relay_management.ts] Got relays from extension:", + relayUrls, + ); } } catch (error) { - console.debug('[relay_management.ts] Extension getRelays() failed:', error); + console.debug( + "[relay_management.ts] Extension getRelays() failed:", + error, + ); } } else { - console.debug('[relay_management.ts] getRelays() method not found on globalThis.nostr'); + console.debug( + "[relay_management.ts] getRelays() method not found on globalThis.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] Extension does not expose relay configuration", + ); } - console.debug('[relay_management.ts] Final extension relays:', extensionRelays); + console.debug( + "[relay_management.ts] Final extension relays:", + extensionRelays, + ); return extensionRelays; } catch (error) { - console.debug('[relay_management.ts] Error getting extension relays:', error); + console.debug( + "[relay_management.ts] Error getting extension relays:", + error, + ); return []; } } @@ -547,36 +615,59 @@ async function testRelaySet(relayUrls: string[], ndk: NDK): Promise { const workingRelays: string[] = []; const maxConcurrent = 2; // Reduce to 2 relays at a time to avoid overwhelming them - console.debug(`[relay_management.ts] Testing ${relayUrls.length} relays in batches of ${maxConcurrent}`); + console.debug( + `[relay_management.ts] Testing ${relayUrls.length} relays in batches of ${maxConcurrent}`, + ); console.debug(`[relay_management.ts] Relay URLs to test:`, relayUrls); for (let i = 0; i < relayUrls.length; i += maxConcurrent) { const batch = relayUrls.slice(i, i + maxConcurrent); - console.debug(`[relay_management.ts] Testing batch ${Math.floor(i/maxConcurrent) + 1}:`, batch); - + console.debug( + `[relay_management.ts] Testing batch ${ + Math.floor(i / maxConcurrent) + 1 + }:`, + batch, + ); + const batchPromises = batch.map(async (url) => { try { console.debug(`[relay_management.ts] Testing relay: ${url}`); const result = await testRelayConnection(url, ndk); - console.debug(`[relay_management.ts] Relay ${url} test result:`, result); + console.debug( + `[relay_management.ts] Relay ${url} test result:`, + result, + ); return result.connected ? url : null; } catch (error) { - console.debug(`[relay_management.ts] Failed to test relay ${url}:`, error); + console.debug( + `[relay_management.ts] Failed to test relay ${url}:`, + error, + ); return null; } }); const batchResults = await Promise.allSettled(batchPromises); const batchWorkingRelays = batchResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') - .map(result => result.value) + .filter((result): result is PromiseFulfilledResult => + result.status === "fulfilled" + ) + .map((result) => result.value) .filter((url): url is string => url !== null); - - console.debug(`[relay_management.ts] Batch ${Math.floor(i/maxConcurrent) + 1} working relays:`, batchWorkingRelays); + + console.debug( + `[relay_management.ts] Batch ${ + Math.floor(i / maxConcurrent) + 1 + } working relays:`, + batchWorkingRelays, + ); workingRelays.push(...batchWorkingRelays); } - console.debug(`[relay_management.ts] Total working relays after testing:`, workingRelays); + console.debug( + `[relay_management.ts] Total working relays after testing:`, + workingRelays, + ); return workingRelays; } @@ -588,13 +679,19 @@ async function testRelaySet(relayUrls: string[], ndk: NDK): Promise { */ export async function buildCompleteRelaySet( ndk: NDK, - user: NDKUser | null + user: NDKUser | null, ): Promise<{ inboxRelays: string[]; outboxRelays: string[] }> { - console.debug('[relay_management.ts] buildCompleteRelaySet: Starting with user:', user?.pubkey || 'null'); - + 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); + console.debug( + "[relay_management.ts] buildCompleteRelaySet: Discovered local relays:", + discoveredLocalRelays, + ); // Get user-specific relays if available let userOutboxRelays: string[] = []; @@ -603,42 +700,75 @@ export async function buildCompleteRelaySet( let extensionRelays: string[] = []; if (user) { - console.debug('[relay_management.ts] buildCompleteRelaySet: Fetching user-specific relays for:', user.pubkey); - + 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); + console.debug( + "[relay_management.ts] buildCompleteRelaySet: User outbox relays:", + userOutboxRelays, + ); } catch (error) { - console.debug('[relay_management.ts] Error fetching user outbox relays:', 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); + console.debug( + "[relay_management.ts] buildCompleteRelaySet: User local relays:", + userLocalRelays, + ); } catch (error) { - console.debug('[relay_management.ts] Error fetching user local relays:', 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); + console.debug( + "[relay_management.ts] buildCompleteRelaySet: User blocked relays:", + blockedRelays, + ); } catch { // Silently ignore blocked relay fetch errors } try { extensionRelays = await getExtensionRelays(); - console.debug('[relay_management.ts] Extension relays gathered:', extensionRelays); + console.debug( + "[relay_management.ts] Extension relays gathered:", + extensionRelays, + ); } catch (error) { - console.debug('[relay_management.ts] Error fetching extension relays:', 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'); + 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, ...extensionRelays]); + const finalInboxRelays = deduplicateRelayUrls([ + ...discoveredLocalRelays, + ...userLocalRelays, + ]); + const finalOutboxRelays = deduplicateRelayUrls([ + ...discoveredLocalRelays, + ...userOutboxRelays, + ...extensionRelays, + ]); // Test relays and filter out non-working ones let testedInboxRelays: string[] = []; @@ -654,21 +784,27 @@ export async function buildCompleteRelaySet( // If no relays passed testing, use remote relays without testing if (testedInboxRelays.length === 0 && testedOutboxRelays.length === 0) { - const remoteRelays = deduplicateRelayUrls([...secondaryRelays, ...searchRelays]); + const remoteRelays = deduplicateRelayUrls([ + ...secondaryRelays, + ...searchRelays, + ]); return { inboxRelays: remoteRelays, - outboxRelays: remoteRelays + outboxRelays: remoteRelays, }; } // Always include some remote relays as fallback, even when local relays are working - const fallbackRelays = deduplicateRelayUrls([...anonymousRelays, ...secondaryRelays]); - + const fallbackRelays = deduplicateRelayUrls([ + ...anonymousRelays, + ...secondaryRelays, + ]); + // Use tested relays and add fallback relays - const inboxRelays = testedInboxRelays.length > 0 + const inboxRelays = testedInboxRelays.length > 0 ? deduplicateRelayUrls([...testedInboxRelays, ...fallbackRelays]) : deduplicateRelayUrls(fallbackRelays); - const outboxRelays = testedOutboxRelays.length > 0 + const outboxRelays = testedOutboxRelays.length > 0 ? deduplicateRelayUrls([...testedOutboxRelays, ...fallbackRelays]) : deduplicateRelayUrls(fallbackRelays); @@ -678,27 +814,51 @@ export async function buildCompleteRelaySet( currentNetworkCondition, discoveredLocalRelays, lowbandwidthRelays, - { inboxRelays, outboxRelays } + { inboxRelays, outboxRelays }, ); // Filter out blocked relays and deduplicate final sets const finalRelaySet = { - inboxRelays: deduplicateRelayUrls(networkOptimizedRelaySet.inboxRelays.filter((r: string) => !blockedRelays.includes(r))), - outboxRelays: deduplicateRelayUrls(networkOptimizedRelaySet.outboxRelays.filter((r: string) => !blockedRelays.includes(r))) + inboxRelays: deduplicateRelayUrls( + networkOptimizedRelaySet.inboxRelays.filter((r: string) => + !blockedRelays.includes(r) + ), + ), + outboxRelays: deduplicateRelayUrls( + networkOptimizedRelaySet.outboxRelays.filter((r: string) => + !blockedRelays.includes(r) + ), + ), }; // Ensure we always have at least some relays - if (finalRelaySet.inboxRelays.length === 0 && finalRelaySet.outboxRelays.length === 0) { - console.warn('[relay_management.ts] No relays available, using anonymous relays as final fallback'); + if ( + finalRelaySet.inboxRelays.length === 0 && + finalRelaySet.outboxRelays.length === 0 + ) { + console.warn( + "[relay_management.ts] No relays available, using anonymous relays as final fallback", + ); return { inboxRelays: deduplicateRelayUrls(anonymousRelays), - outboxRelays: deduplicateRelayUrls(anonymousRelays) + outboxRelays: deduplicateRelayUrls(anonymousRelays), }; } - 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); - + 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 +} diff --git a/src/lib/utils/search_result_formatter.ts b/src/lib/utils/search_result_formatter.ts index 3488b83..2e946d7 100644 --- a/src/lib/utils/search_result_formatter.ts +++ b/src/lib/utils/search_result_formatter.ts @@ -6,17 +6,19 @@ export class SearchResultFormatter { /** * Formats a result message based on search count and type */ - formatResultMessage(searchResultCount: number | null, searchResultType: string | null): string { + formatResultMessage( + searchResultCount: number | null, + searchResultType: string | null, + ): string { if (searchResultCount === 0) { return "Search completed. No results found."; } - const typeLabel = - searchResultType === "n" - ? "profile" - : searchResultType === "nip05" - ? "NIP-05 address" - : "event"; + const typeLabel = searchResultType === "n" + ? "profile" + : searchResultType === "nip05" + ? "NIP-05 address" + : "event"; const countLabel = searchResultType === "n" ? "profiles" : "events"; return searchResultCount === 1 diff --git a/src/lib/utils/search_utility.ts b/src/lib/utils/search_utility.ts index e91da1f..45d8a85 100644 --- a/src/lib/utils/search_utility.ts +++ b/src/lib/utils/search_utility.ts @@ -13,13 +13,13 @@ export { searchBySubscription } from "./subscription_search"; export { searchEvent, searchNip05 } from "./event_search"; export { checkCommunity } from "./community_checker"; export { - wellKnownUrl, - lnurlpWellKnownUrl, - isValidNip05Address, - normalizeSearchTerm, - fieldMatches, - nip05Matches, COMMON_DOMAINS, - isEmojiReaction, createProfileFromEvent, + fieldMatches, + isEmojiReaction, + isValidNip05Address, + lnurlpWellKnownUrl, + nip05Matches, + normalizeSearchTerm, + wellKnownUrl, } from "./search_utils"; diff --git a/src/lib/utils/subscription_search.ts b/src/lib/utils/subscription_search.ts index 169cfb6..dabe1fc 100644 --- a/src/lib/utils/subscription_search.ts +++ b/src/lib/utils/subscription_search.ts @@ -2,28 +2,28 @@ import { ndkInstance } from "../ndk.ts"; import { getMatchingTags, getNpubFromNip05 } from "./nostrUtils.ts"; import { nip19 } from "./nostrUtils.ts"; -import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk"; +import { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk"; import { searchCache } from "./searchCache.ts"; import { communityRelays, searchRelays } from "../consts.ts"; import { get } from "svelte/store"; import type { + SearchCallbacks, + SearchFilter, SearchResult, SearchSubscriptionType, - SearchFilter, - SearchCallbacks, } from "./search_types.ts"; import { - fieldMatches, - nip05Matches, COMMON_DOMAINS, + fieldMatches, isEmojiReaction, + nip05Matches, } from "./search_utils.ts"; -import { TIMEOUTS, SEARCH_LIMITS } from "./search_constants.ts"; +import { SEARCH_LIMITS, TIMEOUTS } from "./search_constants.ts"; import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts"; // Helper function to normalize URLs for comparison const normalizeUrl = (url: string): string => { - return url.replace(/\/$/, ''); // Remove trailing slash + return url.replace(/\/$/, ""); // Remove trailing slash }; /** @@ -62,7 +62,9 @@ export async function searchBySubscription( // AI-NOTE: 2025-01-24 - For profile searches, return cached results immediately // The EventSearch component now handles cache checking before calling this function if (searchType === "n") { - console.log("subscription_search: Returning cached profile result immediately"); + console.log( + "subscription_search: Returning cached profile result immediately", + ); return cachedResult; } else { return cachedResult; @@ -147,8 +149,10 @@ export async function searchBySubscription( // AI-NOTE: 2025-01-08 - For profile searches, return immediately when found // but still start background search for second-order results if (searchType === "n") { - console.log("subscription_search: Profile found, returning immediately but starting background second-order search"); - + console.log( + "subscription_search: Profile found, returning immediately but starting background second-order search", + ); + // Start Phase 2 in background for second-order results searchOtherRelaysInBackground( searchType, @@ -157,9 +161,11 @@ export async function searchBySubscription( callbacks, cleanup, ); - + const elapsed = Date.now() - startTime; - console.log(`subscription_search: Profile search completed in ${elapsed}ms`); + console.log( + `subscription_search: Profile search completed in ${elapsed}ms`, + ); return immediateResult; } @@ -177,7 +183,7 @@ export async function searchBySubscription( console.log( "subscription_search: No results from primary relay", ); - + // AI-NOTE: 2025-01-08 - For profile searches, if no results found in search relays, // try all relays as fallback if (searchType === "n") { @@ -185,20 +191,23 @@ export async function searchBySubscription( "subscription_search: No profile found in search relays, trying all relays", ); // Try with all relays as fallback - const allRelaySet = new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())) as any, ndk); + const allRelaySet = new NDKRelaySet( + new Set(Array.from(ndk.pool.relays.values())) as any, + ndk, + ); try { const fallbackEvents = await ndk.fetchEvents( searchFilter.filter, { closeOnEose: true }, allRelaySet, ); - + console.log( "subscription_search: Fallback search returned", fallbackEvents.size, "events", ); - + processPrimaryRelayResults( fallbackEvents, searchType, @@ -208,7 +217,7 @@ export async function searchBySubscription( abortSignal, cleanup, ); - + if (hasResults(searchState, searchType)) { console.log( "subscription_search: Found profile in fallback search, returning immediately", @@ -220,21 +229,31 @@ export async function searchBySubscription( ); searchCache.set(searchType, normalizedSearchTerm, fallbackResult); const elapsed = Date.now() - startTime; - console.log(`subscription_search: Profile search completed in ${elapsed}ms (fallback)`); + console.log( + `subscription_search: Profile search completed in ${elapsed}ms (fallback)`, + ); return fallbackResult; } } catch (fallbackError) { - console.error("subscription_search: Fallback search failed:", fallbackError); + console.error( + "subscription_search: Fallback search failed:", + fallbackError, + ); } - + console.log( "subscription_search: Profile not found in any relays, returning empty result", ); - const emptyResult = createEmptySearchResult(searchType, normalizedSearchTerm); + const emptyResult = createEmptySearchResult( + searchType, + normalizedSearchTerm, + ); // AI-NOTE: 2025-01-08 - Don't cache empty profile results as they may be due to search issues // rather than the profile not existing const elapsed = Date.now() - startTime; - console.log(`subscription_search: Profile search completed in ${elapsed}ms (not found)`); + console.log( + `subscription_search: Profile search completed in ${elapsed}ms (not found)`, + ); return emptyResult; } else { console.log( @@ -262,13 +281,15 @@ export async function searchBySubscription( callbacks, cleanup, ); - + // AI-NOTE: 2025-01-08 - Log performance for non-profile searches if (searchType !== "n") { const elapsed = Date.now() - startTime; - console.log(`subscription_search: ${searchType} search completed in ${elapsed}ms`); + console.log( + `subscription_search: ${searchType} search completed in ${elapsed}ms`, + ); } - + return result; } @@ -324,7 +345,10 @@ async function createSearchFilter( switch (searchType) { case "d": { const dFilter = { - filter: { "#d": [normalizedSearchTerm], limit: SEARCH_LIMITS.GENERAL_CONTENT }, + filter: { + "#d": [normalizedSearchTerm], + limit: SEARCH_LIMITS.GENERAL_CONTENT, + }, subscriptionType: "d-tag", }; console.log("subscription_search: Created d-tag filter:", dFilter); @@ -332,7 +356,10 @@ async function createSearchFilter( } case "t": { const tFilter = { - filter: { "#t": [normalizedSearchTerm], limit: SEARCH_LIMITS.GENERAL_CONTENT }, + filter: { + "#t": [normalizedSearchTerm], + limit: SEARCH_LIMITS.GENERAL_CONTENT, + }, subscriptionType: "t-tag", }; console.log("subscription_search: Created t-tag filter:", tFilter); @@ -412,11 +439,14 @@ function createPrimaryRelaySet( ): NDKRelaySet { // 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)); - + console.debug( + "subscription_search: NDK pool relays:", + poolRelays.map((r: any) => r.url), + ); + // AI-NOTE: 2025-01-24 - Use ALL available relays for comprehensive search coverage // This ensures searches don't fail due to missing relays and provides maximum event discovery - + if (searchType === "n") { // For profile searches, prioritize search relays for speed but include all relays const searchRelaySet = poolRelays.filter( @@ -426,29 +456,43 @@ function createPrimaryRelaySet( normalizeUrl(relay.url) === normalizeUrl(searchRelay), ), ); - + if (searchRelaySet.length > 0) { - console.debug('subscription_search: Profile search - using search relays for speed:', searchRelaySet.map((r: any) => r.url)); + console.debug( + "subscription_search: Profile search - using search relays for speed:", + searchRelaySet.map((r: any) => r.url), + ); // Still include all relays for comprehensive coverage - console.debug('subscription_search: Profile search - also including all relays for comprehensive coverage'); + console.debug( + "subscription_search: Profile search - also including all relays for comprehensive coverage", + ); return new NDKRelaySet(new Set(poolRelays) as any, ndk); } else { // Use all relays if search relays not available - console.debug('subscription_search: Profile search - using all relays:', poolRelays.map((r: any) => r.url)); + console.debug( + "subscription_search: Profile search - using all relays:", + poolRelays.map((r: any) => r.url), + ); return new NDKRelaySet(new Set(poolRelays) as any, ndk); } } else { // For all other searches, use ALL available relays for maximum coverage - const activeRelays = [...get(activeInboxRelays), ...get(activeOutboxRelays)]; - console.debug('subscription_search: Active relay stores:', { + const activeRelays = [ + ...get(activeInboxRelays), + ...get(activeOutboxRelays), + ]; + console.debug("subscription_search: Active relay stores:", { inboxRelays: get(activeInboxRelays), outboxRelays: get(activeOutboxRelays), - activeRelays + activeRelays, }); - + // AI-NOTE: 2025-01-24 - Use all pool relays instead of filtering to active relays only // This ensures we don't miss events that might be on other relays - console.debug('subscription_search: Using ALL pool relays for comprehensive search coverage:', poolRelays.map((r: any) => r.url)); + console.debug( + "subscription_search: Using ALL pool relays for comprehensive search coverage:", + poolRelays.map((r: any) => r.url), + ); return new NDKRelaySet(new Set(poolRelays) as any, ndk); } } @@ -620,12 +664,11 @@ function createSearchResult( normalizedSearchTerm: string, ): SearchResult { return { - events: - searchType === "n" - ? searchState.foundProfiles - : searchType === "t" - ? searchState.tTagEvents - : searchState.firstOrderEvents, + events: searchType === "n" + ? searchState.foundProfiles + : searchType === "t" + ? searchState.tTagEvents + : searchState.firstOrderEvents, secondOrder: [], tTagEvents: [], eventIds: searchState.eventIds, @@ -653,9 +696,11 @@ function searchOtherRelaysInBackground( new Set(Array.from(ndk.pool.relays.values())), ndk, ); - - console.debug('subscription_search: Background search using ALL relays:', - Array.from(ndk.pool.relays.values()).map((r: any) => r.url)); + + console.debug( + "subscription_search: Background search using ALL relays:", + Array.from(ndk.pool.relays.values()).map((r: any) => r.url), + ); // Subscribe to events from other relays const sub = ndk.subscribe( @@ -758,7 +803,10 @@ function processProfileEoseResults( ) { const targetPubkey = dedupedProfiles[0]?.pubkey; if (targetPubkey) { - console.log("subscription_search: Triggering second-order search for npub-specific profile:", targetPubkey); + console.log( + "subscription_search: Triggering second-order search for npub-specific profile:", + targetPubkey, + ); performSecondOrderSearchInBackground( "n", dedupedProfiles, @@ -768,13 +816,18 @@ function processProfileEoseResults( callbacks, ); } else { - console.log("subscription_search: No targetPubkey found for second-order search"); + console.log( + "subscription_search: No targetPubkey found for second-order search", + ); } } else if (searchFilter.subscriptionType === "profile") { // For general profile searches, perform second-order search for each found profile for (const profile of dedupedProfiles) { if (profile.pubkey) { - console.log("subscription_search: Triggering second-order search for general profile:", profile.pubkey); + console.log( + "subscription_search: Triggering second-order search for general profile:", + profile.pubkey, + ); performSecondOrderSearchInBackground( "n", dedupedProfiles, @@ -786,7 +839,10 @@ function processProfileEoseResults( } } } else { - console.log("subscription_search: No second-order search triggered for subscription type:", searchFilter.subscriptionType); + console.log( + "subscription_search: No second-order search triggered for subscription type:", + searchFilter.subscriptionType, + ); } return { @@ -896,7 +952,12 @@ async function performSecondOrderSearchInBackground( callbacks?: SearchCallbacks, ) { try { - console.log("subscription_search: Starting second-order search for", searchType, "with targetPubkey:", targetPubkey); + console.log( + "subscription_search: Starting second-order search for", + searchType, + "with targetPubkey:", + targetPubkey, + ); const ndk = get(ndkInstance); let allSecondOrderEvents: NDKEvent[] = []; @@ -910,20 +971,30 @@ async function performSecondOrderSearchInBackground( const searchPromise = (async () => { if (searchType === "n" && targetPubkey) { - console.log("subscription_search: Searching for events mentioning pubkey:", targetPubkey); - + console.log( + "subscription_search: Searching for events mentioning pubkey:", + targetPubkey, + ); + // AI-NOTE: 2025-01-24 - Use only active relays for second-order profile search to prevent hanging - const activeRelays = [...get(activeInboxRelays), ...get(activeOutboxRelays)]; + const activeRelays = [ + ...get(activeInboxRelays), + ...get(activeOutboxRelays), + ]; const availableRelays = activeRelays - .map(url => ndk.pool.relays.get(url)) + .map((url) => ndk.pool.relays.get(url)) .filter((relay): relay is any => relay !== undefined); const relaySet = new NDKRelaySet( new Set(availableRelays), - ndk + ndk, + ); + + console.log( + "subscription_search: Using", + activeRelays.length, + "active relays for second-order search", ); - - console.log("subscription_search: Using", activeRelays.length, "active relays for second-order search"); - + // Search for events that mention this pubkey via p-tags const pTagFilter = { "#p": [targetPubkey], limit: 50 }; // AI-NOTE: 2025-01-24 - Limit results to prevent hanging const pTagEvents = await ndk.fetchEvents( @@ -931,8 +1002,13 @@ async function performSecondOrderSearchInBackground( { closeOnEose: true }, relaySet, ); - console.log("subscription_search: Found", pTagEvents.size, "events with p-tag for", targetPubkey); - + console.log( + "subscription_search: Found", + pTagEvents.size, + "events with p-tag for", + targetPubkey, + ); + // AI-NOTE: 2025-01-24 - Also search for events written by this pubkey with limit const authorFilter = { authors: [targetPubkey], limit: 50 }; // AI-NOTE: 2025-01-24 - Limit results to prevent hanging const authorEvents = await ndk.fetchEvents( @@ -940,14 +1016,27 @@ async function performSecondOrderSearchInBackground( { closeOnEose: true }, relaySet, ); - console.log("subscription_search: Found", authorEvents.size, "events written by", targetPubkey); - + console.log( + "subscription_search: Found", + authorEvents.size, + "events written by", + targetPubkey, + ); + // Filter out unwanted events from both sets const filteredPTagEvents = filterUnwantedEvents(Array.from(pTagEvents)); - const filteredAuthorEvents = filterUnwantedEvents(Array.from(authorEvents)); - - console.log("subscription_search: After filtering unwanted events:", filteredPTagEvents.length, "p-tag events,", filteredAuthorEvents.length, "author events"); - + const filteredAuthorEvents = filterUnwantedEvents( + Array.from(authorEvents), + ); + + console.log( + "subscription_search: After filtering unwanted events:", + filteredPTagEvents.length, + "p-tag events,", + filteredAuthorEvents.length, + "author events", + ); + // Combine both sets of events allSecondOrderEvents = [...filteredPTagEvents, ...filteredAuthorEvents]; } else if (searchType === "d") { @@ -959,17 +1048,23 @@ async function performSecondOrderSearchInBackground( const [eTagEvents, aTagEvents] = await Promise.all([ eventIds.size > 0 ? ndk.fetchEvents( - { "#e": Array.from(eventIds), limit: SEARCH_LIMITS.SECOND_ORDER_RESULTS }, - { closeOnEose: true }, - relaySet, - ) + { + "#e": Array.from(eventIds), + limit: SEARCH_LIMITS.SECOND_ORDER_RESULTS, + }, + { closeOnEose: true }, + relaySet, + ) : Promise.resolve([]), addresses.size > 0 ? ndk.fetchEvents( - { "#a": Array.from(addresses), limit: SEARCH_LIMITS.SECOND_ORDER_RESULTS }, - { closeOnEose: true }, - relaySet, - ) + { + "#a": Array.from(addresses), + limit: SEARCH_LIMITS.SECOND_ORDER_RESULTS, + }, + { closeOnEose: true }, + relaySet, + ) : Promise.resolve([]), ]); // Filter out unwanted events @@ -1003,17 +1098,20 @@ async function performSecondOrderSearchInBackground( .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) .slice(0, SEARCH_LIMITS.SECOND_ORDER_RESULTS); - console.log("subscription_search: Second-order search completed with", sortedSecondOrder.length, "results"); + console.log( + "subscription_search: Second-order search completed with", + sortedSecondOrder.length, + "results", + ); // Update the search results with second-order events const result: SearchResult = { events: firstOrderEvents, secondOrder: sortedSecondOrder, tTagEvents: [], - eventIds: - searchType === "n" - ? new Set(firstOrderEvents.map((p) => p.id)) - : eventIds, + eventIds: searchType === "n" + ? new Set(firstOrderEvents.map((p) => p.id)) + : eventIds, addresses: searchType === "n" ? new Set() : addresses, searchType: searchType, searchTerm: "", // This will be set by the caller @@ -1021,10 +1119,16 @@ async function performSecondOrderSearchInBackground( // Notify UI of updated results if (callbacks?.onSecondOrderUpdate) { - console.log("subscription_search: Calling onSecondOrderUpdate callback with", sortedSecondOrder.length, "second-order events"); + console.log( + "subscription_search: Calling onSecondOrderUpdate callback with", + sortedSecondOrder.length, + "second-order events", + ); callbacks.onSecondOrderUpdate(result); } else { - console.log("subscription_search: No onSecondOrderUpdate callback available"); + console.log( + "subscription_search: No onSecondOrderUpdate callback available", + ); } })(); diff --git a/src/lib/utils/tag_event_fetch.ts b/src/lib/utils/tag_event_fetch.ts index 077a93e..b3a48e4 100644 --- a/src/lib/utils/tag_event_fetch.ts +++ b/src/lib/utils/tag_event_fetch.ts @@ -1,7 +1,7 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { ndkInstance } from "../ndk"; import { get } from "svelte/store"; -import { extractPubkeysFromEvents, batchFetchProfiles } from "./profileCache"; +import { batchFetchProfiles, extractPubkeysFromEvents } from "./profileCache"; // Constants for publication event kinds const INDEX_EVENT_KIND = 30040; @@ -17,12 +17,12 @@ export interface TagExpansionResult { /** * Fetches publications and their content events from relays based on tags - * + * * This function handles the relay-based fetching portion of tag expansion: * 1. Fetches publication index events that have any of the specified tags * 2. Extracts content event references from those publications * 3. Fetches the referenced content events - * + * * @param tags Array of tags to search for in publications * @param existingEventIds Set of existing event IDs to avoid duplicates * @param baseEvents Array of base events to check for existing content @@ -33,44 +33,46 @@ export async function fetchTaggedEventsFromRelays( tags: string[], existingEventIds: Set, baseEvents: NDKEvent[], - debug?: (...args: any[]) => void + debug?: (...args: any[]) => void, ): Promise { const log = debug || console.debug; - + log("Fetching from relays for tags:", tags); - + // Fetch publications that have any of the specified tags const ndk = get(ndkInstance); const taggedPublications = await ndk.fetchEvents({ kinds: [INDEX_EVENT_KIND], "#t": tags, // Match any of these tags - limit: 30 // Reasonable default limit + limit: 30, // Reasonable default limit }); - + log("Found tagged publications from relays:", taggedPublications.size); - + // Filter to avoid duplicates const newPublications = Array.from(taggedPublications).filter( - (event: NDKEvent) => !existingEventIds.has(event.id) + (event: NDKEvent) => !existingEventIds.has(event.id), ); - + // Extract content event d-tags from new publications const contentEventDTags = new Set(); const existingContentDTags = new Set( baseEvents - .filter(e => e.kind !== undefined && CONTENT_EVENT_KINDS.includes(e.kind)) - .map(e => e.tagValue("d")) - .filter(d => d !== undefined) + .filter((e) => + e.kind !== undefined && CONTENT_EVENT_KINDS.includes(e.kind) + ) + .map((e) => e.tagValue("d")) + .filter((d) => d !== undefined), ); - + newPublications.forEach((event: NDKEvent) => { const aTags = event.getMatchingTags("a"); aTags.forEach((tag: string[]) => { // Parse the 'a' tag identifier: kind:pubkey:d-tag if (tag[1]) { - const parts = tag[1].split(':'); + const parts = tag[1].split(":"); if (parts.length >= 3) { - const dTag = parts.slice(2).join(':'); // Handle d-tags with colons + const dTag = parts.slice(2).join(":"); // Handle d-tags with colons if (!existingContentDTags.has(dTag)) { contentEventDTags.add(dTag); } @@ -78,7 +80,7 @@ export async function fetchTaggedEventsFromRelays( } }); }); - + // Fetch the content events let newContentEvents: NDKEvent[] = []; if (contentEventDTags.size > 0) { @@ -88,21 +90,21 @@ export async function fetchTaggedEventsFromRelays( }); newContentEvents = Array.from(contentEventsSet); } - + return { publications: newPublications, - contentEvents: newContentEvents + contentEvents: newContentEvents, }; } /** * Searches through already fetched events for publications with specified tags - * + * * This function handles the local search portion of tag expansion: * 1. Searches through existing events for publications with matching tags * 2. Extracts content event references from those publications * 3. Finds the referenced content events in existing events - * + * * @param allEvents Array of all fetched events to search through * @param tags Array of tags to search for in publications * @param existingEventIds Set of existing event IDs to avoid duplicates @@ -115,42 +117,44 @@ export function findTaggedEventsInFetched( tags: string[], existingEventIds: Set, baseEvents: NDKEvent[], - debug?: (...args: any[]) => void + debug?: (...args: any[]) => void, ): TagExpansionResult { const log = debug || console.debug; - + log("Searching through already fetched events for tags:", tags); - + // Find publications in allEvents that have the specified tags - const taggedPublications = allEvents.filter(event => { + const taggedPublications = allEvents.filter((event) => { if (event.kind !== INDEX_EVENT_KIND) return false; if (existingEventIds.has(event.id)) return false; // Skip base events - + // Check if event has any of the specified tags - const eventTags = event.getMatchingTags("t").map(tag => tag[1]); - return tags.some(tag => eventTags.includes(tag)); + const eventTags = event.getMatchingTags("t").map((tag) => tag[1]); + return tags.some((tag) => eventTags.includes(tag)); }); - + const newPublications = taggedPublications; log("Found", newPublications.length, "publications in fetched events"); - + // For content events, also search in allEvents const existingContentDTags = new Set( baseEvents - .filter(e => e.kind !== undefined && CONTENT_EVENT_KINDS.includes(e.kind)) - .map(e => e.tagValue("d")) - .filter(d => d !== undefined) + .filter((e) => + e.kind !== undefined && CONTENT_EVENT_KINDS.includes(e.kind) + ) + .map((e) => e.tagValue("d")) + .filter((d) => d !== undefined), ); - + const contentEventDTags = new Set(); newPublications.forEach((event: NDKEvent) => { const aTags = event.getMatchingTags("a"); aTags.forEach((tag: string[]) => { // Parse the 'a' tag identifier: kind:pubkey:d-tag if (tag[1]) { - const parts = tag[1].split(':'); + const parts = tag[1].split(":"); if (parts.length >= 3) { - const dTag = parts.slice(2).join(':'); // Handle d-tags with colons + const dTag = parts.slice(2).join(":"); // Handle d-tags with colons if (!existingContentDTags.has(dTag)) { contentEventDTags.add(dTag); } @@ -158,23 +162,23 @@ export function findTaggedEventsInFetched( } }); }); - + // Find content events in allEvents - const newContentEvents = allEvents.filter(event => { + const newContentEvents = allEvents.filter((event) => { if (!CONTENT_EVENT_KINDS.includes(event.kind || 0)) return false; const dTag = event.tagValue("d"); return dTag !== undefined && contentEventDTags.has(dTag); }); - + return { publications: newPublications, - contentEvents: newContentEvents + contentEvents: newContentEvents, }; } /** * Fetches profiles for new events and updates progress - * + * * @param newPublications Array of new publication events * @param newContentEvents Array of new content events * @param onProgressUpdate Callback to update progress state @@ -184,23 +188,32 @@ export function findTaggedEventsInFetched( export async function fetchProfilesForNewEvents( newPublications: NDKEvent[], newContentEvents: NDKEvent[], - onProgressUpdate: (progress: { current: number; total: number } | null) => void, - debug?: (...args: any[]) => void + onProgressUpdate: ( + progress: { current: number; total: number } | null, + ) => void, + debug?: (...args: any[]) => void, ): Promise { const log = debug || console.debug; - + // Extract pubkeys from new events - const newPubkeys = extractPubkeysFromEvents([...newPublications, ...newContentEvents]); - + const newPubkeys = extractPubkeysFromEvents([ + ...newPublications, + ...newContentEvents, + ]); + if (newPubkeys.size > 0) { - log("Fetching profiles for", newPubkeys.size, "new pubkeys from tag expansion"); - + log( + "Fetching profiles for", + newPubkeys.size, + "new pubkeys from tag expansion", + ); + onProgressUpdate({ current: 0, total: newPubkeys.size }); - + await batchFetchProfiles(Array.from(newPubkeys), (fetched, total) => { onProgressUpdate({ current: fetched, total }); }); - + onProgressUpdate(null); } -} \ No newline at end of file +} diff --git a/src/lib/utils/websocket_utils.ts b/src/lib/utils/websocket_utils.ts index aeeeb65..5113d1c 100644 --- a/src/lib/utils/websocket_utils.ts +++ b/src/lib/utils/websocket_utils.ts @@ -18,7 +18,7 @@ export interface NostrFilter { ids?: string[]; authors?: string[]; kinds?: number[]; - [tag: `#${string}`]: string[] | undefined; + [tag: `#${string}`]: string[] | undefined; since?: number; until?: number; limit?: number; @@ -28,14 +28,16 @@ type ResolveCallback = (value: T | PromiseLike) => void; type RejectCallback = (reason?: any) => void; type EventHandler = (ev: Event) => void; type MessageEventHandler = (ev: MessageEvent) => void; -type EventHandlerReject = (reject: RejectCallback) => EventHandler; -type EventHandlerResolve = (resolve: ResolveCallback) => (reject: RejectCallback) => MessageEventHandler; +type EventHandlerReject = (reject: RejectCallback) => EventHandler; +type EventHandlerResolve = ( + resolve: ResolveCallback, +) => (reject: RejectCallback) => MessageEventHandler; function handleMessage( ev: MessageEvent, subId: string, resolve: (event: NostrEvent) => void, - reject: (reason: any) => void + reject: (reason: any) => void, ) { const data = JSON.parse(ev.data); @@ -64,43 +66,48 @@ function handleMessage( function handleError( ev: Event, - reject: (reason: any) => void + reject: (reason: any) => void, ) { reject(ev); } -export async function fetchNostrEvent(filter: NostrFilter): Promise { +export async function fetchNostrEvent( + filter: NostrFilter, +): Promise { // AI-NOTE: Updated to use active relay stores instead of hardcoded relay URL // This ensures the function uses the user's configured relays and can find events // across multiple relays rather than being limited to a single hardcoded relay. - + // Get available relays from the active relay stores const inboxRelays = get(activeInboxRelays); const outboxRelays = get(activeOutboxRelays); - + // Combine all available relays, prioritizing inbox relays let availableRelays = [...inboxRelays, ...outboxRelays]; - + // AI-NOTE: Use fallback relays when stores are empty (e.g., during SSR) // This ensures publications can still load even when relay stores haven't been populated if (availableRelays.length === 0) { // Import fallback relays from constants const { searchRelays, secondaryRelays } = await import("../consts.ts"); availableRelays = [...searchRelays, ...secondaryRelays]; - + if (availableRelays.length === 0) { availableRelays = ["wss://thecitadel.nostr1.com"]; } } - + // AI-NOTE: 2025-01-24 - Enhanced relay strategy for better event discovery // Always include search relays in the relay set for comprehensive event discovery const { searchRelays, secondaryRelays } = await import("../consts.ts"); const allRelays = [...availableRelays, ...searchRelays, ...secondaryRelays]; const uniqueRelays = [...new Set(allRelays)]; // Remove duplicates - - console.debug(`[fetchNostrEvent] Trying ${uniqueRelays.length} relays for event discovery:`, uniqueRelays); - + + console.debug( + `[fetchNostrEvent] Trying ${uniqueRelays.length} relays for event discovery:`, + uniqueRelays, + ); + // Try all available relays in parallel and return the first result const relayPromises = uniqueRelays.map(async (relay) => { try { @@ -110,16 +117,15 @@ export async function fetchNostrEvent(filter: NostrFilter): Promise (resolve: ResolveCallback) => (reject: RejectCallback) => MessageEventHandler = - (subId) => - (resolve) => - (reject) => - (ev: MessageEvent) => - handleMessage(ev, subId, resolve, reject); - const curriedErrorHandler: EventHandlerReject = - (reject) => - (ev: Event) => - handleError(ev, reject); + const curriedMessageHandler: ( + subId: string, + ) => ( + resolve: ResolveCallback, + ) => (reject: RejectCallback) => MessageEventHandler = + (subId) => (resolve) => (reject) => (ev: MessageEvent) => + handleMessage(ev, subId, resolve, reject); + const curriedErrorHandler: EventHandlerReject = (reject) => (ev: Event) => + handleError(ev, reject); // AI-NOTE: These variables store references to partially-applied handlers so that the `finally` // block receives the correct references to clean up the listeners. @@ -133,20 +139,20 @@ export async function fetchNostrEvent(filter: NostrFilter): Promise { - ws.removeEventListener("message", messageHandler); - ws.removeEventListener("error", errorHandler); - WebSocketPool.instance.release(ws); - }); + .withTimeout(2000) + .finally(() => { + ws.removeEventListener("message", messageHandler); + ws.removeEventListener("error", errorHandler); + WebSocketPool.instance.release(ws); + }); ws.send(JSON.stringify(["REQ", subId, filter])); - + const result = await res; if (result) { return result; } - + return null; } catch (err) { return null; @@ -155,14 +161,14 @@ export async function fetchNostrEvent(filter: NostrFilter): Promise { try { const event = await fetchNostrEvent({ "#d": [dTag], limit: 1 }); if (!event) { - error(404, `Event not found for d-tag: ${dTag}. href="/events?d=${dTag}"`); + error( + 404, + `Event not found for d-tag: ${dTag}. href="/events?d=${dTag}"`, + ); } return event; } catch (err) { @@ -215,7 +224,10 @@ export async function fetchEventByNaddr(naddr: string): Promise { }; const event = await fetchNostrEvent(filter); if (!event) { - error(404, `Event not found for naddr: ${naddr}. href="/events?id=${naddr}"`); + error( + 404, + `Event not found for naddr: ${naddr}. href="/events?id=${naddr}"`, + ); } return event; } catch (err) { @@ -234,7 +246,10 @@ export async function fetchEventByNevent(nevent: string): Promise { const decoded = neventDecode(nevent); const event = await fetchNostrEvent({ ids: [decoded.id], limit: 1 }); if (!event) { - error(404, `Event not found for nevent: ${nevent}. href="/events?id=${nevent}"`); + error( + 404, + `Event not found for nevent: ${nevent}. href="/events?id=${nevent}"`, + ); } return event; } catch (err) { diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts deleted file mode 100644 index a1253a9..0000000 --- a/src/routes/+layout.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { getPersistedLogin, initNdk, ndkInstance } from "../lib/ndk.ts"; -import { - loginWithExtension, - loginWithAmber, - loginWithNpub, -} from "../lib/stores/userStore.ts"; -import { loginMethodStorageKey } from "../lib/stores/userStore.ts"; -import Pharos, { pharosInstance } from "../lib/parser.ts"; -import type { LayoutLoad } from "./$types"; -import { get } from "svelte/store"; -import { browser } from "$app/environment"; - -// AI-NOTE: SSR enabled for better SEO and OpenGraph support -export const ssr = true; - -/** - * Attempts to restore the user's authentication session from localStorage. - * Handles extension, Amber (NIP-46), and npub login methods. - * Only runs on client-side. - */ -function restoreAuthSession() { - // Only run on client-side - if (!browser) return; - - try { - const pubkey = getPersistedLogin(); - const loginMethod = localStorage.getItem(loginMethodStorageKey); - const logoutFlag = localStorage.getItem("alexandria/logout/flag"); - console.log("Layout load - persisted pubkey:", pubkey); - console.log("Layout load - persisted login method:", loginMethod); - console.log("Layout load - logout flag:", logoutFlag); - console.log("All localStorage keys:", Object.keys(localStorage)); - - if (pubkey && loginMethod && !logoutFlag) { - if (loginMethod === "extension") { - console.log("Restoring extension login..."); - loginWithExtension(); - } else if (loginMethod === "amber") { - // 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 }) => { - const ndk = get(ndkInstance); - try { - // deno-lint-ignore no-explicit-any - const amberSigner = (NDKNip46Signer as any).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 { - // If reconnection fails, automatically fallback to npub-only mode - console.warn( - "Amber session could not be restored. Falling back to npub-only mode.", - ); - try { - // Set the flag first, before login - localStorage.setItem("alexandria/amber/fallback", "1"); - console.log("Set fallback flag in localStorage"); - - // Small delay to ensure flag is set - await new Promise((resolve) => setTimeout(resolve, 100)); - - await loginWithNpub(pubkey); - console.log("Successfully fell back to npub-only mode."); - } catch (fallbackErr) { - console.error( - "Failed to fallback to npub-only mode:", - fallbackErr, - ); - } - } - }, - ); - } else { - // No session data, automatically fallback to npub-only mode - console.log( - "No Amber session data found. Falling back to npub-only mode.", - ); - - // Set the flag first, before login - localStorage.setItem("alexandria/amber/fallback", "1"); - console.log("Set fallback flag in localStorage"); - - // Small delay to ensure flag is set - setTimeout(async () => { - try { - await loginWithNpub(pubkey); - console.log("Successfully fell back to npub-only mode."); - } catch (fallbackErr) { - console.error( - "Failed to fallback to npub-only mode:", - fallbackErr, - ); - } - }, 100); - } - } else if (loginMethod === "npub") { - console.log("Restoring npub login..."); - loginWithNpub(pubkey); - } - } else if (logoutFlag) { - console.log("Skipping auto-login due to logout flag"); - localStorage.removeItem("alexandria/logout/flag"); - } - } catch (e) { - console.warn( - `Failed to restore login: ${e}\n\nContinuing with anonymous session.`, - ); - } -} - -export const load: LayoutLoad = () => { - // Initialize NDK with new relay management system - const ndk = initNdk(); - ndkInstance.set(ndk); - - // Only restore auth session on client-side - if (browser) { - restoreAuthSession(); - } - - const parser = new Pharos(ndk); - pharosInstance.set(parser); - - return { - ndk, - parser, - }; -}; diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index fce8b7d..da0b823 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -1,6 +1,5 @@
    @@ -617,11 +574,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";
    - {#await ((result.kind === 6 || result.kind === 16) ? parseRepostContent(result.content) : parseContent(result.content)) then parsedContent} - {@html parsedContent.slice(0, 200)}{parsedContent.length > 200 ? "..." : ""} - {:catch} - {result.content.slice(0, 200)}{result.content.length > 200 ? "..." : ""} - {/await} +
    {/if} {/if} @@ -784,11 +737,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";
    - {#await ((result.kind === 6 || result.kind === 16) ? parseRepostContent(result.content) : parseContent(result.content)) then parsedContent} - {@html parsedContent.slice(0, 200)}{parsedContent.length > 200 ? "..." : ""} - {:catch} - {result.content.slice(0, 200)}{result.content.length > 200 ? "..." : ""} - {/await} +
    {/if} {/if} @@ -938,11 +887,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";
    - {#await ((result.kind === 6 || result.kind === 16) ? parseRepostContent(result.content) : parseContent(result.content)) then parsedContent} - {@html parsedContent.slice(0, 200)}{parsedContent.length > 200 ? "..." : ""} - {:catch} - {result.content.slice(0, 200)}{result.content.length > 200 ? "..." : ""} - {/await} +
    {/if} {/if} @@ -997,7 +942,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; {/if}
    - +
    diff --git a/src/routes/proxy+layout.ts b/src/routes/proxy+layout.ts deleted file mode 100644 index 8a97a72..0000000 --- a/src/routes/proxy+layout.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { LayoutLoad } from "./$types"; - -export const load: LayoutLoad = async () => { - return {}; -}; \ No newline at end of file diff --git a/src/routes/publication/+page.server.ts b/src/routes/publication/+page.server.ts index fa30a0d..f001a1c 100644 --- a/src/routes/publication/+page.server.ts +++ b/src/routes/publication/+page.server.ts @@ -5,7 +5,7 @@ import type { PageServerLoad } from "./$types"; const ROUTES = { PUBLICATION_BASE: "/publication", NADDR: "/publication/naddr", - NEVENT: "/publication/nevent", + NEVENT: "/publication/nevent", ID: "/publication/id", D_TAG: "/publication/d", START: "/start", @@ -38,4 +38,4 @@ export const load: PageServerLoad = ({ url }) => { // If no query parameters, redirect to the start page redirect(301, ROUTES.START); -}; \ No newline at end of file +}; diff --git a/src/routes/publication/[type]/[identifier]/+layout.server.ts b/src/routes/publication/[type]/[identifier]/+layout.server.ts index 2a90624..72e4d67 100644 --- a/src/routes/publication/[type]/[identifier]/+layout.server.ts +++ b/src/routes/publication/[type]/[identifier]/+layout.server.ts @@ -3,7 +3,10 @@ import type { LayoutServerLoad } from "./$types"; import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; // AI-NOTE: Server-side event fetching for SEO metadata -async function fetchEventServerSide(type: string, identifier: string): Promise { +async function fetchEventServerSide( + type: string, + identifier: string, +): Promise { // For now, return null to indicate server-side fetch not implemented // This will fall back to client-side fetching return null; @@ -16,10 +19,12 @@ export const load: LayoutServerLoad = async ({ params, url }) => { const indexEvent = await fetchEventServerSide(type, identifier); // Extract metadata for meta tags (use fallbacks if no event found) - const title = indexEvent?.tags.find((tag) => tag[0] === "title")?.[1] || "Alexandria Publication"; - const summary = indexEvent?.tags.find((tag) => tag[0] === "summary")?.[1] || + const title = indexEvent?.tags.find((tag) => tag[0] === "title")?.[1] || + "Alexandria Publication"; + const summary = indexEvent?.tags.find((tag) => tag[0] === "summary")?.[1] || "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages."; - const image = indexEvent?.tags.find((tag) => tag[0] === "image")?.[1] || "/screenshots/old_books.jpg"; + const image = indexEvent?.tags.find((tag) => tag[0] === "image")?.[1] || + "/screenshots/old_books.jpg"; const currentUrl = `${url.origin}${url.pathname}`; return { @@ -31,4 +36,4 @@ export const load: LayoutServerLoad = async ({ params, url }) => { currentUrl, }, }; -}; \ No newline at end of file +}; diff --git a/src/routes/publication/[type]/[identifier]/+page.ts b/src/routes/publication/[type]/[identifier]/+page.ts index 8f3bbaf..bc43ef0 100644 --- a/src/routes/publication/[type]/[identifier]/+page.ts +++ b/src/routes/publication/[type]/[identifier]/+page.ts @@ -1,30 +1,40 @@ import { error } from "@sveltejs/kit"; import type { PageLoad } from "./$types"; -import { fetchEventByDTag, fetchEventById, fetchEventByNaddr, fetchEventByNevent } from "../../../../lib/utils/websocket_utils.ts"; +import { + fetchEventByDTag, + fetchEventById, + fetchEventByNaddr, + fetchEventByNevent, +} from "../../../../lib/utils/websocket_utils.ts"; import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; -export const load: PageLoad = async ({ params, parent }: { params: { type: string; identifier: string }; parent: any }) => { +export const load: PageLoad = async ( + { params, parent }: { + params: { type: string; identifier: string }; + parent: any; + }, +) => { const { type, identifier } = params; - + // Get layout data (no server-side data since SSR is disabled) const layoutData = await parent(); // AI-NOTE: Always fetch client-side since server-side fetch returns null for now let indexEvent: NostrEvent | null = null; - + try { // Handle different identifier types switch (type) { - case 'id': + case "id": indexEvent = await fetchEventById(identifier); break; - case 'd': + case "d": indexEvent = await fetchEventByDTag(identifier); break; - case 'naddr': + case "naddr": indexEvent = await fetchEventByNaddr(identifier); break; - case 'nevent': + case "nevent": indexEvent = await fetchEventByNevent(identifier); break; default: @@ -33,32 +43,36 @@ export const load: PageLoad = async ({ params, parent }: { params: { type: strin } catch (err) { throw err; } - + if (!indexEvent) { // AI-NOTE: Handle case where no relays are available during preloading // This prevents 404 errors when relay stores haven't been populated yet - + // Create appropriate search link based on type - let searchParam = ''; + let searchParam = ""; switch (type) { - case 'id': + case "id": searchParam = `id=${identifier}`; break; - case 'd': + case "d": searchParam = `d=${identifier}`; break; - case 'naddr': - case 'nevent': + case "naddr": + case "nevent": searchParam = `id=${identifier}`; break; default: searchParam = `q=${identifier}`; } - - error(404, `Event not found for ${type}: ${identifier}. href="/events?${searchParam}"`); + + error( + 404, + `Event not found for ${type}: ${identifier}. href="/events?${searchParam}"`, + ); } - const publicationType = indexEvent.tags.find((tag) => tag[0] === "type")?.[1] ?? ""; + const publicationType = + indexEvent.tags.find((tag) => tag[0] === "type")?.[1] ?? ""; // AI-NOTE: Use proper NDK instance from layout or create one with relays let ndk = layoutData?.ndk; @@ -75,6 +89,6 @@ export const load: PageLoad = async ({ params, parent }: { params: { type: strin indexEvent, ndk, // Use minimal NDK instance }; - + return result; }; diff --git a/src/routes/visualize/+page.ts b/src/routes/visualize/+page.ts index 3a0c7d1..b63dcee 100644 --- a/src/routes/visualize/+page.ts +++ b/src/routes/visualize/+page.ts @@ -1,9 +1,9 @@ -import type { PageLoad } from './$types'; +import type { PageLoad } from "./$types"; export const load: PageLoad = async ({ url }) => { - const eventId = url.searchParams.get('event'); - + const eventId = url.searchParams.get("event"); + return { - eventId + eventId, }; -}; \ No newline at end of file +}; diff --git a/src/styles/notifications.css b/src/styles/notifications.css index 27b193d..c11a0ea 100644 --- a/src/styles/notifications.css +++ b/src/styles/notifications.css @@ -151,7 +151,13 @@ /* Transition utilities */ .transition-colors { - transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, text-decoration-color 0.15s ease-in-out, fill 0.15s ease-in-out, stroke 0.15s ease-in-out; + transition: + color 0.15s ease-in-out, + background-color 0.15s ease-in-out, + border-color 0.15s ease-in-out, + text-decoration-color 0.15s ease-in-out, + fill 0.15s ease-in-out, + stroke 0.15s ease-in-out; } .transition-all { diff --git a/src/styles/publications.css b/src/styles/publications.css index 71b70b6..9ac48b0 100644 --- a/src/styles/publications.css +++ b/src/styles/publications.css @@ -100,7 +100,8 @@ /* blockquote; prose and poetry quotes */ .publication-leather .quoteblock, .publication-leather .verseblock { - @apply p-4 my-4 border-s-4 rounded border-primary-300 bg-primary-50 dark:border-primary-500 dark:bg-primary-700; + @apply p-4 my-4 border-s-4 rounded border-primary-300 bg-primary-50 + dark:border-primary-500 dark:bg-primary-700; } .publication-leather .verseblock pre.content { @@ -154,7 +155,8 @@ } .publication-leather .admonitionblock.tip { - @apply rounded overflow-hidden border border-success-100 dark:border-success-800; + @apply rounded overflow-hidden border border-success-100 + dark:border-success-800; } .publication-leather .admonitionblock.tip .icon, @@ -172,7 +174,8 @@ } .publication-leather .admonitionblock.important { - @apply rounded overflow-hidden border border-primary-200 dark:border-primary-700; + @apply rounded overflow-hidden border border-primary-200 + dark:border-primary-700; } .publication-leather .admonitionblock.important .icon, @@ -181,7 +184,8 @@ } .publication-leather .admonitionblock.caution { - @apply rounded overflow-hidden border border-warning-200 dark:border-warning-700; + @apply rounded overflow-hidden border border-warning-200 + dark:border-warning-700; } .publication-leather .admonitionblock.caution .icon, @@ -190,7 +194,8 @@ } .publication-leather .admonitionblock.warning { - @apply rounded overflow-hidden border border-danger-200 dark:border-danger-800; + @apply rounded overflow-hidden border border-danger-200 + dark:border-danger-800; } .publication-leather .admonitionblock.warning .icon, @@ -201,7 +206,7 @@ /* listingblock, literalblock */ .publication-leather .listingblock, .publication-leather .literalblock { - @apply p-4 rounded bg-highlight dark:bg-primary-700; + @apply p-4 rounded bg-highlight dark:bg-primary-700; } .publication-leather .sidebarblock .title, @@ -254,7 +259,8 @@ @screen lg { @media (hover: hover) { .blog .discreet .card-leather:not(:hover) { - @apply bg-primary-50 dark:bg-primary-1000 opacity-75 transition duration-500 ease-in-out; + @apply bg-primary-50 dark:bg-primary-1000 opacity-75 transition + duration-500 ease-in-out; } .blog .discreet .group { @apply bg-transparent; diff --git a/src/styles/scrollbar.css b/src/styles/scrollbar.css index 4691a9b..c337549 100644 --- a/src/styles/scrollbar.css +++ b/src/styles/scrollbar.css @@ -1,7 +1,8 @@ @layer components { /* Global scrollbar styles */ * { - scrollbar-color: rgba(87, 66, 41, 0.8) transparent; /* Transparent track, default scrollbar thumb */ + scrollbar-color: rgba(87, 66, 41, 0.8) + transparent; /* Transparent track, default scrollbar thumb */ } /* Webkit Browsers (Chrome, Safari, Edge) */ @@ -14,7 +15,8 @@ } *::-webkit-scrollbar-thumb { - @apply bg-primary-500 dark:bg-primary-600 hover:bg-primary-600 dark:hover:bg-primary-800; + @apply bg-primary-500 dark:bg-primary-600 hover:bg-primary-600 + dark:hover:bg-primary-800; border-radius: 6px; /* Rounded scrollbar */ } } diff --git a/src/styles/visualize.css b/src/styles/visualize.css index d0631b5..ea8f9bd 100644 --- a/src/styles/visualize.css +++ b/src/styles/visualize.css @@ -30,7 +30,8 @@ } .legend-letter { - @apply absolute inset-0 flex items-center justify-center text-black text-xs font-bold; + @apply absolute inset-0 flex items-center justify-center text-black text-xs + font-bold; } .legend-text { @@ -39,7 +40,8 @@ /* Network visualization styles - specific to visualization */ .network-container { - @apply flex flex-col w-full h-[calc(100vh-138px)] min-h-[400px] max-h-[900px]; + @apply flex flex-col w-full h-[calc(100vh-138px)] min-h-[400px] + max-h-[900px]; } .network-svg-container { @@ -48,11 +50,15 @@ .network-svg { @apply w-full sm:h-[100%] border; - @apply border border-primary-200 has-[:hover]:border-primary-700 dark:bg-primary-1000 dark:border-primary-800 dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500 rounded; + @apply border border-primary-200 has-[:hover]:border-primary-700 + dark:bg-primary-1000 dark:border-primary-800 + dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500 + rounded; } .network-error { - @apply w-full p-4 bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 rounded-lg mb-4; + @apply w-full p-4 bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 + rounded-lg mb-4; } .network-error-title { @@ -78,8 +84,9 @@ /* Tooltip styles - specific to visualization tooltips */ .tooltip-close-btn { - @apply absolute top-2 right-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 - rounded-full p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200; + @apply absolute top-2 right-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 + dark:hover:bg-gray-600 rounded-full p-1 text-gray-500 hover:text-gray-700 + dark:text-gray-400 dark:hover:text-gray-200; } .tooltip-content { @@ -91,7 +98,8 @@ } .tooltip-title-link { - @apply text-gray-800 hover:text-blue-600 dark:text-gray-200 dark:hover:text-blue-400; + @apply text-gray-800 hover:text-blue-600 dark:text-gray-200 + dark:hover:text-blue-400; } .tooltip-metadata { @@ -99,11 +107,13 @@ } .tooltip-summary { - @apply mt-2 text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-auto max-h-40; + @apply mt-2 text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-auto + max-h-40; } .tooltip-content-preview { - @apply mt-2 text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-auto max-h-40; + @apply mt-2 text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-auto + max-h-40; } .tooltip-help-text { diff --git a/test_data/LaTeXtestfile.md b/test_data/LaTeXtestfile.md index eec857c..01e6264 100644 --- a/test_data/LaTeXtestfile.md +++ b/test_data/LaTeXtestfile.md @@ -1,12 +1,24 @@ # This is a testfile for writing mathematic formulas in NostrMarkup -This document covers the rendering of formulas in TeX/LaTeX and AsciiMath notation, or some combination of those within the same page. It is meant to be rendered by clients utilizing MathJax. - -If you want the entire document to be rendered as mathematics, place the entire thing in a backtick-codeblock, but know that this makes the document slower to load, it is harder to format the prose, and the result is less legible. It also doesn't increase portability, as it's easy to export markup as LaTeX files, or as PDFs, with the formulas rendered. - -The general idea, is that anything placed within `single backticks` is inline code, and inline-code will all be scanned for typical mathematics statements and rendered with best-effort. (For more precise rendering, use Asciidoc.) We will not render text that is not marked as inline code, as mathematical formulas, as that is prose. - -If you want the TeX to be blended into the surrounding text, wrap the text within single `$`. Otherwise, use double `$$` symbols, for display math, and it will appear on its own line. +This document covers the rendering of formulas in TeX/LaTeX and AsciiMath +notation, or some combination of those within the same page. It is meant to be +rendered by clients utilizing MathJax. + +If you want the entire document to be rendered as mathematics, place the entire +thing in a backtick-codeblock, but know that this makes the document slower to +load, it is harder to format the prose, and the result is less legible. It also +doesn't increase portability, as it's easy to export markup as LaTeX files, or +as PDFs, with the formulas rendered. + +The general idea, is that anything placed within `single backticks` is inline +code, and inline-code will all be scanned for typical mathematics statements and +rendered with best-effort. (For more precise rendering, use Asciidoc.) We will +not render text that is not marked as inline code, as mathematical formulas, as +that is prose. + +If you want the TeX to be blended into the surrounding text, wrap the text +within single `$`. Otherwise, use double `$$` symbols, for display math, and it +will appear on its own line. ## TeX Examples @@ -16,36 +28,25 @@ Same equation, in the display mode: `$$\sqrt{x}$$` Something more complex, inline: `$\mathbb{N} = \{ a \in \mathbb{Z} : a > 0 \}$` -Something complex, in display mode: `$$P \left( A=2 \, \middle| \, \dfrac{A^2}{B}>4 \right)$$` +Something complex, in display mode: +`$$P \left( A=2 \, \middle| \, \dfrac{A^2}{B}>4 \right)$$` Another example of `$$\prod_{i=1}^{n} x_i - 1$$` inline formulas. -Function example: -`$$ -f(x)= -\begin{cases} -1/d_{ij} & \quad \text{when $d_{ij} \leq 160$}\\ -0 & \quad \text{otherwise} -\end{cases} +Function example: `$$ f(x)= \begin{cases} 1/d_{ij} & \quad \text{when +$d_{ij} \leq 160$}\\ 0 & \quad \text{otherwise} \end{cases} -$$ -` +$$ ` -And a matrix: -` -$$ +And a matrix: ` $$ -M = -\begin{bmatrix} -\frac{5}{6} & \frac{1}{6} & 0 \\[0.3em] -\frac{5}{6} & 0 & \frac{1}{6} \\[0.3em] -0 & \frac{5}{6} & \frac{1}{6} -\end{bmatrix} +M = \begin{bmatrix} \frac{5}{6} & \frac{1}{6} & 0 \\[0.3em] \frac{5}{6} & 0 & +\frac{1}{6} \\[0.3em] 0 & \frac{5}{6} & \frac{1}{6} \end{bmatrix} -$$ -` +$$ ` -LaTeX ypesetting won't be rendered. Use NostrMarkup delimeter tables for this sort of thing. +LaTeX ypesetting won't be rendered. Use NostrMarkup delimeter tables for this +sort of thing. `\\begin{tabular}{|c|c|c|l|r|} \\hline @@ -69,13 +70,17 @@ We also recognize common LaTeX statements: Greek letters are a snap: `$\Psi$`, `$\psi$`, `$\Phi$`, `$\phi$`. -Equations within text are easy--- A well known Maxwell thermodynamic relation is `$\left.{\partial T \over \partial P}\right|_{s} = \left.{\partial v \over \partial s}\right|_{P}$`. +Equations within text are easy--- A well known Maxwell thermodynamic relation is +`$\left.{\partial T \over \partial P}\right|_{s} = \left.{\partial v \over \partial s}\right|_{P}$`. -You can also set aside equations like so: `\begin{eqnarray} du &=& T\ ds -P\ dv, \qquad \mbox{first law.}\label{fl}\\ ds &\ge& {\delta q \over T}.\qquad \qquad \mbox{second law.} \label{sl} \end {eqnarray}` +You can also set aside equations like so: +`\begin{eqnarray} du &=& T\ ds -P\ dv, \qquad \mbox{first law.}\label{fl}\\ ds &\ge& {\delta q \over T}.\qquad \qquad \mbox{second law.} \label{sl} \end {eqnarray}` ## And some good ole Asciimath -Asciimath doesn't use `$` or `$$` delimiters, but we are using it to make mathy stuff easier to find. If you want it inline, include it inline. If you want it on a separate line, put a hard-return before and after. +Asciimath doesn't use `$` or `$$` delimiters, but we are using it to make mathy +stuff easier to find. If you want it inline, include it inline. If you want it +on a separate line, put a hard-return before and after. Inline text example here `$E=mc^2$` and another `$1/(x+1)$`; very simple. @@ -109,19 +114,23 @@ Using the quadratic formula, the roots of `$x^2-6x+4=0$` are Advanced alignment and matrices looks like this: -A `$3xx3$` matrix, `$$((1,2,3),(4,5,6),(7,8,9))$$` and a `$2xx1$` matrix, or vector, `$$((1),(0))$$`. +A `$3xx3$` matrix, `$$((1,2,3),(4,5,6),(7,8,9))$$` and a `$2xx1$` matrix, or +vector, `$$((1),(0))$$`. The outer brackets determine the delimiters e.g. `$|(a,b),(c,d)|=ad-bc$`. -A general `$m xx n$` matrix `$$((a_(11), cdots , a_(1n)),(vdots, ddots, vdots),(a_(m1), cdots , a_(mn)))$$` +A general `$m xx n$` matrix +`$$((a_(11), cdots , a_(1n)),(vdots, ddots, vdots),(a_(m1), cdots , a_(mn)))$$` ## Mixed Examples Here are some examples mixing LaTeX and AsciiMath: - LaTeX inline: `$\frac{1}{2}$` vs AsciiMath inline: `$1/2$` -- LaTeX display: `$$\sum_{i=1}^n x_i$$` vs AsciiMath display: `$$sum_(i=1)^n x_i$$` -- LaTeX matrix: `$$\begin{pmatrix} a & b \\ c & d \end{pmatrix}$$` vs AsciiMath matrix: `$$((a,b),(c,d))$$` +- LaTeX display: `$$\sum_{i=1}^n x_i$$` vs AsciiMath display: + `$$sum_(i=1)^n x_i$$` +- LaTeX matrix: `$$\begin{pmatrix} a & b \\ c & d \end{pmatrix}$$` vs AsciiMath + matrix: `$$((a,b),(c,d))$$` ## Edge Cases @@ -134,9 +143,9 @@ Here are some examples mixing LaTeX and AsciiMath: - CSS with dollar signs: `color: $primary-color` This document should demonstrate that: + 1. LaTeX is processed within inline code blocks with proper delimiters 2. AsciiMath is processed within inline code blocks with proper delimiters 3. Regular code blocks remain unchanged 4. Mixed content is handled correctly -5. Edge cases are handled gracefully -$$ +5. Edge cases are handled gracefully $$ diff --git a/tests/e2e/my_notes_layout.pw.spec.ts b/tests/e2e/my_notes_layout.pw.spec.ts index 23db168..b45e403 100644 --- a/tests/e2e/my_notes_layout.pw.spec.ts +++ b/tests/e2e/my_notes_layout.pw.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { expect, type Page, test } from "@playwright/test"; // Utility to check for horizontal scroll bar async function hasHorizontalScroll(page: Page, selector: string) { @@ -9,16 +9,16 @@ async function hasHorizontalScroll(page: Page, selector: string) { }, selector); } -test.describe('My Notes Layout', () => { +test.describe("My Notes Layout", () => { test.beforeEach(async ({ page }) => { - await page.goto('/my-notes'); + await page.goto("/my-notes"); await page.waitForSelector('h1:text("My Notes")'); }); - test('no horizontal scroll bar for all tag type and tag filter combinations', async ({ page }) => { + test("no horizontal scroll bar for all tag type and tag filter combinations", async ({ page }) => { // Helper to check scroll for current state async function assertNoScroll() { - const hasScroll = await hasHorizontalScroll(page, 'main, body, html'); + const hasScroll = await hasHorizontalScroll(page, "main, body, html"); expect(hasScroll).toBeFalsy(); } @@ -26,9 +26,11 @@ test.describe('My Notes Layout', () => { await assertNoScroll(); // Get all tag type buttons - const tagTypeButtons = await page.locator('aside button').all(); + const tagTypeButtons = await page.locator("aside button").all(); // Only consider tag type buttons (first N) - const tagTypeCount = await page.locator('aside > div.flex.flex-wrap.gap-2.mb-6 > button').count(); + const tagTypeCount = await page.locator( + "aside > div.flex.flex-wrap.gap-2.mb-6 > button", + ).count(); // For each single tag type for (let i = 0; i < tagTypeCount; i++) { // Click tag type button @@ -36,7 +38,9 @@ test.describe('My Notes Layout', () => { await page.waitForTimeout(100); // Wait for UI update await assertNoScroll(); // Get tag filter buttons (after tag type buttons) - const tagFilterButtons = await page.locator('aside > div.flex.flex-wrap.gap-2.mb-4 > button').all(); + const tagFilterButtons = await page.locator( + "aside > div.flex.flex-wrap.gap-2.mb-4 > button", + ).all(); // Try all single tag filter selections for (let j = 0; j < tagFilterButtons.length; j++) { await tagFilterButtons[j].click(); @@ -72,7 +76,9 @@ test.describe('My Notes Layout', () => { await page.waitForTimeout(100); await assertNoScroll(); // Get tag filter buttons for this combination - const tagFilterButtons = await page.locator('aside > div.flex.flex-wrap.gap-2.mb-4 > button').all(); + const tagFilterButtons = await page.locator( + "aside > div.flex.flex-wrap.gap-2.mb-4 > button", + ).all(); // Try all single tag filter selections for (let k = 0; k < tagFilterButtons.length; k++) { await tagFilterButtons[k].click(); @@ -100,4 +106,4 @@ test.describe('My Notes Layout', () => { } } }); -}); \ No newline at end of file +}); diff --git a/tests/unit/ZettelEditor.test.ts b/tests/unit/ZettelEditor.test.ts index 3490286..3bfe172 100644 --- a/tests/unit/ZettelEditor.test.ts +++ b/tests/unit/ZettelEditor.test.ts @@ -1,37 +1,45 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { AsciiDocMetadata } from "../../src/lib/utils/asciidoc_metadata"; // Mock all Svelte components and dependencies vi.mock("flowbite-svelte", () => ({ Textarea: vi.fn().mockImplementation((props) => { return { - $$render: () => ``, - $$bind: { value: props.bind, oninput: props.oninput } + $$render: () => + ``, + $$bind: { value: props.bind, oninput: props.oninput }, }; }), Button: vi.fn().mockImplementation((props) => { return { - $$render: () => ``, - $$bind: { onclick: props.onclick } + $$render: () => + ``, + $$bind: { onclick: props.onclick }, }; - }) + }), })); vi.mock("flowbite-svelte-icons", () => ({ EyeOutline: vi.fn().mockImplementation(() => ({ - $$render: () => `` - })) + $$render: () => ``, + })), })); vi.mock("asciidoctor", () => ({ default: vi.fn(() => ({ convert: vi.fn((content, options) => { // Mock AsciiDoctor conversion - return simple HTML - return content.replace(/^==\s+(.+)$/gm, '

    $1

    ') - .replace(/\*\*(.+?)\*\*/g, '$1') - .replace(/\*(.+?)\*/g, '$1'); - }) - })) + return content.replace(/^==\s+(.+)$/gm, "

    $1

    ") + .replace(/\*\*(.+?)\*\*/g, "$1") + .replace(/\*(.+?)\*/g, "$1"); + }), + })), })); // Mock sessionStorage @@ -41,21 +49,21 @@ const mockSessionStorage = { removeItem: vi.fn(), clear: vi.fn(), }; -Object.defineProperty(global, 'sessionStorage', { +Object.defineProperty(global, "sessionStorage", { value: mockSessionStorage, - writable: true + writable: true, }); // Mock window object for DOM manipulation -Object.defineProperty(global, 'window', { +Object.defineProperty(global, "window", { value: { sessionStorage: mockSessionStorage, document: { querySelector: vi.fn(), createElement: vi.fn(), - } + }, }, - writable: true + writable: true, }); // Mock DOM methods @@ -64,14 +72,14 @@ const mockCreateElement = vi.fn(); const mockAddEventListener = vi.fn(); const mockRemoveEventListener = vi.fn(); -Object.defineProperty(global, 'document', { +Object.defineProperty(global, "document", { value: { querySelector: mockQuerySelector, createElement: mockCreateElement, addEventListener: mockAddEventListener, removeEventListener: mockRemoveEventListener, }, - writable: true + writable: true, }); describe("ZettelEditor Component Logic", () => { @@ -90,8 +98,9 @@ describe("ZettelEditor Component Logic", () => { describe("Publication Format Detection Logic", () => { it("should detect document header format", () => { - const contentWithDocumentHeader = "= Document Title\n\n== Section 1\nContent"; - + const contentWithDocumentHeader = + "= Document Title\n\n== Section 1\nContent"; + // Test the regex pattern used in the component const hasDocumentHeader = contentWithDocumentHeader.match(/^=\s+/m); expect(hasDocumentHeader).toBeTruthy(); @@ -99,12 +108,12 @@ describe("ZettelEditor Component Logic", () => { it("should detect index card format", () => { const contentWithIndexCard = "index card\n\n== Section 1\nContent"; - + // Test the logic used in the component const lines = contentWithIndexCard.split(/\r?\n/); let hasIndexCard = false; for (const line of lines) { - if (line.trim().toLowerCase() === 'index card') { + if (line.trim().toLowerCase() === "index card") { hasIndexCard = true; break; } @@ -113,8 +122,9 @@ describe("ZettelEditor Component Logic", () => { }); it("should not detect publication format for normal section content", () => { - const normalContent = "== Section 1\nContent\n\n== Section 2\nMore content"; - + const normalContent = + "== Section 1\nContent\n\n== Section 2\nMore content"; + // Test the logic used in the component const lines = normalContent.split(/\r?\n/); let hasPublicationHeader = false; @@ -123,7 +133,7 @@ describe("ZettelEditor Component Logic", () => { hasPublicationHeader = true; break; } - if (line.trim().toLowerCase() === 'index card') { + if (line.trim().toLowerCase() === "index card") { hasPublicationHeader = true; break; } @@ -135,26 +145,30 @@ describe("ZettelEditor Component Logic", () => { describe("Content Parsing Logic", () => { it("should parse sections with document header", () => { const content = "== Section 1\n:author: Test Author\n\nContent 1"; - + // Test the parsing logic const hasDocumentHeader = content.match(/^=\s+/m); expect(hasDocumentHeader).toBeFalsy(); // This content doesn't have a document header - + // Test section splitting logic - const sectionStrings = content.split(/(?=^==\s+)/gm).filter((section: string) => section.trim()); + const sectionStrings = content.split(/(?=^==\s+)/gm).filter(( + section: string, + ) => section.trim()); expect(sectionStrings).toHaveLength(1); expect(sectionStrings[0]).toContain("== Section 1"); }); it("should parse sections without document header", () => { const content = "== Section 1\nContent 1"; - + // Test the parsing logic const hasDocumentHeader = content.match(/^=\s+/m); expect(hasDocumentHeader).toBeFalsy(); - + // Test section splitting logic - const sectionStrings = content.split(/(?=^==\s+)/gm).filter((section: string) => section.trim()); + const sectionStrings = content.split(/(?=^==\s+)/gm).filter(( + section: string, + ) => section.trim()); expect(sectionStrings).toHaveLength(1); expect(sectionStrings[0]).toContain("== Section 1"); }); @@ -168,49 +182,70 @@ describe("ZettelEditor Component Logic", () => { describe("Content Conversion Logic", () => { it("should convert document title to section title", () => { - const contentWithDocumentHeader = "= Document Title\n\n== Section 1\nContent"; - + const contentWithDocumentHeader = + "= Document Title\n\n== Section 1\nContent"; + // Test the conversion logic - let convertedContent = contentWithDocumentHeader.replace(/^=\s+(.+)$/gm, '== $1'); - convertedContent = convertedContent.replace(/^index card$/gim, ''); - const finalContent = convertedContent.replace(/\n\s*\n\s*\n/g, '\n\n'); - + let convertedContent = contentWithDocumentHeader.replace( + /^=\s+(.+)$/gm, + "== $1", + ); + convertedContent = convertedContent.replace(/^index card$/gim, ""); + const finalContent = convertedContent.replace(/\n\s*\n\s*\n/g, "\n\n"); + expect(finalContent).toBe("== Document Title\n\n== Section 1\nContent"); }); it("should remove index card line", () => { const contentWithIndexCard = "index card\n\n== Section 1\nContent"; - + // Test the conversion logic - let convertedContent = contentWithIndexCard.replace(/^=\s+(.+)$/gm, '== $1'); - convertedContent = convertedContent.replace(/^index card$/gim, ''); - const finalContent = convertedContent.replace(/\n\s*\n\s*\n/g, '\n\n'); - + let convertedContent = contentWithIndexCard.replace( + /^=\s+(.+)$/gm, + "== $1", + ); + convertedContent = convertedContent.replace(/^index card$/gim, ""); + const finalContent = convertedContent.replace(/\n\s*\n\s*\n/g, "\n\n"); + expect(finalContent).toBe("\n\n== Section 1\nContent"); }); it("should clean up double newlines", () => { - const contentWithExtraNewlines = "= Document Title\n\n\n== Section 1\nContent"; - + const contentWithExtraNewlines = + "= Document Title\n\n\n== Section 1\nContent"; + // Test the conversion logic - let convertedContent = contentWithExtraNewlines.replace(/^=\s+(.+)$/gm, '== $1'); - convertedContent = convertedContent.replace(/^index card$/gim, ''); - const finalContent = convertedContent.replace(/\n\s*\n\s*\n/g, '\n\n'); - + let convertedContent = contentWithExtraNewlines.replace( + /^=\s+(.+)$/gm, + "== $1", + ); + convertedContent = convertedContent.replace(/^index card$/gim, ""); + const finalContent = convertedContent.replace(/\n\s*\n\s*\n/g, "\n\n"); + expect(finalContent).toBe("== Document Title\n\n== Section 1\nContent"); }); }); describe("SessionStorage Integration", () => { it("should store content in sessionStorage when switching to publication editor", () => { - const contentWithDocumentHeader = "= Document Title\n\n== Section 1\nContent"; - + const contentWithDocumentHeader = + "= Document Title\n\n== Section 1\nContent"; + // Test the sessionStorage logic - mockSessionStorage.setItem('zettelEditorContent', contentWithDocumentHeader); - mockSessionStorage.setItem('zettelEditorSource', 'publication-format'); - - expect(mockSessionStorage.setItem).toHaveBeenCalledWith('zettelEditorContent', contentWithDocumentHeader); - expect(mockSessionStorage.setItem).toHaveBeenCalledWith('zettelEditorSource', 'publication-format'); + mockSessionStorage.setItem( + "zettelEditorContent", + contentWithDocumentHeader, + ); + mockSessionStorage.setItem("zettelEditorSource", "publication-format"); + + expect(mockSessionStorage.setItem).toHaveBeenCalledWith( + "zettelEditorContent", + contentWithDocumentHeader, + ); + expect(mockSessionStorage.setItem).toHaveBeenCalledWith( + "zettelEditorSource", + "publication-format", + ); }); }); @@ -219,7 +254,7 @@ describe("ZettelEditor Component Logic", () => { const sections = [{ title: "Section 1", content: "Content 1", tags: [] }]; const eventCount = sections.length; const eventText = `${eventCount} event${eventCount !== 1 ? "s" : ""}`; - + expect(eventCount).toBe(1); expect(eventText).toBe("1 event"); }); @@ -227,11 +262,11 @@ describe("ZettelEditor Component Logic", () => { it("should calculate correct event count for multiple sections", () => { const sections = [ { title: "Section 1", content: "Content 1", tags: [] }, - { title: "Section 2", content: "Content 2", tags: [] } + { title: "Section 2", content: "Content 2", tags: [] }, ]; const eventCount = sections.length; const eventText = `${eventCount} event${eventCount !== 1 ? "s" : ""}`; - + expect(eventCount).toBe(2); expect(eventText).toBe("2 events"); }); @@ -240,11 +275,17 @@ describe("ZettelEditor Component Logic", () => { describe("Tag Processing Logic", () => { it("should process tags correctly", () => { // Mock the metadataToTags function - const mockMetadataToTags = vi.fn().mockReturnValue([["author", "Test Author"]]); - - const mockMetadata = { title: "Section 1", author: "Test Author" } as AsciiDocMetadata; + const mockMetadataToTags = vi.fn().mockReturnValue([[ + "author", + "Test Author", + ]]); + + const mockMetadata = { + title: "Section 1", + author: "Test Author", + } as AsciiDocMetadata; const tags = mockMetadataToTags(mockMetadata); - + expect(tags).toEqual([["author", "Test Author"]]); expect(mockMetadataToTags).toHaveBeenCalledWith(mockMetadata); }); @@ -252,10 +293,10 @@ describe("ZettelEditor Component Logic", () => { it("should handle empty tags", () => { // Mock the metadataToTags function const mockMetadataToTags = vi.fn().mockReturnValue([]); - + const mockMetadata = { title: "Section 1" } as AsciiDocMetadata; const tags = mockMetadataToTags(mockMetadata); - + expect(tags).toEqual([]); }); }); @@ -264,11 +305,11 @@ describe("ZettelEditor Component Logic", () => { it("should process AsciiDoc content correctly", () => { // Mock the asciidoctor conversion const mockConvert = vi.fn((content, options) => { - return content.replace(/^==\s+(.+)$/gm, '

    $1

    ') - .replace(/\*\*(.+?)\*\*/g, '$1') - .replace(/\*(.+?)\*/g, '$1'); + return content.replace(/^==\s+(.+)$/gm, "

    $1

    ") + .replace(/\*\*(.+?)\*\*/g, "$1") + .replace(/\*(.+?)\*/g, "$1"); }); - + const content = "== Test Section\n\nThis is **bold** and *italic* text."; const processedContent = mockConvert(content, { standalone: false, @@ -278,10 +319,10 @@ describe("ZettelEditor Component Logic", () => { sectids: true, }, }); - - expect(processedContent).toContain('

    Test Section

    '); - expect(processedContent).toContain('bold'); - expect(processedContent).toContain('italic'); + + expect(processedContent).toContain("

    Test Section

    "); + expect(processedContent).toContain("bold"); + expect(processedContent).toContain("italic"); }); }); @@ -291,9 +332,9 @@ describe("ZettelEditor Component Logic", () => { const mockParseFunction = vi.fn().mockImplementation(() => { throw new Error("Parsing error"); }); - + const content = "== Section 1\nContent 1"; - + // Should not throw error when called expect(() => { try { @@ -321,12 +362,12 @@ describe("ZettelEditor Component Logic", () => { onContentChange: vi.fn(), onPreviewToggle: vi.fn(), }; - - expect(expectedProps).toHaveProperty('content'); - expect(expectedProps).toHaveProperty('placeholder'); - expect(expectedProps).toHaveProperty('showPreview'); - expect(expectedProps).toHaveProperty('onContentChange'); - expect(expectedProps).toHaveProperty('onPreviewToggle'); + + expect(expectedProps).toHaveProperty("content"); + expect(expectedProps).toHaveProperty("placeholder"); + expect(expectedProps).toHaveProperty("showPreview"); + expect(expectedProps).toHaveProperty("onContentChange"); + expect(expectedProps).toHaveProperty("onPreviewToggle"); }); }); @@ -334,12 +375,12 @@ describe("ZettelEditor Component Logic", () => { it("should integrate with ZettelParser utilities", () => { // Mock the parseAsciiDocSections function const mockParseAsciiDocSections = vi.fn().mockReturnValue([ - { title: "Section 1", content: "Content 1", tags: [] } + { title: "Section 1", content: "Content 1", tags: [] }, ]); - + const content = "== Section 1\nContent 1"; const sections = mockParseAsciiDocSections(content, 2); - + expect(sections).toHaveLength(1); expect(sections[0].title).toBe("Section 1"); }); @@ -348,21 +389,21 @@ describe("ZettelEditor Component Logic", () => { // Mock the utility functions const mockExtractDocumentMetadata = vi.fn().mockReturnValue({ metadata: { title: "Document Title" } as AsciiDocMetadata, - content: "Document content" + content: "Document content", }); - + const mockExtractSectionMetadata = vi.fn().mockReturnValue({ metadata: { title: "Section Title" } as AsciiDocMetadata, content: "Section content", - title: "Section Title" + title: "Section Title", }); - + const documentContent = "= Document Title\nDocument content"; const sectionContent = "== Section Title\nSection content"; - + const documentResult = mockExtractDocumentMetadata(documentContent); const sectionResult = mockExtractSectionMetadata(sectionContent); - + expect(documentResult.metadata.title).toBe("Document Title"); expect(sectionResult.title).toBe("Section Title"); }); @@ -370,27 +411,35 @@ describe("ZettelEditor Component Logic", () => { describe("Content Validation", () => { it("should validate content structure", () => { - const validContent = "== Section 1\nContent here\n\n== Section 2\nMore content"; + const validContent = + "== Section 1\nContent here\n\n== Section 2\nMore content"; const invalidContent = "Just some text without sections"; - + // Test section detection - const validSections = validContent.split(/(?=^==\s+)/gm).filter((section: string) => section.trim()); - const invalidSections = invalidContent.split(/(?=^==\s+)/gm).filter((section: string) => section.trim()); - + const validSections = validContent.split(/(?=^==\s+)/gm).filter(( + section: string, + ) => section.trim()); + const invalidSections = invalidContent.split(/(?=^==\s+)/gm).filter(( + section: string, + ) => section.trim()); + expect(validSections.length).toBeGreaterThan(0); // The invalid content will have one section (the entire content) since it doesn't start with == expect(invalidSections.length).toBe(1); }); it("should handle mixed content types", () => { - const mixedContent = "= Document Title\n\n== Section 1\nContent\n\n== Section 2\nMore content"; - + const mixedContent = + "= Document Title\n\n== Section 1\nContent\n\n== Section 2\nMore content"; + // Test document header detection const hasDocumentHeader = mixedContent.match(/^=\s+/m); expect(hasDocumentHeader).toBeTruthy(); - + // Test section extraction - const sections = mixedContent.split(/(?=^==\s+)/gm).filter((section: string) => section.trim()); + const sections = mixedContent.split(/(?=^==\s+)/gm).filter(( + section: string, + ) => section.trim()); expect(sections.length).toBeGreaterThan(0); }); }); @@ -398,13 +447,13 @@ describe("ZettelEditor Component Logic", () => { describe("String Manipulation", () => { it("should handle string replacements correctly", () => { const originalContent = "= Title\n\n== Section\nContent"; - + // Test various string manipulations const convertedContent = originalContent - .replace(/^=\s+(.+)$/gm, '== $1') - .replace(/^index card$/gim, '') - .replace(/\n\s*\n\s*\n/g, '\n\n'); - + .replace(/^=\s+(.+)$/gm, "== $1") + .replace(/^index card$/gim, "") + .replace(/\n\s*\n\s*\n/g, "\n\n"); + expect(convertedContent).toBe("== Title\n\n== Section\nContent"); }); @@ -414,16 +463,16 @@ describe("ZettelEditor Component Logic", () => { "index card\n\n== Section\nContent", // Index card "= Title\nindex card\n== Section\nContent", // Both ]; - - edgeCases.forEach(content => { + + edgeCases.forEach((content) => { const converted = content - .replace(/^=\s+(.+)$/gm, '== $1') - .replace(/^index card$/gim, '') - .replace(/\n\s*\n\s*\n/g, '\n\n'); - + .replace(/^=\s+(.+)$/gm, "== $1") + .replace(/^index card$/gim, "") + .replace(/\n\s*\n\s*\n/g, "\n\n"); + expect(converted).toBeDefined(); - expect(typeof converted).toBe('string'); + expect(typeof converted).toBe("string"); }); }); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/eventInput30040.test.ts b/tests/unit/eventInput30040.test.ts index c7dadc3..9fa185c 100644 --- a/tests/unit/eventInput30040.test.ts +++ b/tests/unit/eventInput30040.test.ts @@ -1,6 +1,12 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { build30040EventSet, validate30040EventSet } from "../../src/lib/utils/event_input_utils"; -import { extractDocumentMetadata, parseAsciiDocWithMetadata } from "../../src/lib/utils/asciidoc_metadata"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + build30040EventSet, + validate30040EventSet, +} from "../../src/lib/utils/event_input_utils"; +import { + extractDocumentMetadata, + parseAsciiDocWithMetadata, +} from "../../src/lib/utils/asciidoc_metadata"; // Mock NDK and other dependencies vi.mock("@nostr-dev-kit/ndk", () => ({ @@ -60,16 +66,29 @@ This is the content of the second section.`; const tags: [string, string][] = [["type", "article"]]; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); // Test index event expect(indexEvent.kind).toBe(30040); expect(indexEvent.content).toBe(""); - expect(indexEvent.tags).toContainEqual(["d", "test-document-with-preamble"]); - expect(indexEvent.tags).toContainEqual(["title", "Test Document with Preamble"]); + expect(indexEvent.tags).toContainEqual([ + "d", + "test-document-with-preamble", + ]); + expect(indexEvent.tags).toContainEqual([ + "title", + "Test Document with Preamble", + ]); expect(indexEvent.tags).toContainEqual(["author", "John Doe"]); expect(indexEvent.tags).toContainEqual(["version", "1.0"]); - expect(indexEvent.tags).toContainEqual(["summary", "This is a test document with preamble"]); + expect(indexEvent.tags).toContainEqual([ + "summary", + "This is a test document with preamble", + ]); expect(indexEvent.tags).toContainEqual(["t", "test"]); expect(indexEvent.tags).toContainEqual(["t", "preamble"]); expect(indexEvent.tags).toContainEqual(["t", "asciidoc"]); @@ -80,22 +99,47 @@ This is the content of the second section.`; // First section expect(sectionEvents[0].kind).toBe(30041); - expect(sectionEvents[0].content).toBe("This is the content of the first section."); - expect(sectionEvents[0].tags).toContainEqual(["d", "test-document-with-preamble-first-section"]); + expect(sectionEvents[0].content).toBe( + "This is the content of the first section.", + ); + expect(sectionEvents[0].tags).toContainEqual([ + "d", + "test-document-with-preamble-first-section", + ]); expect(sectionEvents[0].tags).toContainEqual(["title", "First Section"]); - expect(sectionEvents[0].tags).toContainEqual(["author", "Section Author"]); - expect(sectionEvents[0].tags).toContainEqual(["summary", "This is the first section"]); + expect(sectionEvents[0].tags).toContainEqual([ + "author", + "Section Author", + ]); + expect(sectionEvents[0].tags).toContainEqual([ + "summary", + "This is the first section", + ]); // Second section expect(sectionEvents[1].kind).toBe(30041); - expect(sectionEvents[1].content).toBe("This is the content of the second section."); - expect(sectionEvents[1].tags).toContainEqual(["d", "test-document-with-preamble-second-section"]); + expect(sectionEvents[1].content).toBe( + "This is the content of the second section.", + ); + expect(sectionEvents[1].tags).toContainEqual([ + "d", + "test-document-with-preamble-second-section", + ]); expect(sectionEvents[1].tags).toContainEqual(["title", "Second Section"]); - expect(sectionEvents[1].tags).toContainEqual(["summary", "This is the second section"]); + expect(sectionEvents[1].tags).toContainEqual([ + "summary", + "This is the second section", + ]); // Test a-tags in index event - expect(indexEvent.tags).toContainEqual(["a", "30041:test-pubkey:test-document-with-preamble-first-section"]); - expect(indexEvent.tags).toContainEqual(["a", "30041:test-pubkey:test-document-with-preamble-second-section"]); + expect(indexEvent.tags).toContainEqual([ + "a", + "30041:test-pubkey:test-document-with-preamble-first-section", + ]); + expect(indexEvent.tags).toContainEqual([ + "a", + "30041:test-pubkey:test-document-with-preamble-second-section", + ]); }); }); @@ -118,32 +162,64 @@ This is the content of the second section.`; const tags: [string, string][] = [["type", "article"]]; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); // Test index event expect(indexEvent.kind).toBe(30040); expect(indexEvent.content).toBe(""); - expect(indexEvent.tags).toContainEqual(["d", "test-document-without-preamble"]); - expect(indexEvent.tags).toContainEqual(["title", "Test Document without Preamble"]); - expect(indexEvent.tags).toContainEqual(["summary", "This is a test document without preamble"]); + expect(indexEvent.tags).toContainEqual([ + "d", + "test-document-without-preamble", + ]); + expect(indexEvent.tags).toContainEqual([ + "title", + "Test Document without Preamble", + ]); + expect(indexEvent.tags).toContainEqual([ + "summary", + "This is a test document without preamble", + ]); // Test section events expect(sectionEvents).toHaveLength(2); // First section expect(sectionEvents[0].kind).toBe(30041); - expect(sectionEvents[0].content).toBe("This is the content of the first section."); - expect(sectionEvents[0].tags).toContainEqual(["d", "test-document-without-preamble-first-section"]); + expect(sectionEvents[0].content).toBe( + "This is the content of the first section.", + ); + expect(sectionEvents[0].tags).toContainEqual([ + "d", + "test-document-without-preamble-first-section", + ]); expect(sectionEvents[0].tags).toContainEqual(["title", "First Section"]); - expect(sectionEvents[0].tags).toContainEqual(["author", "Section Author"]); - expect(sectionEvents[0].tags).toContainEqual(["summary", "This is the first section"]); + expect(sectionEvents[0].tags).toContainEqual([ + "author", + "Section Author", + ]); + expect(sectionEvents[0].tags).toContainEqual([ + "summary", + "This is the first section", + ]); // Second section expect(sectionEvents[1].kind).toBe(30041); - expect(sectionEvents[1].content).toBe("This is the content of the second section."); - expect(sectionEvents[1].tags).toContainEqual(["d", "test-document-without-preamble-second-section"]); + expect(sectionEvents[1].content).toBe( + "This is the content of the second section.", + ); + expect(sectionEvents[1].tags).toContainEqual([ + "d", + "test-document-without-preamble-second-section", + ]); expect(sectionEvents[1].tags).toContainEqual(["title", "Second Section"]); - expect(sectionEvents[1].tags).toContainEqual(["summary", "This is the second section"]); + expect(sectionEvents[1].tags).toContainEqual([ + "summary", + "This is the second section", + ]); }); }); @@ -163,14 +239,27 @@ This is the preamble content. const tags: [string, string][] = [["type", "skeleton"]]; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); // Test index event expect(indexEvent.kind).toBe(30040); expect(indexEvent.content).toBe(""); - expect(indexEvent.tags).toContainEqual(["d", "skeleton-document-with-preamble"]); - expect(indexEvent.tags).toContainEqual(["title", "Skeleton Document with Preamble"]); - expect(indexEvent.tags).toContainEqual(["summary", "This is a skeleton document with preamble"]); + expect(indexEvent.tags).toContainEqual([ + "d", + "skeleton-document-with-preamble", + ]); + expect(indexEvent.tags).toContainEqual([ + "title", + "Skeleton Document with Preamble", + ]); + expect(indexEvent.tags).toContainEqual([ + "summary", + "This is a skeleton document with preamble", + ]); // Test section events expect(sectionEvents).toHaveLength(3); @@ -179,8 +268,14 @@ This is the preamble content. sectionEvents.forEach((section, index) => { expect(section.kind).toBe(30041); expect(section.content).toBe(""); - expect(section.tags).toContainEqual(["d", `skeleton-document-with-preamble-empty-section-${index + 1}`]); - expect(section.tags).toContainEqual(["title", `Empty Section ${index + 1}`]); + expect(section.tags).toContainEqual([ + "d", + `skeleton-document-with-preamble-empty-section-${index + 1}`, + ]); + expect(section.tags).toContainEqual([ + "title", + `Empty Section ${index + 1}`, + ]); }); }); }); @@ -199,14 +294,27 @@ This is the preamble content. const tags: [string, string][] = [["type", "skeleton"]]; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); // Test index event expect(indexEvent.kind).toBe(30040); expect(indexEvent.content).toBe(""); - expect(indexEvent.tags).toContainEqual(["d", "skeleton-document-without-preamble"]); - expect(indexEvent.tags).toContainEqual(["title", "Skeleton Document without Preamble"]); - expect(indexEvent.tags).toContainEqual(["summary", "This is a skeleton document without preamble"]); + expect(indexEvent.tags).toContainEqual([ + "d", + "skeleton-document-without-preamble", + ]); + expect(indexEvent.tags).toContainEqual([ + "title", + "Skeleton Document without Preamble", + ]); + expect(indexEvent.tags).toContainEqual([ + "summary", + "This is a skeleton document without preamble", + ]); // Test section events expect(sectionEvents).toHaveLength(3); @@ -215,8 +323,14 @@ This is the preamble content. sectionEvents.forEach((section, index) => { expect(section.kind).toBe(30041); expect(section.content).toBe(""); - expect(section.tags).toContainEqual(["d", `skeleton-document-without-preamble-empty-section-${index + 1}`]); - expect(section.tags).toContainEqual(["title", `Empty Section ${index + 1}`]); + expect(section.tags).toContainEqual([ + "d", + `skeleton-document-without-preamble-empty-section-${index + 1}`, + ]); + expect(section.tags).toContainEqual([ + "title", + `Empty Section ${index + 1}`, + ]); }); }); }); @@ -228,7 +342,11 @@ index card`; const tags: [string, string][] = [["type", "index-card"]]; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); // Test index event expect(indexEvent.kind).toBe(30040); @@ -249,14 +367,27 @@ index card`; const tags: [string, string][] = [["type", "index-card"]]; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); // Test index event expect(indexEvent.kind).toBe(30040); expect(indexEvent.content).toBe(""); - expect(indexEvent.tags).toContainEqual(["d", "test-index-card-with-metadata"]); - expect(indexEvent.tags).toContainEqual(["title", "Test Index Card with Metadata"]); - expect(indexEvent.tags).toContainEqual(["summary", "This is an index card with metadata"]); + expect(indexEvent.tags).toContainEqual([ + "d", + "test-index-card-with-metadata", + ]); + expect(indexEvent.tags).toContainEqual([ + "title", + "Test Index Card with Metadata", + ]); + expect(indexEvent.tags).toContainEqual([ + "summary", + "This is an index card with metadata", + ]); expect(indexEvent.tags).toContainEqual(["t", "index"]); expect(indexEvent.tags).toContainEqual(["t", "card"]); expect(indexEvent.tags).toContainEqual(["t", "metadata"]); @@ -303,23 +434,45 @@ This is the section content.`; const tags: [string, string][] = [["type", "complex"]]; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); // Test index event metadata expect(indexEvent.kind).toBe(30040); - expect(indexEvent.tags).toContainEqual(["d", "complex-metadata-document"]); - expect(indexEvent.tags).toContainEqual(["title", "Complex Metadata Document"]); + expect(indexEvent.tags).toContainEqual([ + "d", + "complex-metadata-document", + ]); + expect(indexEvent.tags).toContainEqual([ + "title", + "Complex Metadata Document", + ]); expect(indexEvent.tags).toContainEqual(["author", "Jane Smith"]); // Should use header line author expect(indexEvent.tags).toContainEqual(["author", "Override Author"]); // Additional author from attribute expect(indexEvent.tags).toContainEqual(["author", "Third Author"]); // Additional author from attribute expect(indexEvent.tags).toContainEqual(["version", "2.0"]); // Should use revision line version - expect(indexEvent.tags).toContainEqual(["summary", "This is a complex document with all metadata types Alternative description field"]); + expect(indexEvent.tags).toContainEqual([ + "summary", + "This is a complex document with all metadata types Alternative description field", + ]); expect(indexEvent.tags).toContainEqual(["published_on", "2024-03-01"]); - expect(indexEvent.tags).toContainEqual(["published_by", "Alexandria Complex"]); + expect(indexEvent.tags).toContainEqual([ + "published_by", + "Alexandria Complex", + ]); expect(indexEvent.tags).toContainEqual(["type", "book"]); - expect(indexEvent.tags).toContainEqual(["image", "https://example.com/cover.jpg"]); + expect(indexEvent.tags).toContainEqual([ + "image", + "https://example.com/cover.jpg", + ]); expect(indexEvent.tags).toContainEqual(["i", "978-0-123456-78-9"]); - expect(indexEvent.tags).toContainEqual(["source", "https://github.com/alexandria/complex"]); + expect(indexEvent.tags).toContainEqual([ + "source", + "https://github.com/alexandria/complex", + ]); expect(indexEvent.tags).toContainEqual(["auto-update", "yes"]); expect(indexEvent.tags).toContainEqual(["t", "complex"]); expect(indexEvent.tags).toContainEqual(["t", "metadata"]); @@ -332,13 +485,31 @@ This is the section content.`; expect(sectionEvents).toHaveLength(1); expect(sectionEvents[0].kind).toBe(30041); expect(sectionEvents[0].content).toBe("This is the section content."); - expect(sectionEvents[0].tags).toContainEqual(["d", "complex-metadata-document-section-with-complex-metadata"]); - expect(sectionEvents[0].tags).toContainEqual(["title", "Section with Complex Metadata"]); - expect(sectionEvents[0].tags).toContainEqual(["author", "Section Author"]); - expect(sectionEvents[0].tags).toContainEqual(["author", "Section Co-Author"]); - expect(sectionEvents[0].tags).toContainEqual(["summary", "This section has complex metadata Alternative description for section"]); + expect(sectionEvents[0].tags).toContainEqual([ + "d", + "complex-metadata-document-section-with-complex-metadata", + ]); + expect(sectionEvents[0].tags).toContainEqual([ + "title", + "Section with Complex Metadata", + ]); + expect(sectionEvents[0].tags).toContainEqual([ + "author", + "Section Author", + ]); + expect(sectionEvents[0].tags).toContainEqual([ + "author", + "Section Co-Author", + ]); + expect(sectionEvents[0].tags).toContainEqual([ + "summary", + "This section has complex metadata Alternative description for section", + ]); expect(sectionEvents[0].tags).toContainEqual(["type", "chapter"]); - expect(sectionEvents[0].tags).toContainEqual(["image", "https://example.com/section-image.jpg"]); + expect(sectionEvents[0].tags).toContainEqual([ + "image", + "https://example.com/section-image.jpg", + ]); expect(sectionEvents[0].tags).toContainEqual(["t", "section"]); expect(sectionEvents[0].tags).toContainEqual(["t", "complex"]); expect(sectionEvents[0].tags).toContainEqual(["t", "metadata"]); @@ -387,7 +558,9 @@ index card`; const validation = validate30040EventSet(content); expect(validation.valid).toBe(false); - expect(validation.reason).toContain("30040 events must have a document title"); + expect(validation.reason).toContain( + "30040 events must have a document title", + ); }); }); @@ -400,11 +573,21 @@ This is just preamble content.`; const tags: [string, string][] = []; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); expect(indexEvent.kind).toBe(30040); - expect(indexEvent.tags).toContainEqual(["d", "document-with-no-sections"]); - expect(indexEvent.tags).toContainEqual(["title", "Document with No Sections"]); + expect(indexEvent.tags).toContainEqual([ + "d", + "document-with-no-sections", + ]); + expect(indexEvent.tags).toContainEqual([ + "title", + "Document with No Sections", + ]); expect(sectionEvents).toHaveLength(0); }); @@ -418,16 +601,27 @@ Content here.`; const tags: [string, string][] = []; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); expect(indexEvent.kind).toBe(30040); - expect(indexEvent.tags).toContainEqual(["d", "document-with-special-characters-test-more"]); - expect(indexEvent.tags).toContainEqual(["title", "Document with Special Characters: Test & More!"]); + expect(indexEvent.tags).toContainEqual([ + "d", + "document-with-special-characters-test-more", + ]); + expect(indexEvent.tags).toContainEqual([ + "title", + "Document with Special Characters: Test & More!", + ]); expect(sectionEvents).toHaveLength(1); }); it("should handle document with very long title", () => { - const content = `= This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality + const content = + `= This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality :summary: This document has a very long title == Section 1 @@ -436,11 +630,18 @@ Content here.`; const tags: [string, string][] = []; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); expect(indexEvent.kind).toBe(30040); - expect(indexEvent.tags).toContainEqual(["title", "This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality"]); + expect(indexEvent.tags).toContainEqual([ + "title", + "This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality", + ]); expect(sectionEvents).toHaveLength(1); }); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/latexRendering.test.ts b/tests/unit/latexRendering.test.ts index ed38f4d..eac80c5 100644 --- a/tests/unit/latexRendering.test.ts +++ b/tests/unit/latexRendering.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; import { parseAdvancedmarkup } from "../../src/lib/utils/markup/advancedMarkupParser"; import { readFileSync } from "fs"; import { join } from "path"; diff --git a/tests/unit/metadataExtraction.test.ts b/tests/unit/metadataExtraction.test.ts index 65a50b8..01c7e6e 100644 --- a/tests/unit/metadataExtraction.test.ts +++ b/tests/unit/metadataExtraction.test.ts @@ -1,10 +1,10 @@ -import { describe, it, expect } from "vitest"; -import { - extractDocumentMetadata, - extractSectionMetadata, - parseAsciiDocWithMetadata, +import { describe, expect, it } from "vitest"; +import { + extractDocumentMetadata, + extractSectionMetadata, + extractSmartMetadata, metadataToTags, - extractSmartMetadata + parseAsciiDocWithMetadata, } from "../../src/lib/utils/asciidoc_metadata.ts"; describe("AsciiDoc Metadata Extraction", () => { @@ -39,13 +39,15 @@ This is the content of the second section.`; it("extractDocumentMetadata should extract document metadata correctly", () => { const { metadata, content } = extractDocumentMetadata(testContent); - + expect(metadata.title).toBe("Test Document with Metadata"); expect(metadata.authors).toEqual(["John Doe", "Jane Smith"]); expect(metadata.version).toBe("1.0"); expect(metadata.publicationDate).toBe("2024-01-15"); expect(metadata.publishedBy).toBe("Alexandria Test"); - expect(metadata.summary).toBe("This is a test document for metadata extraction"); + expect(metadata.summary).toBe( + "This is a test document for metadata extraction", + ); expect(metadata.authors).toEqual(["John Doe", "Jane Smith"]); expect(metadata.type).toBe("article"); expect(metadata.tags).toEqual(["test", "metadata", "asciidoc"]); @@ -53,7 +55,7 @@ This is the content of the second section.`; expect(metadata.isbn).toBe("978-0-123456-78-9"); expect(metadata.source).toBe("https://github.com/alexandria/test"); expect(metadata.autoUpdate).toBe("yes"); - + // Content should not include the header metadata expect(content).toContain("This is the preamble content"); expect(content).toContain("== First Section"); @@ -70,7 +72,7 @@ This is the content of the second section.`; This is the content of the first section.`; const { metadata, content, title } = extractSectionMetadata(sectionContent); - + expect(title).toBe("First Section"); expect(metadata.authors).toEqual(["Section Author"]); expect(metadata.summary).toBe("This is the first section"); @@ -86,7 +88,7 @@ Stella Some context text`; const { metadata, content, title } = extractSectionMetadata(sectionContent); - + expect(title).toBe("Section Header1"); expect(metadata.authors).toEqual(["Stella"]); expect(metadata.summary).toBe("Some summary"); @@ -102,7 +104,7 @@ Stella Some context text`; const { metadata, content, title } = extractSectionMetadata(sectionContent); - + expect(title).toBe("Section Header1"); expect(metadata.authors).toEqual(["Stella", "John Doe"]); expect(metadata.summary).toBe("Some summary"); @@ -118,22 +120,26 @@ This is not an author line Some context text`; const { metadata, content, title } = extractSectionMetadata(sectionContent); - + expect(title).toBe("Section Header1"); expect(metadata.authors).toEqual(["Stella"]); expect(metadata.summary).toBe("Some summary"); - expect(content.trim()).toBe("This is not an author line\nSome context text"); + expect(content.trim()).toBe( + "This is not an author line\nSome context text", + ); }); it("parseAsciiDocWithMetadata should parse complete document", () => { const parsed = parseAsciiDocWithMetadata(testContent); - + expect(parsed.metadata.title).toBe("Test Document with Metadata"); expect(parsed.sections).toHaveLength(2); expect(parsed.sections[0].title).toBe("First Section"); expect(parsed.sections[1].title).toBe("Second Section"); expect(parsed.sections[0].metadata.authors).toEqual(["Section Author"]); - expect(parsed.sections[1].metadata.summary).toBe("This is the second section"); + expect(parsed.sections[1].metadata.summary).toBe( + "This is the second section", + ); }); it("metadataToTags should convert metadata to Nostr tags", () => { @@ -142,11 +148,11 @@ Some context text`; authors: ["Author 1", "Author 2"], version: "1.0", summary: "Test summary", - tags: ["tag1", "tag2"] + tags: ["tag1", "tag2"], }; - + const tags = metadataToTags(metadata); - + expect(tags).toContainEqual(["title", "Test Title"]); expect(tags).toContainEqual(["author", "Author 1"]); expect(tags).toContainEqual(["author", "Author 2"]); @@ -161,16 +167,16 @@ Some context text`; index card`; const { metadata, content } = extractDocumentMetadata(indexCardContent); - + expect(metadata.title).toBe("Test Index Card"); expect(content.trim()).toBe("index card"); }); it("should handle empty content gracefully", () => { const emptyContent = ""; - + const { metadata, content } = extractDocumentMetadata(emptyContent); - + expect(metadata.title).toBeUndefined(); expect(content).toBe(""); }); @@ -182,7 +188,7 @@ index card`; Some content here.`; const { metadata } = extractDocumentMetadata(contentWithKeywords); - + expect(metadata.tags).toEqual(["keyword1", "keyword2", "keyword3"]); }); @@ -194,7 +200,7 @@ Some content here.`; Some content here.`; const { metadata } = extractDocumentMetadata(contentWithBoth); - + // Both tags and keywords are valid, both should be accumulated expect(metadata.tags).toEqual(["tag1", "tag2", "keyword1", "keyword2"]); }); @@ -206,7 +212,7 @@ Some content here.`; Content here.`; const { metadata } = extractDocumentMetadata(contentWithTags); - + expect(metadata.tags).toEqual(["tag1", "tag2", "tag3"]); }); @@ -221,15 +227,19 @@ Content here.`; Content here.`; - const { metadata: summaryMetadata } = extractDocumentMetadata(contentWithSummary); - const { metadata: descriptionMetadata } = extractDocumentMetadata(contentWithDescription); - + const { metadata: summaryMetadata } = extractDocumentMetadata( + contentWithSummary, + ); + const { metadata: descriptionMetadata } = extractDocumentMetadata( + contentWithDescription, + ); + expect(summaryMetadata.summary).toBe("This is a summary"); expect(descriptionMetadata.summary).toBe("This is a description"); }); - describe('Smart metadata extraction', () => { - it('should handle section-only content correctly', () => { + describe("Smart metadata extraction", () => { + it("should handle section-only content correctly", () => { const sectionOnlyContent = `== First Section :author: Section Author :description: This is the first section @@ -244,20 +254,20 @@ This is the content of the first section. This is the content of the second section.`; const { metadata, content } = extractSmartMetadata(sectionOnlyContent); - + // Should extract title from first section - expect(metadata.title).toBe('First Section'); - + expect(metadata.title).toBe("First Section"); + // Should not have document-level metadata since there's no document header expect(metadata.authors).toBeUndefined(); expect(metadata.version).toBeUndefined(); expect(metadata.publicationDate).toBeUndefined(); - + // Content should be preserved expect(content).toBe(sectionOnlyContent); }); - it('should handle minimal document header (just title) correctly', () => { + it("should handle minimal document header (just title) correctly", () => { const minimalDocumentHeader = `= Test Document == First Section @@ -273,22 +283,22 @@ This is the content of the first section. This is the content of the second section.`; const { metadata, content } = extractSmartMetadata(minimalDocumentHeader); - + // Should extract title from document header - expect(metadata.title).toBe('Test Document'); - + expect(metadata.title).toBe("Test Document"); + // Should not have document-level metadata since there's no other metadata expect(metadata.authors).toBeUndefined(); // Note: version might be set from section attributes like :type: chapter expect(metadata.publicationDate).toBeUndefined(); - + // Content should preserve the title line for 30040 events - expect(content).toContain('= Test Document'); - expect(content).toContain('== First Section'); - expect(content).toContain('== Second Section'); + expect(content).toContain("= Test Document"); + expect(content).toContain("== First Section"); + expect(content).toContain("== Second Section"); }); - it('should handle document with full header correctly', () => { + it("should handle document with full header correctly", () => { const documentWithHeader = `= Test Document John Doe 1.0, 2024-01-15: Alexandria Test @@ -302,21 +312,21 @@ John Doe This is the content.`; const { metadata, content } = extractSmartMetadata(documentWithHeader); - + // Should extract document-level metadata - expect(metadata.title).toBe('Test Document'); - expect(metadata.authors).toEqual(['John Doe', 'Jane Smith']); - expect(metadata.version).toBe('1.0'); - expect(metadata.publishedBy).toBe('Alexandria Test'); - expect(metadata.publicationDate).toBe('2024-01-15'); - expect(metadata.summary).toBe('This is a test document'); - + expect(metadata.title).toBe("Test Document"); + expect(metadata.authors).toEqual(["John Doe", "Jane Smith"]); + expect(metadata.version).toBe("1.0"); + expect(metadata.publishedBy).toBe("Alexandria Test"); + expect(metadata.publicationDate).toBe("2024-01-15"); + expect(metadata.summary).toBe("This is a test document"); + // Content should be cleaned - expect(content).not.toContain('= Test Document'); - expect(content).not.toContain('John Doe '); - expect(content).not.toContain('1.0, 2024-01-15: Alexandria Test'); - expect(content).not.toContain(':summary: This is a test document'); - expect(content).not.toContain(':author: Jane Smith'); + expect(content).not.toContain("= Test Document"); + expect(content).not.toContain("John Doe "); + expect(content).not.toContain("1.0, 2024-01-15: Alexandria Test"); + expect(content).not.toContain(":summary: This is a test document"); + expect(content).not.toContain(":author: Jane Smith"); }); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/nostr_identifiers.test.ts b/tests/unit/nostr_identifiers.test.ts index d4c2d1f..a70c7bf 100644 --- a/tests/unit/nostr_identifiers.test.ts +++ b/tests/unit/nostr_identifiers.test.ts @@ -1,106 +1,112 @@ -import { describe, it, expect } from 'vitest'; -import { - isEventId, - isCoordinate, - parseCoordinate, +import { describe, expect, it } from "vitest"; +import { createCoordinate, - isNostrIdentifier -} from '../../src/lib/utils/nostr_identifiers'; + isCoordinate, + isEventId, + isNostrIdentifier, + parseCoordinate, +} from "../../src/lib/utils/nostr_identifiers"; -describe('Nostr Identifier Validation', () => { - describe('isEventId', () => { - it('should validate correct hex event IDs', () => { - const validId = 'a'.repeat(64); +describe("Nostr Identifier Validation", () => { + describe("isEventId", () => { + it("should validate correct hex event IDs", () => { + const validId = "a".repeat(64); expect(isEventId(validId)).toBe(true); - - const validIdWithMixedCase = 'A'.repeat(32) + 'f'.repeat(32); + + const validIdWithMixedCase = "A".repeat(32) + "f".repeat(32); expect(isEventId(validIdWithMixedCase)).toBe(true); }); - it('should reject invalid event IDs', () => { - expect(isEventId('')).toBe(false); - expect(isEventId('abc')).toBe(false); - expect(isEventId('a'.repeat(63))).toBe(false); // too short - expect(isEventId('a'.repeat(65))).toBe(false); // too long - expect(isEventId('g'.repeat(64))).toBe(false); // invalid hex char + it("should reject invalid event IDs", () => { + expect(isEventId("")).toBe(false); + expect(isEventId("abc")).toBe(false); + expect(isEventId("a".repeat(63))).toBe(false); // too short + expect(isEventId("a".repeat(65))).toBe(false); // too long + expect(isEventId("g".repeat(64))).toBe(false); // invalid hex char }); }); - describe('isCoordinate', () => { - it('should validate correct coordinates', () => { - const validCoordinate = `30040:${'a'.repeat(64)}:chapter-1`; + describe("isCoordinate", () => { + it("should validate correct coordinates", () => { + const validCoordinate = `30040:${"a".repeat(64)}:chapter-1`; expect(isCoordinate(validCoordinate)).toBe(true); - - const coordinateWithColonsInDTag = `30041:${'b'.repeat(64)}:chapter:with:colons`; + + const coordinateWithColonsInDTag = `30041:${ + "b".repeat(64) + }:chapter:with:colons`; expect(isCoordinate(coordinateWithColonsInDTag)).toBe(true); }); - it('should reject invalid coordinates', () => { - expect(isCoordinate('')).toBe(false); - expect(isCoordinate('abc')).toBe(false); - expect(isCoordinate('30040:abc:chapter-1')).toBe(false); // invalid pubkey - expect(isCoordinate('30040:abc')).toBe(false); // missing d-tag - expect(isCoordinate('abc:def:ghi')).toBe(false); // invalid kind - expect(isCoordinate('-1:abc:def')).toBe(false); // negative kind + it("should reject invalid coordinates", () => { + expect(isCoordinate("")).toBe(false); + expect(isCoordinate("abc")).toBe(false); + expect(isCoordinate("30040:abc:chapter-1")).toBe(false); // invalid pubkey + expect(isCoordinate("30040:abc")).toBe(false); // missing d-tag + expect(isCoordinate("abc:def:ghi")).toBe(false); // invalid kind + expect(isCoordinate("-1:abc:def")).toBe(false); // negative kind }); }); - describe('parseCoordinate', () => { - it('should parse valid coordinates correctly', () => { - const coordinate = `30040:${'a'.repeat(64)}:chapter-1`; + describe("parseCoordinate", () => { + it("should parse valid coordinates correctly", () => { + const coordinate = `30040:${"a".repeat(64)}:chapter-1`; const parsed = parseCoordinate(coordinate); - + expect(parsed).toEqual({ kind: 30040, - pubkey: 'a'.repeat(64), - dTag: 'chapter-1' + pubkey: "a".repeat(64), + dTag: "chapter-1", }); }); - it('should handle d-tags with colons', () => { - const coordinate = `30041:${'b'.repeat(64)}:chapter:with:colons`; + it("should handle d-tags with colons", () => { + const coordinate = `30041:${"b".repeat(64)}:chapter:with:colons`; const parsed = parseCoordinate(coordinate); - + expect(parsed).toEqual({ kind: 30041, - pubkey: 'b'.repeat(64), - dTag: 'chapter:with:colons' + pubkey: "b".repeat(64), + dTag: "chapter:with:colons", }); }); - it('should return null for invalid coordinates', () => { - expect(parseCoordinate('')).toBeNull(); - expect(parseCoordinate('abc')).toBeNull(); - expect(parseCoordinate('30040:abc:chapter-1')).toBeNull(); + it("should return null for invalid coordinates", () => { + expect(parseCoordinate("")).toBeNull(); + expect(parseCoordinate("abc")).toBeNull(); + expect(parseCoordinate("30040:abc:chapter-1")).toBeNull(); }); }); - describe('createCoordinate', () => { - it('should create valid coordinates', () => { - const coordinate = createCoordinate(30040, 'a'.repeat(64), 'chapter-1'); - expect(coordinate).toBe(`30040:${'a'.repeat(64)}:chapter-1`); + describe("createCoordinate", () => { + it("should create valid coordinates", () => { + const coordinate = createCoordinate(30040, "a".repeat(64), "chapter-1"); + expect(coordinate).toBe(`30040:${"a".repeat(64)}:chapter-1`); }); - it('should handle d-tags with colons', () => { - const coordinate = createCoordinate(30041, 'b'.repeat(64), 'chapter:with:colons'); - expect(coordinate).toBe(`30041:${'b'.repeat(64)}:chapter:with:colons`); + it("should handle d-tags with colons", () => { + const coordinate = createCoordinate( + 30041, + "b".repeat(64), + "chapter:with:colons", + ); + expect(coordinate).toBe(`30041:${"b".repeat(64)}:chapter:with:colons`); }); }); - describe('isNostrIdentifier', () => { - it('should accept valid event IDs', () => { - expect(isNostrIdentifier('a'.repeat(64))).toBe(true); + describe("isNostrIdentifier", () => { + it("should accept valid event IDs", () => { + expect(isNostrIdentifier("a".repeat(64))).toBe(true); }); - it('should accept valid coordinates', () => { - const coordinate = `30040:${'a'.repeat(64)}:chapter-1`; + it("should accept valid coordinates", () => { + const coordinate = `30040:${"a".repeat(64)}:chapter-1`; expect(isNostrIdentifier(coordinate)).toBe(true); }); - it('should reject invalid identifiers', () => { - expect(isNostrIdentifier('')).toBe(false); - expect(isNostrIdentifier('abc')).toBe(false); - expect(isNostrIdentifier('30040:abc:chapter-1')).toBe(false); + it("should reject invalid identifiers", () => { + expect(isNostrIdentifier("")).toBe(false); + expect(isNostrIdentifier("abc")).toBe(false); + expect(isNostrIdentifier("30040:abc:chapter-1")).toBe(false); }); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/relayDeduplication.test.ts b/tests/unit/relayDeduplication.test.ts index 9344cc2..4ea6b91 100644 --- a/tests/unit/relayDeduplication.test.ts +++ b/tests/unit/relayDeduplication.test.ts @@ -1,11 +1,11 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import type { NDKEvent } from '@nostr-dev-kit/ndk'; -import { - deduplicateContentEvents, +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { NDKEvent } from "@nostr-dev-kit/ndk"; +import { deduplicateAndCombineEvents, + deduplicateContentEvents, + getEventCoordinate, isReplaceableEvent, - getEventCoordinate -} from '../../src/lib/utils/eventDeduplication'; +} from "../../src/lib/utils/eventDeduplication"; // Mock NDKEvent for testing class MockNDKEvent { @@ -16,162 +16,264 @@ class MockNDKEvent { content: string; tags: string[][]; - constructor(id: string, kind: number, pubkey: string, created_at: number, dTag: string, content: string = '') { + constructor( + id: string, + kind: number, + pubkey: string, + created_at: number, + dTag: string, + content: string = "", + ) { this.id = id; this.kind = kind; this.pubkey = pubkey; this.created_at = created_at; this.content = content; - this.tags = [['d', dTag]]; + this.tags = [["d", dTag]]; } tagValue(tagName: string): string | undefined { - const tag = this.tags.find(t => t[0] === tagName); + const tag = this.tags.find((t) => t[0] === tagName); return tag ? tag[1] : undefined; } } -describe('Relay Deduplication Behavior Tests', () => { +describe("Relay Deduplication Behavior Tests", () => { let mockEvents: MockNDKEvent[]; beforeEach(() => { // Create test events with different timestamps mockEvents = [ // Older version of a publication content event - new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Old content'), + new MockNDKEvent( + "event1", + 30041, + "pubkey1", + 1000, + "chapter-1", + "Old content", + ), // Newer version of the same publication content event - new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-1', 'Updated content'), + new MockNDKEvent( + "event2", + 30041, + "pubkey1", + 2000, + "chapter-1", + "Updated content", + ), // Different publication content event - new MockNDKEvent('event3', 30041, 'pubkey1', 1500, 'chapter-2', 'Different content'), + new MockNDKEvent( + "event3", + 30041, + "pubkey1", + 1500, + "chapter-2", + "Different content", + ), // Publication index event (should not be deduplicated) - new MockNDKEvent('event4', 30040, 'pubkey1', 1200, 'book-1', 'Index content'), + new MockNDKEvent( + "event4", + 30040, + "pubkey1", + 1200, + "book-1", + "Index content", + ), // Regular text note (should not be deduplicated) - new MockNDKEvent('event5', 1, 'pubkey1', 1300, '', 'Regular note'), + new MockNDKEvent("event5", 1, "pubkey1", 1300, "", "Regular note"), ]; }); - describe('Addressable Event Deduplication', () => { - it('should keep only the most recent version of addressable events by coordinate', () => { + describe("Addressable Event Deduplication", () => { + it("should keep only the most recent version of addressable events by coordinate", () => { // Test the deduplication logic for content events - const eventSets = [new Set(mockEvents.filter(e => e.kind === 30041) as NDKEvent[])]; + const eventSets = [ + new Set(mockEvents.filter((e) => e.kind === 30041) as NDKEvent[]), + ]; const result = deduplicateContentEvents(eventSets); - + // Should have 2 unique coordinates: chapter-1 and chapter-2 expect(result.size).toBe(2); - + // Should keep the newer version of chapter-1 - const chapter1Event = result.get('30041:pubkey1:chapter-1'); - expect(chapter1Event?.id).toBe('event2'); - expect(chapter1Event?.content).toBe('Updated content'); - + const chapter1Event = result.get("30041:pubkey1:chapter-1"); + expect(chapter1Event?.id).toBe("event2"); + expect(chapter1Event?.content).toBe("Updated content"); + // Should keep chapter-2 - const chapter2Event = result.get('30041:pubkey1:chapter-2'); - expect(chapter2Event?.id).toBe('event3'); + const chapter2Event = result.get("30041:pubkey1:chapter-2"); + expect(chapter2Event?.id).toBe("event3"); }); - it('should handle events with missing d-tags gracefully', () => { - const eventWithoutDTag = new MockNDKEvent('event6', 30041, 'pubkey1', 1400, '', 'No d-tag'); + it("should handle events with missing d-tags gracefully", () => { + const eventWithoutDTag = new MockNDKEvent( + "event6", + 30041, + "pubkey1", + 1400, + "", + "No d-tag", + ); eventWithoutDTag.tags = []; // Remove d-tag - + const eventSets = [new Set([eventWithoutDTag] as NDKEvent[])]; const result = deduplicateContentEvents(eventSets); - + // Should not include events without d-tags expect(result.size).toBe(0); }); - it('should handle events with missing timestamps', () => { - const eventWithoutTimestamp = new MockNDKEvent('event7', 30041, 'pubkey1', 0, 'chapter-3', 'No timestamp'); - const eventWithTimestamp = new MockNDKEvent('event8', 30041, 'pubkey1', 1500, 'chapter-3', 'With timestamp'); - - const eventSets = [new Set([eventWithoutTimestamp, eventWithTimestamp] as NDKEvent[])]; + it("should handle events with missing timestamps", () => { + const eventWithoutTimestamp = new MockNDKEvent( + "event7", + 30041, + "pubkey1", + 0, + "chapter-3", + "No timestamp", + ); + const eventWithTimestamp = new MockNDKEvent( + "event8", + 30041, + "pubkey1", + 1500, + "chapter-3", + "With timestamp", + ); + + const eventSets = [ + new Set([eventWithoutTimestamp, eventWithTimestamp] as NDKEvent[]), + ]; const result = deduplicateContentEvents(eventSets); - + // Should prefer the event with timestamp - const chapter3Event = result.get('30041:pubkey1:chapter-3'); - expect(chapter3Event?.id).toBe('event8'); + const chapter3Event = result.get("30041:pubkey1:chapter-3"); + expect(chapter3Event?.id).toBe("event8"); }); }); - describe('Mixed Event Type Deduplication', () => { - it('should only deduplicate addressable events (kinds 30000-39999)', () => { + describe("Mixed Event Type Deduplication", () => { + it("should only deduplicate addressable events (kinds 30000-39999)", () => { const result = deduplicateAndCombineEvents( [mockEvents[4]] as NDKEvent[], // Regular text note new Set([mockEvents[3]] as NDKEvent[]), // Publication index - new Set([mockEvents[0], mockEvents[1], mockEvents[2]] as NDKEvent[]) // Content events + new Set([mockEvents[0], mockEvents[1], mockEvents[2]] as NDKEvent[]), // Content events ); - + // Should have 4 events total: // - 1 regular text note (not deduplicated) // - 1 publication index (not deduplicated) // - 2 unique content events (deduplicated from 3) expect(result.length).toBe(4); - + // Verify the content events were deduplicated - const contentEvents = result.filter(e => e.kind === 30041); + const contentEvents = result.filter((e) => e.kind === 30041); expect(contentEvents.length).toBe(2); - + // Verify the newer version was kept - const newerEvent = contentEvents.find(e => e.id === 'event2'); + const newerEvent = contentEvents.find((e) => e.id === "event2"); expect(newerEvent).toBeDefined(); }); - it('should handle non-addressable events correctly', () => { + it("should handle non-addressable events correctly", () => { const regularEvents = [ - new MockNDKEvent('note1', 1, 'pubkey1', 1000, '', 'Note 1'), - new MockNDKEvent('note2', 1, 'pubkey1', 2000, '', 'Note 2'), - new MockNDKEvent('profile1', 0, 'pubkey1', 1500, '', 'Profile 1'), + new MockNDKEvent("note1", 1, "pubkey1", 1000, "", "Note 1"), + new MockNDKEvent("note2", 1, "pubkey1", 2000, "", "Note 2"), + new MockNDKEvent("profile1", 0, "pubkey1", 1500, "", "Profile 1"), ]; - + const result = deduplicateAndCombineEvents( regularEvents as NDKEvent[], new Set(), - new Set() + new Set(), ); - + // All regular events should be included (no deduplication) expect(result.length).toBe(3); }); }); - describe('Coordinate System Validation', () => { - it('should correctly identify event coordinates', () => { - const event = new MockNDKEvent('test', 30041, 'pubkey123', 1000, 'test-chapter'); + describe("Coordinate System Validation", () => { + it("should correctly identify event coordinates", () => { + const event = new MockNDKEvent( + "test", + 30041, + "pubkey123", + 1000, + "test-chapter", + ); const coordinate = getEventCoordinate(event as NDKEvent); - - expect(coordinate).toBe('30041:pubkey123:test-chapter'); + + expect(coordinate).toBe("30041:pubkey123:test-chapter"); }); - it('should handle d-tags with colons correctly', () => { - const event = new MockNDKEvent('test', 30041, 'pubkey123', 1000, 'chapter:with:colons'); + it("should handle d-tags with colons correctly", () => { + const event = new MockNDKEvent( + "test", + 30041, + "pubkey123", + 1000, + "chapter:with:colons", + ); const coordinate = getEventCoordinate(event as NDKEvent); - - expect(coordinate).toBe('30041:pubkey123:chapter:with:colons'); + + expect(coordinate).toBe("30041:pubkey123:chapter:with:colons"); }); - it('should return null for non-replaceable events', () => { - const event = new MockNDKEvent('test', 1, 'pubkey123', 1000, ''); + it("should return null for non-replaceable events", () => { + const event = new MockNDKEvent("test", 1, "pubkey123", 1000, ""); const coordinate = getEventCoordinate(event as NDKEvent); - + expect(coordinate).toBeNull(); }); }); - describe('Replaceable Event Detection', () => { - it('should correctly identify replaceable events', () => { - const addressableEvent = new MockNDKEvent('test', 30041, 'pubkey123', 1000, 'test'); - const regularEvent = new MockNDKEvent('test', 1, 'pubkey123', 1000, ''); - + describe("Replaceable Event Detection", () => { + it("should correctly identify replaceable events", () => { + const addressableEvent = new MockNDKEvent( + "test", + 30041, + "pubkey123", + 1000, + "test", + ); + const regularEvent = new MockNDKEvent("test", 1, "pubkey123", 1000, ""); + expect(isReplaceableEvent(addressableEvent as NDKEvent)).toBe(true); expect(isReplaceableEvent(regularEvent as NDKEvent)).toBe(false); }); - it('should handle edge cases of replaceable event ranges', () => { - const event29999 = new MockNDKEvent('test', 29999, 'pubkey123', 1000, 'test'); - const event30000 = new MockNDKEvent('test', 30000, 'pubkey123', 1000, 'test'); - const event39999 = new MockNDKEvent('test', 39999, 'pubkey123', 1000, 'test'); - const event40000 = new MockNDKEvent('test', 40000, 'pubkey123', 1000, 'test'); - + it("should handle edge cases of replaceable event ranges", () => { + const event29999 = new MockNDKEvent( + "test", + 29999, + "pubkey123", + 1000, + "test", + ); + const event30000 = new MockNDKEvent( + "test", + 30000, + "pubkey123", + 1000, + "test", + ); + const event39999 = new MockNDKEvent( + "test", + 39999, + "pubkey123", + 1000, + "test", + ); + const event40000 = new MockNDKEvent( + "test", + 40000, + "pubkey123", + 1000, + "test", + ); + expect(isReplaceableEvent(event29999 as NDKEvent)).toBe(false); expect(isReplaceableEvent(event30000 as NDKEvent)).toBe(true); expect(isReplaceableEvent(event39999 as NDKEvent)).toBe(true); @@ -179,279 +281,429 @@ describe('Relay Deduplication Behavior Tests', () => { }); }); - describe('Edge Cases', () => { - it('should handle empty event sets', () => { + describe("Edge Cases", () => { + it("should handle empty event sets", () => { const result = deduplicateContentEvents([]); expect(result.size).toBe(0); }); - it('should handle events with null/undefined values', () => { + it("should handle events with null/undefined values", () => { const invalidEvent = { id: undefined, kind: 30041, - pubkey: 'pubkey1', + pubkey: "pubkey1", created_at: 1000, tagValue: () => undefined, // Return undefined for d-tag } as unknown as NDKEvent; - + const eventSets = [new Set([invalidEvent])]; const result = deduplicateContentEvents(eventSets); - + // Should handle gracefully without crashing expect(result.size).toBe(0); }); - it('should handle events from different authors with same d-tag', () => { - const event1 = new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'same-chapter', 'Author 1'); - const event2 = new MockNDKEvent('event2', 30041, 'pubkey2', 1000, 'same-chapter', 'Author 2'); - + it("should handle events from different authors with same d-tag", () => { + const event1 = new MockNDKEvent( + "event1", + 30041, + "pubkey1", + 1000, + "same-chapter", + "Author 1", + ); + const event2 = new MockNDKEvent( + "event2", + 30041, + "pubkey2", + 1000, + "same-chapter", + "Author 2", + ); + const eventSets = [new Set([event1, event2] as NDKEvent[])]; const result = deduplicateContentEvents(eventSets); - + // Should have 2 events (different coordinates due to different authors) expect(result.size).toBe(2); - expect(result.has('30041:pubkey1:same-chapter')).toBe(true); - expect(result.has('30041:pubkey2:same-chapter')).toBe(true); + expect(result.has("30041:pubkey1:same-chapter")).toBe(true); + expect(result.has("30041:pubkey2:same-chapter")).toBe(true); }); }); }); -describe('Relay Behavior Simulation', () => { - it('should simulate what happens when relays return duplicate events', () => { +describe("Relay Behavior Simulation", () => { + it("should simulate what happens when relays return duplicate events", () => { // Simulate a relay that returns multiple versions of the same event const relayEvents = [ - new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Old version'), - new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-1', 'New version'), - new MockNDKEvent('event3', 30041, 'pubkey1', 1500, 'chapter-1', 'Middle version'), + new MockNDKEvent( + "event1", + 30041, + "pubkey1", + 1000, + "chapter-1", + "Old version", + ), + new MockNDKEvent( + "event2", + 30041, + "pubkey1", + 2000, + "chapter-1", + "New version", + ), + new MockNDKEvent( + "event3", + 30041, + "pubkey1", + 1500, + "chapter-1", + "Middle version", + ), ]; - + // This simulates what a "bad" relay might return const eventSets = [new Set(relayEvents as NDKEvent[])]; const result = deduplicateContentEvents(eventSets); - + // Should only keep the newest version expect(result.size).toBe(1); - const keptEvent = result.get('30041:pubkey1:chapter-1'); - expect(keptEvent?.id).toBe('event2'); - expect(keptEvent?.content).toBe('New version'); + const keptEvent = result.get("30041:pubkey1:chapter-1"); + expect(keptEvent?.id).toBe("event2"); + expect(keptEvent?.content).toBe("New version"); }); - it('should simulate multiple relays returning different versions', () => { + it("should simulate multiple relays returning different versions", () => { // Simulate multiple relays returning different versions const relay1Events = [ - new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Relay 1 version'), + new MockNDKEvent( + "event1", + 30041, + "pubkey1", + 1000, + "chapter-1", + "Relay 1 version", + ), ]; - + const relay2Events = [ - new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-1', 'Relay 2 version'), + new MockNDKEvent( + "event2", + 30041, + "pubkey1", + 2000, + "chapter-1", + "Relay 2 version", + ), + ]; + + const eventSets = [ + new Set(relay1Events as NDKEvent[]), + new Set(relay2Events as NDKEvent[]), ]; - - const eventSets = [new Set(relay1Events as NDKEvent[]), new Set(relay2Events as NDKEvent[])]; const result = deduplicateContentEvents(eventSets); - + // Should keep the newest version from any relay expect(result.size).toBe(1); - const keptEvent = result.get('30041:pubkey1:chapter-1'); - expect(keptEvent?.id).toBe('event2'); - expect(keptEvent?.content).toBe('Relay 2 version'); + const keptEvent = result.get("30041:pubkey1:chapter-1"); + expect(keptEvent?.id).toBe("event2"); + expect(keptEvent?.content).toBe("Relay 2 version"); }); }); -describe('Real Relay Deduplication Tests', () => { +describe("Real Relay Deduplication Tests", () => { // These tests actually query real relays to see if they deduplicate // Note: These are integration tests and may be flaky due to network conditions - - it('should detect if relays are returning duplicate replaceable events', async () => { - // This test queries real relays to see if they return duplicates - // We'll use a known author who has published multiple versions of content - - // Known author with multiple publication content events - const testAuthor = 'npub1z4m7gkva6yxgvdyclc7zp0qt69x9zgn8lu8sllg06wx6432h77qs0k97ks'; - - // Query for publication content events (kind 30041) from this author - // We expect relays to return only the most recent version of each d-tag - - // This is a placeholder - in a real test, we would: - // 1. Query multiple relays for the same author's 30041 events - // 2. Check if any relay returns multiple events with the same d-tag - // 3. Verify that if duplicates exist, our deduplication logic handles them - - console.log('Note: This test would require actual relay queries to verify deduplication behavior'); - console.log('To run this test properly, we would need to:'); - console.log('1. Query real relays for replaceable events'); - console.log('2. Check if relays return duplicates'); - console.log('3. Verify our deduplication logic works on real data'); - - // For now, we'll just assert that our logic is ready to handle real data - expect(true).toBe(true); - }, 30000); // 30 second timeout for network requests - - it('should verify that our deduplication logic works on real relay data', async () => { - // This test would: - // 1. Fetch real events from relays - // 2. Apply our deduplication logic - // 3. Verify that the results are correct - - console.log('Note: This test would require actual relay queries'); - console.log('To implement this test, we would need to:'); - console.log('1. Set up NDK with real relays'); - console.log('2. Fetch events for a known author with multiple versions'); - console.log('3. Apply deduplication and verify results'); - - expect(true).toBe(true); - }, 30000); + + it( + "should detect if relays are returning duplicate replaceable events", + async () => { + // This test queries real relays to see if they return duplicates + // We'll use a known author who has published multiple versions of content + + // Known author with multiple publication content events + const testAuthor = + "npub1z4m7gkva6yxgvdyclc7zp0qt69x9zgn8lu8sllg06wx6432h77qs0k97ks"; + + // Query for publication content events (kind 30041) from this author + // We expect relays to return only the most recent version of each d-tag + + // This is a placeholder - in a real test, we would: + // 1. Query multiple relays for the same author's 30041 events + // 2. Check if any relay returns multiple events with the same d-tag + // 3. Verify that if duplicates exist, our deduplication logic handles them + + console.log( + "Note: This test would require actual relay queries to verify deduplication behavior", + ); + console.log("To run this test properly, we would need to:"); + console.log("1. Query real relays for replaceable events"); + console.log("2. Check if relays return duplicates"); + console.log("3. Verify our deduplication logic works on real data"); + + // For now, we'll just assert that our logic is ready to handle real data + expect(true).toBe(true); + }, + 30000, + ); // 30 second timeout for network requests + + it( + "should verify that our deduplication logic works on real relay data", + async () => { + // This test would: + // 1. Fetch real events from relays + // 2. Apply our deduplication logic + // 3. Verify that the results are correct + + console.log("Note: This test would require actual relay queries"); + console.log("To implement this test, we would need to:"); + console.log("1. Set up NDK with real relays"); + console.log("2. Fetch events for a known author with multiple versions"); + console.log("3. Apply deduplication and verify results"); + + expect(true).toBe(true); + }, + 30000, + ); }); -describe('Practical Relay Behavior Analysis', () => { - it('should document what we know about relay deduplication behavior', () => { +describe("Practical Relay Behavior Analysis", () => { + it("should document what we know about relay deduplication behavior", () => { // This test documents our current understanding of relay behavior // based on the code analysis and the comment from onedev - - console.log('\n=== RELAY DEDUPLICATION BEHAVIOR ANALYSIS ==='); - console.log('\nBased on the code analysis and the comment from onedev:'); - console.log('\n1. THEORETICAL BEHAVIOR:'); - console.log(' - Relays SHOULD handle deduplication for replaceable events'); - console.log(' - Only the most recent version of each coordinate should be stored'); - console.log(' - Client-side deduplication should only be needed for cached/local events'); - - console.log('\n2. REALITY CHECK:'); - console.log(' - Not all relays implement deduplication correctly'); - console.log(' - Some relays may return multiple versions of the same event'); - console.log(' - Network conditions and relay availability can cause inconsistencies'); - - console.log('\n3. ALEXANDRIA\'S APPROACH:'); - console.log(' - Implements client-side deduplication as a safety net'); - console.log(' - Uses coordinate system (kind:pubkey:d-tag) for addressable events'); - console.log(' - Keeps the most recent version based on created_at timestamp'); - console.log(' - Only applies to replaceable events (kinds 30000-39999)'); - - console.log('\n4. WHY KEEP THE DEDUPLICATION:'); - console.log(' - Defensive programming against imperfect relay implementations'); - console.log(' - Handles multiple relay sources with different data'); - console.log(' - Works with cached events that might be outdated'); - console.log(' - Ensures consistent user experience regardless of relay behavior'); - - console.log('\n5. TESTING STRATEGY:'); - console.log(' - Unit tests verify our deduplication logic works correctly'); - console.log(' - Integration tests would verify relay behavior (when network allows)'); - console.log(' - Monitoring can help determine if relays improve over time'); - + + console.log("\n=== RELAY DEDUPLICATION BEHAVIOR ANALYSIS ==="); + console.log("\nBased on the code analysis and the comment from onedev:"); + console.log("\n1. THEORETICAL BEHAVIOR:"); + console.log( + " - Relays SHOULD handle deduplication for replaceable events", + ); + console.log( + " - Only the most recent version of each coordinate should be stored", + ); + console.log( + " - Client-side deduplication should only be needed for cached/local events", + ); + + console.log("\n2. REALITY CHECK:"); + console.log(" - Not all relays implement deduplication correctly"); + console.log( + " - Some relays may return multiple versions of the same event", + ); + console.log( + " - Network conditions and relay availability can cause inconsistencies", + ); + + console.log("\n3. ALEXANDRIA'S APPROACH:"); + console.log(" - Implements client-side deduplication as a safety net"); + console.log( + " - Uses coordinate system (kind:pubkey:d-tag) for addressable events", + ); + console.log( + " - Keeps the most recent version based on created_at timestamp", + ); + console.log(" - Only applies to replaceable events (kinds 30000-39999)"); + + console.log("\n4. WHY KEEP THE DEDUPLICATION:"); + console.log( + " - Defensive programming against imperfect relay implementations", + ); + console.log(" - Handles multiple relay sources with different data"); + console.log(" - Works with cached events that might be outdated"); + console.log( + " - Ensures consistent user experience regardless of relay behavior", + ); + + console.log("\n5. TESTING STRATEGY:"); + console.log( + " - Unit tests verify our deduplication logic works correctly", + ); + console.log( + " - Integration tests would verify relay behavior (when network allows)", + ); + console.log( + " - Monitoring can help determine if relays improve over time", + ); + // This test documents our understanding rather than asserting specific behavior expect(true).toBe(true); }); - it('should provide recommendations for when to remove deduplication', () => { - console.log('\n=== RECOMMENDATIONS FOR REMOVING DEDUPLICATION ==='); - console.log('\nThe deduplication logic should be kept until:'); - console.log('\n1. RELAY STANDARDS:'); - console.log(' - NIP-33 (replaceable events) is widely implemented by relays'); - console.log(' - Relays consistently return only the most recent version'); - console.log(' - No major relay implementations return duplicates'); - - console.log('\n2. TESTING EVIDENCE:'); - console.log(' - Real-world testing shows relays don\'t return duplicates'); - console.log(' - Multiple relay operators confirm deduplication behavior'); - console.log(' - No user reports of duplicate content issues'); - - console.log('\n3. MONITORING:'); - console.log(' - Add logging to track when deduplication is actually used'); - console.log(' - Monitor relay behavior over time'); - console.log(' - Collect metrics on duplicate events found'); - - console.log('\n4. GRADUAL REMOVAL:'); - console.log(' - Make deduplication configurable (on/off)'); - console.log(' - Test with deduplication disabled in controlled environments'); - console.log(' - Monitor for issues before removing completely'); - - console.log('\n5. FALLBACK STRATEGY:'); - console.log(' - Keep deduplication as a fallback option'); - console.log(' - Allow users to enable it if they experience issues'); - console.log(' - Maintain the code for potential future use'); - + it("should provide recommendations for when to remove deduplication", () => { + console.log("\n=== RECOMMENDATIONS FOR REMOVING DEDUPLICATION ==="); + console.log("\nThe deduplication logic should be kept until:"); + console.log("\n1. RELAY STANDARDS:"); + console.log( + " - NIP-33 (replaceable events) is widely implemented by relays", + ); + console.log(" - Relays consistently return only the most recent version"); + console.log(" - No major relay implementations return duplicates"); + + console.log("\n2. TESTING EVIDENCE:"); + console.log(" - Real-world testing shows relays don't return duplicates"); + console.log(" - Multiple relay operators confirm deduplication behavior"); + console.log(" - No user reports of duplicate content issues"); + + console.log("\n3. MONITORING:"); + console.log( + " - Add logging to track when deduplication is actually used", + ); + console.log(" - Monitor relay behavior over time"); + console.log(" - Collect metrics on duplicate events found"); + + console.log("\n4. GRADUAL REMOVAL:"); + console.log(" - Make deduplication configurable (on/off)"); + console.log( + " - Test with deduplication disabled in controlled environments", + ); + console.log(" - Monitor for issues before removing completely"); + + console.log("\n5. FALLBACK STRATEGY:"); + console.log(" - Keep deduplication as a fallback option"); + console.log(" - Allow users to enable it if they experience issues"); + console.log(" - Maintain the code for potential future use"); + expect(true).toBe(true); }); }); -describe('Logging and Monitoring Tests', () => { - it('should verify that logging works when duplicates are found', () => { +describe("Logging and Monitoring Tests", () => { + it("should verify that logging works when duplicates are found", () => { // Mock console.log to capture output - const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + // Create events with duplicates const duplicateEvents = [ - new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Old version'), - new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-1', 'New version'), - new MockNDKEvent('event3', 30041, 'pubkey1', 1500, 'chapter-1', 'Middle version'), + new MockNDKEvent( + "event1", + 30041, + "pubkey1", + 1000, + "chapter-1", + "Old version", + ), + new MockNDKEvent( + "event2", + 30041, + "pubkey1", + 2000, + "chapter-1", + "New version", + ), + new MockNDKEvent( + "event3", + 30041, + "pubkey1", + 1500, + "chapter-1", + "Middle version", + ), ]; - + const eventSets = [new Set(duplicateEvents as NDKEvent[])]; const result = deduplicateContentEvents(eventSets); - + // Verify the deduplication worked expect(result.size).toBe(1); - + // Verify that logging was called expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining('[eventDeduplication] Found 2 duplicate events out of 3 total events') + expect.stringContaining( + "[eventDeduplication] Found 2 duplicate events out of 3 total events", + ), ); expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining('[eventDeduplication] Reduced to 1 unique coordinates') + expect.stringContaining( + "[eventDeduplication] Reduced to 1 unique coordinates", + ), ); - + // Restore console.log consoleSpy.mockRestore(); }); - it('should verify that logging works when no duplicates are found', () => { + it("should verify that logging works when no duplicates are found", () => { // Mock console.log to capture output - const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + // Create events without duplicates const uniqueEvents = [ - new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Content 1'), - new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-2', 'Content 2'), + new MockNDKEvent( + "event1", + 30041, + "pubkey1", + 1000, + "chapter-1", + "Content 1", + ), + new MockNDKEvent( + "event2", + 30041, + "pubkey1", + 2000, + "chapter-2", + "Content 2", + ), ]; - + const eventSets = [new Set(uniqueEvents as NDKEvent[])]; const result = deduplicateContentEvents(eventSets); - + // Verify no deduplication was needed expect(result.size).toBe(2); - + // Verify that logging was called with "no duplicates" message expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining('[eventDeduplication] No duplicates found in 2 events') + expect.stringContaining( + "[eventDeduplication] No duplicates found in 2 events", + ), ); - + // Restore console.log consoleSpy.mockRestore(); }); - it('should verify that deduplicateAndCombineEvents logging works', () => { + it("should verify that deduplicateAndCombineEvents logging works", () => { // Mock console.log to capture output - const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + // Create events with duplicates const duplicateEvents = [ - new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Old version'), - new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-1', 'New version'), + new MockNDKEvent( + "event1", + 30041, + "pubkey1", + 1000, + "chapter-1", + "Old version", + ), + new MockNDKEvent( + "event2", + 30041, + "pubkey1", + 2000, + "chapter-1", + "New version", + ), ]; - + const result = deduplicateAndCombineEvents( [] as NDKEvent[], new Set(), - new Set(duplicateEvents as NDKEvent[]) + new Set(duplicateEvents as NDKEvent[]), ); - + // Verify the deduplication worked expect(result.length).toBe(1); - + // Verify that logging was called expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining('[eventDeduplication] deduplicateAndCombineEvents: Found 1 duplicate coordinates') + expect.stringContaining( + "[eventDeduplication] deduplicateAndCombineEvents: Found 1 duplicate coordinates", + ), ); - + // Restore console.log consoleSpy.mockRestore(); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/tagExpansion.test.ts b/tests/unit/tagExpansion.test.ts index 65e71fa..5de5f94 100644 --- a/tests/unit/tagExpansion.test.ts +++ b/tests/unit/tagExpansion.test.ts @@ -1,11 +1,11 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import type { NDKEvent } from '@nostr-dev-kit/ndk'; -import { +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { NDKEvent } from "@nostr-dev-kit/ndk"; +import { + fetchProfilesForNewEvents, fetchTaggedEventsFromRelays, findTaggedEventsInFetched, - fetchProfilesForNewEvents, - type TagExpansionResult -} from '../../src/lib/utils/tag_event_fetch'; + type TagExpansionResult, +} from "../../src/lib/utils/tag_event_fetch"; // Mock NDKEvent for testing class MockNDKEvent { @@ -16,7 +16,14 @@ class MockNDKEvent { content: string; tags: string[][]; - constructor(id: string, kind: number, pubkey: string, created_at: number, content: string = '', tags: string[][] = []) { + constructor( + id: string, + kind: number, + pubkey: string, + created_at: number, + content: string = "", + tags: string[][] = [], + ) { this.id = id; this.kind = kind; this.pubkey = pubkey; @@ -26,151 +33,192 @@ class MockNDKEvent { } tagValue(tagName: string): string | undefined { - const tag = this.tags.find(t => t[0] === tagName); + const tag = this.tags.find((t) => t[0] === tagName); return tag ? tag[1] : undefined; } getMatchingTags(tagName: string): string[][] { - return this.tags.filter(tag => tag[0] === tagName); + return this.tags.filter((tag) => tag[0] === tagName); } } // Mock NDK instance const mockNDK = { - fetchEvents: vi.fn() + fetchEvents: vi.fn(), }; // Mock the ndkInstance store -vi.mock('../../src/lib/ndk', () => ({ +vi.mock("../../src/lib/ndk", () => ({ ndkInstance: { subscribe: vi.fn((fn) => { fn(mockNDK); return { unsubscribe: vi.fn() }; - }) - } + }), + }, })); // Mock the profile cache utilities -vi.mock('../../src/lib/utils/profileCache', () => ({ +vi.mock("../../src/lib/utils/profileCache", () => ({ extractPubkeysFromEvents: vi.fn((events: NDKEvent[]) => { const pubkeys = new Set(); - events.forEach(event => { + events.forEach((event) => { if (event.pubkey) pubkeys.add(event.pubkey); }); return pubkeys; }), - batchFetchProfiles: vi.fn(async (pubkeys: string[], onProgress: (fetched: number, total: number) => void) => { - // Simulate progress updates - onProgress(0, pubkeys.length); - onProgress(pubkeys.length, pubkeys.length); - return []; - }) + batchFetchProfiles: vi.fn( + async ( + pubkeys: string[], + onProgress: (fetched: number, total: number) => void, + ) => { + // Simulate progress updates + onProgress(0, pubkeys.length); + onProgress(pubkeys.length, pubkeys.length); + return []; + }, + ), })); -describe('Tag Expansion Tests', () => { +describe("Tag Expansion Tests", () => { let mockPublications: MockNDKEvent[]; let mockContentEvents: MockNDKEvent[]; let mockAllEvents: MockNDKEvent[]; beforeEach(() => { vi.clearAllMocks(); - + // Create test publication index events (kind 30040) mockPublications = [ - new MockNDKEvent('pub1', 30040, 'author1', 1000, 'Book 1', [ - ['t', 'bitcoin'], - ['t', 'cryptocurrency'], - ['a', '30041:author1:chapter-1'], - ['a', '30041:author1:chapter-2'] + new MockNDKEvent("pub1", 30040, "author1", 1000, "Book 1", [ + ["t", "bitcoin"], + ["t", "cryptocurrency"], + ["a", "30041:author1:chapter-1"], + ["a", "30041:author1:chapter-2"], + ]), + new MockNDKEvent("pub2", 30040, "author2", 1100, "Book 2", [ + ["t", "bitcoin"], + ["t", "blockchain"], + ["a", "30041:author2:chapter-1"], ]), - new MockNDKEvent('pub2', 30040, 'author2', 1100, 'Book 2', [ - ['t', 'bitcoin'], - ['t', 'blockchain'], - ['a', '30041:author2:chapter-1'] + new MockNDKEvent("pub3", 30040, "author3", 1200, "Book 3", [ + ["t", "ethereum"], + ["a", "30041:author3:chapter-1"], ]), - new MockNDKEvent('pub3', 30040, 'author3', 1200, 'Book 3', [ - ['t', 'ethereum'], - ['a', '30041:author3:chapter-1'] - ]) ]; // Create test content events (kind 30041) mockContentEvents = [ - new MockNDKEvent('content1', 30041, 'author1', 1000, 'Chapter 1 content', [['d', 'chapter-1']]), - new MockNDKEvent('content2', 30041, 'author1', 1100, 'Chapter 2 content', [['d', 'chapter-2']]), - new MockNDKEvent('content3', 30041, 'author2', 1200, 'Author 2 Chapter 1', [['d', 'chapter-1']]), - new MockNDKEvent('content4', 30041, 'author3', 1300, 'Author 3 Chapter 1', [['d', 'chapter-1']]) + new MockNDKEvent( + "content1", + 30041, + "author1", + 1000, + "Chapter 1 content", + [["d", "chapter-1"]], + ), + new MockNDKEvent( + "content2", + 30041, + "author1", + 1100, + "Chapter 2 content", + [["d", "chapter-2"]], + ), + new MockNDKEvent( + "content3", + 30041, + "author2", + 1200, + "Author 2 Chapter 1", + [["d", "chapter-1"]], + ), + new MockNDKEvent( + "content4", + 30041, + "author3", + 1300, + "Author 3 Chapter 1", + [["d", "chapter-1"]], + ), ]; // Combine all events for testing mockAllEvents = [...mockPublications, ...mockContentEvents]; }); - describe('fetchTaggedEventsFromRelays', () => { - it('should fetch publications with matching tags from relays', async () => { + describe("fetchTaggedEventsFromRelays", () => { + it("should fetch publications with matching tags from relays", async () => { // Mock the NDK fetch to return publications with 'bitcoin' tag - const bitcoinPublications = mockPublications.filter(pub => - pub.tags.some(tag => tag[0] === 't' && tag[1] === 'bitcoin') + const bitcoinPublications = mockPublications.filter((pub) => + pub.tags.some((tag) => tag[0] === "t" && tag[1] === "bitcoin") + ); + mockNDK.fetchEvents.mockResolvedValueOnce( + new Set(bitcoinPublications as NDKEvent[]), + ); + mockNDK.fetchEvents.mockResolvedValueOnce( + new Set(mockContentEvents as NDKEvent[]), ); - mockNDK.fetchEvents.mockResolvedValueOnce(new Set(bitcoinPublications as NDKEvent[])); - mockNDK.fetchEvents.mockResolvedValueOnce(new Set(mockContentEvents as NDKEvent[])); - const existingEventIds = new Set(['existing-event']); + const existingEventIds = new Set(["existing-event"]); const baseEvents: NDKEvent[] = []; const debug = vi.fn(); const result = await fetchTaggedEventsFromRelays( - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); // Should fetch publications with bitcoin tag expect(mockNDK.fetchEvents).toHaveBeenCalledWith({ kinds: [30040], - "#t": ['bitcoin'], - limit: 30 + "#t": ["bitcoin"], + limit: 30, }); // Should return the matching publications expect(result.publications).toHaveLength(2); - expect(result.publications.map(p => p.id)).toContain('pub1'); - expect(result.publications.map(p => p.id)).toContain('pub2'); + expect(result.publications.map((p) => p.id)).toContain("pub1"); + expect(result.publications.map((p) => p.id)).toContain("pub2"); // Should fetch content events for the publications expect(mockNDK.fetchEvents).toHaveBeenCalledWith({ kinds: [30041, 30818], - "#d": ['chapter-1', 'chapter-2'] + "#d": ["chapter-1", "chapter-2"], }); }); - it('should filter out existing events to avoid duplicates', async () => { - mockNDK.fetchEvents.mockResolvedValueOnce(new Set(mockPublications as NDKEvent[])); - mockNDK.fetchEvents.mockResolvedValueOnce(new Set(mockContentEvents as NDKEvent[])); + it("should filter out existing events to avoid duplicates", async () => { + mockNDK.fetchEvents.mockResolvedValueOnce( + new Set(mockPublications as NDKEvent[]), + ); + mockNDK.fetchEvents.mockResolvedValueOnce( + new Set(mockContentEvents as NDKEvent[]), + ); - const existingEventIds = new Set(['pub1']); // pub1 already exists + const existingEventIds = new Set(["pub1"]); // pub1 already exists const baseEvents: NDKEvent[] = []; const debug = vi.fn(); const result = await fetchTaggedEventsFromRelays( - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); // Should exclude pub1 since it already exists expect(result.publications).toHaveLength(2); - expect(result.publications.map(p => p.id)).not.toContain('pub1'); - expect(result.publications.map(p => p.id)).toContain('pub2'); - expect(result.publications.map(p => p.id)).toContain('pub3'); + expect(result.publications.map((p) => p.id)).not.toContain("pub1"); + expect(result.publications.map((p) => p.id)).toContain("pub2"); + expect(result.publications.map((p) => p.id)).toContain("pub3"); }); - it('should handle empty tag array gracefully', async () => { + it("should handle empty tag array gracefully", async () => { // Mock empty result for empty tags mockNDK.fetchEvents.mockResolvedValueOnce(new Set()); - + const existingEventIds = new Set(); const baseEvents: NDKEvent[] = []; const debug = vi.fn(); @@ -179,7 +227,7 @@ describe('Tag Expansion Tests', () => { [], existingEventIds, baseEvents, - debug + debug, ); expect(result.publications).toHaveLength(0); @@ -187,95 +235,101 @@ describe('Tag Expansion Tests', () => { }); }); - describe('findTaggedEventsInFetched', () => { - it('should find publications with matching tags in already fetched events', () => { - const existingEventIds = new Set(['existing-event']); + describe("findTaggedEventsInFetched", () => { + it("should find publications with matching tags in already fetched events", () => { + const existingEventIds = new Set(["existing-event"]); const baseEvents: NDKEvent[] = []; const debug = vi.fn(); const result = findTaggedEventsInFetched( mockAllEvents as NDKEvent[], - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); // Should find publications with bitcoin tag expect(result.publications).toHaveLength(2); - expect(result.publications.map(p => p.id)).toContain('pub1'); - expect(result.publications.map(p => p.id)).toContain('pub2'); + expect(result.publications.map((p) => p.id)).toContain("pub1"); + expect(result.publications.map((p) => p.id)).toContain("pub2"); // Should find content events for those publications expect(result.contentEvents).toHaveLength(4); - expect(result.contentEvents.map(c => c.id)).toContain('content1'); - expect(result.contentEvents.map(c => c.id)).toContain('content2'); - expect(result.contentEvents.map(c => c.id)).toContain('content3'); - expect(result.contentEvents.map(c => c.id)).toContain('content4'); + expect(result.contentEvents.map((c) => c.id)).toContain("content1"); + expect(result.contentEvents.map((c) => c.id)).toContain("content2"); + expect(result.contentEvents.map((c) => c.id)).toContain("content3"); + expect(result.contentEvents.map((c) => c.id)).toContain("content4"); }); - it('should exclude base events from search results', () => { - const existingEventIds = new Set(['pub1']); // pub1 is a base event + it("should exclude base events from search results", () => { + const existingEventIds = new Set(["pub1"]); // pub1 is a base event const baseEvents: NDKEvent[] = []; const debug = vi.fn(); const result = findTaggedEventsInFetched( mockAllEvents as NDKEvent[], - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); // Should exclude pub1 since it's a base event expect(result.publications).toHaveLength(1); - expect(result.publications.map(p => p.id)).not.toContain('pub1'); - expect(result.publications.map(p => p.id)).toContain('pub2'); + expect(result.publications.map((p) => p.id)).not.toContain("pub1"); + expect(result.publications.map((p) => p.id)).toContain("pub2"); }); - it('should handle multiple tags (OR logic)', () => { + it("should handle multiple tags (OR logic)", () => { const existingEventIds = new Set(); const baseEvents: NDKEvent[] = []; const debug = vi.fn(); const result = findTaggedEventsInFetched( mockAllEvents as NDKEvent[], - ['bitcoin', 'ethereum'], + ["bitcoin", "ethereum"], existingEventIds, baseEvents, - debug + debug, ); // Should find publications with either bitcoin OR ethereum tags expect(result.publications).toHaveLength(3); - expect(result.publications.map(p => p.id)).toContain('pub1'); // bitcoin - expect(result.publications.map(p => p.id)).toContain('pub2'); // bitcoin - expect(result.publications.map(p => p.id)).toContain('pub3'); // ethereum + expect(result.publications.map((p) => p.id)).toContain("pub1"); // bitcoin + expect(result.publications.map((p) => p.id)).toContain("pub2"); // bitcoin + expect(result.publications.map((p) => p.id)).toContain("pub3"); // ethereum }); - it('should handle events without tags gracefully', () => { - const eventWithoutTags = new MockNDKEvent('no-tags', 30040, 'author4', 1000, 'No tags'); + it("should handle events without tags gracefully", () => { + const eventWithoutTags = new MockNDKEvent( + "no-tags", + 30040, + "author4", + 1000, + "No tags", + ); const allEventsWithNoTags = [...mockAllEvents, eventWithoutTags]; - + const existingEventIds = new Set(); const baseEvents: NDKEvent[] = []; const debug = vi.fn(); const result = findTaggedEventsInFetched( allEventsWithNoTags as NDKEvent[], - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); // Should not include events without tags - expect(result.publications.map(p => p.id)).not.toContain('no-tags'); + expect(result.publications.map((p) => p.id)).not.toContain("no-tags"); }); }); - describe('fetchProfilesForNewEvents', () => { - it('should extract pubkeys and fetch profiles for new events', async () => { + describe("fetchProfilesForNewEvents", () => { + it("should extract pubkeys and fetch profiles for new events", async () => { const onProgressUpdate = vi.fn(); const debug = vi.fn(); @@ -283,7 +337,7 @@ describe('Tag Expansion Tests', () => { mockPublications as NDKEvent[], mockContentEvents as NDKEvent[], onProgressUpdate, - debug + debug, ); // Should call progress update with initial state @@ -296,7 +350,7 @@ describe('Tag Expansion Tests', () => { expect(onProgressUpdate).toHaveBeenCalledWith(null); }); - it('should handle empty event arrays gracefully', async () => { + it("should handle empty event arrays gracefully", async () => { const onProgressUpdate = vi.fn(); const debug = vi.fn(); @@ -304,7 +358,7 @@ describe('Tag Expansion Tests', () => { [], [], onProgressUpdate, - debug + debug, ); // Should not call progress update for empty arrays @@ -312,27 +366,31 @@ describe('Tag Expansion Tests', () => { }); }); - describe('Tag Expansion Integration', () => { - it('should demonstrate the complete tag expansion flow', async () => { + describe("Tag Expansion Integration", () => { + it("should demonstrate the complete tag expansion flow", async () => { // This test simulates the complete flow from the visualize page - + // Step 1: Mock relay fetch for 'bitcoin' tag - const bitcoinPublications = mockPublications.filter(pub => - pub.tags.some(tag => tag[0] === 't' && tag[1] === 'bitcoin') + const bitcoinPublications = mockPublications.filter((pub) => + pub.tags.some((tag) => tag[0] === "t" && tag[1] === "bitcoin") + ); + mockNDK.fetchEvents.mockResolvedValueOnce( + new Set(bitcoinPublications as NDKEvent[]), + ); + mockNDK.fetchEvents.mockResolvedValueOnce( + new Set(mockContentEvents as NDKEvent[]), ); - mockNDK.fetchEvents.mockResolvedValueOnce(new Set(bitcoinPublications as NDKEvent[])); - mockNDK.fetchEvents.mockResolvedValueOnce(new Set(mockContentEvents as NDKEvent[])); - const existingEventIds = new Set(['base-event']); + const existingEventIds = new Set(["base-event"]); const baseEvents: NDKEvent[] = []; const debug = vi.fn(); // Step 2: Fetch from relays const relayResult = await fetchTaggedEventsFromRelays( - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); expect(relayResult.publications).toHaveLength(2); @@ -341,10 +399,10 @@ describe('Tag Expansion Tests', () => { // Step 3: Search in fetched events const searchResult = findTaggedEventsInFetched( mockAllEvents as NDKEvent[], - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); expect(searchResult.publications).toHaveLength(2); @@ -356,20 +414,27 @@ describe('Tag Expansion Tests', () => { relayResult.publications, relayResult.contentEvents, onProgressUpdate, - debug + debug, ); expect(onProgressUpdate).toHaveBeenCalledWith(null); }); }); - describe('Edge Cases and Error Handling', () => { - it('should handle malformed a-tags gracefully', () => { - const malformedPublication = new MockNDKEvent('malformed', 30040, 'author1', 1000, 'Malformed', [ - ['t', 'bitcoin'], - ['a', 'invalid-tag-format'], // Missing parts - ['a', '30041:author1:chapter-1'] // Valid format - ]); + describe("Edge Cases and Error Handling", () => { + it("should handle malformed a-tags gracefully", () => { + const malformedPublication = new MockNDKEvent( + "malformed", + 30040, + "author1", + 1000, + "Malformed", + [ + ["t", "bitcoin"], + ["a", "invalid-tag-format"], // Missing parts + ["a", "30041:author1:chapter-1"], // Valid format + ], + ); const allEventsWithMalformed = [...mockAllEvents, malformedPublication]; const existingEventIds = new Set(); @@ -378,10 +443,10 @@ describe('Tag Expansion Tests', () => { const result = findTaggedEventsInFetched( allEventsWithMalformed as NDKEvent[], - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); // Should still work and include the publication with valid a-tags @@ -389,32 +454,50 @@ describe('Tag Expansion Tests', () => { expect(result.contentEvents.length).toBeGreaterThan(0); }); - it('should handle events with d-tags containing colons', () => { - const publicationWithColonDTag = new MockNDKEvent('colon-pub', 30040, 'author1', 1000, 'Colon d-tag', [ - ['t', 'bitcoin'], - ['a', '30041:author1:chapter:with:colons'] - ]); + it("should handle events with d-tags containing colons", () => { + const publicationWithColonDTag = new MockNDKEvent( + "colon-pub", + 30040, + "author1", + 1000, + "Colon d-tag", + [ + ["t", "bitcoin"], + ["a", "30041:author1:chapter:with:colons"], + ], + ); - const contentWithColonDTag = new MockNDKEvent('colon-content', 30041, 'author1', 1100, 'Content with colon d-tag', [ - ['d', 'chapter:with:colons'] - ]); + const contentWithColonDTag = new MockNDKEvent( + "colon-content", + 30041, + "author1", + 1100, + "Content with colon d-tag", + [ + ["d", "chapter:with:colons"], + ], + ); - const allEventsWithColons = [...mockAllEvents, publicationWithColonDTag, contentWithColonDTag]; + const allEventsWithColons = [ + ...mockAllEvents, + publicationWithColonDTag, + contentWithColonDTag, + ]; const existingEventIds = new Set(); const baseEvents: NDKEvent[] = []; const debug = vi.fn(); const result = findTaggedEventsInFetched( allEventsWithColons as NDKEvent[], - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); // Should handle d-tags with colons correctly expect(result.publications).toHaveLength(3); - expect(result.contentEvents.map(c => c.id)).toContain('colon-content'); + expect(result.contentEvents.map((c) => c.id)).toContain("colon-content"); }); }); -}); \ No newline at end of file +}); diff --git a/vite.config.ts b/vite.config.ts index 47552d4..a81279c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -43,18 +43,20 @@ export default defineConfig({ // Expose the app version as a global variable "import.meta.env.APP_VERSION": JSON.stringify(getAppVersionString()), // Enable debug logging for relays when needed - "process.env.DEBUG_RELAYS": JSON.stringify(process.env.DEBUG_RELAYS || "false"), + "process.env.DEBUG_RELAYS": JSON.stringify( + process.env.DEBUG_RELAYS || "false", + ), }, optimizeDeps: { esbuildOptions: { define: { - global: 'globalThis', + global: "globalThis", }, }, }, server: { fs: { - allow: ['..'], + allow: [".."], }, hmr: { overlay: false, // Disable HMR overlay to prevent ESM URL scheme errors From 98dba98b93ba6fae69ce5e2542a0b9106ca79f62 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 16 Aug 2025 00:24:15 -0500 Subject: [PATCH 67/98] Tidy and organize Nostr event embedding markup - Svelte components and snippets that support embedded event rendering are moved to `src/lib/components/embedded_events` dir. - Some now-unused components are removed entirely. - Imports are updated. --- src/lib/components/CommentViewer.svelte | 2 +- .../ContentWithEmbeddedEvents.svelte | 100 ------------------ .../components/EmbeddedEventRenderer.svelte | 83 --------------- src/lib/components/EventDetails.svelte | 2 +- src/lib/components/Notifications.svelte | 4 +- .../EmbeddedEvent.svelte | 2 +- .../EmbeddedSnippets.svelte} | 0 src/routes/events/+page.svelte | 2 +- 8 files changed, 6 insertions(+), 189 deletions(-) delete mode 100644 src/lib/components/ContentWithEmbeddedEvents.svelte delete mode 100644 src/lib/components/EmbeddedEventRenderer.svelte rename src/lib/components/{ => embedded_events}/EmbeddedEvent.svelte (99%) rename src/lib/components/{util/Notifications.svelte => embedded_events/EmbeddedSnippets.svelte} (100%) diff --git a/src/lib/components/CommentViewer.svelte b/src/lib/components/CommentViewer.svelte index ec5a069..1937f80 100644 --- a/src/lib/components/CommentViewer.svelte +++ b/src/lib/components/CommentViewer.svelte @@ -6,7 +6,7 @@ import { goto } from "$app/navigation"; import { onMount } from "svelte"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; - import EmbeddedEvent from "./EmbeddedEvent.svelte"; + import EmbeddedEvent from "./embedded_events/EmbeddedEvent.svelte"; const { event } = $props<{ event: NDKEvent }>(); diff --git a/src/lib/components/ContentWithEmbeddedEvents.svelte b/src/lib/components/ContentWithEmbeddedEvents.svelte deleted file mode 100644 index 75d9008..0000000 --- a/src/lib/components/ContentWithEmbeddedEvents.svelte +++ /dev/null @@ -1,100 +0,0 @@ - - -
    - {@html parsedContent} - - - {#each embeddedEvents as eventInfo} -
    - -
    - {/each} -
    - - diff --git a/src/lib/components/EmbeddedEventRenderer.svelte b/src/lib/components/EmbeddedEventRenderer.svelte deleted file mode 100644 index d1752e9..0000000 --- a/src/lib/components/EmbeddedEventRenderer.svelte +++ /dev/null @@ -1,83 +0,0 @@ - - -
    - {@html renderContent()} - - - {#each embeddedEvents as eventInfo} -
    - -
    - {/each} -
    - - diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index 08dc627..687b9f8 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -14,7 +14,7 @@ import { navigateToEvent } from "$lib/utils/nostrEventService"; import ContainingIndexes from "$lib/components/util/ContainingIndexes.svelte"; import Notifications from "$lib/components/Notifications.svelte"; - import EmbeddedEvent from "./EmbeddedEvent.svelte"; + import EmbeddedEvent from "./embedded_events/EmbeddedEvent.svelte"; const { event, diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index f66baba..74bfaf5 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -18,11 +18,11 @@ getNotificationType, fetchAuthorProfiles, quotedContent, - } from "$lib/components/util/Notifications.svelte"; + } from "$lib/components/embedded_events/EmbeddedSnippets.svelte"; import { buildCompleteRelaySet } from "$lib/utils/relay_management"; import { formatDate, neventEncode } from "$lib/utils"; import { NDKRelaySetFromNDK } from "$lib/utils/nostrUtils"; - import EmbeddedEvent from "./EmbeddedEvent.svelte"; + import EmbeddedEvent from "./embedded_events/EmbeddedEvent.svelte"; const { event } = $props<{ event: NDKEvent }>(); diff --git a/src/lib/components/EmbeddedEvent.svelte b/src/lib/components/embedded_events/EmbeddedEvent.svelte similarity index 99% rename from src/lib/components/EmbeddedEvent.svelte rename to src/lib/components/embedded_events/EmbeddedEvent.svelte index 54d4633..33e324b 100644 --- a/src/lib/components/EmbeddedEvent.svelte +++ b/src/lib/components/embedded_events/EmbeddedEvent.svelte @@ -4,7 +4,7 @@ import { fetchEventWithFallback } from "$lib/utils/nostrUtils"; import { getUserMetadata, toNpub } from "$lib/utils/nostrUtils"; import { userBadge } from "$lib/snippets/UserSnippets.svelte"; - import { parsedContent } from "$lib/components/util/Notifications.svelte"; + import { parsedContent } from "$lib/components/embedded_events/EmbeddedSnippets.svelte"; import { naddrEncode } from "$lib/utils"; import { activeInboxRelays, ndkInstance } from "$lib/ndk"; import { goto } from "$app/navigation"; diff --git a/src/lib/components/util/Notifications.svelte b/src/lib/components/embedded_events/EmbeddedSnippets.svelte similarity index 100% rename from src/lib/components/util/Notifications.svelte rename to src/lib/components/embedded_events/EmbeddedSnippets.svelte diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index da0b823..0757726 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -18,7 +18,7 @@ import { getEventType } from "$lib/utils/mime"; import ViewPublicationLink from "$lib/components/util/ViewPublicationLink.svelte"; import { checkCommunity } from "$lib/utils/search_utility"; - import EmbeddedEvent from "$lib/components/EmbeddedEvent.svelte"; + import EmbeddedEvent from "$lib/components/embedded_events/EmbeddedEvent.svelte"; let loading = $state(false); let error = $state(null); From 5e01e126bc0a076e3a570e991f406bf20083e62d Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 16 Aug 2025 00:25:14 -0500 Subject: [PATCH 68/98] Remove an unused import --- src/lib/components/embedded_events/EmbeddedEvent.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/components/embedded_events/EmbeddedEvent.svelte b/src/lib/components/embedded_events/EmbeddedEvent.svelte index 33e324b..1c78441 100644 --- a/src/lib/components/embedded_events/EmbeddedEvent.svelte +++ b/src/lib/components/embedded_events/EmbeddedEvent.svelte @@ -1,5 +1,4 @@
    diff --git a/src/lib/components/RelayDisplay.svelte b/src/lib/components/RelayDisplay.svelte index 02ff24b..941e697 100644 --- a/src/lib/components/RelayDisplay.svelte +++ b/src/lib/components/RelayDisplay.svelte @@ -1,7 +1,7 @@ diff --git a/src/routes/publication/+page.server.ts b/src/routes/publication/+page.server.ts index f001a1c..0be4172 100644 --- a/src/routes/publication/+page.server.ts +++ b/src/routes/publication/+page.server.ts @@ -17,7 +17,7 @@ const IDENTIFIER_PREFIXES = { NEVENT: "nevent", } as const; -export const load: PageServerLoad = ({ url }) => { +export const load: PageServerLoad = ({ url }: { url: URL }) => { const id = url.searchParams.get("id"); const dTag = url.searchParams.get("d"); diff --git a/src/routes/publication/[type]/[identifier]/+layout.server.ts b/src/routes/publication/[type]/[identifier]/+layout.server.ts index 72e4d67..4670248 100644 --- a/src/routes/publication/[type]/[identifier]/+layout.server.ts +++ b/src/routes/publication/[type]/[identifier]/+layout.server.ts @@ -1,38 +1,11 @@ -import { error } from "@sveltejs/kit"; import type { LayoutServerLoad } from "./$types"; -import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; -// AI-NOTE: Server-side event fetching for SEO metadata -async function fetchEventServerSide( - type: string, - identifier: string, -): Promise { - // For now, return null to indicate server-side fetch not implemented - // This will fall back to client-side fetching - return null; -} -export const load: LayoutServerLoad = async ({ params, url }) => { - const { type, identifier } = params; - - // Try to fetch event server-side for metadata - const indexEvent = await fetchEventServerSide(type, identifier); - - // Extract metadata for meta tags (use fallbacks if no event found) - const title = indexEvent?.tags.find((tag) => tag[0] === "title")?.[1] || - "Alexandria Publication"; - const summary = indexEvent?.tags.find((tag) => tag[0] === "summary")?.[1] || - "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages."; - const image = indexEvent?.tags.find((tag) => tag[0] === "image")?.[1] || - "/screenshots/old_books.jpg"; +export const load: LayoutServerLoad = ({ url }: { url: URL }) => { const currentUrl = `${url.origin}${url.pathname}`; return { - indexEvent, // Will be null, triggering client-side fetch metadata: { - title, - summary, - image, currentUrl, }, }; diff --git a/src/routes/publication/[type]/[identifier]/+page.ts b/src/routes/publication/[type]/[identifier]/+page.ts index bc43ef0..69d8a59 100644 --- a/src/routes/publication/[type]/[identifier]/+page.ts +++ b/src/routes/publication/[type]/[identifier]/+page.ts @@ -9,16 +9,12 @@ import { import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; export const load: PageLoad = async ( - { params, parent }: { + { params }: { params: { type: string; identifier: string }; - parent: any; }, ) => { const { type, identifier } = params; - // Get layout data (no server-side data since SSR is disabled) - const layoutData = await parent(); - // AI-NOTE: Always fetch client-side since server-side fetch returns null for now let indexEvent: NostrEvent | null = null; @@ -74,20 +70,9 @@ export const load: PageLoad = async ( const publicationType = indexEvent.tags.find((tag) => tag[0] === "type")?.[1] ?? ""; - // AI-NOTE: Use proper NDK instance from layout or create one with relays - let ndk = layoutData?.ndk; - if (!ndk) { - // Import NDK dynamically to avoid SSR issues - const NDK = (await import("@nostr-dev-kit/ndk")).default; - // Import initNdk to get properly configured NDK with relays - const { initNdk } = await import("$lib/ndk"); - ndk = initNdk(); - } - const result = { publicationType, indexEvent, - ndk, // Use minimal NDK instance }; return result; diff --git a/src/routes/visualize/+page.svelte b/src/routes/visualize/+page.svelte index 91925ec..7d8124a 100644 --- a/src/routes/visualize/+page.svelte +++ b/src/routes/visualize/+page.svelte @@ -8,7 +8,6 @@ import { onMount } from "svelte"; import { get } from "svelte/store"; import EventNetwork from "$lib/navigator/EventNetwork/index.svelte"; - import { ndkInstance } from "$lib/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { filterValidIndexEvents } from "$lib/utils"; import { networkFetchLimit } from "$lib/state"; @@ -17,7 +16,7 @@ import type { PageData } from './$types'; import { getEventKindColor, getEventKindName } from "$lib/utils/eventColors"; import { extractPubkeysFromEvents, batchFetchProfiles } from "$lib/utils/profileCache"; - import { activePubkey } from "$lib/ndk"; + import { activePubkey, getNdkContext } from "$lib/ndk"; // Import utility functions for tag-based event fetching // These functions handle the complex logic of finding publications by tags // and extracting their associated content events @@ -28,6 +27,8 @@ } from "$lib/utils/tag_event_fetch"; import { deduplicateAndCombineEvents } from "$lib/utils/eventDeduplication"; import type { EventCounts } from "$lib/types"; + + const ndk = getNdkContext(); // Configuration const DEBUG = true; // Set to true to enable debug logging @@ -130,7 +131,7 @@ // If limit is 1, only fetch the current user's follow list if (config.limit === 1) { - const userFollowList = await $ndkInstance.fetchEvents({ + const userFollowList = await ndk.fetchEvents({ kinds: [3], authors: [currentUserPubkey], limit: 1 @@ -148,7 +149,7 @@ debug(`Fetched user's follow list`); } else { // If limit > 1, fetch the user's follow list plus additional ones from people they follow - const userFollowList = await $ndkInstance.fetchEvents({ + const userFollowList = await ndk.fetchEvents({ kinds: [3], authors: [currentUserPubkey], limit: 1 @@ -180,7 +181,7 @@ debug(`Fetching ${pubkeysToFetch.length} additional follow lists (total limit: ${config.limit})`); - const additionalFollowLists = await $ndkInstance.fetchEvents({ + const additionalFollowLists = await ndk.fetchEvents({ kinds: [3], authors: pubkeysToFetch }); @@ -215,7 +216,7 @@ debug(`Fetching level ${level} follow lists for ${currentLevelPubkeys.length} pubkeys`); // Fetch follow lists for this level - const levelFollowLists = await $ndkInstance.fetchEvents({ + const levelFollowLists = await ndk.fetchEvents({ kinds: [3], authors: currentLevelPubkeys }); @@ -362,7 +363,7 @@ const followEvents = await fetchFollowLists(config); allFetchedEvents.push(...followEvents); } else { - const fetchedEvents = await $ndkInstance.fetchEvents( + const fetchedEvents = await ndk.fetchEvents( { kinds: [config.kind], limit: config.limit @@ -394,7 +395,7 @@ if (data.eventId) { // Fetch specific publication debug(`Fetching specific publication: ${data.eventId}`); - const event = await $ndkInstance.fetchEvent(data.eventId); + const event = await ndk.fetchEvent(data.eventId); if (!event) { throw new Error(`Publication not found: ${data.eventId}`); @@ -414,7 +415,7 @@ const indexConfig = publicationConfigs.find(ec => ec.kind === INDEX_EVENT_KIND); const indexLimit = indexConfig?.limit || 20; - const indexEvents = await $ndkInstance.fetchEvents( + const indexEvents = await ndk.fetchEvents( { kinds: [INDEX_EVENT_KIND], limit: indexLimit @@ -455,7 +456,7 @@ const contentEventPromises = Array.from(referencesByAuthor.entries()).map( async ([author, refs]) => { const dTags = [...new Set(refs.map(r => r.dTag))]; // Dedupe d-tags - return $ndkInstance.fetchEvents({ + return ndk.fetchEvents({ kinds: enabledContentKinds, // Only fetch enabled kinds authors: [author], "#d": dTags, diff --git a/src/styles/events.css b/src/styles/events.css deleted file mode 100644 index 3c61536..0000000 --- a/src/styles/events.css +++ /dev/null @@ -1,5 +0,0 @@ -@layer components { - canvas.qr-code { - @apply block mx-auto my-4; - } -} diff --git a/tests/unit/eventInput30040.test.ts b/tests/unit/eventInput30040.test.ts index 9fa185c..a7064c3 100644 --- a/tests/unit/eventInput30040.test.ts +++ b/tests/unit/eventInput30040.test.ts @@ -3,10 +3,6 @@ import { build30040EventSet, validate30040EventSet, } from "../../src/lib/utils/event_input_utils"; -import { - extractDocumentMetadata, - parseAsciiDocWithMetadata, -} from "../../src/lib/utils/asciidoc_metadata"; // Mock NDK and other dependencies vi.mock("@nostr-dev-kit/ndk", () => ({ @@ -22,6 +18,7 @@ vi.mock("@nostr-dev-kit/ndk", () => ({ })), })); +// TODO: Replace with getNdkContext mock. vi.mock("../../src/lib/ndk", () => ({ ndkInstance: { subscribe: vi.fn(), @@ -265,7 +262,7 @@ This is the preamble content. expect(sectionEvents).toHaveLength(3); // All sections should have empty content - sectionEvents.forEach((section, index) => { + sectionEvents.forEach((section: any, index: number) => { expect(section.kind).toBe(30041); expect(section.content).toBe(""); expect(section.tags).toContainEqual([ @@ -320,7 +317,7 @@ This is the preamble content. expect(sectionEvents).toHaveLength(3); // All sections should have empty content - sectionEvents.forEach((section, index) => { + sectionEvents.forEach((section: any, index: number) => { expect(section.kind).toBe(30041); expect(section.content).toBe(""); expect(section.tags).toContainEqual([ diff --git a/tests/unit/tagExpansion.test.ts b/tests/unit/tagExpansion.test.ts index 5de5f94..307ebd9 100644 --- a/tests/unit/tagExpansion.test.ts +++ b/tests/unit/tagExpansion.test.ts @@ -4,7 +4,6 @@ import { fetchProfilesForNewEvents, fetchTaggedEventsFromRelays, findTaggedEventsInFetched, - type TagExpansionResult, } from "../../src/lib/utils/tag_event_fetch"; // Mock NDKEvent for testing @@ -48,6 +47,7 @@ const mockNDK = { }; // Mock the ndkInstance store +// TODO: Replace with getNdkContext mock. vi.mock("../../src/lib/ndk", () => ({ ndkInstance: { subscribe: vi.fn((fn) => { @@ -179,8 +179,8 @@ describe("Tag Expansion Tests", () => { // Should return the matching publications expect(result.publications).toHaveLength(2); - expect(result.publications.map((p) => p.id)).toContain("pub1"); - expect(result.publications.map((p) => p.id)).toContain("pub2"); + expect(result.publications.map((p: any) => p.id)).toContain("pub1"); + expect(result.publications.map((p: any) => p.id)).toContain("pub2"); // Should fetch content events for the publications expect(mockNDK.fetchEvents).toHaveBeenCalledWith({ @@ -210,9 +210,9 @@ describe("Tag Expansion Tests", () => { // Should exclude pub1 since it already exists expect(result.publications).toHaveLength(2); - expect(result.publications.map((p) => p.id)).not.toContain("pub1"); - expect(result.publications.map((p) => p.id)).toContain("pub2"); - expect(result.publications.map((p) => p.id)).toContain("pub3"); + expect(result.publications.map((p: any) => p.id)).not.toContain("pub1"); + expect(result.publications.map((p: any) => p.id)).toContain("pub2"); + expect(result.publications.map((p: any) => p.id)).toContain("pub3"); }); it("should handle empty tag array gracefully", async () => { @@ -251,15 +251,15 @@ describe("Tag Expansion Tests", () => { // Should find publications with bitcoin tag expect(result.publications).toHaveLength(2); - expect(result.publications.map((p) => p.id)).toContain("pub1"); - expect(result.publications.map((p) => p.id)).toContain("pub2"); + expect(result.publications.map((p: any) => p.id)).toContain("pub1"); + expect(result.publications.map((p: any) => p.id)).toContain("pub2"); // Should find content events for those publications expect(result.contentEvents).toHaveLength(4); - expect(result.contentEvents.map((c) => c.id)).toContain("content1"); - expect(result.contentEvents.map((c) => c.id)).toContain("content2"); - expect(result.contentEvents.map((c) => c.id)).toContain("content3"); - expect(result.contentEvents.map((c) => c.id)).toContain("content4"); + expect(result.contentEvents.map((c: any) => c.id)).toContain("content1"); + expect(result.contentEvents.map((c: any) => c.id)).toContain("content2"); + expect(result.contentEvents.map((c: any) => c.id)).toContain("content3"); + expect(result.contentEvents.map((c: any) => c.id)).toContain("content4"); }); it("should exclude base events from search results", () => { @@ -277,8 +277,8 @@ describe("Tag Expansion Tests", () => { // Should exclude pub1 since it's a base event expect(result.publications).toHaveLength(1); - expect(result.publications.map((p) => p.id)).not.toContain("pub1"); - expect(result.publications.map((p) => p.id)).toContain("pub2"); + expect(result.publications.map((p: any) => p.id)).not.toContain("pub1"); + expect(result.publications.map((p: any) => p.id)).toContain("pub2"); }); it("should handle multiple tags (OR logic)", () => { @@ -296,9 +296,9 @@ describe("Tag Expansion Tests", () => { // Should find publications with either bitcoin OR ethereum tags expect(result.publications).toHaveLength(3); - expect(result.publications.map((p) => p.id)).toContain("pub1"); // bitcoin - expect(result.publications.map((p) => p.id)).toContain("pub2"); // bitcoin - expect(result.publications.map((p) => p.id)).toContain("pub3"); // ethereum + expect(result.publications.map((p: any) => p.id)).toContain("pub1"); // bitcoin + expect(result.publications.map((p: any) => p.id)).toContain("pub2"); // bitcoin + expect(result.publications.map((p: any) => p.id)).toContain("pub3"); // ethereum }); it("should handle events without tags gracefully", () => { @@ -324,7 +324,7 @@ describe("Tag Expansion Tests", () => { ); // Should not include events without tags - expect(result.publications.map((p) => p.id)).not.toContain("no-tags"); + expect(result.publications.map((p: any) => p.id)).not.toContain("no-tags"); }); }); @@ -497,7 +497,7 @@ describe("Tag Expansion Tests", () => { // Should handle d-tags with colons correctly expect(result.publications).toHaveLength(3); - expect(result.contentEvents.map((c) => c.id)).toContain("colon-content"); + expect(result.contentEvents.map((c: any) => c.id)).toContain("colon-content"); }); }); }); From 41da2df0b77d91677dd0783892adacbcbeb21e43 Mon Sep 17 00:00:00 2001 From: silberengel Date: Tue, 19 Aug 2025 09:57:41 +0200 Subject: [PATCH 71/98] fixed profile searches --- deno.lock | 444 +--------- src/lib/components/CommentBox.svelte | 27 +- src/lib/components/EventDetails.svelte | 465 +++++----- src/lib/components/EventInput.svelte | 13 +- src/lib/components/EventSearch.svelte | 153 +--- src/lib/components/Notifications.svelte | 1 - src/lib/components/cards/ProfileHeader.svelte | 68 +- src/lib/navigator/EventNetwork/Legend.svelte | 2 +- .../navigator/EventNetwork/NodeTooltip.svelte | 26 +- src/lib/navigator/EventNetwork/index.svelte | 26 +- .../utils/personNetworkBuilder.ts | 17 +- src/lib/ndk.ts | 8 +- src/lib/state.ts | 4 +- src/lib/stores/authStore.Svelte.ts | 11 - src/lib/stores/userStore.ts | 575 +++++++------ src/lib/utils/eventColors.ts | 2 + src/lib/utils/event_search.ts | 18 +- src/lib/utils/nostrUtils.ts | 14 +- src/lib/utils/npubCache.ts | 2 +- src/lib/utils/profileCache.ts | 34 +- src/lib/utils/profile_search.ts | 803 ++++++++++++++---- src/lib/utils/search_constants.ts | 9 + src/lib/utils/search_types.ts | 3 + src/lib/utils/search_utils.ts | 3 + src/lib/utils/subscription_search.ts | 568 ++++++------- src/lib/utils/user_lists.ts | 253 ++++++ src/routes/events/+page.svelte | 167 +++- src/routes/visualize/+page.svelte | 6 +- 28 files changed, 2098 insertions(+), 1624 deletions(-) delete mode 100644 src/lib/stores/authStore.Svelte.ts create mode 100644 src/lib/utils/user_lists.ts diff --git a/deno.lock b/deno.lock index ef86772..7083352 100644 --- a/deno.lock +++ b/deno.lock @@ -3,9 +3,6 @@ "specifiers": { "npm:@noble/curves@^1.9.4": "1.9.4", "npm:@noble/hashes@^1.8.0": "1.8.0", - "npm:@nostr-dev-kit/ndk-cache-dexie@2.6": "2.6.33_nostr-tools@2.15.1__typescript@5.8.3_typescript@5.8.3", - "npm:@nostr-dev-kit/ndk-cache-dexie@^2.6.33": "2.6.33_nostr-tools@2.15.1__typescript@5.8.3_typescript@5.8.3", - "npm:@nostr-dev-kit/ndk@^2.14.32": "2.14.32_nostr-tools@2.15.1__typescript@5.8.3_typescript@5.8.3", "npm:@playwright/test@^1.54.1": "1.54.1", "npm:@popperjs/core@2.11": "2.11.8", "npm:@tailwindcss/forms@0.5": "0.5.10_tailwindcss@3.4.17__postcss@8.5.6", @@ -18,20 +15,14 @@ "npm:asciidoctor@3.0": "3.0.4_@asciidoctor+core@3.0.4", "npm:autoprefixer@^10.4.21": "10.4.21_postcss@8.5.6", "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@^3.11.0": "3.11.0_eslint@9.31.0_svelte@5.36.8__acorn@8.15.0_postcss@8.5.6", "npm:flowbite-svelte-icons@2.1": "2.1.1_svelte@5.36.8__acorn@8.15.0_tailwind-merge@3.3.1", - "npm:flowbite-svelte-icons@^2.2.1": "2.2.1_svelte@5.36.8__acorn@8.15.0", "npm:flowbite-svelte@0.48": "0.48.6_svelte@5.36.8__acorn@8.15.0", - "npm:flowbite-svelte@^1.10.10": "1.10.10_svelte@5.36.8__acorn@8.15.0_tailwindcss@3.4.17__postcss@8.5.6", "npm:flowbite@2": "2.5.2", "npm:flowbite@^3.1.2": "3.1.2", "npm:he@1.2": "1.2.0", "npm:highlight.js@^11.11.1": "11.11.1", "npm:node-emoji@^2.2.0": "2.2.0", - "npm:nostr-tools@2.15": "2.15.1_typescript@5.8.3", - "npm:nostr-tools@^2.15.1": "2.15.1_typescript@5.8.3", "npm:plantuml-encoder@^1.4.0": "1.4.0", "npm:playwright@^1.50.1": "1.54.1", "npm:playwright@^1.54.1": "1.54.1", @@ -350,39 +341,15 @@ "@jridgewell/sourcemap-codec" ] }, - "@noble/ciphers@0.5.3": { - "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==" - }, - "@noble/curves@1.1.0": { - "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", - "dependencies": [ - "@noble/hashes@1.3.1" - ] - }, - "@noble/curves@1.2.0": { - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", - "dependencies": [ - "@noble/hashes@1.3.2" - ] - }, "@noble/curves@1.9.4": { "integrity": "sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==", "dependencies": [ - "@noble/hashes@1.8.0" + "@noble/hashes" ] }, - "@noble/hashes@1.3.1": { - "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==" - }, - "@noble/hashes@1.3.2": { - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" - }, "@noble/hashes@1.8.0": { "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==" }, - "@noble/secp256k1@2.3.0": { - "integrity": "sha512-0TQed2gcBbIrh7Ccyw+y/uZQvbJwm7Ao4scBUxqpBCcsOlZG0O4KGfjtNAy/li4W8n1xt3dxrwJ0beZ2h2G6Kw==" - }, "@nodelib/fs.scandir@2.1.5": { "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dependencies": [ @@ -400,30 +367,6 @@ "fastq" ] }, - "@nostr-dev-kit/ndk-cache-dexie@2.6.33_nostr-tools@2.15.1__typescript@5.8.3_typescript@5.8.3": { - "integrity": "sha512-JzUD5cuJbGQDUXYuW1530vy347Kk3AhdtvPO8tL6kFpV3KzGt/QPZ0SHxcjMhJdf7r6cAIpCEWj9oUlStr0gsg==", - "dependencies": [ - "@nostr-dev-kit/ndk", - "debug", - "dexie", - "nostr-tools", - "typescript-lru-cache" - ] - }, - "@nostr-dev-kit/ndk@2.14.32_nostr-tools@2.15.1__typescript@5.8.3_typescript@5.8.3": { - "integrity": "sha512-LUBO35RCB9/emBYsXNDece7m/WO2rGYR8j4SD0Crb3z8GcKTJq6P8OjpZ6+Kr+sLNo8N0uL07XxtAvEBnp2OqQ==", - "dependencies": [ - "@noble/curves@1.9.4", - "@noble/hashes@1.8.0", - "@noble/secp256k1", - "@scure/base@1.2.6", - "debug", - "light-bolt11-decoder", - "nostr-tools", - "tseep", - "typescript-lru-cache" - ] - }, "@pkgjs/parseargs@0.11.0": { "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==" }, @@ -559,27 +502,6 @@ "os": ["win32"], "cpu": ["x64"] }, - "@scure/base@1.1.1": { - "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==" - }, - "@scure/base@1.2.6": { - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==" - }, - "@scure/bip32@1.3.1": { - "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", - "dependencies": [ - "@noble/curves@1.1.0", - "@noble/hashes@1.3.2", - "@scure/base@1.1.1" - ] - }, - "@scure/bip39@1.2.1": { - "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", - "dependencies": [ - "@noble/hashes@1.3.2", - "@scure/base@1.1.1" - ] - }, "@sindresorhus/is@4.6.0": { "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==" }, @@ -589,34 +511,6 @@ "acorn@8.15.0" ] }, - "@svgdotjs/svg.draggable.js@3.0.6_@svgdotjs+svg.js@3.2.4": { - "integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==", - "dependencies": [ - "@svgdotjs/svg.js" - ] - }, - "@svgdotjs/svg.filter.js@3.0.9": { - "integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==", - "dependencies": [ - "@svgdotjs/svg.js" - ] - }, - "@svgdotjs/svg.js@3.2.4": { - "integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==" - }, - "@svgdotjs/svg.resize.js@2.0.5_@svgdotjs+svg.js@3.2.4_@svgdotjs+svg.select.js@4.0.3__@svgdotjs+svg.js@3.2.4": { - "integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==", - "dependencies": [ - "@svgdotjs/svg.js", - "@svgdotjs/svg.select.js" - ] - }, - "@svgdotjs/svg.select.js@4.0.3_@svgdotjs+svg.js@3.2.4": { - "integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==", - "dependencies": [ - "@svgdotjs/svg.js" - ] - }, "@tailwindcss/forms@0.5.10_tailwindcss@3.4.17__postcss@8.5.6": { "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", "dependencies": [ @@ -896,17 +790,6 @@ "svg.select.js@3.0.1" ] }, - "apexcharts@4.7.0_@svgdotjs+svg.js@3.2.4_@svgdotjs+svg.select.js@4.0.3__@svgdotjs+svg.js@3.2.4": { - "integrity": "sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==", - "dependencies": [ - "@svgdotjs/svg.draggable.js", - "@svgdotjs/svg.filter.js", - "@svgdotjs/svg.js", - "@svgdotjs/svg.resize.js", - "@svgdotjs/svg.select.js", - "@yr/monotone-cubic-spline" - ] - }, "arg@5.0.2": { "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" }, @@ -1094,9 +977,6 @@ "commander@5.1.0": { "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" }, - "commander@7.2.0": { - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" - }, "concat-map@0.0.1": { "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, @@ -1119,212 +999,6 @@ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "bin": true }, - "d3-array@3.2.4": { - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "dependencies": [ - "internmap" - ] - }, - "d3-axis@3.0.0": { - "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==" - }, - "d3-brush@3.0.0_d3-selection@3.0.0": { - "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", - "dependencies": [ - "d3-dispatch", - "d3-drag", - "d3-interpolate", - "d3-selection", - "d3-transition" - ] - }, - "d3-chord@3.0.1": { - "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", - "dependencies": [ - "d3-path" - ] - }, - "d3-color@3.1.0": { - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" - }, - "d3-contour@4.0.2": { - "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", - "dependencies": [ - "d3-array" - ] - }, - "d3-delaunay@6.0.4": { - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", - "dependencies": [ - "delaunator" - ] - }, - "d3-dispatch@3.0.1": { - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" - }, - "d3-drag@3.0.0": { - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "dependencies": [ - "d3-dispatch", - "d3-selection" - ] - }, - "d3-dsv@3.0.1": { - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", - "dependencies": [ - "commander@7.2.0", - "iconv-lite", - "rw" - ], - "bin": true - }, - "d3-ease@3.0.1": { - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" - }, - "d3-fetch@3.0.1": { - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", - "dependencies": [ - "d3-dsv" - ] - }, - "d3-force@3.0.0": { - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "dependencies": [ - "d3-dispatch", - "d3-quadtree", - "d3-timer" - ] - }, - "d3-format@3.1.0": { - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" - }, - "d3-geo@3.1.1": { - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", - "dependencies": [ - "d3-array" - ] - }, - "d3-hierarchy@3.1.2": { - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==" - }, - "d3-interpolate@3.0.1": { - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "dependencies": [ - "d3-color" - ] - }, - "d3-path@3.1.0": { - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==" - }, - "d3-polygon@3.0.1": { - "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==" - }, - "d3-quadtree@3.0.1": { - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==" - }, - "d3-random@3.0.1": { - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==" - }, - "d3-scale-chromatic@3.1.0": { - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "dependencies": [ - "d3-color", - "d3-interpolate" - ] - }, - "d3-scale@4.0.2": { - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "dependencies": [ - "d3-array", - "d3-format", - "d3-interpolate", - "d3-time", - "d3-time-format" - ] - }, - "d3-selection@3.0.0": { - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" - }, - "d3-shape@3.2.0": { - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "dependencies": [ - "d3-path" - ] - }, - "d3-time-format@4.1.0": { - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "dependencies": [ - "d3-time" - ] - }, - "d3-time@3.1.0": { - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "dependencies": [ - "d3-array" - ] - }, - "d3-timer@3.0.1": { - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" - }, - "d3-transition@3.0.1_d3-selection@3.0.0": { - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "dependencies": [ - "d3-color", - "d3-dispatch", - "d3-ease", - "d3-interpolate", - "d3-selection", - "d3-timer" - ] - }, - "d3-zoom@3.0.0_d3-selection@3.0.0": { - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "dependencies": [ - "d3-dispatch", - "d3-drag", - "d3-interpolate", - "d3-selection", - "d3-transition" - ] - }, - "d3@7.9.0_d3-selection@3.0.0": { - "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", - "dependencies": [ - "d3-array", - "d3-axis", - "d3-brush", - "d3-chord", - "d3-color", - "d3-contour", - "d3-delaunay", - "d3-dispatch", - "d3-drag", - "d3-dsv", - "d3-ease", - "d3-fetch", - "d3-force", - "d3-format", - "d3-geo", - "d3-hierarchy", - "d3-interpolate", - "d3-path", - "d3-polygon", - "d3-quadtree", - "d3-random", - "d3-scale", - "d3-scale-chromatic", - "d3-selection", - "d3-shape", - "d3-time", - "d3-time-format", - "d3-timer", - "d3-transition", - "d3-zoom" - ] - }, - "date-fns@4.1.0": { - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==" - }, "debug@4.4.1": { "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dependencies": [ @@ -1340,15 +1014,6 @@ "deepmerge@4.3.1": { "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" }, - "delaunator@5.0.1": { - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", - "dependencies": [ - "robust-predicates" - ] - }, - "dexie@4.0.11": { - "integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==" - }, "didyoumean@1.2.2": { "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, @@ -1608,40 +1273,17 @@ "integrity": "sha512-VNNMcekjbM1bQEGgbdGsdYR9mRdTj/L0A5ba0P1tiFv5QB9GvbvJMABJoiD80eqpZUkfR2QVOmiZfgCwHicT/Q==", "dependencies": [ "svelte", - "tailwind-merge@3.3.1" - ] - }, - "flowbite-svelte-icons@2.2.1_svelte@5.36.8__acorn@8.15.0": { - "integrity": "sha512-SH59319zN4TFpmvFMD7+0ETyDxez4Wyw3mgz7hkjhvrx8HawNAS3Fp7au84pZEs1gniX4hvXIg54U+4YybV2rA==", - "dependencies": [ - "clsx", - "svelte", - "tailwind-merge@3.3.1" + "tailwind-merge" ] }, "flowbite-svelte@0.48.6_svelte@5.36.8__acorn@8.15.0": { "integrity": "sha512-/PmeR3ipHHvda8vVY9MZlymaRoJsk8VddEeoLzIygfYwJV68ey8gHuQPC1dq9J6NDCTE5+xOPtBiYUtVjCfvZw==", "dependencies": [ "@floating-ui/dom", - "apexcharts@3.54.1", - "flowbite@3.1.2", - "svelte", - "tailwind-merge@3.3.1" - ] - }, - "flowbite-svelte@1.10.10_svelte@5.36.8__acorn@8.15.0_tailwindcss@3.4.17__postcss@8.5.6": { - "integrity": "sha512-9YCB3EqQKlu7in9pxE46eeA+zt98vhUK1nb0eR2o5wpRfsWj60u9v43lMtfhpxSTsh2Jebh+wVLNYyyrYa0UGA==", - "dependencies": [ - "@floating-ui/dom", - "@floating-ui/utils", - "apexcharts@4.7.0_@svgdotjs+svg.js@3.2.4_@svgdotjs+svg.select.js@4.0.3__@svgdotjs+svg.js@3.2.4", - "clsx", - "date-fns", + "apexcharts", "flowbite@3.1.2", "svelte", - "tailwind-merge@3.3.1", - "tailwind-variants", - "tailwindcss" + "tailwind-merge" ] }, "flowbite@2.5.2": { @@ -1794,12 +1436,6 @@ "highlight.js@11.11.1": { "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==" }, - "iconv-lite@0.6.3": { - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": [ - "safer-buffer" - ] - }, "ignore@5.3.2": { "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" }, @@ -1824,9 +1460,6 @@ "inherits@2.0.4": { "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "internmap@2.0.3": { - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" - }, "is-binary-path@2.1.0": { "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dependencies": [ @@ -1950,12 +1583,6 @@ "type-check" ] }, - "light-bolt11-decoder@3.2.0": { - "integrity": "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==", - "dependencies": [ - "@scure/base@1.1.1" - ] - }, "lilconfig@2.1.0": { "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==" }, @@ -2081,25 +1708,6 @@ "normalize-range@0.1.2": { "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" }, - "nostr-tools@2.15.1_typescript@5.8.3": { - "integrity": "sha512-LpetHDR9ltnkpJDkva/SONgyKBbsoV+5yLB8DWc0/U3lCWGtoWJw6Nbc2vR2Ai67RIQYrBQeZLyMlhwVZRK/9A==", - "dependencies": [ - "@noble/ciphers", - "@noble/curves@1.2.0", - "@noble/hashes@1.3.1", - "@scure/base@1.1.1", - "@scure/bip32", - "@scure/bip39", - "nostr-wasm", - "typescript" - ], - "optionalPeers": [ - "typescript" - ] - }, - "nostr-wasm@0.1.0": { - "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==" - }, "nunjucks@3.2.4": { "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", "dependencies": [ @@ -2477,9 +2085,6 @@ "reusify@1.1.0": { "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==" }, - "robust-predicates@3.0.2": { - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" - }, "rollup@4.45.1": { "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", "dependencies": [ @@ -2516,18 +2121,12 @@ "queue-microtask" ] }, - "rw@1.3.3": { - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" - }, "sade@1.8.1": { "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", "dependencies": [ "mri" ] }, - "safer-buffer@2.1.2": { - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, "semver@7.7.2": { "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "bin": true @@ -2705,19 +2304,9 @@ "svg.js" ] }, - "tailwind-merge@3.0.2": { - "integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==" - }, "tailwind-merge@3.3.1": { "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==" }, - "tailwind-variants@1.0.0_tailwindcss@3.4.17__postcss@8.5.6": { - "integrity": "sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA==", - "dependencies": [ - "tailwind-merge@3.0.2", - "tailwindcss" - ] - }, "tailwindcss@3.4.17_postcss@8.5.6": { "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "dependencies": [ @@ -2770,9 +2359,6 @@ "ts-interface-checker@0.1.13": { "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, - "tseep@1.3.1": { - "integrity": "sha512-ZPtfk1tQnZVyr7BPtbJ93qaAh2lZuIOpTMjhrYa4XctT8xe7t4SAW9LIxrySDuYMsfNNayE51E/WNGrNVgVicQ==" - }, "tslib@2.8.1": { "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, @@ -2782,9 +2368,6 @@ "prelude-ls" ] }, - "typescript-lru-cache@2.0.0": { - "integrity": "sha512-Jp57Qyy8wXeMkdNuZiglE6v2Cypg13eDA1chHwDG6kq51X7gk4K7P7HaDdzZKCxkegXkVHNcPD0n5aW6OZH3aA==" - }, "typescript@5.8.3": { "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "bin": true @@ -2945,18 +2528,25 @@ }, "workspace": { "dependencies": [ - "npm:@nostr-dev-kit/ndk-cache-dexie@^2.6.33", + "npm:@noble/curves@^1.9.4", + "npm:@noble/hashes@^1.8.0", + "npm:@nostr-dev-kit/ndk-cache-dexie@2.6", "npm:@nostr-dev-kit/ndk@^2.14.32", "npm:@popperjs/core@2.11", "npm:@tailwindcss/forms@0.5", "npm:@tailwindcss/typography@0.5", "npm:asciidoctor@3.0", - "npm:d3@7.9", - "npm:flowbite-svelte-icons@^2.2.1", - "npm:flowbite-svelte@^1.10.10", - "npm:flowbite@^3.1.2", + "npm:bech32@2", + "npm:d3@^7.9.0", + "npm:flowbite-svelte-icons@2.1", + "npm:flowbite-svelte@0.48", + "npm:flowbite@2", "npm:he@1.2", - "npm:nostr-tools@^2.15.1", + "npm:highlight.js@^11.11.1", + "npm:node-emoji@^2.2.0", + "npm:nostr-tools@2.15", + "npm:plantuml-encoder@^1.4.0", + "npm:qrcode@^1.5.4", "npm:svelte@^5.36.8", "npm:tailwind-merge@^3.3.1" ], diff --git a/src/lib/components/CommentBox.svelte b/src/lib/components/CommentBox.svelte index b25557f..1e23ce2 100644 --- a/src/lib/components/CommentBox.svelte +++ b/src/lib/components/CommentBox.svelte @@ -10,7 +10,7 @@ ProfileSearchResult, } from "$lib/utils/search_utility"; - import { userPubkey } from "$lib/stores/authStore.Svelte"; + import { userStore } from "$lib/stores/userStore"; import type { NDKEvent } from "$lib/utils/nostrUtils"; import { @@ -174,7 +174,7 @@ success = null; try { - const pk = $userPubkey || ""; + const pk = $userStore.pubkey || ""; const npub = toNpub(pk); if (!npub) { @@ -430,7 +430,22 @@ class="w-full text-left cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 p-2 rounded flex items-center gap-3" onclick={() => selectMention(profile)} > - {#if profile.pubkey && communityStatus[profile.pubkey]} + {#if profile.isInUserLists} +
    + + + +
    + {:else if profile.pubkey && communityStatus[profile.pubkey]}
    handleSubmit()} - disabled={isSubmitting || !content.trim() || !$userPubkey} + disabled={isSubmitting || !content.trim() || !$userStore.pubkey} class="w-full md:w-auto" > - {#if !$userPubkey} + {#if !$userStore.pubkey} Not Signed In {:else if isSubmitting} Publishing... @@ -617,7 +632,7 @@
    - {#if !$userPubkey} + {#if !$userStore.pubkey} Please sign in to post comments. Your comments will be signed with your current account. diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index ab3b865..a27a024 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -1,13 +1,12 @@ - {metadata.title} - + {meta.title || "Alexandria - Nostr Publications"} + - - - + + + - + {#if meta.image} + + {/if} - - - + + + {#if meta.image} + + {/if} {@render children()} \ No newline at end of file diff --git a/src/routes/visualize/+page.svelte b/src/routes/visualize/+page.svelte index 7d8124a..fa46745 100644 --- a/src/routes/visualize/+page.svelte +++ b/src/routes/visualize/+page.svelte @@ -578,7 +578,8 @@ profileEvents = await batchFetchProfiles( Array.from(allPubkeys), - (fetched, total) => { + ndk, + (fetched: number, total: number) => { profileLoadingProgress = { current: fetched, total }; } ); @@ -681,6 +682,7 @@ tags, existingEventIds, baseEvents, + ndk, debug ); newPublications = result.publications; @@ -698,6 +700,7 @@ await fetchProfilesForNewEvents( newPublications, newContentEvents, + ndk, (progress: { current: number; total: number } | null) => { profileLoadingProgress = progress; }, debug ); diff --git a/tests/unit/eventInput30040.test.ts b/tests/unit/eventInput30040.test.ts index a7064c3..b7687bd 100644 --- a/tests/unit/eventInput30040.test.ts +++ b/tests/unit/eventInput30040.test.ts @@ -18,13 +18,12 @@ vi.mock("@nostr-dev-kit/ndk", () => ({ })), })); -// TODO: Replace with getNdkContext mock. -vi.mock("../../src/lib/ndk", () => ({ - ndkInstance: { - subscribe: vi.fn(), - }, - getNdk: vi.fn(() => ({})), -})); +// Mock NDK context +const mockNdk = { + subscribe: vi.fn(), + fetchEvents: vi.fn(), + pool: { relays: new Map() }, +}; vi.mock("svelte/store", () => ({ get: vi.fn(() => ({})), @@ -67,6 +66,7 @@ This is the content of the second section.`; content, tags, baseEvent, + mockNdk as any, ); // Test index event @@ -163,6 +163,7 @@ This is the content of the second section.`; content, tags, baseEvent, + mockNdk as any, ); // Test index event @@ -240,6 +241,7 @@ This is the preamble content. content, tags, baseEvent, + mockNdk as any, ); // Test index event @@ -295,6 +297,7 @@ This is the preamble content. content, tags, baseEvent, + mockNdk as any, ); // Test index event @@ -343,6 +346,7 @@ index card`; content, tags, baseEvent, + mockNdk as any, ); // Test index event @@ -368,6 +372,7 @@ index card`; content, tags, baseEvent, + mockNdk as any, ); // Test index event @@ -435,6 +440,7 @@ This is the section content.`; content, tags, baseEvent, + mockNdk as any, ); // Test index event metadata @@ -574,6 +580,7 @@ This is just preamble content.`; content, tags, baseEvent, + mockNdk as any, ); expect(indexEvent.kind).toBe(30040); @@ -602,6 +609,7 @@ Content here.`; content, tags, baseEvent, + mockNdk as any, ); expect(indexEvent.kind).toBe(30040); @@ -631,6 +639,7 @@ Content here.`; content, tags, baseEvent, + mockNdk as any, ); expect(indexEvent.kind).toBe(30040); diff --git a/tests/unit/tagExpansion.test.ts b/tests/unit/tagExpansion.test.ts index 307ebd9..cc55fb9 100644 --- a/tests/unit/tagExpansion.test.ts +++ b/tests/unit/tagExpansion.test.ts @@ -44,7 +44,12 @@ class MockNDKEvent { // Mock NDK instance const mockNDK = { fetchEvents: vi.fn(), -}; + pool: {}, + debug: false, + mutedIds: new Set(), + queuesZapConfig: {}, + // Add other required properties as needed for the mock +} as any; // Mock the ndkInstance store // TODO: Replace with getNdkContext mock. @@ -167,6 +172,7 @@ describe("Tag Expansion Tests", () => { ["bitcoin"], existingEventIds, baseEvents, + mockNDK as any, debug, ); @@ -205,6 +211,7 @@ describe("Tag Expansion Tests", () => { ["bitcoin"], existingEventIds, baseEvents, + mockNDK as any, debug, ); @@ -227,6 +234,7 @@ describe("Tag Expansion Tests", () => { [], existingEventIds, baseEvents, + mockNDK as any, debug, ); @@ -336,6 +344,7 @@ describe("Tag Expansion Tests", () => { await fetchProfilesForNewEvents( mockPublications as NDKEvent[], mockContentEvents as NDKEvent[], + mockNDK as any, onProgressUpdate, debug, ); @@ -357,6 +366,7 @@ describe("Tag Expansion Tests", () => { await fetchProfilesForNewEvents( [], [], + mockNDK as any, onProgressUpdate, debug, ); @@ -390,6 +400,7 @@ describe("Tag Expansion Tests", () => { ["bitcoin"], existingEventIds, baseEvents, + mockNDK as any, debug, ); @@ -413,6 +424,7 @@ describe("Tag Expansion Tests", () => { await fetchProfilesForNewEvents( relayResult.publications, relayResult.contentEvents, + mockNDK as any, onProgressUpdate, debug, ); From 9c53a56413ec4d00acee2ffd3f559ffed1f8be38 Mon Sep 17 00:00:00 2001 From: silberengel Date: Tue, 19 Aug 2025 13:40:41 +0200 Subject: [PATCH 73/98] all tests passing --- src/lib/utils/asciidoc_metadata.ts | 275 +- src/lib/utils/event_input_utils.ts | 8 + src/lib/utils/markup/advancedMarkupParser.ts | 321 +- test_data/LaTeXtestfile.json | 22 - test_data/LaTeXtestfile.md | 151 - test_output.log | 4178 ++++++++++++++++++ tests/unit/latexRendering.test.ts | 83 - tests/unit/mathProcessing.test.ts | 186 + tests/unit/tagExpansion.test.ts | 9 +- 9 files changed, 4619 insertions(+), 614 deletions(-) delete mode 100644 test_data/LaTeXtestfile.json delete mode 100644 test_data/LaTeXtestfile.md create mode 100644 test_output.log delete mode 100644 tests/unit/latexRendering.test.ts create mode 100644 tests/unit/mathProcessing.test.ts diff --git a/src/lib/utils/asciidoc_metadata.ts b/src/lib/utils/asciidoc_metadata.ts index 8367469..810c965 100644 --- a/src/lib/utils/asciidoc_metadata.ts +++ b/src/lib/utils/asciidoc_metadata.ts @@ -115,6 +115,13 @@ function mapAttributesToMetadata( } else if (metadataKey === "tags") { // Skip tags mapping since it's handled by extractTagsFromAttributes continue; + } else if (metadataKey === "summary") { + // Handle summary specially - combine with existing summary if present + if (metadata.summary) { + metadata.summary = `${metadata.summary} ${value}`; + } else { + metadata.summary = value; + } } else { (metadata as any)[metadataKey] = value; } @@ -123,84 +130,178 @@ function mapAttributesToMetadata( } /** - * Extracts authors from header line (document or section) + * Extracts authors from document header only (not sections) */ -function extractAuthorsFromHeader( - sourceContent: string, - isSection: boolean = false, -): string[] { +function extractDocumentAuthors(sourceContent: string): string[] { const authors: string[] = []; const lines = sourceContent.split(/\r?\n/); - const headerPattern = isSection ? /^==\s+/ : /^=\s+/; - + + // Find the document title line + let titleLineIndex = -1; for (let i = 0; i < lines.length; i++) { + if (lines[i].match(/^=\s+/)) { + titleLineIndex = i; + break; + } + } + + if (titleLineIndex === -1) { + return authors; + } + + // Look for authors in the lines immediately following the title + let i = titleLineIndex + 1; + while (i < lines.length) { const line = lines[i]; - if (line.match(headerPattern)) { - // Found title line, check subsequent lines for authors - let j = i + 1; - while (j < lines.length) { - const authorLine = lines[j]; - - // Stop if we hit a blank line or content that's not an author - if (authorLine.trim() === "") { - break; - } - - if (authorLine.includes("<") && !authorLine.startsWith(":")) { - // This is an author line like "John Doe " - const authorName = authorLine.split("<")[0].trim(); - if (authorName) { - authors.push(authorName); - } - } else if ( - isSection && authorLine.match(/^[A-Za-z\s]+$/) && - authorLine.trim() !== "" && authorLine.trim().split(/\s+/).length <= 2 - ) { - // This is a simple author name without email (for sections) - authors.push(authorLine.trim()); - } else if (authorLine.startsWith(":")) { - // This is an attribute line, skip it - attributes are handled by mapAttributesToMetadata - // Don't break here, continue to next line - } else { - // Not an author line, stop looking - break; - } - - j++; + + // Stop if we hit a blank line, section header, or content that's not an author + if (line.trim() === "" || line.match(/^==\s+/)) { + break; + } + + if (line.includes("<") && !line.startsWith(":")) { + // This is an author line like "John Doe " + const authorName = line.split("<")[0].trim(); + if (authorName) { + authors.push(authorName); } + } else if (line.startsWith(":")) { + // This is an attribute line, skip it + // Don't break here, continue to next line + } else { + // Not an author line, stop looking break; } + + i++; } + + return authors; +} +/** + * Extracts authors from section header only + */ +function extractSectionAuthors(sectionContent: string): string[] { + const authors: string[] = []; + const lines = sectionContent.split(/\r?\n/); + + // Find the section title line + let titleLineIndex = -1; + for (let i = 0; i < lines.length; i++) { + if (lines[i].match(/^==\s+/)) { + titleLineIndex = i; + break; + } + } + + if (titleLineIndex === -1) { + return authors; + } + + // Look for authors in the lines immediately following the section title + let i = titleLineIndex + 1; + while (i < lines.length) { + const line = lines[i]; + + // Stop if we hit a blank line, another section header, or content that's not an author + if (line.trim() === "" || line.match(/^==\s+/)) { + break; + } + + if (line.includes("<") && !line.startsWith(":")) { + // This is an author line like "John Doe " + const authorName = line.split("<")[0].trim(); + if (authorName) { + authors.push(authorName); + } + } else if ( + line.match(/^[A-Za-z\s]+$/) && + line.trim() !== "" && + line.trim().split(/\s+/).length <= 2 && + !line.startsWith(":") + ) { + // This is a simple author name without email (for sections) + authors.push(line.trim()); + } else if (line.startsWith(":")) { + // This is an attribute line, skip it + // Don't break here, continue to next line + } else { + // Not an author line, stop looking + break; + } + + i++; + } + return authors; } /** - * Strips header and attribute lines from content + * Strips document header and attribute lines from content */ -function stripHeaderAndAttributes( - content: string, - isSection: boolean = false, -): string { +function stripDocumentHeader(content: string): string { const lines = content.split(/\r?\n/); let contentStart = 0; - const headerPattern = isSection ? /^==\s+/ : /^=\s+/; - + + // Find where the document header ends for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Skip title line, author line, revision line, and attribute lines if ( - !line.match(headerPattern) && !line.includes("<") && + !line.match(/^=\s+/) && + !line.includes("<") && !line.match(/^.+,\s*.+:\s*.+$/) && - !line.match(/^:[^:]+:\s*.+$/) && line.trim() !== "" + !line.match(/^:[^:]+:\s*.+$/) && + line.trim() !== "" ) { contentStart = i; break; } } - + // Filter out all attribute lines and author lines from the content const contentLines = lines.slice(contentStart); + const filteredLines = contentLines.filter((line) => { + // Skip attribute lines + if (line.match(/^:[^:]+:\s*.+$/)) { + return false; + } + return true; + }); + + // Remove extra blank lines and normalize newlines + return filteredLines.join("\n").replace(/\n\s*\n\s*\n/g, "\n\n").replace( + /\n\s*\n/g, + "\n", + ).trim(); +} + +/** + * Strips section header and attribute lines from content + */ +function stripSectionHeader(sectionContent: string): string { + const lines = sectionContent.split(/\r?\n/); + let contentStart = 0; + + // Find where the section header ends + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + // Skip section title line, author line, and attribute lines + if ( + !line.match(/^==\s+/) && + !line.includes("<") && + !line.match(/^:[^:]+:\s*.+$/) && + line.trim() !== "" && + !(line.match(/^[A-Za-z\s]+$/) && line.trim() !== "" && line.trim().split(/\s+/).length <= 2) + ) { + contentStart = i; + break; + } + } + + // Filter out all attribute lines, author lines, and section headers from the content + const contentLines = lines.slice(contentStart); const filteredLines = contentLines.filter((line) => { // Skip attribute lines if (line.match(/^:[^:]+:\s*.+$/)) { @@ -208,14 +309,19 @@ function stripHeaderAndAttributes( } // Skip author lines (simple names without email) if ( - isSection && line.match(/^[A-Za-z\s]+$/) && line.trim() !== "" && + line.match(/^[A-Za-z\s]+$/) && + line.trim() !== "" && line.trim().split(/\s+/).length <= 2 ) { return false; } + // Skip section headers + if (line.match(/^==\s+/)) { + return false; + } return true; }); - + // Remove extra blank lines and normalize newlines return filteredLines.join("\n").replace(/\n\s*\n\s*\n/g, "\n\n").replace( /\n\s*\n/g, @@ -258,18 +364,40 @@ export function extractDocumentMetadata(inputContent: string): { // Extract basic metadata const title = document.getTitle(); - if (title) metadata.title = title; + if (title) { + // Decode HTML entities in the title + metadata.title = title + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/ /g, " "); + } // Handle multiple authors - combine header line and attributes - const authors = extractAuthorsFromHeader(document.getSource()); - - // Get authors from attributes (but avoid duplicates) - const attrAuthor = attributes["author"]; - if ( - attrAuthor && typeof attrAuthor === "string" && - !authors.includes(attrAuthor) - ) { - authors.push(attrAuthor); + const authors = extractDocumentAuthors(document.getSource()); + + // Get authors from attributes in the document header only (including multiple :author: lines) + const lines = document.getSource().split(/\r?\n/); + let inDocumentHeader = true; + for (const line of lines) { + // Stop scanning when we hit a section header + if (line.match(/^==\s+/)) { + inDocumentHeader = false; + break; + } + + // Process :author: attributes regardless of other content + if (inDocumentHeader) { + const match = line.match(/^:author:\s*(.+)$/); + if (match) { + const authorName = match[1].trim(); + if (authorName && !authors.includes(authorName)) { + authors.push(authorName); + } + } + } } if (authors.length > 0) { @@ -305,7 +433,7 @@ export function extractDocumentMetadata(inputContent: string): { metadata.tags = tags; } - const content = stripHeaderAndAttributes(document.getSource()); + const content = stripDocumentHeader(document.getSource()); return { metadata, content }; } @@ -335,13 +463,28 @@ export function extractSectionMetadata(inputSectionContent: string): { const attributes = parseSectionAttributes(inputSectionContent); // Extract authors from section content - const authors = extractAuthorsFromHeader(inputSectionContent, true); + const authors = extractSectionAuthors(inputSectionContent); + + // Get authors from attributes (including multiple :author: lines) + const lines = inputSectionContent.split(/\r?\n/); + for (const line of lines) { + const match = line.match(/^:author:\s*(.+)$/); + if (match) { + const authorName = match[1].trim(); + if (authorName && !authors.includes(authorName)) { + authors.push(authorName); + } + } + } + if (authors.length > 0) { metadata.authors = authors; } - // Map attributes to metadata (sections can have authors) - mapAttributesToMetadata(attributes, metadata, false); + // Map attributes to metadata (sections can have authors, but skip author mapping to avoid duplication) + const attributesWithoutAuthor = { ...attributes }; + delete attributesWithoutAuthor.author; + mapAttributesToMetadata(attributesWithoutAuthor, metadata, false); // Handle tags and keywords const tags = extractTagsFromAttributes(attributes); @@ -349,7 +492,7 @@ export function extractSectionMetadata(inputSectionContent: string): { metadata.tags = tags; } - const content = stripHeaderAndAttributes(inputSectionContent, true); + const content = stripSectionHeader(inputSectionContent); return { metadata, content, title }; } diff --git a/src/lib/utils/event_input_utils.ts b/src/lib/utils/event_input_utils.ts index 876fe31..cdeb501 100644 --- a/src/lib/utils/event_input_utils.ts +++ b/src/lib/utils/event_input_utils.ts @@ -170,6 +170,14 @@ export function validate30040EventSet(content: string): { function normalizeDTagValue(header: string): string { return header .toLowerCase() + // Decode common HTML entities first + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/ /g, " ") + // Then normalize as before .replace(/[^\p{L}\p{N}]+/gu, "-") .replace(/^-+|-+$/g, ""); } diff --git a/src/lib/utils/markup/advancedMarkupParser.ts b/src/lib/utils/markup/advancedMarkupParser.ts index f1973d9..3a4816c 100644 --- a/src/lib/utils/markup/advancedMarkupParser.ts +++ b/src/lib/utils/markup/advancedMarkupParser.ts @@ -30,6 +30,7 @@ function escapeHtml(text: string): string { const HEADING_REGEX = /^(#{1,6})\s+(.+)$/gm; const ALTERNATE_HEADING_REGEX = /^([^\n]+)\n(=+|-+)\n/gm; const INLINE_CODE_REGEX = /`([^`\n]+)`/g; +const MULTILINE_CODE_REGEX = /`([\s\S]*?)`/g; const HORIZONTAL_RULE_REGEX = /^(?:[-*_]\s*){3,}$/gm; const FOOTNOTE_REFERENCE_REGEX = /\[\^([^\]]+)\]/g; const FOOTNOTE_DEFINITION_REGEX = /^\[\^([^\]]+)\]:\s*(.+)$/gm; @@ -390,296 +391,41 @@ function restoreCodeBlocks(text: string, blocks: Map): string { } /** - * Process $...$ and $$...$$ math blocks: render as LaTeX if recognized, otherwise as AsciiMath - * This must run BEFORE any paragraph or inline code formatting. + * Process math expressions inside inline code blocks + * Only processes math that is inside backticks and contains $...$ or $$...$$ markings */ -function processDollarMath(content: string): string { - // Display math: $$...$$ (multi-line, not empty) - content = content.replace(/\$\$([\s\S]*?\S[\s\S]*?)\$\$/g, (_match, expr) => { - if (isLaTeXContent(expr)) { - return `
    $$${expr}$$
    `; - } else { - // Strip all $ or $$ from AsciiMath - const clean = expr.replace(/\$+/g, "").trim(); - return `
    ${clean}
    `; +function processInlineCodeMath(content: string): string { + return content.replace(MULTILINE_CODE_REGEX, (match, codeContent) => { + // Check if the code content contains math expressions + const hasInlineMath = /\$((?:[^$\\]|\\.)*?)\$/.test(codeContent); + const hasDisplayMath = /\$\$[\s\S]*?\$\$/.test(codeContent); + + if (!hasInlineMath && !hasDisplayMath) { + // No math found, return the original inline code + return match; } - }); - // Inline math: $...$ (not empty, not just whitespace) - content = content.replace(/\$([^\s$][^$\n]*?)\$/g, (_match, expr) => { - if (isLaTeXContent(expr)) { - return `$${expr}$`; - } else { - const clean = expr.replace(/\$+/g, "").trim(); - return `${clean}`; + + // Process display math ($$...$$) first to avoid conflicts with inline math + let processedContent = codeContent.replace(/\$\$([\s\S]*?)\$\$/g, (mathMatch: string, mathContent: string) => { + // Skip empty math expressions + if (!mathContent.trim()) { + return mathMatch; } + return `\\[${mathContent}\\]`; }); - return content; -} - -/** - * Process LaTeX math expressions only within inline code blocks - */ -function processMathExpressions(content: string): string { - // Only process LaTeX within inline code blocks (backticks) - return content.replace(INLINE_CODE_REGEX, (_match, code) => { - const trimmedCode = code.trim(); - - // Check for unsupported LaTeX environments (like tabular) first - if (/\\begin\{tabular\}|\\\\begin\{tabular\}/.test(trimmedCode)) { - return `
    -

    - Unrendered, as it is LaTeX typesetting, not a formula: -

    -
    -          ${escapeHtml(trimmedCode)}
    -        
    -
    `; - } - - // Check if the code contains LaTeX syntax - if (isLaTeXContent(trimmedCode)) { - // Detect LaTeX display math (\\[...\\]) - if (/^\\\[[\s\S]*\\\]$/.test(trimmedCode)) { - // Remove the delimiters for rendering - const inner = trimmedCode.replace(/^\\\[|\\\]$/g, ""); - return `
    $$${inner}$$
    `; - } - // Detect display math ($$...$$) - if (/^\$\$[\s\S]*\$\$$/.test(trimmedCode)) { - // Remove the delimiters for rendering - const inner = trimmedCode.replace(/^\$\$|\$\$$/g, ""); - return `
    $$${inner}$$
    `; - } - // Detect inline math ($...$) - if (/^\$[\s\S]*\$$/.test(trimmedCode)) { - // Remove the delimiters for rendering - const inner = trimmedCode.replace(/^\$|\$$/g, ""); - return `$${inner}$`; - } - // Default to inline math for any other LaTeX content - return `$${trimmedCode}$`; - } else { - // Check for edge cases that should remain as code, not math - // These patterns indicate code that contains dollar signs but is not math - const codePatterns = [ - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=/, // Variable assignment like "const price =" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/, // Function call like "echo(" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*\{/, // Object literal like "const obj = {" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*\[/, // Array literal like "const arr = [" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*!&|^~]/, // Operator like "const x = 1 +" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Two identifiers like "const price = amount" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]/, // Number like "const x = 1" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]/, // Complex expression like "const price = amount +" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[a-zA-Z0-9_$]*\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Three identifiers like "const price = amount + tax" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]/, // Two identifiers and number like "const price = amount + 1" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]/, // Identifier, number, operator like "const x = 1 +" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Identifier, number, identifier like "const x = 1 + y" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[0-9]/, // Identifier, number, number like "const x = 1 + 2" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Complex like "const x = 1 + y" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]/, // Complex like "const x = 1 + 2" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]/, // Very complex like "const x = 1 + y +" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Very complex like "const x = 1 + y + z" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]\s*[0-9]/, // Very complex like "const x = 1 + y + 2" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]\s*[+\-*/%=<>!&|^~]/, // Very complex like "const x = 1 + 2 +" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Very complex like "const x = 1 + 2 + y" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]/, // Very complex like "const x = 1 + 2 + 3" - // Additional patterns for JavaScript template literals and other code - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*`/, // Template literal assignment like "const str = `" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*'/, // String assignment like "const str = '" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*"/, // String assignment like "const str = \"" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[0-9]/, // Number assignment like "const x = 1" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Variable assignment like "const x = y" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[+\-*/%=<>!&|^~]/, // Assignment with operator like "const x = +" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]/, // Assignment with variable and operator like "const x = y +" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Assignment with two variables and operator like "const x = y + z" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[0-9]\s*[+\-*/%=<>!&|^~]/, // Assignment with number and operator like "const x = 1 +" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Assignment with number, operator, variable like "const x = 1 + y" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]\s*[0-9]/, // Assignment with variable, operator, number like "const x = y + 1" - /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]/, // Assignment with number, operator, number like "const x = 1 + 2" - ]; - - // If it matches code patterns, treat as regular code - if (codePatterns.some((pattern) => pattern.test(trimmedCode))) { - const escapedCode = trimmedCode - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - return `${escapedCode}`; - } - - // Return as regular inline code - const escapedCode = trimmedCode - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - return `${escapedCode}`; + + // Process inline math ($...$) after display math + // Use a more sophisticated regex that handles escaped dollar signs + processedContent = processedContent.replace(/\$((?:[^$\\]|\\.)*?)\$/g, (mathMatch: string, mathContent: string) => { + // Skip empty math expressions + if (!mathContent.trim()) { + return mathMatch; } + return `\\(${mathContent}\\)`; + }); + + return `\`${processedContent}\``; }); -} - -/** - * Checks if content contains LaTeX syntax - */ -function isLaTeXContent(content: string): boolean { - const trimmed = content.trim(); - - // Check for simple math expressions first (like AsciiMath) - if (/^\$[^$]+\$$/.test(trimmed)) { - return true; - } - - // Check for display math - if (/^\$\$[\s\S]*\$\$$/.test(trimmed)) { - return true; - } - - // Check for LaTeX display math - if (/^\\\[[\s\S]*\\\]$/.test(trimmed)) { - return true; - } - - // Check for LaTeX environments with double backslashes (like tabular) - if (/\\\\begin\{[^}]+\}/.test(trimmed) || /\\\\end\{[^}]+\}/.test(trimmed)) { - return true; - } - - // Check for common LaTeX patterns - const latexPatterns = [ - /\\[a-zA-Z]+/, // LaTeX commands like \frac, \sum, etc. - /\\\\[a-zA-Z]+/, // LaTeX commands with double backslashes like \\frac, \\sum, etc. - /\\[\(\)\[\]]/, // LaTeX delimiters like \(, \), \[, \] - /\\\\[\(\)\[\]]/, // LaTeX delimiters with double backslashes like \\(, \\), \\[, \\] - /\\\[[\s\S]*?\\\]/, // LaTeX display math \[ ... \] - /\\\\\[[\s\S]*?\\\\\]/, // LaTeX display math with double backslashes \\[ ... \\] - /\\begin\{/, // LaTeX environments - /\\\\begin\{/, // LaTeX environments with double backslashes - /\\end\{/, // LaTeX environments - /\\\\end\{/, // LaTeX environments with double backslashes - /\\begin\{array\}/, // LaTeX array environment - /\\\\begin\{array\}/, // LaTeX array environment with double backslashes - /\\end\{array\}/, - /\\\\end\{array\}/, - /\\begin\{matrix\}/, // LaTeX matrix environment - /\\\\begin\{matrix\}/, // LaTeX matrix environment with double backslashes - /\\end\{matrix\}/, - /\\\\end\{matrix\}/, - /\\begin\{bmatrix\}/, // LaTeX bmatrix environment - /\\\\begin\{bmatrix\}/, // LaTeX bmatrix environment with double backslashes - /\\end\{bmatrix\}/, - /\\\\end\{bmatrix\}/, - /\\begin\{pmatrix\}/, // LaTeX pmatrix environment - /\\\\begin\{pmatrix\}/, // LaTeX pmatrix environment with double backslashes - /\\end\{pmatrix\}/, - /\\\\end\{pmatrix\}/, - /\\begin\{tabular\}/, // LaTeX tabular environment - /\\\\begin\{tabular\}/, // LaTeX tabular environment with double backslashes - /\\end\{tabular\}/, - /\\\\end\{tabular\}/, - /\$\$/, // Display math delimiters - /\$[^$]+\$/, // Inline math delimiters - /\\text\{/, // LaTeX text command - /\\\\text\{/, // LaTeX text command with double backslashes - /\\mathrm\{/, // LaTeX mathrm command - /\\\\mathrm\{/, // LaTeX mathrm command with double backslashes - /\\mathbf\{/, // LaTeX bold command - /\\\\mathbf\{/, // LaTeX bold command with double backslashes - /\\mathit\{/, // LaTeX italic command - /\\\\mathit\{/, // LaTeX italic command with double backslashes - /\\sqrt/, // Square root - /\\\\sqrt/, // Square root with double backslashes - /\\frac/, // Fraction - /\\\\frac/, // Fraction with double backslashes - /\\sum/, // Sum - /\\\\sum/, // Sum with double backslashes - /\\int/, // Integral - /\\\\int/, // Integral with double backslashes - /\\lim/, // Limit - /\\\\lim/, // Limit with double backslashes - /\\infty/, // Infinity - /\\\\infty/, // Infinity with double backslashes - /\\alpha/, // Greek letters - /\\\\alpha/, // Greek letters with double backslashes - /\\beta/, - /\\\\beta/, - /\\gamma/, - /\\\\gamma/, - /\\delta/, - /\\\\delta/, - /\\theta/, - /\\\\theta/, - /\\lambda/, - /\\\\lambda/, - /\\mu/, - /\\\\mu/, - /\\pi/, - /\\\\pi/, - /\\sigma/, - /\\\\sigma/, - /\\phi/, - /\\\\phi/, - /\\omega/, - /\\\\omega/, - /\\partial/, // Partial derivative - /\\\\partial/, // Partial derivative with double backslashes - /\\nabla/, // Nabla - /\\\\nabla/, // Nabla with double backslashes - /\\cdot/, // Dot product - /\\\\cdot/, // Dot product with double backslashes - /\\times/, // Times - /\\\\times/, // Times with double backslashes - /\\div/, // Division - /\\\\div/, // Division with double backslashes - /\\pm/, // Plus-minus - /\\\\pm/, // Plus-minus with double backslashes - /\\mp/, // Minus-plus - /\\\\mp/, // Minus-plus with double backslashes - /\\leq/, // Less than or equal - /\\\\leq/, // Less than or equal with double backslashes - /\\geq/, // Greater than or equal - /\\\\geq/, // Greater than or equal with double backslashes - /\\neq/, // Not equal - /\\\\neq/, // Not equal with double backslashes - /\\approx/, // Approximately equal - /\\\\approx/, // Approximately equal with double backslashes - /\\equiv/, // Equivalent - /\\\\equiv/, // Equivalent with double backslashes - /\\propto/, // Proportional - /\\\\propto/, // Proportional with double backslashes - /\\in/, // Element of - /\\\\in/, // Element of with double backslashes - /\\notin/, // Not element of - /\\\\notin/, // Not element of with double backslashes - /\\subset/, // Subset - /\\\\subset/, // Subset with double backslashes - /\\supset/, // Superset - /\\\\supset/, // Superset with double backslashes - /\\cup/, // Union - /\\\\cup/, // Union with double backslashes - /\\cap/, // Intersection - /\\\\cap/, // Intersection with double backslashes - /\\emptyset/, // Empty set - /\\\\emptyset/, // Empty set with double backslashes - /\\mathbb\{/, // Blackboard bold - /\\\\mathbb\{/, // Blackboard bold with double backslashes - /\\mathcal\{/, // Calligraphic - /\\\\mathcal\{/, // Calligraphic with double backslashes - /\\mathfrak\{/, // Fraktur - /\\\\mathfrak\{/, // Fraktur with double backslashes - /\\mathscr\{/, // Script - /\\\\mathscr\{/, // Script with double backslashes - ]; - - return latexPatterns.some((pattern) => pattern.test(trimmed)); } /** @@ -693,11 +439,8 @@ export async function parseAdvancedmarkup(text: string): Promise { const { text: withoutCode, blocks } = processCodeBlocks(text); let processedText = withoutCode; - // Step 2: Process $...$ and $$...$$ math blocks (LaTeX or AsciiMath) - processedText = processDollarMath(processedText); - - // Step 3: Process LaTeX math expressions ONLY within inline code blocks (legacy support) - processedText = processMathExpressions(processedText); + // Step 2: Process math inside inline code blocks + processedText = processInlineCodeMath(processedText); // Step 4: Process block-level elements (tables, headings, horizontal rules) // AI-NOTE: 2025-01-24 - Removed duplicate processBlockquotes call to fix image rendering issues diff --git a/test_data/LaTeXtestfile.json b/test_data/LaTeXtestfile.json deleted file mode 100644 index 1dc63f1..0000000 --- a/test_data/LaTeXtestfile.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "created_at": 1752150799, - "content": "# This is a test file for writing mathematical formulas in #NostrMarkup\n\nThis document covers the rendering of formulas in TeX/LaTeX and AsciiMath notation, or some combination of those within the same page. It is meant to be rendered by clients utilizing MathJax.\n\nIf you want the entire document to be rendered as mathematics, place the entire thing in a back-tick code-block, but know that this makes the document slower to load, it is harder to format the prose, and the result is less legible. It also doesn't increase portability, as it's easy to export markup as LaTeX files, or as PDFs, with the formulas rendered.\n\nThe general idea, is that anything placed within `single back-ticks` is inline code, and inline-code will all be scanned for typical mathematics statements and rendered with best-effort. (For more precise rendering, use AsciiDoc.) We will not render text that is not marked as inline code, as mathematical formulas, as that is prose.\n\nIf you want the TeX to be blended into the surrounding text, wrap the text within single `$`. Otherwise, use double `$$` symbols, for display math, and it will appear on its own line.\n\n## TeX Examples\n\nInline equation: `$\\sqrt{x}$`\n\nSame equation, in the display mode: `$$\\sqrt{x}$$`\n\nSomething more complex, inline: `$\\mathbb{N} = \\{ a \\in \\mathbb{Z} : a > 0 \\}$`\n\nSomething complex, in display mode: `$$P \\left( A=2 \\, \\middle| \\, \\dfrac{A^2}{B}>4 \\right)$$`\n\nAnother example of `$$\\prod_{i=1}^{n} x_i - 1$$` inline formulas.\n\nFunction example: \n`$$\nf(x)=\n\\begin{cases}\n1/d_{ij} & \\quad \\text{when $d_{ij} \\leq 160$}\\\\ \n0 & \\quad \\text{otherwise}\n\\end{cases}\n$$`\n\nAnd a matrix:\n`$$\nM = \n\\begin{bmatrix}\n\\frac{5}{6} & \\frac{1}{6} & 0 \\\\[0.3em]\n\\frac{5}{6} & 0 & \\frac{1}{6} \\\\[0.3em]\n0 & \\frac{5}{6} & \\frac{1}{6}\n\\end{bmatrix}\n$$`\n\nLaTeX ypesetting won't be rendered. Use NostrMarkup delimeter tables for this sort of thing.\n\n`\\\\begin{tabular}{|c|c|c|l|r|}\n\\\\hline\n\\\\multicolumn{3}{|l|}{test} & A & B \\\\\\\\\n\\\\hline\n1 & 2 & 3 & 4 & 5 \\\\\\\\\n\\\\hline\n\\\\end{tabular}`\n\nWe also recognize common LaTeX statements:\n\n`\\[\n\\begin{array}{ccccc}\n1 & 2 & 3 & 4 & 5 \\\\\n\\end{array}\n\\]`\n\n`\\[ x^n + y^n = z^n \\]`\n\n`\\sqrt{x^2+1}`\n\nGreek letters are a snap: `$\\Psi$`, `$\\psi$`, `$\\Phi$`, `$\\phi$`. \n\nEquations within text are easy--- A well known Maxwell thermodynamic relation is `$\\left.{\\partial T \\over \\partial P}\\right|_{s} = \\left.{\\partial v \\over \\partial s}\\right|_{P}$`.\n\nYou can also set aside equations like so: `\\begin{eqnarray} du &=& T\\ ds -P\\ dv, \\qquad \\mbox{first law.}\\label{fl}\\\\ ds &\\ge& {\\delta q \\over T}.\\qquad \\qquad \\mbox{second law.} \\label{sl} \\end {eqnarray}`\n\n## And some good ole Asciimath\n\nAsciimath doesn't use `$` or `$$` delimiters, but we are using it to make mathy stuff easier to find. If you want it inline, include it inline. If you want it on a separate line, put a hard-return before and after.\n\nInline text example here `$E=mc^2$` and another `$1/(x+1)$`; very simple.\n\nDisplaying on a separate line:\n\n`$$sum_(k=1)^n k = 1+2+ cdots +n=(n(n+1))/2$$`\n\n`$$int_0^1 x^2 dx$$`\n\n`$$x = (-6 +- sqrt((-6)^2 - 4 (1)(4)))/(2 xx 1)$$`\n\n`$$|x|= {(x , if x ge 0 text(,)),(-x , if x <0.):}$$`\n\nDisplaying with wider spacing:\n\n`$a=3, \\ \\ \\ b=-3,\\ \\ $` and `$ \\ \\ c=2$`.\n\nThus `$(a+b)(c+b)=0$`.\n\nDisplaying with indentations:\n\nUsing the quadratic formula, the roots of `$x^2-6x+4=0$` are\n\n`$$x = (-6 +- sqrt((-6)^2 - 4 (1)(4)))/(2 xx 1)$$`\n\n`$$ \\ \\ = (-6 +- sqrt(36 - 16))/2$$`\n\n`$$ \\ \\ =(-6 +- sqrt(20))/2$$`\n\n`$$ \\ \\ = -0.8 or 2.2 \\ \\ \\ $$` to 1 decimal place.\n\nAdvanced alignment and matrices looks like this:\n\nA `$3xx3$` matrix, `$$((1,2,3),(4,5,6),(7,8,9))$$` and a `$2xx1$` matrix, or vector, `$$((1),(0))$$`.\n\nThe outer brackets determine the delimiters e.g. `$|(a,b),(c,d)|=ad-bc$`.\n\nA general `$m xx n$` matrix `$$((a_(11), cdots , a_(1n)),(vdots, ddots, vdots),(a_(m1), cdots , a_(mn)))$$`\n\n## Mixed Examples\n\nHere are some examples mixing LaTeX and AsciiMath:\n\n- LaTeX inline: `$\\frac{1}{2}$` vs AsciiMath inline: `$1/2$`\n- LaTeX display: `$$\\sum_{i=1}^n x_i$$` vs AsciiMath display: `$$sum_(i=1)^n x_i$$`\n- LaTeX matrix: `$$\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}$$` vs AsciiMath matrix: `$$((a,b),(c,d))$$`\n\n## Edge Cases\n\n- Empty math: `$$`\n- Just delimiters: `$ $`\n- Dollar signs in text: The price is $10.50\n- Currency: `$19.99`\n- Shell command: `echo \"Price: $100\"`\n- JavaScript template: `const price = \\`$${amount}\\``\n- CSS with dollar signs: `color: $primary-color`\n\nThis document should demonstrate that:\n1. LaTeX is processed within inline code blocks with proper delimiters\n2. AsciiMath is processed within inline code blocks with proper delimiters\n3. Regular code blocks remain unchanged\n4. Mixed content is handled correctly\n5. Edge cases are handled gracefully", - "tags": [ - ["t", "test"], - ["t", "Asciimath"], - ["t", "TeX"], - ["t", "LaTeX"], - [ - "d", - "this-is-a-test-file-for-writing-mathematical-formulas-in-nostrmarkup" - ], - [ - "title", - "This is a test file for writing mathematical formulas in #NostrMarkup" - ] - ], - "kind": 30023, - "pubkey": "fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1", - "id": "91be487e67cb68cfe3c7e965a654642b7bcedecb68340523a8c1b865b21fa5dc", - "sig": "59b7f87fe2c2d318152cf5b4796580f79a26936d515a816ddcb89b89ba337992eaa3d50896d3bde345d25be99c9caa3a237d476abeb8537589256cbcceeb2e75" -} diff --git a/test_data/LaTeXtestfile.md b/test_data/LaTeXtestfile.md deleted file mode 100644 index 01e6264..0000000 --- a/test_data/LaTeXtestfile.md +++ /dev/null @@ -1,151 +0,0 @@ -# This is a testfile for writing mathematic formulas in NostrMarkup - -This document covers the rendering of formulas in TeX/LaTeX and AsciiMath -notation, or some combination of those within the same page. It is meant to be -rendered by clients utilizing MathJax. - -If you want the entire document to be rendered as mathematics, place the entire -thing in a backtick-codeblock, but know that this makes the document slower to -load, it is harder to format the prose, and the result is less legible. It also -doesn't increase portability, as it's easy to export markup as LaTeX files, or -as PDFs, with the formulas rendered. - -The general idea, is that anything placed within `single backticks` is inline -code, and inline-code will all be scanned for typical mathematics statements and -rendered with best-effort. (For more precise rendering, use Asciidoc.) We will -not render text that is not marked as inline code, as mathematical formulas, as -that is prose. - -If you want the TeX to be blended into the surrounding text, wrap the text -within single `$`. Otherwise, use double `$$` symbols, for display math, and it -will appear on its own line. - -## TeX Examples - -Inline equation: `$\sqrt{x}$` - -Same equation, in the display mode: `$$\sqrt{x}$$` - -Something more complex, inline: `$\mathbb{N} = \{ a \in \mathbb{Z} : a > 0 \}$` - -Something complex, in display mode: -`$$P \left( A=2 \, \middle| \, \dfrac{A^2}{B}>4 \right)$$` - -Another example of `$$\prod_{i=1}^{n} x_i - 1$$` inline formulas. - -Function example: `$$ f(x)= \begin{cases} 1/d_{ij} & \quad \text{when -$d_{ij} \leq 160$}\\ 0 & \quad \text{otherwise} \end{cases} - -$$ ` - -And a matrix: ` $$ - -M = \begin{bmatrix} \frac{5}{6} & \frac{1}{6} & 0 \\[0.3em] \frac{5}{6} & 0 & -\frac{1}{6} \\[0.3em] 0 & \frac{5}{6} & \frac{1}{6} \end{bmatrix} - -$$ ` - -LaTeX ypesetting won't be rendered. Use NostrMarkup delimeter tables for this -sort of thing. - -`\\begin{tabular}{|c|c|c|l|r|} -\\hline -\\multicolumn{3}{|l|}{test} & A & B \\\\ -\\hline -1 & 2 & 3 & 4 & 5 \\\\ -\\hline -\\end{tabular}` - -We also recognize common LaTeX statements: - -`\[ -\begin{array}{ccccc} -1 & 2 & 3 & 4 & 5 \\ -\end{array} -\]` - -`\[ x^n + y^n = z^n \]` - -`\sqrt{x^2+1}` - -Greek letters are a snap: `$\Psi$`, `$\psi$`, `$\Phi$`, `$\phi$`. - -Equations within text are easy--- A well known Maxwell thermodynamic relation is -`$\left.{\partial T \over \partial P}\right|_{s} = \left.{\partial v \over \partial s}\right|_{P}$`. - -You can also set aside equations like so: -`\begin{eqnarray} du &=& T\ ds -P\ dv, \qquad \mbox{first law.}\label{fl}\\ ds &\ge& {\delta q \over T}.\qquad \qquad \mbox{second law.} \label{sl} \end {eqnarray}` - -## And some good ole Asciimath - -Asciimath doesn't use `$` or `$$` delimiters, but we are using it to make mathy -stuff easier to find. If you want it inline, include it inline. If you want it -on a separate line, put a hard-return before and after. - -Inline text example here `$E=mc^2$` and another `$1/(x+1)$`; very simple. - -Displaying on a separate line: - -`$$sum_(k=1)^n k = 1+2+ cdots +n=(n(n+1))/2$$` - -`$$int_0^1 x^2 dx$$` - -`$$x = (-6 +- sqrt((-6)^2 - 4 (1)(4)))/(2 xx 1)$$` - -`$$|x|= {(x , if x ge 0 text(,)),(-x , if x <0.):}$$` - -Displaying with wider spacing: - -`$a=3, \ \ \ b=-3,\ \ $` and `$ \ \ c=2$`. - -Thus `$(a+b)(c+b)=0$`. - -Displaying with indentations: - -Using the quadratic formula, the roots of `$x^2-6x+4=0$` are - -`$$x = (-6 +- sqrt((-6)^2 - 4 (1)(4)))/(2 xx 1)$$` - -`$$ \ \ = (-6 +- sqrt(36 - 16))/2$$` - -`$$ \ \ =(-6 +- sqrt(20))/2$$` - -`$$ \ \ = -0.8 or 2.2 \ \ \ $$` to 1 decimal place. - -Advanced alignment and matrices looks like this: - -A `$3xx3$` matrix, `$$((1,2,3),(4,5,6),(7,8,9))$$` and a `$2xx1$` matrix, or -vector, `$$((1),(0))$$`. - -The outer brackets determine the delimiters e.g. `$|(a,b),(c,d)|=ad-bc$`. - -A general `$m xx n$` matrix -`$$((a_(11), cdots , a_(1n)),(vdots, ddots, vdots),(a_(m1), cdots , a_(mn)))$$` - -## Mixed Examples - -Here are some examples mixing LaTeX and AsciiMath: - -- LaTeX inline: `$\frac{1}{2}$` vs AsciiMath inline: `$1/2$` -- LaTeX display: `$$\sum_{i=1}^n x_i$$` vs AsciiMath display: - `$$sum_(i=1)^n x_i$$` -- LaTeX matrix: `$$\begin{pmatrix} a & b \\ c & d \end{pmatrix}$$` vs AsciiMath - matrix: `$$((a,b),(c,d))$$` - -## Edge Cases - -- Empty math: `$$` -- Just delimiters: `$ $` -- Dollar signs in text: The price is $10.50 -- Currency: `$19.99` -- Shell command: `echo "Price: $100"` -- JavaScript template: `const price = \`$${amount}\`` -- CSS with dollar signs: `color: $primary-color` - -This document should demonstrate that: - -1. LaTeX is processed within inline code blocks with proper delimiters -2. AsciiMath is processed within inline code blocks with proper delimiters -3. Regular code blocks remain unchanged -4. Mixed content is handled correctly -5. Edge cases are handled gracefully $$ diff --git a/test_output.log b/test_output.log new file mode 100644 index 0000000..ce551de --- /dev/null +++ b/test_output.log @@ -0,0 +1,4178 @@ + +> alexandria@0.0.2 test +> vitest + + + DEV v3.2.4 /home/madmin/Projects/GitCitadel/gc-alexandria + + ✓ tests/unit/ZettelEditor.test.ts (24 tests) 20ms +stdout | tests/unit/relayDeduplication.test.ts > Relay Deduplication Behavior Tests > Addressable Event Deduplication > should keep only the most recent version of addressable events by coordinate +[eventDeduplication] Found 1 duplicate events out of 3 total events +[eventDeduplication] Reduced to 2 unique coordinates +[eventDeduplication] Duplicate details: [ + { + coordinate: '30041:pubkey1:chapter-1', + count: 2, + events: [ 'event1 (created_at: 1000)', 'event2 (created_at: 2000)' ] + } +] + +stdout | tests/unit/relayDeduplication.test.ts > Relay Deduplication Behavior Tests > Addressable Event Deduplication > should handle events with missing d-tags gracefully +[eventDeduplication] No duplicates found in 1 events + +stdout | tests/unit/relayDeduplication.test.ts > Relay Deduplication Behavior Tests > Addressable Event Deduplication > should handle events with missing timestamps +[eventDeduplication] Found 1 duplicate events out of 2 total events +[eventDeduplication] Reduced to 1 unique coordinates +[eventDeduplication] Duplicate details: [ + { + coordinate: '30041:pubkey1:chapter-3', + count: 2, + events: [ 'event7 (created_at: 0)', 'event8 (created_at: 1500)' ] + } +] + +stdout | tests/unit/relayDeduplication.test.ts > Relay Deduplication Behavior Tests > Mixed Event Type Deduplication > should only deduplicate addressable events (kinds 30000-39999) +[eventDeduplication] deduplicateAndCombineEvents: Found 1 duplicate coordinates out of 4 replaceable events +[eventDeduplication] deduplicateAndCombineEvents: Reduced from 5 to 4 events (1 removed) +[eventDeduplication] deduplicateAndCombineEvents: Duplicate details: [ + { + coordinate: '30041:pubkey1:chapter-1', + count: 2, + events: [ 'event1 (created_at: 1000)', 'event2 (created_at: 2000)' ] + } +] + +stdout | tests/unit/relayDeduplication.test.ts > Relay Deduplication Behavior Tests > Edge Cases > should handle events with null/undefined values +[eventDeduplication] No duplicates found in 1 events + +stdout | tests/unit/relayDeduplication.test.ts > Relay Deduplication Behavior Tests > Edge Cases > should handle events from different authors with same d-tag +[eventDeduplication] No duplicates found in 2 events + +stdout | tests/unit/relayDeduplication.test.ts > Relay Behavior Simulation > should simulate what happens when relays return duplicate events +[eventDeduplication] Found 2 duplicate events out of 3 total events +[eventDeduplication] Reduced to 1 unique coordinates +[eventDeduplication] Duplicate details: [ + { + coordinate: '30041:pubkey1:chapter-1', + count: 3, + events: [ + 'event1 (created_at: 1000)', + 'event2 (created_at: 2000)', + 'event3 (created_at: 1500)' + ] + } +] + +stdout | tests/unit/relayDeduplication.test.ts > Relay Behavior Simulation > should simulate multiple relays returning different versions +[eventDeduplication] Found 1 duplicate events out of 2 total events +[eventDeduplication] Reduced to 1 unique coordinates +[eventDeduplication] Duplicate details: [ + { + coordinate: '30041:pubkey1:chapter-1', + count: 2, + events: [ 'event1 (created_at: 1000)', 'event2 (created_at: 2000)' ] + } +] + +stdout | tests/unit/relayDeduplication.test.ts > Real Relay Deduplication Tests > should detect if relays are returning duplicate replaceable events +Note: This test would require actual relay queries to verify deduplication behavior +To run this test properly, we would need to: +1. Query real relays for replaceable events +2. Check if relays return duplicates +3. Verify our deduplication logic works on real data + +stdout | tests/unit/relayDeduplication.test.ts > Real Relay Deduplication Tests > should verify that our deduplication logic works on real relay data +Note: This test would require actual relay queries +To implement this test, we would need to: +1. Set up NDK with real relays +2. Fetch events for a known author with multiple versions +3. Apply deduplication and verify results + +stdout | tests/unit/relayDeduplication.test.ts > Practical Relay Behavior Analysis > should document what we know about relay deduplication behavior + +=== RELAY DEDUPLICATION BEHAVIOR ANALYSIS === + +Based on the code analysis and the comment from onedev: + +1. THEORETICAL BEHAVIOR: + - Relays SHOULD handle deduplication for replaceable events + - Only the most recent version of each coordinate should be stored + - Client-side deduplication should only be needed for cached/local events + +2. REALITY CHECK: + - Not all relays implement deduplication correctly + - Some relays may return multiple versions of the same event + - Network conditions and relay availability can cause inconsistencies + +3. ALEXANDRIA'S APPROACH: + - Implements client-side deduplication as a safety net + - Uses coordinate system (kind:pubkey:d-tag) for addressable events + - Keeps the most recent version based on created_at timestamp + - Only applies to replaceable events (kinds 30000-39999) + +4. WHY KEEP THE DEDUPLICATION: + - Defensive programming against imperfect relay implementations + - Handles multiple relay sources with different data + - Works with cached events that might be outdated + - Ensures consistent user experience regardless of relay behavior + +5. TESTING STRATEGY: + - Unit tests verify our deduplication logic works correctly + - Integration tests would verify relay behavior (when network allows) + - Monitoring can help determine if relays improve over time + +stdout | tests/unit/relayDeduplication.test.ts > Practical Relay Behavior Analysis > should provide recommendations for when to remove deduplication + +=== RECOMMENDATIONS FOR REMOVING DEDUPLICATION === + +The deduplication logic should be kept until: + +1. RELAY STANDARDS: + - NIP-33 (replaceable events) is widely implemented by relays + - Relays consistently return only the most recent version + - No major relay implementations return duplicates + +2. TESTING EVIDENCE: + - Real-world testing shows relays don't return duplicates + - Multiple relay operators confirm deduplication behavior + - No user reports of duplicate content issues + +3. MONITORING: + - Add logging to track when deduplication is actually used + - Monitor relay behavior over time + - Collect metrics on duplicate events found + +4. GRADUAL REMOVAL: + - Make deduplication configurable (on/off) + - Test with deduplication disabled in controlled environments + - Monitor for issues before removing completely + +5. FALLBACK STRATEGY: + - Keep deduplication as a fallback option + - Allow users to enable it if they experience issues + - Maintain the code for potential future use + + ✓ tests/unit/relayDeduplication.test.ts (22 tests) 22ms + ✓ tests/unit/nostr_identifiers.test.ts (12 tests) 9ms + ✓ tests/unit/tagExpansion.test.ts (12 tests) 23ms +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Normal Structure with Preamble > should build 30040 event set with preamble content +Parsed AsciiDoc: { + metadata: { + title: 'Test Document with Preamble', + authors: [ 'John Doe', 'Section Author' ], + version: '1.0', + publicationDate: '2024-01-15, Alexandria Test', + summary: 'This is a test document with preamble', + tags: [ 'test', 'preamble', 'asciidoc' ] + }, + content: '= Test Document with Preamble\n' + + 'John Doe \n' + + '1.0, 2024-01-15, Alexandria Test\n' + + ':summary: This is a test document with preamble\n' + + ':keywords: test, preamble, asciidoc\n' + + '\n' + + 'This is the preamble content that should be included.\n' + + '\n' + + '== First Section\n' + + ':author: Section Author\n' + + ':summary: This is the first section\n' + + '\n' + + 'This is the content of the first section.\n' + + '\n' + + '== Second Section\n' + + ':summary: This is the second section\n' + + '\n' + + 'This is the content of the second section.', + sections: [ + { + metadata: [Object], + content: 'This is the content of the first section.', + title: 'First Section' + }, + { + metadata: [Object], + content: 'This is the content of the second section.', + title: 'Second Section' + } + ] +} +Index event: { + documentTitle: 'Test Document with Preamble', + indexDTag: 'test-document-with-preamble' +} +Creating section 0: { + title: 'First Section', + dTag: 'test-document-with-preamble-first-section', + content: 'This is the content of the first section.', + metadata: { + title: 'First Section', + authors: [ 'Section Author' ], + summary: 'This is the first section' + } +} +Creating section 1: { + title: 'Second Section', + dTag: 'test-document-with-preamble-second-section', + content: 'This is the content of the second section.', + metadata: { title: 'Second Section', summary: 'This is the second section' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-second-section' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'article' ], + [ 'title', 'Test Document with Preamble' ], + [ 'author', 'John Doe' ], + [ 'author', 'Section Author' ], + [ 'version', '1.0' ], + [ 'published_on', '2024-01-15, Alexandria Test' ], + [ 'summary', 'This is a test document with preamble' ], + [ 't', 'test' ], + [ 't', 'preamble' ], + [ 't', 'asciidoc' ], + [ 'd', 'test-document-with-preamble' ], + [ 'title', 'Test Document with Preamble' ], + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-second-section' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Normal Structure without Preamble > should build 30040 event set without preamble content +Parsed AsciiDoc: { + metadata: { + title: 'Test Document without Preamble', + authors: [ 'Section Author' ], + version: 'Version', + summary: 'This is a test document without preamble', + tags: [ 'test', 'no-preamble', 'asciidoc' ] + }, + content: '= Test Document without Preamble\n' + + ':summary: This is a test document without preamble\n' + + ':keywords: test, no-preamble, asciidoc\n' + + '\n' + + '== First Section\n' + + ':author: Section Author\n' + + ':summary: This is the first section\n' + + '\n' + + 'This is the content of the first section.\n' + + '\n' + + '== Second Section\n' + + ':summary: This is the second section\n' + + '\n' + + 'This is the content of the second section.', + sections: [ + { + metadata: [Object], + content: 'This is the content of the first section.', + title: 'First Section' + }, + { + metadata: [Object], + content: 'This is the content of the second section.', + title: 'Second Section' + } + ] +} +Index event: { + documentTitle: 'Test Document without Preamble', + indexDTag: 'test-document-without-preamble' +} +Creating section 0: { + title: 'First Section', + dTag: 'test-document-without-preamble-first-section', + content: 'This is the content of the first section.', + metadata: { + title: 'First Section', + authors: [ 'Section Author' ], + summary: 'This is the first section' + } +} +Creating section 1: { + title: 'Second Section', + dTag: 'test-document-without-preamble-second-section', + content: 'This is the content of the second section.', + metadata: { title: 'Second Section', summary: 'This is the second section' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-second-section' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'article' ], + [ 'title', 'Test Document without Preamble' ], + [ 'author', 'Section Author' ], + [ 'version', 'Version' ], + [ 'summary', 'This is a test document without preamble' ], + [ 't', 'test' ], + [ 't', 'no-preamble' ], + [ 't', 'asciidoc' ], + [ 'd', 'test-document-without-preamble' ], + [ 'title', 'Test Document without Preamble' ], + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-second-section' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + + ❯ tests/unit/metadataExtraction.test.ts (16 tests | 2 failed) 231ms + × AsciiDoc Metadata Extraction > extractDocumentMetadata should extract document metadata correctly 124ms + → expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should extract section metadata correctly 8ms + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should extract standalone author names and remove them from content 5ms + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should handle multiple standalone author names 5ms + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should not extract non-author lines as authors 4ms + ✓ AsciiDoc Metadata Extraction > parseAsciiDocWithMetadata should parse complete document 23ms + ✓ AsciiDoc Metadata Extraction > metadataToTags should convert metadata to Nostr tags 2ms + ✓ AsciiDoc Metadata Extraction > should handle index card format correctly 4ms + ✓ AsciiDoc Metadata Extraction > should handle empty content gracefully 4ms + ✓ AsciiDoc Metadata Extraction > should handle keywords as tags 4ms + ✓ AsciiDoc Metadata Extraction > should handle both tags and keywords 5ms + ✓ AsciiDoc Metadata Extraction > should handle tags only 8ms + ✓ AsciiDoc Metadata Extraction > should handle both summary and description 15ms + ✓ AsciiDoc Metadata Extraction > Smart metadata extraction > should handle section-only content correctly 8ms + ✓ AsciiDoc Metadata Extraction > Smart metadata extraction > should handle minimal document header (just title) correctly 1ms + × AsciiDoc Metadata Extraction > Smart metadata extraction > should handle document with full header correctly 7ms + → expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Skeleton Structure with Preamble > should build 30040 event set with skeleton structure and preamble +Parsed AsciiDoc: { + metadata: { + title: 'Skeleton Document with Preamble', + version: 'Version', + summary: 'This is a skeleton document with preamble', + tags: [ 'skeleton', 'preamble', 'empty' ] + }, + content: '= Skeleton Document with Preamble\n' + + ':summary: This is a skeleton document with preamble\n' + + ':keywords: skeleton, preamble, empty\n' + + '\n' + + 'This is the preamble content.\n' + + '\n' + + '== Empty Section 1\n' + + '\n' + + '== Empty Section 2\n' + + '\n' + + '== Empty Section 3', + sections: [ + { metadata: [Object], content: '', title: 'Empty Section 1' }, + { metadata: [Object], content: '', title: 'Empty Section 2' }, + { metadata: [Object], content: '', title: 'Empty Section 3' } + ] +} +Index event: { + documentTitle: 'Skeleton Document with Preamble', + indexDTag: 'skeleton-document-with-preamble' +} +Creating section 0: { + title: 'Empty Section 1', + dTag: 'skeleton-document-with-preamble-empty-section-1', + content: '', + metadata: { title: 'Empty Section 1' } +} +Creating section 1: { + title: 'Empty Section 2', + dTag: 'skeleton-document-with-preamble-empty-section-2', + content: '', + metadata: { title: 'Empty Section 2' } +} +Creating section 2: { + title: 'Empty Section 3', + dTag: 'skeleton-document-with-preamble-empty-section-3', + content: '', + metadata: { title: 'Empty Section 3' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-3' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'skeleton' ], + [ 'title', 'Skeleton Document with Preamble' ], + [ 'version', 'Version' ], + [ 'summary', 'This is a skeleton document with preamble' ], + [ 't', 'skeleton' ], + [ 't', 'preamble' ], + [ 't', 'empty' ], + [ 'd', 'skeleton-document-with-preamble' ], + [ 'title', 'Skeleton Document with Preamble' ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-3' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Skeleton Structure without Preamble > should build 30040 event set with skeleton structure without preamble +Parsed AsciiDoc: { + metadata: { + title: 'Skeleton Document without Preamble', + version: 'Version', + summary: 'This is a skeleton document without preamble', + tags: [ 'skeleton', 'no-preamble', 'empty' ] + }, + content: '= Skeleton Document without Preamble\n' + + ':summary: This is a skeleton document without preamble\n' + + ':keywords: skeleton, no-preamble, empty\n' + + '\n' + + '== Empty Section 1\n' + + '\n' + + '== Empty Section 2\n' + + '\n' + + '== Empty Section 3', + sections: [ + { metadata: [Object], content: '', title: 'Empty Section 1' }, + { metadata: [Object], content: '', title: 'Empty Section 2' }, + { metadata: [Object], content: '', title: 'Empty Section 3' } + ] +} +Index event: { + documentTitle: 'Skeleton Document without Preamble', + indexDTag: 'skeleton-document-without-preamble' +} +Creating section 0: { + title: 'Empty Section 1', + dTag: 'skeleton-document-without-preamble-empty-section-1', + content: '', + metadata: { title: 'Empty Section 1' } +} +Creating section 1: { + title: 'Empty Section 2', + dTag: 'skeleton-document-without-preamble-empty-section-2', + content: '', + metadata: { title: 'Empty Section 2' } +} +Creating section 2: { + title: 'Empty Section 3', + dTag: 'skeleton-document-without-preamble-empty-section-3', + content: '', + metadata: { title: 'Empty Section 3' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-3' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'skeleton' ], + [ 'title', 'Skeleton Document without Preamble' ], + [ 'version', 'Version' ], + [ 'summary', 'This is a skeleton document without preamble' ], + [ 't', 'skeleton' ], + [ 't', 'no-preamble' ], + [ 't', 'empty' ], + [ 'd', 'skeleton-document-without-preamble' ], + [ 'title', 'Skeleton Document without Preamble' ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-3' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Index Card Format > should build 30040 event set for index card format +Parsed AsciiDoc: { + metadata: { title: 'Test Index Card', version: 'Version' }, + content: '= Test Index Card\nindex card', + sections: [] +} +Creating index card format (no sections) + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Index Card Format > should build 30040 event set for index card with metadata +Parsed AsciiDoc: { + metadata: { + title: 'Test Index Card with Metadata', + version: 'Version', + summary: 'This is an index card with metadata', + tags: [ 'index', 'card', 'metadata' ] + }, + content: '= Test Index Card with Metadata\n' + + ':summary: This is an index card with metadata\n' + + ':keywords: index, card, metadata\n' + + 'index card', + sections: [] +} +Index event: { + documentTitle: 'Test Index Card with Metadata', + indexDTag: 'test-index-card-with-metadata' +} +A tags: [] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'index-card' ], + [ 'title', 'Test Index Card with Metadata' ], + [ 'version', 'Version' ], + [ 'summary', 'This is an index card with metadata' ], + [ 't', 'index' ], + [ 't', 'card' ], + [ 't', 'metadata' ], + [ 'd', 'test-index-card-with-metadata' ], + [ 'title', 'Test Index Card with Metadata' ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Complex Metadata Structures > should handle complex metadata with all attribute types +Parsed AsciiDoc: { + metadata: { + title: 'Complex Metadata Document', + authors: [ + 'Jane Smith', + 'Override Author', + 'Third Author', + 'Section Author', + 'Section Co-Author' + ], + version: '2.0', + publicationDate: '2024-03-01', + summary: 'This is a complex document with all metadata types Alternative description field', + publishedBy: 'Alexandria Complex', + type: 'book', + coverImage: 'https://example.com/cover.jpg', + isbn: '978-0-123456-78-9', + source: 'https://github.com/alexandria/complex', + autoUpdate: 'yes', + tags: [ + 'additional', + 'tags', + 'here', + 'complex', + 'metadata', + 'all-types' + ] + }, + content: '= Complex Metadata Document\n' + + 'Jane Smith \n' + + '2.0, 2024-02-20, Alexandria Complex\n' + + ':summary: This is a complex document with all metadata types\n' + + ':description: Alternative description field\n' + + ':keywords: complex, metadata, all-types\n' + + ':tags: additional, tags, here\n' + + ':author: Override Author\n' + + ':author: Third Author\n' + + ':version: 3.0\n' + + ':published_on: 2024-03-01\n' + + ':published_by: Alexandria Complex\n' + + ':type: book\n' + + ':image: https://example.com/cover.jpg\n' + + ':isbn: 978-0-123456-78-9\n' + + ':source: https://github.com/alexandria/complex\n' + + ':auto-update: yes\n' + + '\n' + + 'This is the preamble content.\n' + + '\n' + + '== Section with Complex Metadata\n' + + ':author: Section Author\n' + + ':author: Section Co-Author\n' + + ':summary: This section has complex metadata\n' + + ':description: Alternative description for section\n' + + ':keywords: section, complex, metadata\n' + + ':tags: section, tags\n' + + ':type: chapter\n' + + ':image: https://example.com/section-image.jpg\n' + + '\n' + + 'This is the section content.', + sections: [ + { + metadata: [Object], + content: 'This is the section content.', + title: 'Section with Complex Metadata' + } + ] +} +Index event: { + documentTitle: 'Complex Metadata Document', + indexDTag: 'complex-metadata-document' +} +Creating section 0: { + title: 'Section with Complex Metadata', + dTag: 'complex-metadata-document-section-with-complex-metadata', + content: 'This is the section content.', + metadata: { + title: 'Section with Complex Metadata', + authors: [ 'Section Author', 'Section Co-Author' ], + summary: 'This section has complex metadata Alternative description for section', + type: 'chapter', + coverImage: 'https://example.com/section-image.jpg', + tags: [ 'section', 'tags', 'complex', 'metadata' ] + } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:complex-metadata-document-section-with-complex-metadata' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'complex' ], + [ 'title', 'Complex Metadata Document' ], + [ 'author', 'Jane Smith' ], + [ 'author', 'Override Author' ], + [ 'author', 'Third Author' ], + [ 'author', 'Section Author' ], + [ 'author', 'Section Co-Author' ], + [ 'version', '2.0' ], + [ 'published_on', '2024-03-01' ], + [ 'published_by', 'Alexandria Complex' ], + [ + 'summary', + 'This is a complex document with all metadata types Alternative description field' + ], + [ 'image', 'https://example.com/cover.jpg' ], + [ 'i', '978-0-123456-78-9' ], + [ 'source', 'https://github.com/alexandria/complex' ], + [ 'type', 'book' ], + [ 'auto-update', 'yes' ], + [ 't', 'additional' ], + [ 't', 'tags' ], + [ 't', 'here' ], + [ 't', 'complex' ], + [ 't', 'metadata' ], + [ 't', 'all-types' ], + [ 'd', 'complex-metadata-document' ], + [ 'title', 'Complex Metadata Document' ], + [ + 'a', + '30041:test-pubkey:complex-metadata-document-section-with-complex-metadata' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Edge Cases > should handle document with only title and no sections +Parsed AsciiDoc: { + metadata: { + title: 'Document with No Sections', + version: 'Version', + summary: 'This document has no sections' + }, + content: '= Document with No Sections\n' + + ':summary: This document has no sections\n' + + '\n' + + 'This is just preamble content.', + sections: [] +} +Index event: { + documentTitle: 'Document with No Sections', + indexDTag: 'document-with-no-sections' +} +A tags: [] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'title', 'Document with No Sections' ], + [ 'version', 'Version' ], + [ 'summary', 'This document has no sections' ], + [ 'd', 'document-with-no-sections' ], + [ 'title', 'Document with No Sections' ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Edge Cases > should handle document with special characters in title +Parsed AsciiDoc: { + metadata: { + title: 'Document with Special Characters: Test & More!', + version: 'Version', + summary: 'This document has special characters in the title' + }, + content: '= Document with Special Characters: Test & More!\n' + + ':summary: This document has special characters in the title\n' + + '\n' + + '== Section 1\n' + + '\n' + + 'Content here.', + sections: [ + { + metadata: [Object], + content: 'Content here.', + title: 'Section 1' + } + ] +} +Index event: { + documentTitle: 'Document with Special Characters: Test & More!', + indexDTag: 'document-with-special-characters-test-more' +} +Creating section 0: { + title: 'Section 1', + dTag: 'document-with-special-characters-test-more-section-1', + content: 'Content here.', + metadata: { title: 'Section 1' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:document-with-special-characters-test-more-section-1' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'title', 'Document with Special Characters: Test & More!' ], + [ 'version', 'Version' ], + [ 'summary', 'This document has special characters in the title' ], + [ 'd', 'document-with-special-characters-test-more' ], + [ 'title', 'Document with Special Characters: Test & More!' ], + [ + 'a', + '30041:test-pubkey:document-with-special-characters-test-more-section-1' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Edge Cases > should handle document with very long title +Parsed AsciiDoc: { + metadata: { + title: 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality', + version: 'Version', + summary: 'This document has a very long title' + }, + content: '= This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality\n' + + ':summary: This document has a very long title\n' + + '\n' + + '== Section 1\n' + + '\n' + + 'Content here.', + sections: [ + { + metadata: [Object], + content: 'Content here.', + title: 'Section 1' + } + ] +} +Index event: { + documentTitle: 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality', + indexDTag: 'this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality' +} +Creating section 0: { + title: 'Section 1', + dTag: 'this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality-section-1', + content: 'Content here.', + metadata: { title: 'Section 1' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality-section-1' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ + 'title', + 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality' + ], + [ 'version', 'Version' ], + [ 'summary', 'This document has a very long title' ], + [ + 'd', + 'this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality' + ], + [ + 'title', + 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality' + ], + [ + 'a', + '30041:test-pubkey:this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality-section-1' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + + ✓ tests/unit/eventInput30040.test.ts (14 tests) 389ms +(node:1443840) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension. +(Use `node --trace-warnings ...` to show where the warning was created) + ✓ tests/unit/mathProcessing.test.ts (18 tests) 16ms + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/unit/metadataExtraction.test.ts > AsciiDoc Metadata Extraction > extractDocumentMetadata should extract document metadata correctly +AssertionError: expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] + +- Expected ++ Received + + [ + "John Doe", + "Jane Smith", ++ "Section Author", + ] + + ❯ tests/unit/metadataExtraction.test.ts:44:30 + 42| + 43| expect(metadata.title).toBe("Test Document with Metadata"); + 44| expect(metadata.authors).toEqual(["John Doe", "Jane Smith"]); + | ^ + 45| expect(metadata.version).toBe("1.0"); + 46| expect(metadata.publicationDate).toBe("2024-01-15"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ + + FAIL tests/unit/metadataExtraction.test.ts > AsciiDoc Metadata Extraction > Smart metadata extraction > should handle document with full header correctly +AssertionError: expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] + +- Expected ++ Received + + [ + "John Doe", + "Jane Smith", ++ "Section Author", + ] + + ❯ tests/unit/metadataExtraction.test.ts:318:32 + 316| // Should extract document-level metadata + 317| expect(metadata.title).toBe("Test Document"); + 318| expect(metadata.authors).toEqual(["John Doe", "Jane Smith"]); + | ^ + 319| expect(metadata.version).toBe("1.0"); + 320| expect(metadata.publishedBy).toBe("Alexandria Test"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯ + + + Test Files 1 failed | 6 passed (7) + Tests 2 failed | 116 passed (118) + Start at 13:16:23 + Duration 2.53s (transform 1.97s, setup 0ms, collect 3.66s, tests 710ms, environment 2ms, prepare 1.10s) + + FAIL Tests failed. Watching for file changes... + press h to show help, press q to quit +c RERUN src/lib/utils/asciidoc_metadata.ts + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Normal Structure with Preamble > should build 30040 event set with preamble content +Parsed AsciiDoc: { + metadata: { + title: 'Test Document with Preamble', + authors: [ 'John Doe', 'Section Author' ], + version: '1.0', + publicationDate: '2024-01-15, Alexandria Test', + summary: 'This is a test document with preamble', + tags: [ 'test', 'preamble', 'asciidoc' ] + }, + content: '= Test Document with Preamble\n' + + 'John Doe \n' + + '1.0, 2024-01-15, Alexandria Test\n' + + ':summary: This is a test document with preamble\n' + + ':keywords: test, preamble, asciidoc\n' + + '\n' + + 'This is the preamble content that should be included.\n' + + '\n' + + '== First Section\n' + + ':author: Section Author\n' + + ':summary: This is the first section\n' + + '\n' + + 'This is the content of the first section.\n' + + '\n' + + '== Second Section\n' + + ':summary: This is the second section\n' + + '\n' + + 'This is the content of the second section.', + sections: [ + { + metadata: [Object], + content: 'This is the content of the first section.', + title: 'First Section' + }, + { + metadata: [Object], + content: 'This is the content of the second section.', + title: 'Second Section' + } + ] +} +Index event: { + documentTitle: 'Test Document with Preamble', + indexDTag: 'test-document-with-preamble' +} +Creating section 0: { + title: 'First Section', + dTag: 'test-document-with-preamble-first-section', + content: 'This is the content of the first section.', + metadata: { + title: 'First Section', + authors: [ 'Section Author' ], + summary: 'This is the first section' + } +} +Creating section 1: { + title: 'Second Section', + dTag: 'test-document-with-preamble-second-section', + content: 'This is the content of the second section.', + metadata: { title: 'Second Section', summary: 'This is the second section' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-second-section' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'article' ], + [ 'title', 'Test Document with Preamble' ], + [ 'author', 'John Doe' ], + [ 'author', 'Section Author' ], + [ 'version', '1.0' ], + [ 'published_on', '2024-01-15, Alexandria Test' ], + [ 'summary', 'This is a test document with preamble' ], + [ 't', 'test' ], + [ 't', 'preamble' ], + [ 't', 'asciidoc' ], + [ 'd', 'test-document-with-preamble' ], + [ 'title', 'Test Document with Preamble' ], + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-second-section' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Normal Structure without Preamble > should build 30040 event set without preamble content +Parsed AsciiDoc: { + metadata: { + title: 'Test Document without Preamble', + authors: [ 'Section Author' ], + version: 'Version', + summary: 'This is a test document without preamble', + tags: [ 'test', 'no-preamble', 'asciidoc' ] + }, + content: '= Test Document without Preamble\n' + + ':summary: This is a test document without preamble\n' + + ':keywords: test, no-preamble, asciidoc\n' + + '\n' + + '== First Section\n' + + ':author: Section Author\n' + + ':summary: This is the first section\n' + + '\n' + + 'This is the content of the first section.\n' + + '\n' + + '== Second Section\n' + + ':summary: This is the second section\n' + + '\n' + + 'This is the content of the second section.', + sections: [ + { + metadata: [Object], + content: 'This is the content of the first section.', + title: 'First Section' + }, + { + metadata: [Object], + content: 'This is the content of the second section.', + title: 'Second Section' + } + ] +} +Index event: { + documentTitle: 'Test Document without Preamble', + indexDTag: 'test-document-without-preamble' +} +Creating section 0: { + title: 'First Section', + dTag: 'test-document-without-preamble-first-section', + content: 'This is the content of the first section.', + metadata: { + title: 'First Section', + authors: [ 'Section Author' ], + summary: 'This is the first section' + } +} +Creating section 1: { + title: 'Second Section', + dTag: 'test-document-without-preamble-second-section', + content: 'This is the content of the second section.', + metadata: { title: 'Second Section', summary: 'This is the second section' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-second-section' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'article' ], + [ 'title', 'Test Document without Preamble' ], + [ 'author', 'Section Author' ], + [ 'version', 'Version' ], + [ 'summary', 'This is a test document without preamble' ], + [ 't', 'test' ], + [ 't', 'no-preamble' ], + [ 't', 'asciidoc' ], + [ 'd', 'test-document-without-preamble' ], + [ 'title', 'Test Document without Preamble' ], + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-second-section' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Skeleton Structure with Preamble > should build 30040 event set with skeleton structure and preamble +Parsed AsciiDoc: { + metadata: { + title: 'Skeleton Document with Preamble', + version: 'Version', + summary: 'This is a skeleton document with preamble', + tags: [ 'skeleton', 'preamble', 'empty' ] + }, + content: '= Skeleton Document with Preamble\n' + + ':summary: This is a skeleton document with preamble\n' + + ':keywords: skeleton, preamble, empty\n' + + '\n' + + 'This is the preamble content.\n' + + '\n' + + '== Empty Section 1\n' + + '\n' + + '== Empty Section 2\n' + + '\n' + + '== Empty Section 3', + sections: [ + { metadata: [Object], content: '', title: 'Empty Section 1' }, + { metadata: [Object], content: '', title: 'Empty Section 2' }, + { metadata: [Object], content: '', title: 'Empty Section 3' } + ] +} +Index event: { + documentTitle: 'Skeleton Document with Preamble', + indexDTag: 'skeleton-document-with-preamble' +} +Creating section 0: { + title: 'Empty Section 1', + dTag: 'skeleton-document-with-preamble-empty-section-1', + content: '', + metadata: { title: 'Empty Section 1' } +} +Creating section 1: { + title: 'Empty Section 2', + dTag: 'skeleton-document-with-preamble-empty-section-2', + content: '', + metadata: { title: 'Empty Section 2' } +} +Creating section 2: { + title: 'Empty Section 3', + dTag: 'skeleton-document-with-preamble-empty-section-3', + content: '', + metadata: { title: 'Empty Section 3' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-3' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'skeleton' ], + [ 'title', 'Skeleton Document with Preamble' ], + [ 'version', 'Version' ], + [ 'summary', 'This is a skeleton document with preamble' ], + [ 't', 'skeleton' ], + [ 't', 'preamble' ], + [ 't', 'empty' ], + [ 'd', 'skeleton-document-with-preamble' ], + [ 'title', 'Skeleton Document with Preamble' ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-3' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + + ❯ tests/unit/metadataExtraction.test.ts (16 tests | 2 failed) 202ms + × AsciiDoc Metadata Extraction > extractDocumentMetadata should extract document metadata correctly 116ms + → expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should extract section metadata correctly 7ms + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should extract standalone author names and remove them from content 4ms + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should handle multiple standalone author names 5ms + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should not extract non-author lines as authors 4ms + ✓ AsciiDoc Metadata Extraction > parseAsciiDocWithMetadata should parse complete document 20ms + ✓ AsciiDoc Metadata Extraction > metadataToTags should convert metadata to Nostr tags 2ms + ✓ AsciiDoc Metadata Extraction > should handle index card format correctly 4ms + ✓ AsciiDoc Metadata Extraction > should handle empty content gracefully 4ms + ✓ AsciiDoc Metadata Extraction > should handle keywords as tags 4ms + ✓ AsciiDoc Metadata Extraction > should handle both tags and keywords 4ms + ✓ AsciiDoc Metadata Extraction > should handle tags only 3ms + ✓ AsciiDoc Metadata Extraction > should handle both summary and description 7ms + ✓ AsciiDoc Metadata Extraction > Smart metadata extraction > should handle section-only content correctly 8ms + ✓ AsciiDoc Metadata Extraction > Smart metadata extraction > should handle minimal document header (just title) correctly 1ms + × AsciiDoc Metadata Extraction > Smart metadata extraction > should handle document with full header correctly 7ms + → expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Skeleton Structure without Preamble > should build 30040 event set with skeleton structure without preamble +Parsed AsciiDoc: { + metadata: { + title: 'Skeleton Document without Preamble', + version: 'Version', + summary: 'This is a skeleton document without preamble', + tags: [ 'skeleton', 'no-preamble', 'empty' ] + }, + content: '= Skeleton Document without Preamble\n' + + ':summary: This is a skeleton document without preamble\n' + + ':keywords: skeleton, no-preamble, empty\n' + + '\n' + + '== Empty Section 1\n' + + '\n' + + '== Empty Section 2\n' + + '\n' + + '== Empty Section 3', + sections: [ + { metadata: [Object], content: '', title: 'Empty Section 1' }, + { metadata: [Object], content: '', title: 'Empty Section 2' }, + { metadata: [Object], content: '', title: 'Empty Section 3' } + ] +} +Index event: { + documentTitle: 'Skeleton Document without Preamble', + indexDTag: 'skeleton-document-without-preamble' +} +Creating section 0: { + title: 'Empty Section 1', + dTag: 'skeleton-document-without-preamble-empty-section-1', + content: '', + metadata: { title: 'Empty Section 1' } +} +Creating section 1: { + title: 'Empty Section 2', + dTag: 'skeleton-document-without-preamble-empty-section-2', + content: '', + metadata: { title: 'Empty Section 2' } +} +Creating section 2: { + title: 'Empty Section 3', + dTag: 'skeleton-document-without-preamble-empty-section-3', + content: '', + metadata: { title: 'Empty Section 3' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-3' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'skeleton' ], + [ 'title', 'Skeleton Document without Preamble' ], + [ 'version', 'Version' ], + [ 'summary', 'This is a skeleton document without preamble' ], + [ 't', 'skeleton' ], + [ 't', 'no-preamble' ], + [ 't', 'empty' ], + [ 'd', 'skeleton-document-without-preamble' ], + [ 'title', 'Skeleton Document without Preamble' ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-3' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Index Card Format > should build 30040 event set for index card format +Parsed AsciiDoc: { + metadata: { title: 'Test Index Card', version: 'Version' }, + content: '= Test Index Card\nindex card', + sections: [] +} +Creating index card format (no sections) + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Index Card Format > should build 30040 event set for index card with metadata +Parsed AsciiDoc: { + metadata: { + title: 'Test Index Card with Metadata', + version: 'Version', + summary: 'This is an index card with metadata', + tags: [ 'index', 'card', 'metadata' ] + }, + content: '= Test Index Card with Metadata\n' + + ':summary: This is an index card with metadata\n' + + ':keywords: index, card, metadata\n' + + 'index card', + sections: [] +} +Index event: { + documentTitle: 'Test Index Card with Metadata', + indexDTag: 'test-index-card-with-metadata' +} +A tags: [] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'index-card' ], + [ 'title', 'Test Index Card with Metadata' ], + [ 'version', 'Version' ], + [ 'summary', 'This is an index card with metadata' ], + [ 't', 'index' ], + [ 't', 'card' ], + [ 't', 'metadata' ], + [ 'd', 'test-index-card-with-metadata' ], + [ 'title', 'Test Index Card with Metadata' ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Complex Metadata Structures > should handle complex metadata with all attribute types +Parsed AsciiDoc: { + metadata: { + title: 'Complex Metadata Document', + authors: [ + 'Jane Smith', + 'Override Author', + 'Third Author', + 'Section Author', + 'Section Co-Author' + ], + version: '2.0', + publicationDate: '2024-03-01', + summary: 'This is a complex document with all metadata types Alternative description field', + publishedBy: 'Alexandria Complex', + type: 'book', + coverImage: 'https://example.com/cover.jpg', + isbn: '978-0-123456-78-9', + source: 'https://github.com/alexandria/complex', + autoUpdate: 'yes', + tags: [ + 'additional', + 'tags', + 'here', + 'complex', + 'metadata', + 'all-types' + ] + }, + content: '= Complex Metadata Document\n' + + 'Jane Smith \n' + + '2.0, 2024-02-20, Alexandria Complex\n' + + ':summary: This is a complex document with all metadata types\n' + + ':description: Alternative description field\n' + + ':keywords: complex, metadata, all-types\n' + + ':tags: additional, tags, here\n' + + ':author: Override Author\n' + + ':author: Third Author\n' + + ':version: 3.0\n' + + ':published_on: 2024-03-01\n' + + ':published_by: Alexandria Complex\n' + + ':type: book\n' + + ':image: https://example.com/cover.jpg\n' + + ':isbn: 978-0-123456-78-9\n' + + ':source: https://github.com/alexandria/complex\n' + + ':auto-update: yes\n' + + '\n' + + 'This is the preamble content.\n' + + '\n' + + '== Section with Complex Metadata\n' + + ':author: Section Author\n' + + ':author: Section Co-Author\n' + + ':summary: This section has complex metadata\n' + + ':description: Alternative description for section\n' + + ':keywords: section, complex, metadata\n' + + ':tags: section, tags\n' + + ':type: chapter\n' + + ':image: https://example.com/section-image.jpg\n' + + '\n' + + 'This is the section content.', + sections: [ + { + metadata: [Object], + content: 'This is the section content.', + title: 'Section with Complex Metadata' + } + ] +} +Index event: { + documentTitle: 'Complex Metadata Document', + indexDTag: 'complex-metadata-document' +} +Creating section 0: { + title: 'Section with Complex Metadata', + dTag: 'complex-metadata-document-section-with-complex-metadata', + content: 'This is the section content.', + metadata: { + title: 'Section with Complex Metadata', + authors: [ 'Section Author', 'Section Co-Author' ], + summary: 'This section has complex metadata Alternative description for section', + type: 'chapter', + coverImage: 'https://example.com/section-image.jpg', + tags: [ 'section', 'tags', 'complex', 'metadata' ] + } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:complex-metadata-document-section-with-complex-metadata' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'complex' ], + [ 'title', 'Complex Metadata Document' ], + [ 'author', 'Jane Smith' ], + [ 'author', 'Override Author' ], + [ 'author', 'Third Author' ], + [ 'author', 'Section Author' ], + [ 'author', 'Section Co-Author' ], + [ 'version', '2.0' ], + [ 'published_on', '2024-03-01' ], + [ 'published_by', 'Alexandria Complex' ], + [ + 'summary', + 'This is a complex document with all metadata types Alternative description field' + ], + [ 'image', 'https://example.com/cover.jpg' ], + [ 'i', '978-0-123456-78-9' ], + [ 'source', 'https://github.com/alexandria/complex' ], + [ 'type', 'book' ], + [ 'auto-update', 'yes' ], + [ 't', 'additional' ], + [ 't', 'tags' ], + [ 't', 'here' ], + [ 't', 'complex' ], + [ 't', 'metadata' ], + [ 't', 'all-types' ], + [ 'd', 'complex-metadata-document' ], + [ 'title', 'Complex Metadata Document' ], + [ + 'a', + '30041:test-pubkey:complex-metadata-document-section-with-complex-metadata' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Edge Cases > should handle document with only title and no sections +Parsed AsciiDoc: { + metadata: { + title: 'Document with No Sections', + version: 'Version', + summary: 'This document has no sections' + }, + content: '= Document with No Sections\n' + + ':summary: This document has no sections\n' + + '\n' + + 'This is just preamble content.', + sections: [] +} +Index event: { + documentTitle: 'Document with No Sections', + indexDTag: 'document-with-no-sections' +} +A tags: [] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'title', 'Document with No Sections' ], + [ 'version', 'Version' ], + [ 'summary', 'This document has no sections' ], + [ 'd', 'document-with-no-sections' ], + [ 'title', 'Document with No Sections' ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Edge Cases > should handle document with special characters in title +Parsed AsciiDoc: { + metadata: { + title: 'Document with Special Characters: Test & More!', + version: 'Version', + summary: 'This document has special characters in the title' + }, + content: '= Document with Special Characters: Test & More!\n' + + ':summary: This document has special characters in the title\n' + + '\n' + + '== Section 1\n' + + '\n' + + 'Content here.', + sections: [ + { + metadata: [Object], + content: 'Content here.', + title: 'Section 1' + } + ] +} +Index event: { + documentTitle: 'Document with Special Characters: Test & More!', + indexDTag: 'document-with-special-characters-test-more' +} +Creating section 0: { + title: 'Section 1', + dTag: 'document-with-special-characters-test-more-section-1', + content: 'Content here.', + metadata: { title: 'Section 1' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:document-with-special-characters-test-more-section-1' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'title', 'Document with Special Characters: Test & More!' ], + [ 'version', 'Version' ], + [ 'summary', 'This document has special characters in the title' ], + [ 'd', 'document-with-special-characters-test-more' ], + [ 'title', 'Document with Special Characters: Test & More!' ], + [ + 'a', + '30041:test-pubkey:document-with-special-characters-test-more-section-1' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Edge Cases > should handle document with very long title +Parsed AsciiDoc: { + metadata: { + title: 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality', + version: 'Version', + summary: 'This document has a very long title' + }, + content: '= This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality\n' + + ':summary: This document has a very long title\n' + + '\n' + + '== Section 1\n' + + '\n' + + 'Content here.', + sections: [ + { + metadata: [Object], + content: 'Content here.', + title: 'Section 1' + } + ] +} +Index event: { + documentTitle: 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality', + indexDTag: 'this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality' +} +Creating section 0: { + title: 'Section 1', + dTag: 'this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality-section-1', + content: 'Content here.', + metadata: { title: 'Section 1' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality-section-1' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ + 'title', + 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality' + ], + [ 'version', 'Version' ], + [ 'summary', 'This document has a very long title' ], + [ + 'd', + 'this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality' + ], + [ + 'title', + 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality' + ], + [ + 'a', + '30041:test-pubkey:this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality-section-1' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + + ✓ tests/unit/eventInput30040.test.ts (14 tests) 333ms + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/unit/metadataExtraction.test.ts > AsciiDoc Metadata Extraction > extractDocumentMetadata should extract document metadata correctly +AssertionError: expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] + +- Expected ++ Received + + [ + "John Doe", + "Jane Smith", ++ "Section Author", + ] + + ❯ tests/unit/metadataExtraction.test.ts:44:30 + 42| + 43| expect(metadata.title).toBe("Test Document with Metadata"); + 44| expect(metadata.authors).toEqual(["John Doe", "Jane Smith"]); + | ^ + 45| expect(metadata.version).toBe("1.0"); + 46| expect(metadata.publicationDate).toBe("2024-01-15"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ + + FAIL tests/unit/metadataExtraction.test.ts > AsciiDoc Metadata Extraction > Smart metadata extraction > should handle document with full header correctly +AssertionError: expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] + +- Expected ++ Received + + [ + "John Doe", + "Jane Smith", ++ "Section Author", + ] + + ❯ tests/unit/metadataExtraction.test.ts:318:32 + 316| // Should extract document-level metadata + 317| expect(metadata.title).toBe("Test Document"); + 318| expect(metadata.authors).toEqual(["John Doe", "Jane Smith"]); + | ^ + 319| expect(metadata.version).toBe("1.0"); + 320| expect(metadata.publishedBy).toBe("Alexandria Test"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯ + + + Test Files 1 failed | 1 passed (2) + Tests 2 failed | 28 passed (30) + Start at 13:18:57 + Duration 1.00s + + FAIL Tests failed. Watching for file changes... + press h to show help, press q to quit +c RERUN src/lib/utils/asciidoc_metadata.ts + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Normal Structure with Preamble > should build 30040 event set with preamble content +Parsed AsciiDoc: { + metadata: { + title: 'Test Document with Preamble', + authors: [ 'John Doe', 'Section Author' ], + version: '1.0', + publicationDate: '2024-01-15, Alexandria Test', + summary: 'This is a test document with preamble', + tags: [ 'test', 'preamble', 'asciidoc' ] + }, + content: '= Test Document with Preamble\n' + + 'John Doe \n' + + '1.0, 2024-01-15, Alexandria Test\n' + + ':summary: This is a test document with preamble\n' + + ':keywords: test, preamble, asciidoc\n' + + '\n' + + 'This is the preamble content that should be included.\n' + + '\n' + + '== First Section\n' + + ':author: Section Author\n' + + ':summary: This is the first section\n' + + '\n' + + 'This is the content of the first section.\n' + + '\n' + + '== Second Section\n' + + ':summary: This is the second section\n' + + '\n' + + 'This is the content of the second section.', + sections: [ + { + metadata: [Object], + content: 'This is the content of the first section.', + title: 'First Section' + }, + { + metadata: [Object], + content: 'This is the content of the second section.', + title: 'Second Section' + } + ] +} +Index event: { + documentTitle: 'Test Document with Preamble', + indexDTag: 'test-document-with-preamble' +} +Creating section 0: { + title: 'First Section', + dTag: 'test-document-with-preamble-first-section', + content: 'This is the content of the first section.', + metadata: { + title: 'First Section', + authors: [ 'Section Author' ], + summary: 'This is the first section' + } +} +Creating section 1: { + title: 'Second Section', + dTag: 'test-document-with-preamble-second-section', + content: 'This is the content of the second section.', + metadata: { title: 'Second Section', summary: 'This is the second section' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-second-section' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'article' ], + [ 'title', 'Test Document with Preamble' ], + [ 'author', 'John Doe' ], + [ 'author', 'Section Author' ], + [ 'version', '1.0' ], + [ 'published_on', '2024-01-15, Alexandria Test' ], + [ 'summary', 'This is a test document with preamble' ], + [ 't', 'test' ], + [ 't', 'preamble' ], + [ 't', 'asciidoc' ], + [ 'd', 'test-document-with-preamble' ], + [ 'title', 'Test Document with Preamble' ], + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-second-section' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Normal Structure without Preamble > should build 30040 event set without preamble content +Parsed AsciiDoc: { + metadata: { + title: 'Test Document without Preamble', + authors: [ 'Section Author' ], + version: 'Version', + summary: 'This is a test document without preamble', + tags: [ 'test', 'no-preamble', 'asciidoc' ] + }, + content: '= Test Document without Preamble\n' + + ':summary: This is a test document without preamble\n' + + ':keywords: test, no-preamble, asciidoc\n' + + '\n' + + '== First Section\n' + + ':author: Section Author\n' + + ':summary: This is the first section\n' + + '\n' + + 'This is the content of the first section.\n' + + '\n' + + '== Second Section\n' + + ':summary: This is the second section\n' + + '\n' + + 'This is the content of the second section.', + sections: [ + { + metadata: [Object], + content: 'This is the content of the first section.', + title: 'First Section' + }, + { + metadata: [Object], + content: 'This is the content of the second section.', + title: 'Second Section' + } + ] +} +Index event: { + documentTitle: 'Test Document without Preamble', + indexDTag: 'test-document-without-preamble' +} +Creating section 0: { + title: 'First Section', + dTag: 'test-document-without-preamble-first-section', + content: 'This is the content of the first section.', + metadata: { + title: 'First Section', + authors: [ 'Section Author' ], + summary: 'This is the first section' + } +} +Creating section 1: { + title: 'Second Section', + dTag: 'test-document-without-preamble-second-section', + content: 'This is the content of the second section.', + metadata: { title: 'Second Section', summary: 'This is the second section' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-second-section' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'article' ], + [ 'title', 'Test Document without Preamble' ], + [ 'author', 'Section Author' ], + [ 'version', 'Version' ], + [ 'summary', 'This is a test document without preamble' ], + [ 't', 'test' ], + [ 't', 'no-preamble' ], + [ 't', 'asciidoc' ], + [ 'd', 'test-document-without-preamble' ], + [ 'title', 'Test Document without Preamble' ], + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-second-section' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + + ❯ tests/unit/metadataExtraction.test.ts (16 tests | 2 failed) 228ms + × AsciiDoc Metadata Extraction > extractDocumentMetadata should extract document metadata correctly 129ms + → expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should extract section metadata correctly 8ms + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should extract standalone author names and remove them from content 5ms + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should handle multiple standalone author names 5ms + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should not extract non-author lines as authors 4ms + ✓ AsciiDoc Metadata Extraction > parseAsciiDocWithMetadata should parse complete document 21ms + ✓ AsciiDoc Metadata Extraction > metadataToTags should convert metadata to Nostr tags 2ms + ✓ AsciiDoc Metadata Extraction > should handle index card format correctly 5ms + ✓ AsciiDoc Metadata Extraction > should handle empty content gracefully 4ms + ✓ AsciiDoc Metadata Extraction > should handle keywords as tags 4ms + ✓ AsciiDoc Metadata Extraction > should handle both tags and keywords 5ms + ✓ AsciiDoc Metadata Extraction > should handle tags only 4ms + ✓ AsciiDoc Metadata Extraction > should handle both summary and description 8ms + ✓ AsciiDoc Metadata Extraction > Smart metadata extraction > should handle section-only content correctly 10ms + ✓ AsciiDoc Metadata Extraction > Smart metadata extraction > should handle minimal document header (just title) correctly 1ms + × AsciiDoc Metadata Extraction > Smart metadata extraction > should handle document with full header correctly 9ms + → expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Skeleton Structure with Preamble > should build 30040 event set with skeleton structure and preamble +Parsed AsciiDoc: { + metadata: { + title: 'Skeleton Document with Preamble', + version: 'Version', + summary: 'This is a skeleton document with preamble', + tags: [ 'skeleton', 'preamble', 'empty' ] + }, + content: '= Skeleton Document with Preamble\n' + + ':summary: This is a skeleton document with preamble\n' + + ':keywords: skeleton, preamble, empty\n' + + '\n' + + 'This is the preamble content.\n' + + '\n' + + '== Empty Section 1\n' + + '\n' + + '== Empty Section 2\n' + + '\n' + + '== Empty Section 3', + sections: [ + { metadata: [Object], content: '', title: 'Empty Section 1' }, + { metadata: [Object], content: '', title: 'Empty Section 2' }, + { metadata: [Object], content: '', title: 'Empty Section 3' } + ] +} +Index event: { + documentTitle: 'Skeleton Document with Preamble', + indexDTag: 'skeleton-document-with-preamble' +} +Creating section 0: { + title: 'Empty Section 1', + dTag: 'skeleton-document-with-preamble-empty-section-1', + content: '', + metadata: { title: 'Empty Section 1' } +} +Creating section 1: { + title: 'Empty Section 2', + dTag: 'skeleton-document-with-preamble-empty-section-2', + content: '', + metadata: { title: 'Empty Section 2' } +} +Creating section 2: { + title: 'Empty Section 3', + dTag: 'skeleton-document-with-preamble-empty-section-3', + content: '', + metadata: { title: 'Empty Section 3' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-3' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'skeleton' ], + [ 'title', 'Skeleton Document with Preamble' ], + [ 'version', 'Version' ], + [ 'summary', 'This is a skeleton document with preamble' ], + [ 't', 'skeleton' ], + [ 't', 'preamble' ], + [ 't', 'empty' ], + [ 'd', 'skeleton-document-with-preamble' ], + [ 'title', 'Skeleton Document with Preamble' ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-3' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Skeleton Structure without Preamble > should build 30040 event set with skeleton structure without preamble +Parsed AsciiDoc: { + metadata: { + title: 'Skeleton Document without Preamble', + version: 'Version', + summary: 'This is a skeleton document without preamble', + tags: [ 'skeleton', 'no-preamble', 'empty' ] + }, + content: '= Skeleton Document without Preamble\n' + + ':summary: This is a skeleton document without preamble\n' + + ':keywords: skeleton, no-preamble, empty\n' + + '\n' + + '== Empty Section 1\n' + + '\n' + + '== Empty Section 2\n' + + '\n' + + '== Empty Section 3', + sections: [ + { metadata: [Object], content: '', title: 'Empty Section 1' }, + { metadata: [Object], content: '', title: 'Empty Section 2' }, + { metadata: [Object], content: '', title: 'Empty Section 3' } + ] +} +Index event: { + documentTitle: 'Skeleton Document without Preamble', + indexDTag: 'skeleton-document-without-preamble' +} +Creating section 0: { + title: 'Empty Section 1', + dTag: 'skeleton-document-without-preamble-empty-section-1', + content: '', + metadata: { title: 'Empty Section 1' } +} +Creating section 1: { + title: 'Empty Section 2', + dTag: 'skeleton-document-without-preamble-empty-section-2', + content: '', + metadata: { title: 'Empty Section 2' } +} +Creating section 2: { + title: 'Empty Section 3', + dTag: 'skeleton-document-without-preamble-empty-section-3', + content: '', + metadata: { title: 'Empty Section 3' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-3' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'skeleton' ], + [ 'title', 'Skeleton Document without Preamble' ], + [ 'version', 'Version' ], + [ 'summary', 'This is a skeleton document without preamble' ], + [ 't', 'skeleton' ], + [ 't', 'no-preamble' ], + [ 't', 'empty' ], + [ 'd', 'skeleton-document-without-preamble' ], + [ 'title', 'Skeleton Document without Preamble' ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-3' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Index Card Format > should build 30040 event set for index card format +Parsed AsciiDoc: { + metadata: { title: 'Test Index Card', version: 'Version' }, + content: '= Test Index Card\nindex card', + sections: [] +} +Creating index card format (no sections) + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Index Card Format > should build 30040 event set for index card with metadata +Parsed AsciiDoc: { + metadata: { + title: 'Test Index Card with Metadata', + version: 'Version', + summary: 'This is an index card with metadata', + tags: [ 'index', 'card', 'metadata' ] + }, + content: '= Test Index Card with Metadata\n' + + ':summary: This is an index card with metadata\n' + + ':keywords: index, card, metadata\n' + + 'index card', + sections: [] +} +Index event: { + documentTitle: 'Test Index Card with Metadata', + indexDTag: 'test-index-card-with-metadata' +} +A tags: [] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'index-card' ], + [ 'title', 'Test Index Card with Metadata' ], + [ 'version', 'Version' ], + [ 'summary', 'This is an index card with metadata' ], + [ 't', 'index' ], + [ 't', 'card' ], + [ 't', 'metadata' ], + [ 'd', 'test-index-card-with-metadata' ], + [ 'title', 'Test Index Card with Metadata' ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Complex Metadata Structures > should handle complex metadata with all attribute types +Parsed AsciiDoc: { + metadata: { + title: 'Complex Metadata Document', + authors: [ + 'Jane Smith', + 'Override Author', + 'Third Author', + 'Section Author', + 'Section Co-Author' + ], + version: '2.0', + publicationDate: '2024-03-01', + summary: 'This is a complex document with all metadata types Alternative description field', + publishedBy: 'Alexandria Complex', + type: 'book', + coverImage: 'https://example.com/cover.jpg', + isbn: '978-0-123456-78-9', + source: 'https://github.com/alexandria/complex', + autoUpdate: 'yes', + tags: [ + 'additional', + 'tags', + 'here', + 'complex', + 'metadata', + 'all-types' + ] + }, + content: '= Complex Metadata Document\n' + + 'Jane Smith \n' + + '2.0, 2024-02-20, Alexandria Complex\n' + + ':summary: This is a complex document with all metadata types\n' + + ':description: Alternative description field\n' + + ':keywords: complex, metadata, all-types\n' + + ':tags: additional, tags, here\n' + + ':author: Override Author\n' + + ':author: Third Author\n' + + ':version: 3.0\n' + + ':published_on: 2024-03-01\n' + + ':published_by: Alexandria Complex\n' + + ':type: book\n' + + ':image: https://example.com/cover.jpg\n' + + ':isbn: 978-0-123456-78-9\n' + + ':source: https://github.com/alexandria/complex\n' + + ':auto-update: yes\n' + + '\n' + + 'This is the preamble content.\n' + + '\n' + + '== Section with Complex Metadata\n' + + ':author: Section Author\n' + + ':author: Section Co-Author\n' + + ':summary: This section has complex metadata\n' + + ':description: Alternative description for section\n' + + ':keywords: section, complex, metadata\n' + + ':tags: section, tags\n' + + ':type: chapter\n' + + ':image: https://example.com/section-image.jpg\n' + + '\n' + + 'This is the section content.', + sections: [ + { + metadata: [Object], + content: 'This is the section content.', + title: 'Section with Complex Metadata' + } + ] +} +Index event: { + documentTitle: 'Complex Metadata Document', + indexDTag: 'complex-metadata-document' +} +Creating section 0: { + title: 'Section with Complex Metadata', + dTag: 'complex-metadata-document-section-with-complex-metadata', + content: 'This is the section content.', + metadata: { + title: 'Section with Complex Metadata', + authors: [ 'Section Author', 'Section Co-Author' ], + summary: 'This section has complex metadata Alternative description for section', + type: 'chapter', + coverImage: 'https://example.com/section-image.jpg', + tags: [ 'section', 'tags', 'complex', 'metadata' ] + } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:complex-metadata-document-section-with-complex-metadata' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'complex' ], + [ 'title', 'Complex Metadata Document' ], + [ 'author', 'Jane Smith' ], + [ 'author', 'Override Author' ], + [ 'author', 'Third Author' ], + [ 'author', 'Section Author' ], + [ 'author', 'Section Co-Author' ], + [ 'version', '2.0' ], + [ 'published_on', '2024-03-01' ], + [ 'published_by', 'Alexandria Complex' ], + [ + 'summary', + 'This is a complex document with all metadata types Alternative description field' + ], + [ 'image', 'https://example.com/cover.jpg' ], + [ 'i', '978-0-123456-78-9' ], + [ 'source', 'https://github.com/alexandria/complex' ], + [ 'type', 'book' ], + [ 'auto-update', 'yes' ], + [ 't', 'additional' ], + [ 't', 'tags' ], + [ 't', 'here' ], + [ 't', 'complex' ], + [ 't', 'metadata' ], + [ 't', 'all-types' ], + [ 'd', 'complex-metadata-document' ], + [ 'title', 'Complex Metadata Document' ], + [ + 'a', + '30041:test-pubkey:complex-metadata-document-section-with-complex-metadata' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Edge Cases > should handle document with only title and no sections +Parsed AsciiDoc: { + metadata: { + title: 'Document with No Sections', + version: 'Version', + summary: 'This document has no sections' + }, + content: '= Document with No Sections\n' + + ':summary: This document has no sections\n' + + '\n' + + 'This is just preamble content.', + sections: [] +} +Index event: { + documentTitle: 'Document with No Sections', + indexDTag: 'document-with-no-sections' +} +A tags: [] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'title', 'Document with No Sections' ], + [ 'version', 'Version' ], + [ 'summary', 'This document has no sections' ], + [ 'd', 'document-with-no-sections' ], + [ 'title', 'Document with No Sections' ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Edge Cases > should handle document with special characters in title +Parsed AsciiDoc: { + metadata: { + title: 'Document with Special Characters: Test & More!', + version: 'Version', + summary: 'This document has special characters in the title' + }, + content: '= Document with Special Characters: Test & More!\n' + + ':summary: This document has special characters in the title\n' + + '\n' + + '== Section 1\n' + + '\n' + + 'Content here.', + sections: [ + { + metadata: [Object], + content: 'Content here.', + title: 'Section 1' + } + ] +} +Index event: { + documentTitle: 'Document with Special Characters: Test & More!', + indexDTag: 'document-with-special-characters-test-more' +} +Creating section 0: { + title: 'Section 1', + dTag: 'document-with-special-characters-test-more-section-1', + content: 'Content here.', + metadata: { title: 'Section 1' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:document-with-special-characters-test-more-section-1' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'title', 'Document with Special Characters: Test & More!' ], + [ 'version', 'Version' ], + [ 'summary', 'This document has special characters in the title' ], + [ 'd', 'document-with-special-characters-test-more' ], + [ 'title', 'Document with Special Characters: Test & More!' ], + [ + 'a', + '30041:test-pubkey:document-with-special-characters-test-more-section-1' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Edge Cases > should handle document with very long title +Parsed AsciiDoc: { + metadata: { + title: 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality', + version: 'Version', + summary: 'This document has a very long title' + }, + content: '= This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality\n' + + ':summary: This document has a very long title\n' + + '\n' + + '== Section 1\n' + + '\n' + + 'Content here.', + sections: [ + { + metadata: [Object], + content: 'Content here.', + title: 'Section 1' + } + ] +} +Index event: { + documentTitle: 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality', + indexDTag: 'this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality' +} +Creating section 0: { + title: 'Section 1', + dTag: 'this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality-section-1', + content: 'Content here.', + metadata: { title: 'Section 1' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality-section-1' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ + 'title', + 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality' + ], + [ 'version', 'Version' ], + [ 'summary', 'This document has a very long title' ], + [ + 'd', + 'this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality' + ], + [ + 'title', + 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality' + ], + [ + 'a', + '30041:test-pubkey:this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality-section-1' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + + ✓ tests/unit/eventInput30040.test.ts (14 tests) 424ms + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/unit/metadataExtraction.test.ts > AsciiDoc Metadata Extraction > extractDocumentMetadata should extract document metadata correctly +AssertionError: expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] + +- Expected ++ Received + + [ + "John Doe", + "Jane Smith", ++ "Section Author", + ] + + ❯ tests/unit/metadataExtraction.test.ts:44:30 + 42| + 43| expect(metadata.title).toBe("Test Document with Metadata"); + 44| expect(metadata.authors).toEqual(["John Doe", "Jane Smith"]); + | ^ + 45| expect(metadata.version).toBe("1.0"); + 46| expect(metadata.publicationDate).toBe("2024-01-15"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ + + FAIL tests/unit/metadataExtraction.test.ts > AsciiDoc Metadata Extraction > Smart metadata extraction > should handle document with full header correctly +AssertionError: expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] + +- Expected ++ Received + + [ + "John Doe", + "Jane Smith", ++ "Section Author", + ] + + ❯ tests/unit/metadataExtraction.test.ts:318:32 + 316| // Should extract document-level metadata + 317| expect(metadata.title).toBe("Test Document"); + 318| expect(metadata.authors).toEqual(["John Doe", "Jane Smith"]); + | ^ + 319| expect(metadata.version).toBe("1.0"); + 320| expect(metadata.publishedBy).toBe("Alexandria Test"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯ + + + Test Files 1 failed | 1 passed (2) + Tests 2 failed | 28 passed (30) + Start at 13:19:15 + Duration 1.04s + + FAIL Tests failed. Watching for file changes... + press h to show help, press q to quit +c RERUN src/lib/utils/asciidoc_metadata.ts + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Normal Structure with Preamble > should build 30040 event set with preamble content +Parsed AsciiDoc: { + metadata: { + title: 'Test Document with Preamble', + authors: [ 'John Doe', 'Section Author' ], + version: '1.0', + publicationDate: '2024-01-15, Alexandria Test', + summary: 'This is a test document with preamble', + tags: [ 'test', 'preamble', 'asciidoc' ] + }, + content: '= Test Document with Preamble\n' + + 'John Doe \n' + + '1.0, 2024-01-15, Alexandria Test\n' + + ':summary: This is a test document with preamble\n' + + ':keywords: test, preamble, asciidoc\n' + + '\n' + + 'This is the preamble content that should be included.\n' + + '\n' + + '== First Section\n' + + ':author: Section Author\n' + + ':summary: This is the first section\n' + + '\n' + + 'This is the content of the first section.\n' + + '\n' + + '== Second Section\n' + + ':summary: This is the second section\n' + + '\n' + + 'This is the content of the second section.', + sections: [ + { + metadata: [Object], + content: 'This is the content of the first section.', + title: 'First Section' + }, + { + metadata: [Object], + content: 'This is the content of the second section.', + title: 'Second Section' + } + ] +} +Index event: { + documentTitle: 'Test Document with Preamble', + indexDTag: 'test-document-with-preamble' +} +Creating section 0: { + title: 'First Section', + dTag: 'test-document-with-preamble-first-section', + content: 'This is the content of the first section.', + metadata: { + title: 'First Section', + authors: [ 'Section Author' ], + summary: 'This is the first section' + } +} +Creating section 1: { + title: 'Second Section', + dTag: 'test-document-with-preamble-second-section', + content: 'This is the content of the second section.', + metadata: { title: 'Second Section', summary: 'This is the second section' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-second-section' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'article' ], + [ 'title', 'Test Document with Preamble' ], + [ 'author', 'John Doe' ], + [ 'author', 'Section Author' ], + [ 'version', '1.0' ], + [ 'published_on', '2024-01-15, Alexandria Test' ], + [ 'summary', 'This is a test document with preamble' ], + [ 't', 'test' ], + [ 't', 'preamble' ], + [ 't', 'asciidoc' ], + [ 'd', 'test-document-with-preamble' ], + [ 'title', 'Test Document with Preamble' ], + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-second-section' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Normal Structure without Preamble > should build 30040 event set without preamble content +Parsed AsciiDoc: { + metadata: { + title: 'Test Document without Preamble', + authors: [ 'Section Author' ], + version: 'Version', + summary: 'This is a test document without preamble', + tags: [ 'test', 'no-preamble', 'asciidoc' ] + }, + content: '= Test Document without Preamble\n' + + ':summary: This is a test document without preamble\n' + + ':keywords: test, no-preamble, asciidoc\n' + + '\n' + + '== First Section\n' + + ':author: Section Author\n' + + ':summary: This is the first section\n' + + '\n' + + 'This is the content of the first section.\n' + + '\n' + + '== Second Section\n' + + ':summary: This is the second section\n' + + '\n' + + 'This is the content of the second section.', + sections: [ + { + metadata: [Object], + content: 'This is the content of the first section.', + title: 'First Section' + }, + { + metadata: [Object], + content: 'This is the content of the second section.', + title: 'Second Section' + } + ] +} +Index event: { + documentTitle: 'Test Document without Preamble', + indexDTag: 'test-document-without-preamble' +} +Creating section 0: { + title: 'First Section', + dTag: 'test-document-without-preamble-first-section', + content: 'This is the content of the first section.', + metadata: { + title: 'First Section', + authors: [ 'Section Author' ], + summary: 'This is the first section' + } +} +Creating section 1: { + title: 'Second Section', + dTag: 'test-document-without-preamble-second-section', + content: 'This is the content of the second section.', + metadata: { title: 'Second Section', summary: 'This is the second section' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-second-section' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'article' ], + [ 'title', 'Test Document without Preamble' ], + [ 'author', 'Section Author' ], + [ 'version', 'Version' ], + [ 'summary', 'This is a test document without preamble' ], + [ 't', 'test' ], + [ 't', 'no-preamble' ], + [ 't', 'asciidoc' ], + [ 'd', 'test-document-without-preamble' ], + [ 'title', 'Test Document without Preamble' ], + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-second-section' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Skeleton Structure with Preamble > should build 30040 event set with skeleton structure and preamble +Parsed AsciiDoc: { + metadata: { + title: 'Skeleton Document with Preamble', + version: 'Version', + summary: 'This is a skeleton document with preamble', + tags: [ 'skeleton', 'preamble', 'empty' ] + }, + content: '= Skeleton Document with Preamble\n' + + ':summary: This is a skeleton document with preamble\n' + + ':keywords: skeleton, preamble, empty\n' + + '\n' + + 'This is the preamble content.\n' + + '\n' + + '== Empty Section 1\n' + + '\n' + + '== Empty Section 2\n' + + '\n' + + '== Empty Section 3', + sections: [ + { metadata: [Object], content: '', title: 'Empty Section 1' }, + { metadata: [Object], content: '', title: 'Empty Section 2' }, + { metadata: [Object], content: '', title: 'Empty Section 3' } + ] +} +Index event: { + documentTitle: 'Skeleton Document with Preamble', + indexDTag: 'skeleton-document-with-preamble' +} +Creating section 0: { + title: 'Empty Section 1', + dTag: 'skeleton-document-with-preamble-empty-section-1', + content: '', + metadata: { title: 'Empty Section 1' } +} +Creating section 1: { + title: 'Empty Section 2', + dTag: 'skeleton-document-with-preamble-empty-section-2', + content: '', + metadata: { title: 'Empty Section 2' } +} +Creating section 2: { + title: 'Empty Section 3', + dTag: 'skeleton-document-with-preamble-empty-section-3', + content: '', + metadata: { title: 'Empty Section 3' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-3' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'skeleton' ], + [ 'title', 'Skeleton Document with Preamble' ], + [ 'version', 'Version' ], + [ 'summary', 'This is a skeleton document with preamble' ], + [ 't', 'skeleton' ], + [ 't', 'preamble' ], + [ 't', 'empty' ], + [ 'd', 'skeleton-document-with-preamble' ], + [ 'title', 'Skeleton Document with Preamble' ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-3' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Skeleton Structure without Preamble > should build 30040 event set with skeleton structure without preamble +Parsed AsciiDoc: { + metadata: { + title: 'Skeleton Document without Preamble', + version: 'Version', + summary: 'This is a skeleton document without preamble', + tags: [ 'skeleton', 'no-preamble', 'empty' ] + }, + content: '= Skeleton Document without Preamble\n' + + ':summary: This is a skeleton document without preamble\n' + + ':keywords: skeleton, no-preamble, empty\n' + + '\n' + + '== Empty Section 1\n' + + '\n' + + '== Empty Section 2\n' + + '\n' + + '== Empty Section 3', + sections: [ + { metadata: [Object], content: '', title: 'Empty Section 1' }, + { metadata: [Object], content: '', title: 'Empty Section 2' }, + { metadata: [Object], content: '', title: 'Empty Section 3' } + ] +} +Index event: { + documentTitle: 'Skeleton Document without Preamble', + indexDTag: 'skeleton-document-without-preamble' +} +Creating section 0: { + title: 'Empty Section 1', + dTag: 'skeleton-document-without-preamble-empty-section-1', + content: '', + metadata: { title: 'Empty Section 1' } +} +Creating section 1: { + title: 'Empty Section 2', + dTag: 'skeleton-document-without-preamble-empty-section-2', + content: '', + metadata: { title: 'Empty Section 2' } +} +Creating section 2: { + title: 'Empty Section 3', + dTag: 'skeleton-document-without-preamble-empty-section-3', + content: '', + metadata: { title: 'Empty Section 3' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-3' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'skeleton' ], + [ 'title', 'Skeleton Document without Preamble' ], + [ 'version', 'Version' ], + [ 'summary', 'This is a skeleton document without preamble' ], + [ 't', 'skeleton' ], + [ 't', 'no-preamble' ], + [ 't', 'empty' ], + [ 'd', 'skeleton-document-without-preamble' ], + [ 'title', 'Skeleton Document without Preamble' ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-3' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + + ❯ tests/unit/metadataExtraction.test.ts (16 tests | 2 failed) 246ms + × AsciiDoc Metadata Extraction > extractDocumentMetadata should extract document metadata correctly 154ms + → expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should extract section metadata correctly 8ms + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should extract standalone author names and remove them from content 5ms + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should handle multiple standalone author names 5ms + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should not extract non-author lines as authors 4ms + ✓ AsciiDoc Metadata Extraction > parseAsciiDocWithMetadata should parse complete document 21ms + ✓ AsciiDoc Metadata Extraction > metadataToTags should convert metadata to Nostr tags 2ms + ✓ AsciiDoc Metadata Extraction > should handle index card format correctly 4ms + ✓ AsciiDoc Metadata Extraction > should handle empty content gracefully 4ms + ✓ AsciiDoc Metadata Extraction > should handle keywords as tags 5ms + ✓ AsciiDoc Metadata Extraction > should handle both tags and keywords 4ms + ✓ AsciiDoc Metadata Extraction > should handle tags only 4ms + ✓ AsciiDoc Metadata Extraction > should handle both summary and description 7ms + ✓ AsciiDoc Metadata Extraction > Smart metadata extraction > should handle section-only content correctly 8ms + ✓ AsciiDoc Metadata Extraction > Smart metadata extraction > should handle minimal document header (just title) correctly 1ms + × AsciiDoc Metadata Extraction > Smart metadata extraction > should handle document with full header correctly 7ms + → expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Index Card Format > should build 30040 event set for index card format +Parsed AsciiDoc: { + metadata: { title: 'Test Index Card', version: 'Version' }, + content: '= Test Index Card\nindex card', + sections: [] +} +Creating index card format (no sections) + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Index Card Format > should build 30040 event set for index card with metadata +Parsed AsciiDoc: { + metadata: { + title: 'Test Index Card with Metadata', + version: 'Version', + summary: 'This is an index card with metadata', + tags: [ 'index', 'card', 'metadata' ] + }, + content: '= Test Index Card with Metadata\n' + + ':summary: This is an index card with metadata\n' + + ':keywords: index, card, metadata\n' + + 'index card', + sections: [] +} +Index event: { + documentTitle: 'Test Index Card with Metadata', + indexDTag: 'test-index-card-with-metadata' +} +A tags: [] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'index-card' ], + [ 'title', 'Test Index Card with Metadata' ], + [ 'version', 'Version' ], + [ 'summary', 'This is an index card with metadata' ], + [ 't', 'index' ], + [ 't', 'card' ], + [ 't', 'metadata' ], + [ 'd', 'test-index-card-with-metadata' ], + [ 'title', 'Test Index Card with Metadata' ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Complex Metadata Structures > should handle complex metadata with all attribute types +Parsed AsciiDoc: { + metadata: { + title: 'Complex Metadata Document', + authors: [ + 'Jane Smith', + 'Override Author', + 'Third Author', + 'Section Author', + 'Section Co-Author' + ], + version: '2.0', + publicationDate: '2024-03-01', + summary: 'This is a complex document with all metadata types Alternative description field', + publishedBy: 'Alexandria Complex', + type: 'book', + coverImage: 'https://example.com/cover.jpg', + isbn: '978-0-123456-78-9', + source: 'https://github.com/alexandria/complex', + autoUpdate: 'yes', + tags: [ + 'additional', + 'tags', + 'here', + 'complex', + 'metadata', + 'all-types' + ] + }, + content: '= Complex Metadata Document\n' + + 'Jane Smith \n' + + '2.0, 2024-02-20, Alexandria Complex\n' + + ':summary: This is a complex document with all metadata types\n' + + ':description: Alternative description field\n' + + ':keywords: complex, metadata, all-types\n' + + ':tags: additional, tags, here\n' + + ':author: Override Author\n' + + ':author: Third Author\n' + + ':version: 3.0\n' + + ':published_on: 2024-03-01\n' + + ':published_by: Alexandria Complex\n' + + ':type: book\n' + + ':image: https://example.com/cover.jpg\n' + + ':isbn: 978-0-123456-78-9\n' + + ':source: https://github.com/alexandria/complex\n' + + ':auto-update: yes\n' + + '\n' + + 'This is the preamble content.\n' + + '\n' + + '== Section with Complex Metadata\n' + + ':author: Section Author\n' + + ':author: Section Co-Author\n' + + ':summary: This section has complex metadata\n' + + ':description: Alternative description for section\n' + + ':keywords: section, complex, metadata\n' + + ':tags: section, tags\n' + + ':type: chapter\n' + + ':image: https://example.com/section-image.jpg\n' + + '\n' + + 'This is the section content.', + sections: [ + { + metadata: [Object], + content: 'This is the section content.', + title: 'Section with Complex Metadata' + } + ] +} +Index event: { + documentTitle: 'Complex Metadata Document', + indexDTag: 'complex-metadata-document' +} +Creating section 0: { + title: 'Section with Complex Metadata', + dTag: 'complex-metadata-document-section-with-complex-metadata', + content: 'This is the section content.', + metadata: { + title: 'Section with Complex Metadata', + authors: [ 'Section Author', 'Section Co-Author' ], + summary: 'This section has complex metadata Alternative description for section', + type: 'chapter', + coverImage: 'https://example.com/section-image.jpg', + tags: [ 'section', 'tags', 'complex', 'metadata' ] + } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:complex-metadata-document-section-with-complex-metadata' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'complex' ], + [ 'title', 'Complex Metadata Document' ], + [ 'author', 'Jane Smith' ], + [ 'author', 'Override Author' ], + [ 'author', 'Third Author' ], + [ 'author', 'Section Author' ], + [ 'author', 'Section Co-Author' ], + [ 'version', '2.0' ], + [ 'published_on', '2024-03-01' ], + [ 'published_by', 'Alexandria Complex' ], + [ + 'summary', + 'This is a complex document with all metadata types Alternative description field' + ], + [ 'image', 'https://example.com/cover.jpg' ], + [ 'i', '978-0-123456-78-9' ], + [ 'source', 'https://github.com/alexandria/complex' ], + [ 'type', 'book' ], + [ 'auto-update', 'yes' ], + [ 't', 'additional' ], + [ 't', 'tags' ], + [ 't', 'here' ], + [ 't', 'complex' ], + [ 't', 'metadata' ], + [ 't', 'all-types' ], + [ 'd', 'complex-metadata-document' ], + [ 'title', 'Complex Metadata Document' ], + [ + 'a', + '30041:test-pubkey:complex-metadata-document-section-with-complex-metadata' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Edge Cases > should handle document with only title and no sections +Parsed AsciiDoc: { + metadata: { + title: 'Document with No Sections', + version: 'Version', + summary: 'This document has no sections' + }, + content: '= Document with No Sections\n' + + ':summary: This document has no sections\n' + + '\n' + + 'This is just preamble content.', + sections: [] +} +Index event: { + documentTitle: 'Document with No Sections', + indexDTag: 'document-with-no-sections' +} +A tags: [] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'title', 'Document with No Sections' ], + [ 'version', 'Version' ], + [ 'summary', 'This document has no sections' ], + [ 'd', 'document-with-no-sections' ], + [ 'title', 'Document with No Sections' ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Edge Cases > should handle document with special characters in title +Parsed AsciiDoc: { + metadata: { + title: 'Document with Special Characters: Test & More!', + version: 'Version', + summary: 'This document has special characters in the title' + }, + content: '= Document with Special Characters: Test & More!\n' + + ':summary: This document has special characters in the title\n' + + '\n' + + '== Section 1\n' + + '\n' + + 'Content here.', + sections: [ + { + metadata: [Object], + content: 'Content here.', + title: 'Section 1' + } + ] +} +Index event: { + documentTitle: 'Document with Special Characters: Test & More!', + indexDTag: 'document-with-special-characters-test-more' +} +Creating section 0: { + title: 'Section 1', + dTag: 'document-with-special-characters-test-more-section-1', + content: 'Content here.', + metadata: { title: 'Section 1' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:document-with-special-characters-test-more-section-1' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'title', 'Document with Special Characters: Test & More!' ], + [ 'version', 'Version' ], + [ 'summary', 'This document has special characters in the title' ], + [ 'd', 'document-with-special-characters-test-more' ], + [ 'title', 'Document with Special Characters: Test & More!' ], + [ + 'a', + '30041:test-pubkey:document-with-special-characters-test-more-section-1' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Edge Cases > should handle document with very long title +Parsed AsciiDoc: { + metadata: { + title: 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality', + version: 'Version', + summary: 'This document has a very long title' + }, + content: '= This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality\n' + + ':summary: This document has a very long title\n' + + '\n' + + '== Section 1\n' + + '\n' + + 'Content here.', + sections: [ + { + metadata: [Object], + content: 'Content here.', + title: 'Section 1' + } + ] +} +Index event: { + documentTitle: 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality', + indexDTag: 'this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality' +} +Creating section 0: { + title: 'Section 1', + dTag: 'this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality-section-1', + content: 'Content here.', + metadata: { title: 'Section 1' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality-section-1' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ + 'title', + 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality' + ], + [ 'version', 'Version' ], + [ 'summary', 'This document has a very long title' ], + [ + 'd', + 'this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality' + ], + [ + 'title', + 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality' + ], + [ + 'a', + '30041:test-pubkey:this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality-section-1' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + + ✓ tests/unit/eventInput30040.test.ts (14 tests) 374ms + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/unit/metadataExtraction.test.ts > AsciiDoc Metadata Extraction > extractDocumentMetadata should extract document metadata correctly +AssertionError: expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] + +- Expected ++ Received + + [ + "John Doe", + "Jane Smith", ++ "Section Author", + ] + + ❯ tests/unit/metadataExtraction.test.ts:44:30 + 42| + 43| expect(metadata.title).toBe("Test Document with Metadata"); + 44| expect(metadata.authors).toEqual(["John Doe", "Jane Smith"]); + | ^ + 45| expect(metadata.version).toBe("1.0"); + 46| expect(metadata.publicationDate).toBe("2024-01-15"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ + + FAIL tests/unit/metadataExtraction.test.ts > AsciiDoc Metadata Extraction > Smart metadata extraction > should handle document with full header correctly +AssertionError: expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] + +- Expected ++ Received + + [ + "John Doe", + "Jane Smith", ++ "Section Author", + ] + + ❯ tests/unit/metadataExtraction.test.ts:318:32 + 316| // Should extract document-level metadata + 317| expect(metadata.title).toBe("Test Document"); + 318| expect(metadata.authors).toEqual(["John Doe", "Jane Smith"]); + | ^ + 319| expect(metadata.version).toBe("1.0"); + 320| expect(metadata.publishedBy).toBe("Alexandria Test"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯ + + + Test Files 1 failed | 1 passed (2) + Tests 2 failed | 28 passed (30) + Start at 13:20:56 + Duration 1.17s + + FAIL Tests failed. Watching for file changes... + press h to show help, press q to quit +c RERUN src/lib/utils/asciidoc_metadata.ts + + ❯ tests/unit/metadataExtraction.test.ts (16 tests | 2 failed) 270ms + × AsciiDoc Metadata Extraction > extractDocumentMetadata should extract document metadata correctly 158ms + → expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should extract section metadata correctly 8ms + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should extract standalone author names and remove them from content 6ms + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should handle multiple standalone author names 6ms + ✓ AsciiDoc Metadata Extraction > extractSectionMetadata should not extract non-author lines as authors 6ms + ✓ AsciiDoc Metadata Extraction > parseAsciiDocWithMetadata should parse complete document 26ms + ✓ AsciiDoc Metadata Extraction > metadataToTags should convert metadata to Nostr tags 2ms + ✓ AsciiDoc Metadata Extraction > should handle index card format correctly 5ms + ✓ AsciiDoc Metadata Extraction > should handle empty content gracefully 5ms + ✓ AsciiDoc Metadata Extraction > should handle keywords as tags 5ms + ✓ AsciiDoc Metadata Extraction > should handle both tags and keywords 4ms + ✓ AsciiDoc Metadata Extraction > should handle tags only 4ms + ✓ AsciiDoc Metadata Extraction > should handle both summary and description 9ms + ✓ AsciiDoc Metadata Extraction > Smart metadata extraction > should handle section-only content correctly 9ms + ✓ AsciiDoc Metadata Extraction > Smart metadata extraction > should handle minimal document header (just title) correctly 1ms + × AsciiDoc Metadata Extraction > Smart metadata extraction > should handle document with full header correctly 11ms + → expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Normal Structure with Preamble > should build 30040 event set with preamble content +Parsed AsciiDoc: { + metadata: { + title: 'Test Document with Preamble', + authors: [ 'John Doe', 'Section Author' ], + version: '1.0', + publicationDate: '2024-01-15, Alexandria Test', + summary: 'This is a test document with preamble', + tags: [ 'test', 'preamble', 'asciidoc' ] + }, + content: '= Test Document with Preamble\n' + + 'John Doe \n' + + '1.0, 2024-01-15, Alexandria Test\n' + + ':summary: This is a test document with preamble\n' + + ':keywords: test, preamble, asciidoc\n' + + '\n' + + 'This is the preamble content that should be included.\n' + + '\n' + + '== First Section\n' + + ':author: Section Author\n' + + ':summary: This is the first section\n' + + '\n' + + 'This is the content of the first section.\n' + + '\n' + + '== Second Section\n' + + ':summary: This is the second section\n' + + '\n' + + 'This is the content of the second section.', + sections: [ + { + metadata: [Object], + content: 'This is the content of the first section.', + title: 'First Section' + }, + { + metadata: [Object], + content: 'This is the content of the second section.', + title: 'Second Section' + } + ] +} +Index event: { + documentTitle: 'Test Document with Preamble', + indexDTag: 'test-document-with-preamble' +} +Creating section 0: { + title: 'First Section', + dTag: 'test-document-with-preamble-first-section', + content: 'This is the content of the first section.', + metadata: { + title: 'First Section', + authors: [ 'Section Author' ], + summary: 'This is the first section' + } +} +Creating section 1: { + title: 'Second Section', + dTag: 'test-document-with-preamble-second-section', + content: 'This is the content of the second section.', + metadata: { title: 'Second Section', summary: 'This is the second section' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-second-section' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'article' ], + [ 'title', 'Test Document with Preamble' ], + [ 'author', 'John Doe' ], + [ 'author', 'Section Author' ], + [ 'version', '1.0' ], + [ 'published_on', '2024-01-15, Alexandria Test' ], + [ 'summary', 'This is a test document with preamble' ], + [ 't', 'test' ], + [ 't', 'preamble' ], + [ 't', 'asciidoc' ], + [ 'd', 'test-document-with-preamble' ], + [ 'title', 'Test Document with Preamble' ], + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-with-preamble-second-section' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Normal Structure without Preamble > should build 30040 event set without preamble content +Parsed AsciiDoc: { + metadata: { + title: 'Test Document without Preamble', + authors: [ 'Section Author' ], + version: 'Version', + summary: 'This is a test document without preamble', + tags: [ 'test', 'no-preamble', 'asciidoc' ] + }, + content: '= Test Document without Preamble\n' + + ':summary: This is a test document without preamble\n' + + ':keywords: test, no-preamble, asciidoc\n' + + '\n' + + '== First Section\n' + + ':author: Section Author\n' + + ':summary: This is the first section\n' + + '\n' + + 'This is the content of the first section.\n' + + '\n' + + '== Second Section\n' + + ':summary: This is the second section\n' + + '\n' + + 'This is the content of the second section.', + sections: [ + { + metadata: [Object], + content: 'This is the content of the first section.', + title: 'First Section' + }, + { + metadata: [Object], + content: 'This is the content of the second section.', + title: 'Second Section' + } + ] +} +Index event: { + documentTitle: 'Test Document without Preamble', + indexDTag: 'test-document-without-preamble' +} +Creating section 0: { + title: 'First Section', + dTag: 'test-document-without-preamble-first-section', + content: 'This is the content of the first section.', + metadata: { + title: 'First Section', + authors: [ 'Section Author' ], + summary: 'This is the first section' + } +} +Creating section 1: { + title: 'Second Section', + dTag: 'test-document-without-preamble-second-section', + content: 'This is the content of the second section.', + metadata: { title: 'Second Section', summary: 'This is the second section' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-second-section' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'article' ], + [ 'title', 'Test Document without Preamble' ], + [ 'author', 'Section Author' ], + [ 'version', 'Version' ], + [ 'summary', 'This is a test document without preamble' ], + [ 't', 'test' ], + [ 't', 'no-preamble' ], + [ 't', 'asciidoc' ], + [ 'd', 'test-document-without-preamble' ], + [ 'title', 'Test Document without Preamble' ], + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-first-section' + ], + [ + 'a', + '30041:test-pubkey:test-document-without-preamble-second-section' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Skeleton Structure with Preamble > should build 30040 event set with skeleton structure and preamble +Parsed AsciiDoc: { + metadata: { + title: 'Skeleton Document with Preamble', + version: 'Version', + summary: 'This is a skeleton document with preamble', + tags: [ 'skeleton', 'preamble', 'empty' ] + }, + content: '= Skeleton Document with Preamble\n' + + ':summary: This is a skeleton document with preamble\n' + + ':keywords: skeleton, preamble, empty\n' + + '\n' + + 'This is the preamble content.\n' + + '\n' + + '== Empty Section 1\n' + + '\n' + + '== Empty Section 2\n' + + '\n' + + '== Empty Section 3', + sections: [ + { metadata: [Object], content: '', title: 'Empty Section 1' }, + { metadata: [Object], content: '', title: 'Empty Section 2' }, + { metadata: [Object], content: '', title: 'Empty Section 3' } + ] +} +Index event: { + documentTitle: 'Skeleton Document with Preamble', + indexDTag: 'skeleton-document-with-preamble' +} +Creating section 0: { + title: 'Empty Section 1', + dTag: 'skeleton-document-with-preamble-empty-section-1', + content: '', + metadata: { title: 'Empty Section 1' } +} +Creating section 1: { + title: 'Empty Section 2', + dTag: 'skeleton-document-with-preamble-empty-section-2', + content: '', + metadata: { title: 'Empty Section 2' } +} +Creating section 2: { + title: 'Empty Section 3', + dTag: 'skeleton-document-with-preamble-empty-section-3', + content: '', + metadata: { title: 'Empty Section 3' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-3' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'skeleton' ], + [ 'title', 'Skeleton Document with Preamble' ], + [ 'version', 'Version' ], + [ 'summary', 'This is a skeleton document with preamble' ], + [ 't', 'skeleton' ], + [ 't', 'preamble' ], + [ 't', 'empty' ], + [ 'd', 'skeleton-document-with-preamble' ], + [ 'title', 'Skeleton Document with Preamble' ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-with-preamble-empty-section-3' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Skeleton Structure without Preamble > should build 30040 event set with skeleton structure without preamble +Parsed AsciiDoc: { + metadata: { + title: 'Skeleton Document without Preamble', + version: 'Version', + summary: 'This is a skeleton document without preamble', + tags: [ 'skeleton', 'no-preamble', 'empty' ] + }, + content: '= Skeleton Document without Preamble\n' + + ':summary: This is a skeleton document without preamble\n' + + ':keywords: skeleton, no-preamble, empty\n' + + '\n' + + '== Empty Section 1\n' + + '\n' + + '== Empty Section 2\n' + + '\n' + + '== Empty Section 3', + sections: [ + { metadata: [Object], content: '', title: 'Empty Section 1' }, + { metadata: [Object], content: '', title: 'Empty Section 2' }, + { metadata: [Object], content: '', title: 'Empty Section 3' } + ] +} +Index event: { + documentTitle: 'Skeleton Document without Preamble', + indexDTag: 'skeleton-document-without-preamble' +} +Creating section 0: { + title: 'Empty Section 1', + dTag: 'skeleton-document-without-preamble-empty-section-1', + content: '', + metadata: { title: 'Empty Section 1' } +} +Creating section 1: { + title: 'Empty Section 2', + dTag: 'skeleton-document-without-preamble-empty-section-2', + content: '', + metadata: { title: 'Empty Section 2' } +} +Creating section 2: { + title: 'Empty Section 3', + dTag: 'skeleton-document-without-preamble-empty-section-3', + content: '', + metadata: { title: 'Empty Section 3' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-3' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'skeleton' ], + [ 'title', 'Skeleton Document without Preamble' ], + [ 'version', 'Version' ], + [ 'summary', 'This is a skeleton document without preamble' ], + [ 't', 'skeleton' ], + [ 't', 'no-preamble' ], + [ 't', 'empty' ], + [ 'd', 'skeleton-document-without-preamble' ], + [ 'title', 'Skeleton Document without Preamble' ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-1' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-2' + ], + [ + 'a', + '30041:test-pubkey:skeleton-document-without-preamble-empty-section-3' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Index Card Format > should build 30040 event set for index card format +Parsed AsciiDoc: { + metadata: { title: 'Test Index Card', version: 'Version' }, + content: '= Test Index Card\nindex card', + sections: [] +} +Creating index card format (no sections) + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Index Card Format > should build 30040 event set for index card with metadata +Parsed AsciiDoc: { + metadata: { + title: 'Test Index Card with Metadata', + version: 'Version', + summary: 'This is an index card with metadata', + tags: [ 'index', 'card', 'metadata' ] + }, + content: '= Test Index Card with Metadata\n' + + ':summary: This is an index card with metadata\n' + + ':keywords: index, card, metadata\n' + + 'index card', + sections: [] +} +Index event: { + documentTitle: 'Test Index Card with Metadata', + indexDTag: 'test-index-card-with-metadata' +} +A tags: [] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'index-card' ], + [ 'title', 'Test Index Card with Metadata' ], + [ 'version', 'Version' ], + [ 'summary', 'This is an index card with metadata' ], + [ 't', 'index' ], + [ 't', 'card' ], + [ 't', 'metadata' ], + [ 'd', 'test-index-card-with-metadata' ], + [ 'title', 'Test Index Card with Metadata' ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Complex Metadata Structures > should handle complex metadata with all attribute types +Parsed AsciiDoc: { + metadata: { + title: 'Complex Metadata Document', + authors: [ + 'Jane Smith', + 'Override Author', + 'Third Author', + 'Section Author', + 'Section Co-Author' + ], + version: '2.0', + publicationDate: '2024-03-01', + summary: 'This is a complex document with all metadata types Alternative description field', + publishedBy: 'Alexandria Complex', + type: 'book', + coverImage: 'https://example.com/cover.jpg', + isbn: '978-0-123456-78-9', + source: 'https://github.com/alexandria/complex', + autoUpdate: 'yes', + tags: [ + 'additional', + 'tags', + 'here', + 'complex', + 'metadata', + 'all-types' + ] + }, + content: '= Complex Metadata Document\n' + + 'Jane Smith \n' + + '2.0, 2024-02-20, Alexandria Complex\n' + + ':summary: This is a complex document with all metadata types\n' + + ':description: Alternative description field\n' + + ':keywords: complex, metadata, all-types\n' + + ':tags: additional, tags, here\n' + + ':author: Override Author\n' + + ':author: Third Author\n' + + ':version: 3.0\n' + + ':published_on: 2024-03-01\n' + + ':published_by: Alexandria Complex\n' + + ':type: book\n' + + ':image: https://example.com/cover.jpg\n' + + ':isbn: 978-0-123456-78-9\n' + + ':source: https://github.com/alexandria/complex\n' + + ':auto-update: yes\n' + + '\n' + + 'This is the preamble content.\n' + + '\n' + + '== Section with Complex Metadata\n' + + ':author: Section Author\n' + + ':author: Section Co-Author\n' + + ':summary: This section has complex metadata\n' + + ':description: Alternative description for section\n' + + ':keywords: section, complex, metadata\n' + + ':tags: section, tags\n' + + ':type: chapter\n' + + ':image: https://example.com/section-image.jpg\n' + + '\n' + + 'This is the section content.', + sections: [ + { + metadata: [Object], + content: 'This is the section content.', + title: 'Section with Complex Metadata' + } + ] +} +Index event: { + documentTitle: 'Complex Metadata Document', + indexDTag: 'complex-metadata-document' +} +Creating section 0: { + title: 'Section with Complex Metadata', + dTag: 'complex-metadata-document-section-with-complex-metadata', + content: 'This is the section content.', + metadata: { + title: 'Section with Complex Metadata', + authors: [ 'Section Author', 'Section Co-Author' ], + summary: 'This section has complex metadata Alternative description for section', + type: 'chapter', + coverImage: 'https://example.com/section-image.jpg', + tags: [ 'section', 'tags', 'complex', 'metadata' ] + } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:complex-metadata-document-section-with-complex-metadata' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'type', 'complex' ], + [ 'title', 'Complex Metadata Document' ], + [ 'author', 'Jane Smith' ], + [ 'author', 'Override Author' ], + [ 'author', 'Third Author' ], + [ 'author', 'Section Author' ], + [ 'author', 'Section Co-Author' ], + [ 'version', '2.0' ], + [ 'published_on', '2024-03-01' ], + [ 'published_by', 'Alexandria Complex' ], + [ + 'summary', + 'This is a complex document with all metadata types Alternative description field' + ], + [ 'image', 'https://example.com/cover.jpg' ], + [ 'i', '978-0-123456-78-9' ], + [ 'source', 'https://github.com/alexandria/complex' ], + [ 'type', 'book' ], + [ 'auto-update', 'yes' ], + [ 't', 'additional' ], + [ 't', 'tags' ], + [ 't', 'here' ], + [ 't', 'complex' ], + [ 't', 'metadata' ], + [ 't', 'all-types' ], + [ 'd', 'complex-metadata-document' ], + [ 'title', 'Complex Metadata Document' ], + [ + 'a', + '30041:test-pubkey:complex-metadata-document-section-with-complex-metadata' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Edge Cases > should handle document with only title and no sections +Parsed AsciiDoc: { + metadata: { + title: 'Document with No Sections', + version: 'Version', + summary: 'This document has no sections' + }, + content: '= Document with No Sections\n' + + ':summary: This document has no sections\n' + + '\n' + + 'This is just preamble content.', + sections: [] +} +Index event: { + documentTitle: 'Document with No Sections', + indexDTag: 'document-with-no-sections' +} +A tags: [] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'title', 'Document with No Sections' ], + [ 'version', 'Version' ], + [ 'summary', 'This document has no sections' ], + [ 'd', 'document-with-no-sections' ], + [ 'title', 'Document with No Sections' ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Edge Cases > should handle document with special characters in title +Parsed AsciiDoc: { + metadata: { + title: 'Document with Special Characters: Test & More!', + version: 'Version', + summary: 'This document has special characters in the title' + }, + content: '= Document with Special Characters: Test & More!\n' + + ':summary: This document has special characters in the title\n' + + '\n' + + '== Section 1\n' + + '\n' + + 'Content here.', + sections: [ + { + metadata: [Object], + content: 'Content here.', + title: 'Section 1' + } + ] +} +Index event: { + documentTitle: 'Document with Special Characters: Test & More!', + indexDTag: 'document-with-special-characters-test-more' +} +Creating section 0: { + title: 'Section 1', + dTag: 'document-with-special-characters-test-more-section-1', + content: 'Content here.', + metadata: { title: 'Section 1' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:document-with-special-characters-test-more-section-1' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ 'title', 'Document with Special Characters: Test & More!' ], + [ 'version', 'Version' ], + [ 'summary', 'This document has special characters in the title' ], + [ 'd', 'document-with-special-characters-test-more' ], + [ 'title', 'Document with Special Characters: Test & More!' ], + [ + 'a', + '30041:test-pubkey:document-with-special-characters-test-more-section-1' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + +stdout | tests/unit/eventInput30040.test.ts > EventInput 30040 Publishing > Edge Cases > should handle document with very long title +Parsed AsciiDoc: { + metadata: { + title: 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality', + version: 'Version', + summary: 'This document has a very long title' + }, + content: '= This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality\n' + + ':summary: This document has a very long title\n' + + '\n' + + '== Section 1\n' + + '\n' + + 'Content here.', + sections: [ + { + metadata: [Object], + content: 'Content here.', + title: 'Section 1' + } + ] +} +Index event: { + documentTitle: 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality', + indexDTag: 'this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality' +} +Creating section 0: { + title: 'Section 1', + dTag: 'this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality-section-1', + content: 'Content here.', + metadata: { title: 'Section 1' } +} +A tags: [ + [ + 'a', + '30041:test-pubkey:this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality-section-1' + ] +] +Final index event: { + kind: 30040, + content: '', + tags: [ + [ + 'title', + 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality' + ], + [ 'version', 'Version' ], + [ 'summary', 'This document has a very long title' ], + [ + 'd', + 'this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality' + ], + [ + 'title', + 'This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality' + ], + [ + 'a', + '30041:test-pubkey:this-is-a-very-long-document-title-that-should-be-handled-properly-by-the-system-and-should-not-cause-any-issues-with-the-d-tag-generation-or-any-other-functionality-section-1' + ] + ], + pubkey: 'test-pubkey', + created_at: 1234567890, + id: 'mock-event-id', + sig: 'mock-signature' +} +=== build30040EventSet completed === + + ✓ tests/unit/eventInput30040.test.ts (14 tests) 517ms + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/unit/metadataExtraction.test.ts > AsciiDoc Metadata Extraction > extractDocumentMetadata should extract document metadata correctly +AssertionError: expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] + +- Expected ++ Received + + [ + "John Doe", + "Jane Smith", ++ "Section Author", + ] + + ❯ tests/unit/metadataExtraction.test.ts:44:30 + 42| + 43| expect(metadata.title).toBe("Test Document with Metadata"); + 44| expect(metadata.authors).toEqual(["John Doe", "Jane Smith"]); + | ^ + 45| expect(metadata.version).toBe("1.0"); + 46| expect(metadata.publicationDate).toBe("2024-01-15"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ + + FAIL tests/unit/metadataExtraction.test.ts > AsciiDoc Metadata Extraction > Smart metadata extraction > should handle document with full header correctly +AssertionError: expected [ 'John Doe', 'Jane Smith', …(1) ] to deeply equal [ 'John Doe', 'Jane Smith' ] + +- Expected ++ Received + + [ + "John Doe", + "Jane Smith", ++ "Section Author", + ] + + ❯ tests/unit/metadataExtraction.test.ts:318:32 + 316| // Should extract document-level metadata + 317| expect(metadata.title).toBe("Test Document"); + 318| expect(metadata.authors).toEqual(["John Doe", "Jane Smith"]); + | ^ + 319| expect(metadata.version).toBe("1.0"); + 320| expect(metadata.publishedBy).toBe("Alexandria Test"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯ + + + Test Files 1 failed | 1 passed (2) + Tests 2 failed | 28 passed (30) + Start at 13:21:50 + Duration 1.36s + + FAIL Tests failed. Watching for file changes... + press h to show help, press q to quit diff --git a/tests/unit/latexRendering.test.ts b/tests/unit/latexRendering.test.ts deleted file mode 100644 index eac80c5..0000000 --- a/tests/unit/latexRendering.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { parseAdvancedmarkup } from "../../src/lib/utils/markup/advancedMarkupParser"; -import { readFileSync } from "fs"; -import { join } from "path"; - -describe("LaTeX and AsciiMath Rendering in Inline Code Blocks", () => { - const jsonPath = join(__dirname, "../../test_data/LaTeXtestfile.json"); - const raw = readFileSync(jsonPath, "utf-8"); - // Extract the markdown content field from the JSON event - const content = JSON.parse(raw).content; - - it("renders LaTeX inline and display math correctly", async () => { - const html = await parseAdvancedmarkup(content); - // Test basic LaTeX examples from the test document - expect(html).toMatch(/\$\\sqrt\{x\}\$<\/span>/); - expect(html).toMatch(/
    \$\$\\sqrt\{x\}\$\$<\/div>/); - expect(html).toMatch( - /\$\\mathbb\{N\} = \\{ a \\in \\mathbb\{Z\} : a > 0 \\}\$<\/span>/, - ); - expect(html).toMatch( - /
    \$\$P \\left\( A=2 \\, \\middle\| \\, \\dfrac\{A\^2\}\{B\}>4 \\right\)\$\$<\/div>/, - ); - }); - - it("renders AsciiMath inline and display math correctly", async () => { - const html = await parseAdvancedmarkup(content); - // Test AsciiMath examples - expect(html).toMatch(/\$E=mc\^2\$<\/span>/); - expect(html).toMatch( - /
    \$\$sum_\(k=1\)\^n k = 1\+2\+ cdots \+n=\(n\(n\+1\)\)\/2\$\$<\/div>/, - ); - expect(html).toMatch( - /
    \$\$int_0\^1 x\^2 dx\$\$<\/div>/, - ); - }); - - it("renders LaTeX array and matrix environments as math", async () => { - const html = await parseAdvancedmarkup(content); - // Test array and matrix environments - expect(html).toMatch( - /
    \$\$[\s\S]*\\begin\{array\}\{ccccc\}[\s\S]*\\end\{array\}[\s\S]*\$\$<\/div>/, - ); - expect(html).toMatch( - /
    \$\$[\s\S]*\\begin\{bmatrix\}[\s\S]*\\end\{bmatrix\}[\s\S]*\$\$<\/div>/, - ); - }); - - it("handles unsupported LaTeX environments gracefully", async () => { - const html = await parseAdvancedmarkup(content); - // Should show a message and plaintext for tabular - expect(html).toMatch(/
    /); - expect(html).toMatch( - /Unrendered, as it is LaTeX typesetting, not a formula:/, - ); - expect(html).toMatch(/\\\\begin\{tabular\}/); - }); - - it("renders mixed LaTeX and AsciiMath correctly", async () => { - const html = await parseAdvancedmarkup(content); - // Test mixed content - expect(html).toMatch( - /\$\\frac\{1\}\{2\}\$<\/span>/, - ); - expect(html).toMatch(/\$1\/2\$<\/span>/); - expect(html).toMatch( - /
    \$\$\\sum_\{i=1\}\^n x_i\$\$<\/div>/, - ); - expect(html).toMatch( - /
    \$\$sum_\(i=1\)\^n x_i\$\$<\/div>/, - ); - }); - - it("handles edge cases and regular code blocks", async () => { - const html = await parseAdvancedmarkup(content); - // Test regular code blocks (should remain as code, not math) - expect(html).toMatch(/]*>\$19\.99<\/code>/); - expect(html).toMatch(/]*>echo "Price: \$100"<\/code>/); - expect(html).toMatch( - /]*>const price = \\`\$\$\{amount\}\\`<\/code>/, - ); - expect(html).toMatch(/]*>color: \$primary-color<\/code>/); - }); -}); diff --git a/tests/unit/mathProcessing.test.ts b/tests/unit/mathProcessing.test.ts new file mode 100644 index 0000000..acf7378 --- /dev/null +++ b/tests/unit/mathProcessing.test.ts @@ -0,0 +1,186 @@ +import { describe, expect, it } from "vitest"; +import { parseAdvancedmarkup } from "../../src/lib/utils/markup/advancedMarkupParser.ts"; + +describe("Math Processing in Advanced Markup Parser", () => { + it("should process inline math inside code blocks", async () => { + const input = "Here is some inline math: `$x^2 + y^2 = z^2$` in a sentence."; + const result = await parseAdvancedmarkup(input); + + expect(result).toContain('\\(x^2 + y^2 = z^2\\)'); + expect(result).toContain("Here is some inline math:"); + expect(result).toContain("in a sentence."); + }); + + it("should process display math inside code blocks", async () => { + const input = "Here is a display equation:\n\n`$$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$`\n\nThis is after the equation."; + const result = await parseAdvancedmarkup(input); + + expect(result).toContain('\\[\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n\\]'); + expect(result).toContain('

    Here is a display equation:

    '); + expect(result).toContain('

    This is after the equation.

    '); + }); + + it("should process both inline and display math in the same code block", async () => { + const input = "Mixed math: `$\\alpha$ and $$\\beta = \\frac{1}{2}$$` in one block."; + const result = await parseAdvancedmarkup(input); + + expect(result).toContain('\\(\\alpha\\)'); + expect(result).toContain('\\[\\beta = \\frac{1}{2}\\]'); + expect(result).toContain("Mixed math:"); + expect(result).toContain("in one block."); + }); + + it("should NOT process math outside of code blocks", async () => { + const input = "This math $x^2 + y^2 = z^2$ should not be processed."; + const result = await parseAdvancedmarkup(input); + + expect(result).toContain("$x^2 + y^2 = z^2$"); + expect(result).not.toContain(''); + expect(result).not.toContain(''); + }); + + it("should NOT process display math outside of code blocks", async () => { + const input = "This display math $$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$ should not be processed."; + const result = await parseAdvancedmarkup(input); + + expect(result).toContain("$$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$"); + expect(result).not.toContain(''); + expect(result).not.toContain(''); + }); + + it("should handle code blocks without math normally", async () => { + const input = "Here is some code: `console.log('hello world')` that should not be processed."; + const result = await parseAdvancedmarkup(input); + + expect(result).toContain("`console.log('hello world')`"); + expect(result).not.toContain(''); + expect(result).not.toContain(''); + }); + + it("should handle complex math expressions with nested structures", async () => { + const input = "Complex math: `$$\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix} \\cdot \\begin{pmatrix} x \\\\ y \\end{pmatrix} = \\begin{pmatrix} ax + by \\\\ cx + dy \\end{pmatrix}$$`"; + const result = await parseAdvancedmarkup(input); + + expect(result).toContain(''); + expect(result).toContain("\\begin{pmatrix}"); + expect(result).toContain("\\end{pmatrix}"); + expect(result).toContain("\\cdot"); + }); + + it("should handle inline math with special characters", async () => { + const input = "Special chars: `$\\alpha, \\beta, \\gamma, \\delta$` and `$\\sum_{i=1}^{n} x_i$`"; + const result = await parseAdvancedmarkup(input); + + expect(result).toContain('\\(\\alpha, \\beta, \\gamma, \\delta\\)'); + expect(result).toContain('\\(\\sum_{i=1}^{n} x_i\\)'); + }); + + it("should handle multiple math expressions in separate code blocks", async () => { + const input = "First: `$E = mc^2$` and second: `$$F = G\\frac{m_1 m_2}{r^2}$$`"; + const result = await parseAdvancedmarkup(input); + + expect(result).toContain('\\(E = mc^2\\)'); + expect(result).toContain('\\[F = G\\frac{m_1 m_2}{r^2}\\]'); + }); + + it("should handle math expressions with line breaks in display mode", async () => { + const input = "Multi-line: `$$\n\\begin{align}\nx &= a + b \\\\\ny &= c + d\n\\end{align}\n$$`"; + const result = await parseAdvancedmarkup(input); + + expect(result).toContain(''); + expect(result).toContain("\\begin{align}"); + expect(result).toContain("\\end{align}"); + expect(result).toContain("x &= a + b"); + expect(result).toContain("y &= c + d"); + }); + + it("should handle edge case with empty math expressions", async () => { + const input = "Empty math: `$$` and `$`"; + const result = await parseAdvancedmarkup(input); + + // Should not crash and should preserve the original content + expect(result).toContain("`$$`"); + expect(result).toContain("`$`"); + }); + + it("should handle mixed content with regular text, code, and math", async () => { + const input = `This is a paragraph with regular text. + +Here is some code: \`console.log('hello')\` + +And here is math: \`$\\pi \\approx 3.14159$\` + +And display math: \`$$\n\\int_0^1 x^2 dx = \\frac{1}{3}\n$$\` + +And more regular text.`; + + const result = await parseAdvancedmarkup(input); + + // Should preserve regular text + expect(result).toContain("This is a paragraph with regular text."); + expect(result).toContain("And more regular text."); + + // Should preserve regular code blocks + expect(result).toContain("`console.log('hello')`"); + + // Should process math + expect(result).toContain('\\(\\pi \\approx 3.14159\\)'); + expect(result).toContain(''); + expect(result).toContain("\\int_0^1 x^2 dx = \\frac{1}{3}"); + }); + + it("should handle math expressions with dollar signs in the content", async () => { + const input = "Price math: `$\\text{Price} = \\$19.99$`"; + const result = await parseAdvancedmarkup(input); + + expect(result).toContain(''); + expect(result).toContain("\\text{Price} = \\$19.99"); + }); + + it("should handle display math with dollar signs in the content", async () => { + const input = "Price display: `$$\n\\text{Total} = \\$19.99 + \\$5.99 = \\$25.98\n$$`"; + const result = await parseAdvancedmarkup(input); + + expect(result).toContain(''); + expect(result).toContain("\\text{Total} = \\$19.99 + \\$5.99 = \\$25.98"); + }); + + it("should handle JSON content with escaped backslashes", async () => { + // Simulate content from JSON where backslashes are escaped + const jsonContent = "Math from JSON: `$\\\\alpha + \\\\beta = \\\\gamma$`"; + const result = await parseAdvancedmarkup(jsonContent); + + expect(result).toContain(''); + expect(result).toContain("\\\\alpha + \\\\beta = \\\\gamma"); + }); + + it("should handle JSON content with escaped display math", async () => { + // Simulate content from JSON where backslashes are escaped + const jsonContent = "Display math from JSON: `$$\\\\int_0^1 x^2 dx = \\\\frac{1}{3}$$`"; + const result = await parseAdvancedmarkup(jsonContent); + + expect(result).toContain(''); + expect(result).toContain("\\\\int_0^1 x^2 dx = \\\\frac{1}{3}"); + }); + + it("should handle JSON content with escaped dollar signs", async () => { + // Simulate content from JSON where dollar signs are escaped + const jsonContent = "Price math from JSON: `$\\\\text{Price} = \\\\\\$19.99$`"; + const result = await parseAdvancedmarkup(jsonContent); + + expect(result).toContain(''); + expect(result).toContain("\\\\text{Price} = \\\\\\$19.99"); + }); + + it("should handle complex JSON content with multiple escaped characters", async () => { + // Simulate complex content from JSON + const jsonContent = "Complex JSON math: `$$\\\\begin{pmatrix} a & b \\\\\\\\ c & d \\\\end{pmatrix} \\\\cdot \\\\begin{pmatrix} x \\\\\\\\ y \\\\end{pmatrix}$$`"; + const result = await parseAdvancedmarkup(jsonContent); + + expect(result).toContain(''); + expect(result).toContain("\\\\begin{pmatrix}"); + expect(result).toContain("\\\\end{pmatrix}"); + expect(result).toContain("\\\\cdot"); + expect(result).toContain("\\\\\\\\"); + }); +}); diff --git a/tests/unit/tagExpansion.test.ts b/tests/unit/tagExpansion.test.ts index cc55fb9..e47f74b 100644 --- a/tests/unit/tagExpansion.test.ts +++ b/tests/unit/tagExpansion.test.ts @@ -74,11 +74,14 @@ vi.mock("../../src/lib/utils/profileCache", () => ({ batchFetchProfiles: vi.fn( async ( pubkeys: string[], - onProgress: (fetched: number, total: number) => void, + ndk: any, + onProgress?: (fetched: number, total: number) => void, ) => { // Simulate progress updates - onProgress(0, pubkeys.length); - onProgress(pubkeys.length, pubkeys.length); + if (onProgress) { + onProgress(0, pubkeys.length); + onProgress(pubkeys.length, pubkeys.length); + } return []; }, ), From fc34d68a8312ddbd61ae74d882cadfb53549624f Mon Sep 17 00:00:00 2001 From: silberengel Date: Tue, 19 Aug 2025 14:15:20 +0200 Subject: [PATCH 74/98] fixed publications --- src/app.d.ts | 4 + .../publications/Publication.svelte | 100 ++++++++--- .../publications/PublicationSection.svelte | 6 +- .../publications/TableOfContents.svelte | 7 +- src/lib/snippets/UserSnippets.svelte | 2 +- .../[type]/[identifier]/+page.svelte | 165 ++++++++++++++---- .../publication/[type]/[identifier]/+page.ts | 84 ++++----- 7 files changed, 258 insertions(+), 110 deletions(-) diff --git a/src/app.d.ts b/src/app.d.ts index 1e997cc..3f8a7f6 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -13,6 +13,10 @@ declare global { publicationType?: string; indexEvent?: NDKEvent; url?: URL; + identifierInfo?: { + type: string; + identifier: string; + }; } // interface Platform {} } diff --git a/src/lib/components/publications/Publication.svelte b/src/lib/components/publications/Publication.svelte index 87bc44b..6398a0c 100644 --- a/src/lib/components/publications/Publication.svelte +++ b/src/lib/components/publications/Publication.svelte @@ -24,43 +24,67 @@ import TableOfContents from "./TableOfContents.svelte"; import type { TableOfContents as TocType } from "./table_of_contents.svelte"; - let { rootAddress, publicationType, indexEvent } = $props<{ + let { rootAddress, publicationType, indexEvent, publicationTree, toc } = $props<{ rootAddress: string; publicationType: string; indexEvent: NDKEvent; + publicationTree: SveltePublicationTree; + toc: TocType; }>(); - const publicationTree = getContext( - "publicationTree", - ) as SveltePublicationTree; - const toc = getContext("toc") as TocType; - // #region Loading - let leaves = $state>([]); - let isLoading = $state(false); - let isDone = $state(false); + let isLoading = $state(false); + let isDone = $state(false); let lastElementRef = $state(null); let activeAddress = $state(null); + let loadedAddresses = $state>(new Set()); + let hasInitialized = $state(false); let observer: IntersectionObserver; async function loadMore(count: number) { + if (!publicationTree) { + console.warn("[Publication] publicationTree is not available"); + return; + } + + console.log(`[Publication] Loading ${count} more events. Current leaves: ${leaves.length}, loaded addresses: ${loadedAddresses.size}`); + isLoading = true; - for (let i = 0; i < count; i++) { - const iterResult = await publicationTree.next(); - const { done, value } = iterResult; - - if (done) { - isDone = true; - break; + try { + for (let i = 0; i < count; i++) { + const iterResult = await publicationTree.next(); + const { done, value } = iterResult; + + if (done) { + console.log("[Publication] Iterator done, no more events"); + isDone = true; + break; + } + + if (value) { + const address = value.tagAddress(); + console.log(`[Publication] Got event: ${address} (${value.id})`); + if (!loadedAddresses.has(address)) { + loadedAddresses.add(address); + leaves.push(value); + console.log(`[Publication] Added event: ${address}`); + } else { + console.warn(`[Publication] Duplicate event detected: ${address}`); + } + } else { + console.log("[Publication] Got null event"); + leaves.push(null); + } } - - leaves.push(value); + } catch (error) { + console.error("[Publication] Error loading more content:", error); + } finally { + isLoading = false; + console.log(`[Publication] Finished loading. Total leaves: ${leaves.length}, loaded addresses: ${loadedAddresses.size}`); } - - isLoading = false; } function setLastElementRef(el: HTMLElement, i: number) { @@ -85,6 +109,27 @@ // #endregion + // AI-NOTE: Load initial content when publicationTree becomes available + $effect(() => { + if (publicationTree && leaves.length === 0 && !isLoading && !isDone && !hasInitialized) { + console.log("[Publication] Loading initial content"); + hasInitialized = true; + loadMore(12); + } + }); + + // AI-NOTE: Reset state when publicationTree changes + $effect(() => { + if (publicationTree) { + leaves = []; + isLoading = false; + isDone = false; + lastElementRef = null; + loadedAddresses = new Set(); + hasInitialized = false; + } + }); + // #region Columns visibility let currentBlog: null | string = $state(null); @@ -175,14 +220,18 @@ observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { - if (entry.isIntersecting && !isLoading && !isDone) { + if (entry.isIntersecting && !isLoading && !isDone && publicationTree) { loadMore(1); } }); }, { threshold: 0.5 }, ); - loadMore(12); + + // Only load initial content if publicationTree is available + if (publicationTree) { + loadMore(12); + } return () => { observer.disconnect(); @@ -207,11 +256,12 @@ /> publicationTree.setBookmark(address)} onLoadMore={() => { - if (!isLoading && !isDone) { + if (!isLoading && !isDone && publicationTree) { loadMore(4); } }} @@ -241,6 +291,8 @@ {rootAddress} {leaves} {address} + {publicationTree} + {toc} ref={(el) => onPublicationSectionMounted(el, address)} /> {/if} @@ -300,6 +352,8 @@ {rootAddress} {leaves} address={leaf.tagAddress()} + {publicationTree} + {toc} ref={(el) => setLastElementRef(el, i)} /> diff --git a/src/lib/components/publications/PublicationSection.svelte b/src/lib/components/publications/PublicationSection.svelte index 2de5292..2b9aace 100644 --- a/src/lib/components/publications/PublicationSection.svelte +++ b/src/lib/components/publications/PublicationSection.svelte @@ -9,6 +9,7 @@ import type { Asciidoctor, Document } from "asciidoctor"; import { getMatchingTags } from "$lib/utils/nostrUtils"; import type { SveltePublicationTree } from "./svelte_publication_tree.svelte"; + import type { TableOfContents as TocType } from "./table_of_contents.svelte"; import { postProcessAdvancedAsciidoctorHtml } from "$lib/utils/markup/advancedAsciidoctorPostProcessor"; import { parseAdvancedmarkup } from "$lib/utils/markup/advancedMarkupParser"; @@ -16,15 +17,18 @@ address, rootAddress, leaves, + publicationTree, + toc, ref, }: { address: string; rootAddress: string; leaves: Array; + publicationTree: SveltePublicationTree; + toc: TocType; ref: (ref: HTMLElement) => void; } = $props(); - const publicationTree: SveltePublicationTree = getContext("publicationTree"); const asciidoctor: Asciidoctor = getContext("asciidoctor"); let leafEvent: Promise = $derived.by( diff --git a/src/lib/components/publications/TableOfContents.svelte b/src/lib/components/publications/TableOfContents.svelte index a2fc748..cacee90 100644 --- a/src/lib/components/publications/TableOfContents.svelte +++ b/src/lib/components/publications/TableOfContents.svelte @@ -12,15 +12,14 @@ import Self from "./TableOfContents.svelte"; import { onMount, onDestroy } from "svelte"; - let { depth, onSectionFocused, onLoadMore } = $props<{ + let { depth, onSectionFocused, onLoadMore, toc } = $props<{ rootAddress: string; depth: number; + toc: TableOfContents; onSectionFocused?: (address: string) => void; onLoadMore?: () => void; }>(); - let toc = getContext("toc") as TableOfContents; - let entries = $derived.by(() => { const newEntries = []; for (const [_, entry] of toc.addressMap) { @@ -175,7 +174,7 @@ btnClass="flex items-center p-2 w-full font-normal text-gray-900 rounded-lg transition duration-75 group hover:bg-primary-50 dark:text-white dark:hover:bg-primary-800 {isVisible ? 'toc-highlight' : ''} {isLastEntry ? 'pb-4' : ''}" bind:isOpen={() => expanded, (open) => setEntryExpanded(address, open)} > - + {/if} {/each} diff --git a/src/lib/snippets/UserSnippets.svelte b/src/lib/snippets/UserSnippets.svelte index a4b4a17..6e96719 100644 --- a/src/lib/snippets/UserSnippets.svelte +++ b/src/lib/snippets/UserSnippets.svelte @@ -21,7 +21,7 @@ {@const npub = toNpub(identifier)} {#if npub} {#if !displayText || displayText.trim().toLowerCase() === "unknown"} - {#await getUserMetadata(npub) then profile} + {#await getUserMetadata(npub, undefined, false) then profile} {@const p = profile as NostrProfileWithLegacy}
    +{:else if loading} +
    +
    +

    Loading publication...

    +
    +
    +{:else if error} +
    +
    +
    +

    Failed to load publication

    +

    {error}

    + +
    +
    +
    {:else} - {@const debugInfo = `indexEvent: ${!!indexEvent}, data.indexEvent: ${!!data.indexEvent}`} + {@const debugInfo = `indexEvent: ${!!indexEvent}, publicationTree: ${!!publicationTree}, toc: ${!!toc}`} {@const debugElement = console.debug('[Publication] NOT rendering publication with:', debugInfo)}
    diff --git a/src/routes/publication/[type]/[identifier]/+page.ts b/src/routes/publication/[type]/[identifier]/+page.ts index 69d8a59..5a4a288 100644 --- a/src/routes/publication/[type]/[identifier]/+page.ts +++ b/src/routes/publication/[type]/[identifier]/+page.ts @@ -7,6 +7,7 @@ import { fetchEventByNevent, } from "../../../../lib/utils/websocket_utils.ts"; import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; +import { browser } from "$app/environment"; export const load: PageLoad = async ( { params }: { @@ -15,64 +16,49 @@ export const load: PageLoad = async ( ) => { const { type, identifier } = params; - // AI-NOTE: Always fetch client-side since server-side fetch returns null for now + // AI-NOTE: Only fetch client-side since server-side fetch fails due to missing relay connections + // This prevents 404 errors when refreshing publication pages during SSR let indexEvent: NostrEvent | null = null; - try { - // Handle different identifier types - switch (type) { - case "id": - indexEvent = await fetchEventById(identifier); - break; - case "d": - indexEvent = await fetchEventByDTag(identifier); - break; - case "naddr": - indexEvent = await fetchEventByNaddr(identifier); - break; - case "nevent": - indexEvent = await fetchEventByNevent(identifier); - break; - default: - error(400, `Unsupported identifier type: ${type}`); + // Only attempt to fetch if we're in a browser environment + if (browser) { + try { + // Handle different identifier types + switch (type) { + case "id": + indexEvent = await fetchEventById(identifier); + break; + case "d": + indexEvent = await fetchEventByDTag(identifier); + break; + case "naddr": + indexEvent = await fetchEventByNaddr(identifier); + break; + case "nevent": + indexEvent = await fetchEventByNevent(identifier); + break; + default: + error(400, `Unsupported identifier type: ${type}`); + } + } catch (err) { + // AI-NOTE: Don't throw error immediately - let the component handle it + // This allows for better error handling and retry logic + console.warn(`[Publication Load] Failed to fetch event:`, err); } - } catch (err) { - throw err; } - if (!indexEvent) { - // AI-NOTE: Handle case where no relays are available during preloading - // This prevents 404 errors when relay stores haven't been populated yet - - // Create appropriate search link based on type - let searchParam = ""; - switch (type) { - case "id": - searchParam = `id=${identifier}`; - break; - case "d": - searchParam = `d=${identifier}`; - break; - case "naddr": - case "nevent": - searchParam = `id=${identifier}`; - break; - default: - searchParam = `q=${identifier}`; - } - - error( - 404, - `Event not found for ${type}: ${identifier}. href="/events?${searchParam}"`, - ); - } - - const publicationType = - indexEvent.tags.find((tag) => tag[0] === "type")?.[1] ?? ""; + // AI-NOTE: Return null for indexEvent during SSR or when fetch fails + // The component will handle client-side loading and error states + const publicationType = indexEvent?.tags.find((tag) => tag[0] === "type")?.[1] ?? ""; const result = { publicationType, indexEvent, + // AI-NOTE: Pass the identifier info for client-side retry + identifierInfo: { + type, + identifier, + }, }; return result; From b9cd3ba10c599aee6c1aed716c8d8cf20909fc98 Mon Sep 17 00:00:00 2001 From: silberengel Date: Tue, 19 Aug 2025 14:20:33 +0200 Subject: [PATCH 75/98] load publication in order --- src/lib/data_structures/publication_tree.ts | 35 +++++++++++---------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index a64489b..2e0ee1e 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -788,9 +788,7 @@ export class PublicationTree implements AsyncIterable { }); } - return Promise.resolve( - this.#buildNodeFromEvent(event, address, parentNode), - ); + return await this.#buildNodeFromEvent(event, address, parentNode); } /** @@ -851,7 +849,7 @@ export class PublicationTree implements AsyncIterable { this.#eventCache.set(address, fetchedEvent); this.#events.set(address, fetchedEvent); - return this.#buildNodeFromEvent(fetchedEvent, address, parentNode); + return await this.#buildNodeFromEvent(fetchedEvent, address, parentNode); } } catch (error) { console.debug( @@ -894,11 +892,11 @@ export class PublicationTree implements AsyncIterable { * AI-NOTE: 2025-01-24 - Helper method to build a node from an event * This extracts the common logic for building nodes from events */ - #buildNodeFromEvent( + async #buildNodeFromEvent( event: NDKEvent, address: string, parentNode: PublicationTreeNode, - ): PublicationTreeNode { + ): Promise { this.#events.set(address, event); const childAddresses = event.tags @@ -978,16 +976,21 @@ export class PublicationTree implements AsyncIterable { children: [], }; - // Add children asynchronously - const childPromises = childAddresses.map((address) => - this.addEventByAddress(address, event) - ); - Promise.all(childPromises).catch((error) => { - console.warn( - `[PublicationTree] Error adding children for ${address}:`, - error, - ); - }); + // Add children in the order they appear in the a-tags to preserve section order + // Use sequential processing to ensure order is maintained + console.log(`[PublicationTree] Adding ${childAddresses.length} children in order:`, childAddresses); + for (const address of childAddresses) { + console.log(`[PublicationTree] Adding child: ${address}`); + try { + await this.addEventByAddress(address, event); + console.log(`[PublicationTree] Successfully added child: ${address}`); + } catch (error) { + console.warn( + `[PublicationTree] Error adding child ${address} for ${node.address}:`, + error, + ); + } + } this.#nodeResolvedObservers.forEach((observer) => observer(address)); From 358e81096f525720bb6e88a808d16a725716a8c8 Mon Sep 17 00:00:00 2001 From: silberengel Date: Tue, 19 Aug 2025 14:34:30 +0200 Subject: [PATCH 76/98] Added tagline --- src/lib/components/Navigation.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/components/Navigation.svelte b/src/lib/components/Navigation.svelte index 1d37f14..675ab46 100644 --- a/src/lib/components/Navigation.svelte +++ b/src/lib/components/Navigation.svelte @@ -18,7 +18,10 @@
    -

    Alexandria

    +
    +

    Alexandria

    +

    READ THE ORIGINAL. MAKE CONNECTIONS. CULTIVATE KNOWLEDGE.

    +
    From ce40eade26b4fe45c9bf735d178363171fdf0e1e Mon Sep 17 00:00:00 2001 From: silberengel Date: Tue, 19 Aug 2025 14:40:01 +0200 Subject: [PATCH 77/98] fixed ToC order --- .../publications/table_of_contents.svelte.ts | 35 +++---------------- 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/src/lib/components/publications/table_of_contents.svelte.ts b/src/lib/components/publications/table_of_contents.svelte.ts index aae250b..69b783f 100644 --- a/src/lib/components/publications/table_of_contents.svelte.ts +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -219,7 +219,9 @@ export class TableOfContents { this.addressMap.set(childAddress, childEntry); } - await this.#matchChildrenToTagOrder(entry); + // AI-NOTE: 2025-01-24 - Removed redundant sorting since the publication tree already preserves 'a' tag order + // The children are already in the correct order from the publication tree + // await this.#matchChildrenToTagOrder(entry); entry.childrenResolved = true; }; @@ -253,35 +255,8 @@ 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. - * - * 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); - 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; - }); - } - } + // AI-NOTE: 2025-01-24 - Removed #matchChildrenToTagOrder method since the publication tree already preserves 'a' tag order + // The children are already in the correct order from the publication tree, so no additional sorting is needed #buildTocEntryFromResolvedNode(address: string) { if (this.addressMap.has(address)) { From d7483e934f96195d0d74eeb8627afa086a910300 Mon Sep 17 00:00:00 2001 From: silberengel Date: Tue, 19 Aug 2025 14:53:34 +0200 Subject: [PATCH 78/98] fixed event embedding --- .../embedded_events/EmbeddedEvent.svelte | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/lib/components/embedded_events/EmbeddedEvent.svelte b/src/lib/components/embedded_events/EmbeddedEvent.svelte index eb8a711..eda8a0e 100644 --- a/src/lib/components/embedded_events/EmbeddedEvent.svelte +++ b/src/lib/components/embedded_events/EmbeddedEvent.svelte @@ -1,7 +1,6 @@

    Publish Nostr Event

    + + +
    +

    Load Existing Event

    +
    + { + if (e.key === 'Enter' && !loadingEvent && eventIdSearch.trim()) { + e.preventDefault(); + loadEventById(); + } + }} + /> + +
    +

    + Load an existing event to edit and publish as a replacement with your signature. +

    +
    +
    @@ -596,25 +770,8 @@

    Extracted Metadata (from AsciiDoc header)

    -
    - {#each extractedMetadata as [key, value], i} -
    - {key}: - - -
    - {/each} +
    + {extractedMetadata.map(([key, value]) => `${key}: ${value}`).join(', ')}
    {/if} @@ -629,8 +786,6 @@ class="input input-bordered flex-1" placeholder="tag key (e.g., q, p, e)" bind:value={tags[i][0]} - oninput={(e) => - updateTag(i, (e.target as HTMLInputElement).value, tags[i][1] || "")} />
    -
    - - -
    -
    - - - {#if dTagError} -
    {dTagError}
    - {/if} -
    -
    + +
    +
    {/if} + {/if} + + + +
    +
    +

    Event Preview

    + +
    + + {#if showJsonPreview} + {@const preview = eventPreview()} + {#if preview} +
    + {#if preview.type === 'error'} +
    + {preview.message} +
    + {:else} +
    + + Event Type: {preview.type === '30040_index_event' ? '30040 Publication Index' : 'Standard Event'} + +
    +
    {JSON.stringify(preview.event, null, 2)}
    {/if} - +
    + {:else} +
    +
    + Please log in to see the event preview. +
    +
    + {/if} + {/if}
    {#if showWarning} @@ -779,3 +953,4 @@
    {/if} +
    diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index f2cc770..ef3e8ca 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -9,6 +9,7 @@ import { communityRelays, searchRelays, secondaryRelays, + localRelays, } from "../consts.ts"; import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts"; import { NDKRelaySet as NDKRelaySetFromNDK } from "@nostr-dev-kit/ndk"; @@ -389,7 +390,7 @@ Promise.prototype.withTimeout = function ( export async function fetchEventWithFallback( ndk: NDK, filterOrId: string | Filter, - timeoutMs: number = 3000, + timeoutMs: number = 10000, ): Promise { // AI-NOTE: 2025-01-24 - Use ALL available relays for comprehensive event discovery // This ensures we don't miss events that might be on any available relay @@ -417,7 +418,15 @@ export async function fetchEventWithFallback( "fetchEventWithFallback: No relays available for event fetch, using fallback relays", ); // Use fallback relays when no relays are available - allRelays = [...secondaryRelays, ...searchRelays, ...anonymousRelays]; + // AI-NOTE: 2025-01-24 - Include ALL available relays for comprehensive event discovery + // This ensures we don't miss events that might be on any available relay + allRelays = [ + ...secondaryRelays, + ...searchRelays, + ...anonymousRelays, + ...inboxRelays, // Include user's inbox relays + ...outboxRelays, // Include user's outbox relays + ]; console.log("fetchEventWithFallback: Using fallback relays:", allRelays); } From b6be86a6ae78113193c8d1ac78ee4401ce5d441d Mon Sep 17 00:00:00 2001 From: silberengel Date: Tue, 19 Aug 2025 21:47:20 +0200 Subject: [PATCH 89/98] Implemented event publisher subfiles --- src/lib/components/EventInput.svelte | 983 +++--------------- .../components/event_input/EventForm.svelte | 162 +++ .../event_input/EventPreview.svelte | 172 +++ .../components/event_input/TagManager.svelte | 342 ++++++ .../components/event_input/eventServices.ts | 277 +++++ src/lib/components/event_input/types.ts | 63 ++ src/lib/components/event_input/validation.ts | 90 ++ 7 files changed, 1265 insertions(+), 824 deletions(-) create mode 100644 src/lib/components/event_input/EventForm.svelte create mode 100644 src/lib/components/event_input/EventPreview.svelte create mode 100644 src/lib/components/event_input/TagManager.svelte create mode 100644 src/lib/components/event_input/eventServices.ts create mode 100644 src/lib/components/event_input/types.ts create mode 100644 src/lib/components/event_input/validation.ts diff --git a/src/lib/components/EventInput.svelte b/src/lib/components/EventInput.svelte index bf83f4d..d296827 100644 --- a/src/lib/components/EventInput.svelte +++ b/src/lib/components/EventInput.svelte @@ -1,129 +1,47 @@ -
    -

    Publish Nostr Event

    +
    +
    +

    Publish Nostr Event

    +
    + + +
    +
    @@ -728,229 +229,63 @@

    -
    -
    - - - {#if !isValidKind(kind)} -
    - Kind must be an integer between 0 and 65535 (NIP-01). -
    - {/if} - {#if Number(kind) === 30040} -
    - Publication Index - - -
    - 30040 - Publication Index: Events that organize AsciiDoc content into structured publications with metadata tags and section references. -
    -
    -
    - {/if} -
    -
    - - - - {#if extractedMetadata.length > 0} -
    -

    - Extracted Metadata (from AsciiDoc header) -

    -
    - {extractedMetadata.map(([key, value]) => `${key}: ${value}`).join(', ')} -
    -
    - {/if} - -
    - {#each tags as tag, i} -
    -
    - Tag: - - -
    - -
    -
    - Values: - -
    - - {#each tag.slice(1) as value, valueIndex} -
    - {valueIndex + 1}: - - {#if tag.length > 2} - - {/if} -
    - {/each} -
    -
    - {/each} -
    - -
    -
    -
    -
    - - -
    + + + + + + + +
    + +
    -
    - - + + {#if loading} +
    Publishing...
    + {/if} + {#if error} +
    {error}
    + {/if} + {#if success} +
    {success}
    +
    + Relays: {publishedRelays.join(", ")}
    - {#if loading} - Publishing... - {/if} - {#if error} -
    {error}
    - {/if} - {#if success} -
    {success}
    -
    - Relays: {publishedRelays.join(", ")} + {#if lastPublishedEventId} +
    + Event ID: {lastPublishedEventId} +
    - {#if lastPublishedEventId} -
    - Event ID: {lastPublishedEventId} - -
    - {/if} {/if} - - - -
    -
    -

    Event Preview

    - -
    - - {#if showJsonPreview} - {@const preview = eventPreview()} - {#if preview} -
    - {#if preview.type === 'error'} -
    - {preview.message} -
    - {:else} -
    - - Event Type: {preview.type === '30040_index_event' ? '30040 Publication Index' : 'Standard Event'} - -
    -
    {JSON.stringify(preview.event, null, 2)}
    - {/if} -
    - {:else} -
    -
    - Please log in to see the event preview. -
    -
    - {/if} - {/if} -
    - - {#if showWarning} -
    -
    -

    Warning

    -

    {warningMessage}

    -
    - - -
    -
    -
    {/if} + + + showJsonPreview = !showJsonPreview} + />
    diff --git a/src/lib/components/event_input/EventForm.svelte b/src/lib/components/event_input/EventForm.svelte new file mode 100644 index 0000000..94bf99f --- /dev/null +++ b/src/lib/components/event_input/EventForm.svelte @@ -0,0 +1,162 @@ + + +
    + +
    + + + {#if !isValidKind(eventData.kind)} +
    + Kind must be an integer between 0 and 65535 (NIP-01). +
    + {/if} + {#if isValidKind(eventData.kind)} +
    + + {getKindDescription(eventData.kind)} + + {#if eventData.kind === 30040} + + +
    + 30040 - Publication Index: Events that organize AsciiDoc content into structured publications with metadata tags and section references. +
    +
    + {/if} +
    + {/if} +
    + + +
    + + + + + {#if eventData.kind === 30023} +
    + Use Markdown format for long-form content. Do not use AsciiDoc headers (=). +
    + {:else if eventData.kind === 30040 || eventData.kind === 30041 || eventData.kind === 30818} +
    + Use AsciiDoc format. Start with a document title (=) and include section headers (==). +
    + {/if} +
    + + + {#if validationError} +
    + {validationError} +
    + {/if} + {#if validationWarning} +
    + Warning: {validationWarning} +
    + {/if} + + +
    diff --git a/src/lib/components/event_input/EventPreview.svelte b/src/lib/components/event_input/EventPreview.svelte new file mode 100644 index 0000000..55742fb --- /dev/null +++ b/src/lib/components/event_input/EventPreview.svelte @@ -0,0 +1,172 @@ + + + +
    +
    +

    Event Preview

    + +
    + + {#if showJsonPreview} + {#if eventPreview} +
    + {#if eventPreview.type === 'error'} +
    + {eventPreview.message} +
    + {:else} +
    + + Event Type: {eventPreview.type === '30040_index_event' ? '30040 Publication Index' : 'Standard Event'} + +
    +
    {JSON.stringify(eventPreview.event, null, 2)}
    + {/if} +
    + {:else} +
    +
    + Please log in to see the event preview. +
    +
    + {/if} + {/if} +
    diff --git a/src/lib/components/event_input/TagManager.svelte b/src/lib/components/event_input/TagManager.svelte new file mode 100644 index 0000000..648fe56 --- /dev/null +++ b/src/lib/components/event_input/TagManager.svelte @@ -0,0 +1,342 @@ + + +
    + + + + {#if extractedMetadata.length > 0} +
    +

    + Extracted Metadata (from AsciiDoc header) +

    +
    + {extractedMetadata.map(([key, value]) => `${key}: ${value}`).join(', ')} +
    +
    + {/if} + + +
    + {#each tags as tag, i} +
    + +
    + Tag: + updateTagKey(i, (e.target as HTMLInputElement).value)} + /> + {#if isPresetTag(tag.key)} + + Preset + + {/if} + +
    + + + {#if isPresetTag(tag.key)} + {@const presetInfo = getPresetTagInfo(tag.key)} + {#if presetInfo} +
    + {presetInfo.description} + {#if presetInfo.autoUpdate} + (auto-updates from content) + {/if} +
    + {/if} + {/if} + + +
    +
    + Values: + +
    + + {#each tag.values as value, valueIndex} +
    + + {valueIndex + 1}: + + updateTagValue(i, valueIndex, (e.target as HTMLInputElement).value)} + /> + {#if tag.values.length > 1} + + {/if} +
    + {/each} +
    +
    + {/each} + + +
    + +
    +
    +
    diff --git a/src/lib/components/event_input/eventServices.ts b/src/lib/components/event_input/eventServices.ts new file mode 100644 index 0000000..84cb072 --- /dev/null +++ b/src/lib/components/event_input/eventServices.ts @@ -0,0 +1,277 @@ +/** + * Event publishing and loading services + */ + +import { get } from "svelte/store"; +import { userStore } from "$lib/stores/userStore"; +import NDK, { NDKEvent as NDKEventClass } from "@nostr-dev-kit/ndk"; +import type { NDKEvent } from "$lib/utils/nostrUtils"; +import { prefixNostrAddresses } from "$lib/utils/nostrUtils"; +import { fetchEventWithFallback } from "$lib/utils/nostrUtils"; + +import { WebSocketPool } from "$lib/data_structures/websocket_pool"; +import { anonymousRelays } from "$lib/consts"; +import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; +import { removeMetadataFromContent } from "$lib/utils/asciidoc_metadata"; +import { build30040EventSet } from "$lib/utils/event_input_utils"; +import type { EventData, TagData, PublishResult, LoadEventResult } from "./types"; + +/** + * Converts TagData array to NDK-compatible format + */ +function convertTagsToNDKFormat(tags: TagData[]): string[][] { + return tags + .filter(tag => tag.key.trim() !== "") + .map(tag => [tag.key, ...tag.values]); +} + +/** + * Publishes an event to relays + */ +export async function publishEvent(ndk: any, eventData: EventData, tags: TagData[]): Promise { + if (!ndk) { + return { success: false, error: "NDK context not available" }; + } + + const userState = get(userStore); + const pubkey = userState.pubkey; + + if (!pubkey) { + return { success: false, error: "User not logged in." }; + } + + const pubkeyString = String(pubkey); + if (!/^[a-fA-F0-9]{64}$/.test(pubkeyString)) { + return { success: false, error: "Invalid public key: must be a 64-character hex string." }; + } + + const baseEvent = { pubkey: pubkeyString, created_at: eventData.createdAt }; + let events: NDKEvent[] = []; + + console.log("Publishing event with kind:", eventData.kind); + console.log("Content length:", eventData.content.length); + console.log("Content preview:", eventData.content.substring(0, 100)); + console.log("Tags:", tags); + + if (Number(eventData.kind) === 30040) { + console.log("=== 30040 EVENT CREATION START ==="); + console.log("Creating 30040 event set with content:", eventData.content); + + try { + // Get the current d and title values from the UI + const dTagValue = tags.find(tag => tag.key === "d")?.values[0] || ""; + const titleTagValue = tags.find(tag => tag.key === "title")?.values[0] || ""; + + // Convert multi-value tags to the format expected by build30040EventSet + // Filter out d and title tags since we'll add them manually + const compatibleTags: [string, string][] = tags + .filter(tag => tag.key.trim() !== "" && tag.key !== "d" && tag.key !== "title") + .map(tag => [tag.key, tag.values[0] || ""] as [string, string]); + + const { indexEvent, sectionEvents } = build30040EventSet( + eventData.content, + compatibleTags, + baseEvent, + ndk, + ); + + // Override the d and title tags with the UI values if they exist + const finalTags = indexEvent.tags.filter(tag => tag[0] !== "d" && tag[0] !== "title"); + if (dTagValue) { + finalTags.push(["d", dTagValue]); + } + if (titleTagValue) { + finalTags.push(["title", titleTagValue]); + } + + // Update the index event with the correct tags + indexEvent.tags = finalTags; + console.log("Index event:", indexEvent); + console.log("Section events:", sectionEvents); + + // Publish all 30041 section events first, then the 30040 index event + events = [...sectionEvents, indexEvent]; + console.log("Total events to publish:", events.length); + console.log("=== 30040 EVENT CREATION END ==="); + } catch (error) { + console.error("Error in build30040EventSet:", error); + return { + success: false, + error: `Failed to build 30040 event set: ${error instanceof Error ? error.message : "Unknown error"}` + }; + } + } else { + // Convert multi-value tags to the format expected by NDK + let eventTags = convertTagsToNDKFormat(tags); + + // For AsciiDoc events, remove metadata from content + let finalContent = eventData.content; + if (eventData.kind === 30040 || eventData.kind === 30041) { + finalContent = removeMetadataFromContent(eventData.content); + } + + // Prefix Nostr addresses before publishing + const prefixedContent = prefixNostrAddresses(finalContent); + + // Create event with proper serialization + const eventDataForNDK = { + kind: eventData.kind, + content: prefixedContent, + tags: eventTags, + pubkey: pubkeyString, + created_at: eventData.createdAt, + }; + + events = [new NDKEventClass(ndk, eventDataForNDK)]; + } + + let atLeastOne = false; + let relaysPublished: string[] = []; + let lastEventId: string | null = null; + + for (let i = 0; i < events.length; i++) { + const event = events[i]; + try { + console.log("Publishing event:", { + kind: event.kind, + content: event.content, + tags: event.tags, + hasContent: event.content && event.content.length > 0, + }); + + // Always sign with a plain object if window.nostr is available + // Create a completely plain object to avoid proxy cloning issues + const plainEvent = { + kind: Number(event.kind), + pubkey: String(event.pubkey), + created_at: Number( + event.created_at ?? Math.floor(Date.now() / 1000), + ), + tags: event.tags.map((tag) => tag.map(String)), + content: String(event.content), + }; + + if ( + typeof window !== "undefined" && + window.nostr && + window.nostr.signEvent + ) { + const signed = await window.nostr.signEvent(plainEvent); + event.sig = signed.sig; + if ("id" in signed) { + event.id = signed.id as string; + } + } else { + await event.sign(); + } + + // Use direct WebSocket publishing like CommentBox does + const signedEvent = { + ...plainEvent, + id: event.id, + sig: event.sig, + }; + + // Try to publish to relays directly + const relays = [ + ...anonymousRelays, + ...get(activeOutboxRelays), + ...get(activeInboxRelays), + ]; + let published = false; + + for (const relayUrl of relays) { + try { + const ws = await WebSocketPool.instance.acquire(relayUrl); + + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + WebSocketPool.instance.release(ws); + reject(new Error("Timeout")); + }, 5000); + + ws.onmessage = (e) => { + const [type, id, ok, message] = JSON.parse(e.data); + if (type === "OK" && id === signedEvent.id) { + clearTimeout(timeout); + if (ok) { + published = true; + relaysPublished.push(relayUrl); + WebSocketPool.instance.release(ws); + resolve(); + } else { + WebSocketPool.instance.release(ws); + reject(new Error(message)); + } + } + }; + + // Send the event to the relay + ws.send(JSON.stringify(["EVENT", signedEvent])); + }); + if (published) break; + } catch (e) { + console.error(`Failed to publish to ${relayUrl}:`, e); + } + } + + if (published) { + atLeastOne = true; + // For 30040, set lastEventId to the index event (last in array) + if (Number(eventData.kind) === 30040) { + if (i === events.length - 1) { + lastEventId = event.id; + } + } else { + lastEventId = event.id; + } + } + } catch (signError) { + console.error("Error signing/publishing event:", signError); + return { + success: false, + error: `Failed to sign event: ${signError instanceof Error ? signError.message : "Unknown error"}` + }; + } + } + + if (atLeastOne) { + return { + success: true, + eventId: lastEventId || undefined, + relays: relaysPublished + }; + } else { + return { success: false, error: "Failed to publish to any relay." }; + } +} + +/** + * Loads an event by its hex ID + */ +export async function loadEvent(ndk: any, eventId: string): Promise { + if (!ndk) { + throw new Error("NDK context not available"); + } + + const foundEvent = await fetchEventWithFallback(ndk, eventId, 10000); + + if (foundEvent) { + // Convert NDK event format to our format + const eventData: EventData = { + kind: foundEvent.kind || 30040, + content: foundEvent.content || "", + createdAt: Math.floor(Date.now() / 1000), // Use current time for replacement + }; + + // Convert NDK tags format to our format + const tags: TagData[] = foundEvent.tags.map((tag: string[]) => ({ + key: tag[0] || "", + values: tag.slice(1) + })); + + return { eventData, tags }; + } + + return null; +} diff --git a/src/lib/components/event_input/types.ts b/src/lib/components/event_input/types.ts new file mode 100644 index 0000000..df7e8f9 --- /dev/null +++ b/src/lib/components/event_input/types.ts @@ -0,0 +1,63 @@ +/** + * Type definitions for the EventInput component system + */ + +export interface EventData { + kind: number; + content: string; + createdAt: number; +} + +export interface TagData { + key: string; + values: string[]; +} + +export interface ValidationResult { + valid: boolean; + reason?: string; + warning?: string; +} + +export interface PublishResult { + success: boolean; + eventId?: string; + relays?: string[]; + error?: string; +} + +export interface LoadEventResult { + eventData: EventData; + tags: TagData[]; +} + +export interface EventPreview { + type: 'standard_event' | '30040_index_event' | 'error'; + event?: { + id: string; + pubkey: string; + created_at: number; + kind: number; + tags: string[][]; + content: string; + sig: string; + }; + message?: string; +} + +export interface PresetTag { + key: string; + defaultValue: string; + required: boolean; + autoUpdate: boolean; + description: string; +} + +export interface KindConfig { + kind: number; + name: string; + description: string; + presetTags: PresetTag[]; + requiresContent: boolean; + contentValidation?: (content: string) => ValidationResult; +} diff --git a/src/lib/components/event_input/validation.ts b/src/lib/components/event_input/validation.ts new file mode 100644 index 0000000..7fb6609 --- /dev/null +++ b/src/lib/components/event_input/validation.ts @@ -0,0 +1,90 @@ +/** + * Event validation utilities + */ + +import { get } from "svelte/store"; +import { userStore } from "$lib/stores/userStore"; +import type { EventData, TagData, ValidationResult } from "./types"; +import { + validateNotAsciidoc, + validateAsciiDoc, + validate30040EventSet, +} from "$lib/utils/event_input_utils"; + +/** + * Validates an event and its tags + */ +export function validateEvent(eventData: EventData, tags: TagData[]): ValidationResult { + const userState = get(userStore); + + const pubkey = userState.pubkey; + if (!pubkey) { + return { valid: false, reason: "Not logged in." }; + } + + // Content validation - 30040 events don't require content + if (eventData.kind !== 30040 && !eventData.content.trim()) { + return { valid: false, reason: "Content required." }; + } + + // Kind-specific validation + if (eventData.kind === 30023) { + const v = validateNotAsciidoc(eventData.content); + if (!v.valid) return v; + } + + if (eventData.kind === 30040) { + // Check for required tags + const versionTag = tags.find(t => t.key === "version"); + const dTag = tags.find(t => t.key === "d"); + const titleTag = tags.find(t => t.key === "title"); + + if (!versionTag || !versionTag.values[0] || versionTag.values[0].trim() === "") { + return { valid: false, reason: "30040 events require a 'version' tag." }; + } + + if (!dTag || !dTag.values[0] || dTag.values[0].trim() === "") { + return { valid: false, reason: "30040 events require a 'd' tag." }; + } + + if (!titleTag || !titleTag.values[0] || titleTag.values[0].trim() === "") { + return { valid: false, reason: "30040 events require a 'title' tag." }; + } + + // Validate content format if present + if (eventData.content.trim()) { + const v = validate30040EventSet(eventData.content); + if (!v.valid) return v; + if (v.warning) return { valid: true, warning: v.warning }; + } + } + + if (eventData.kind === 30041 || eventData.kind === 30818) { + const v = validateAsciiDoc(eventData.content); + if (!v.valid) return v; + } + + return { valid: true }; +} + +/** + * Validates that a kind is within valid range + */ +export function isValidKind(kind: number | string): boolean { + const n = Number(kind); + return Number.isInteger(n) && n >= 0 && n <= 65535; +} + +/** + * Validates that a tag has a valid key + */ +export function isValidTagKey(key: string): boolean { + return key.trim().length > 0; +} + +/** + * Validates that a tag has at least one value + */ +export function isValidTag(tag: TagData): boolean { + return isValidTagKey(tag.key) && tag.values.some(v => v.trim().length > 0); +} From af4fde59b232ee9053674648a860af25b2dca646 Mon Sep 17 00:00:00 2001 From: silberengel Date: Tue, 19 Aug 2025 22:16:06 +0200 Subject: [PATCH 90/98] fixed event input --- src/lib/components/EventInput.svelte | 145 +++++++++++++++--- .../components/event_input/eventServices.ts | 4 +- 2 files changed, 126 insertions(+), 23 deletions(-) diff --git a/src/lib/components/EventInput.svelte b/src/lib/components/EventInput.svelte index d296827..0e1b4e5 100644 --- a/src/lib/components/EventInput.svelte +++ b/src/lib/components/EventInput.svelte @@ -38,7 +38,9 @@ // Event loading state let eventIdSearch = $state(""); + let eventJsonInput = $state(""); let loadingEvent = $state(false); + let loadMethod = $state<'hex' | 'json'>('hex'); // Session storage loading let hasLoadedFromStorage = $state(false); @@ -143,6 +145,55 @@ } } + /** + * Loads an event from JSON string for editing + */ + function loadEventFromJson(): void { + if (!eventJsonInput.trim()) { + error = "Please enter event JSON."; + return; + } + + try { + const eventJson = JSON.parse(eventJsonInput.trim()); + + // Validate required fields + if (typeof eventJson.kind !== 'number') { + error = "Invalid event JSON: missing or invalid 'kind' field."; + return; + } + + if (typeof eventJson.content !== 'string') { + error = "Invalid event JSON: missing or invalid 'content' field."; + return; + } + + if (!Array.isArray(eventJson.tags)) { + error = "Invalid event JSON: missing or invalid 'tags' field."; + return; + } + + // Extract event data (drop fields that need to be regenerated) + eventData = { + kind: eventJson.kind, + content: eventJson.content, + createdAt: Math.floor(Date.now() / 1000), // Use current time + }; + + // Convert tags from NDK format to our format + tags = eventJson.tags.map((tag: string[]) => ({ + key: tag[0] || "", + values: tag.slice(1) + })); + + success = "Loaded event from JSON successfully."; + error = null; + } catch (err) { + console.error("Error parsing event JSON:", err); + error = `Failed to parse event JSON: ${err instanceof Error ? err.message : "Invalid JSON format"}`; + } + } + /** * Clears all form fields and resets to initial state */ @@ -158,6 +209,7 @@ publishedRelays = []; lastPublishedEventId = null; eventIdSearch = ""; + eventJsonInput = ""; showJsonPreview = false; } @@ -201,32 +253,83 @@

    Load Existing Event

    -
    - { - if (e.key === 'Enter' && !loadingEvent && eventIdSearch.trim()) { - e.preventDefault(); - loadEventById(); - } - }} - /> + + +
    +
    -

    - Load an existing event to edit and publish as a replacement with your signature. -

    + + {#if loadMethod === 'hex'} + +
    + { + if (e.key === 'Enter' && !loadingEvent && eventIdSearch.trim()) { + e.preventDefault(); + loadEventById(); + } + }} + /> + +
    +

    + Load an existing event from relays by its hex ID. +

    + {:else} + +
    + +
    + + +
    +
    +

    + Paste a complete event JSON to load it into the form. Fields like id, pubkey, created_at, and sig will be regenerated. +

    + {/if}
    diff --git a/src/lib/components/event_input/eventServices.ts b/src/lib/components/event_input/eventServices.ts index 84cb072..3a6db85 100644 --- a/src/lib/components/event_input/eventServices.ts +++ b/src/lib/components/event_input/eventServices.ts @@ -259,8 +259,8 @@ export async function loadEvent(ndk: any, eventId: string): Promise Date: Tue, 19 Aug 2025 23:06:05 +0200 Subject: [PATCH 91/98] display nested publications --- .../publications/Publication.svelte | 34 +++--- src/lib/data_structures/publication_tree.ts | 110 ++++++++++++++---- 2 files changed, 109 insertions(+), 35 deletions(-) diff --git a/src/lib/components/publications/Publication.svelte b/src/lib/components/publications/Publication.svelte index 6398a0c..6170e15 100644 --- a/src/lib/components/publications/Publication.svelte +++ b/src/lib/components/publications/Publication.svelte @@ -109,24 +109,31 @@ // #endregion - // AI-NOTE: Load initial content when publicationTree becomes available - $effect(() => { - if (publicationTree && leaves.length === 0 && !isLoading && !isDone && !hasInitialized) { - console.log("[Publication] Loading initial content"); - hasInitialized = true; - loadMore(12); - } - }); - - // AI-NOTE: Reset state when publicationTree changes + // AI-NOTE: 2025-01-24 - Combined effect to handle publicationTree changes and initial loading + // This prevents conflicts between separate effects that could cause duplicate loading $effect(() => { if (publicationTree) { + // Reset state when publicationTree changes leaves = []; isLoading = false; isDone = false; lastElementRef = null; loadedAddresses = new Set(); hasInitialized = false; + + // Reset the publication tree iterator to prevent duplicate events + if (typeof publicationTree.resetIterator === 'function') { + publicationTree.resetIterator(); + } + + // AI-NOTE: 2025-01-24 - Use setTimeout to ensure iterator reset completes before loading + // This prevents race conditions where loadMore is called before the iterator is fully reset + setTimeout(() => { + // Load initial content after reset + console.log("[Publication] Loading initial content after reset"); + hasInitialized = true; + loadMore(12); + }, 0); } }); @@ -228,10 +235,9 @@ { threshold: 0.5 }, ); - // Only load initial content if publicationTree is available - if (publicationTree) { - loadMore(12); - } + // AI-NOTE: 2025-01-24 - Removed duplicate loadMore call + // Initial content loading is handled by the $effect that watches publicationTree + // This prevents duplicate loading when both onMount and $effect trigger return () => { observer.disconnect(); diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index 2e0ee1e..44f5374 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -69,6 +69,12 @@ export class PublicationTree implements AsyncIterable { */ #bookmark?: string; + /** + * AI-NOTE: 2025-01-24 - Track visited nodes to prevent duplicate iteration + * This ensures that each node is only yielded once during iteration + */ + #visitedNodes: Set = new Set(); + /** * The NDK instance used to fetch events. */ @@ -227,6 +233,38 @@ export class PublicationTree implements AsyncIterable { }); } + /** + * AI-NOTE: 2025-01-24 - Reset the cursor to the beginning of the tree + * This is useful when the component state is reset and we want to start iteration from the beginning + */ + resetCursor() { + this.#bookmark = undefined; + this.#cursor.target = null; + } + + /** + * AI-NOTE: 2025-01-24 - Reset the iterator state to start from the beginning + * This ensures that when the component resets, the iterator starts fresh + */ + resetIterator() { + this.resetCursor(); + // Clear visited nodes to allow fresh iteration + this.#visitedNodes.clear(); + // Clear all nodes except the root to force fresh loading + const rootAddress = this.#root.address; + this.#nodes.clear(); + this.#nodes.set(rootAddress, new Lazy(() => Promise.resolve(this.#root))); + // Clear events cache to ensure fresh data + this.#events.clear(); + this.#eventCache.clear(); + // Force the cursor to move to the root node to restart iteration + this.#cursor.tryMoveTo().then((success) => { + if (!success) { + console.warn("[PublicationTree] Failed to reset iterator to root node"); + } + }); + } + onBookmarkMoved(observer: (address: string) => void) { this.#bookmarkMovedObservers.push(observer); } @@ -458,7 +496,19 @@ export class PublicationTree implements AsyncIterable { if (!this.#cursor.target) { return { done, value: null }; } - const value = (await this.getEvent(this.#cursor.target.address)) ?? null; + + const address = this.#cursor.target.address; + + // AI-NOTE: 2025-01-24 - Check if this node has already been visited + if (this.#visitedNodes.has(address)) { + console.debug(`[PublicationTree] Skipping already visited node: ${address}`); + return { done: false, value: null }; + } + + // Mark this node as visited + this.#visitedNodes.add(address); + + const value = (await this.getEvent(address)) ?? null; return { done, value }; } @@ -711,6 +761,9 @@ export class PublicationTree implements AsyncIterable { } #addNode(address: string, parentNode: PublicationTreeNode) { + // AI-NOTE: 2025-01-24 - Add debugging to track node addition + console.debug(`[PublicationTree] Adding node ${address} to parent ${parentNode.address}`); + const lazyNode = new Lazy(() => this.#resolveNode(address, parentNode) ); @@ -961,11 +1014,10 @@ export class PublicationTree implements AsyncIterable { } }); - // Note: We can't await here since this is a synchronous method - // The e-tag resolution will happen when the children are processed - // For now, we'll add the e-tags as potential child addresses - const eTagAddresses = eTags.map((tag) => tag[1]); - childAddresses.push(...eTagAddresses); + // AI-NOTE: 2025-01-24 - Remove e-tag processing from synchronous method + // E-tags should be resolved asynchronously in #resolveNode method + // Adding raw event IDs here causes duplicate processing + console.debug(`[PublicationTree] Found ${eTags.length} e-tags but skipping processing in buildNodeFromEvent`); } const node: PublicationTreeNode = { @@ -976,17 +1028,21 @@ export class PublicationTree implements AsyncIterable { children: [], }; + // AI-NOTE: 2025-01-24 - Fixed child node addition in buildNodeFromEvent + // Previously called addEventByAddress which expected parent to be in tree + // Now directly adds child nodes to current node's children array // Add children in the order they appear in the a-tags to preserve section order // Use sequential processing to ensure order is maintained console.log(`[PublicationTree] Adding ${childAddresses.length} children in order:`, childAddresses); - for (const address of childAddresses) { - console.log(`[PublicationTree] Adding child: ${address}`); + for (const childAddress of childAddresses) { + console.log(`[PublicationTree] Adding child: ${childAddress}`); try { - await this.addEventByAddress(address, event); - console.log(`[PublicationTree] Successfully added child: ${address}`); + // Add the child node directly to the current node's children + this.#addNode(childAddress, node); + console.log(`[PublicationTree] Successfully added child: ${childAddress}`); } catch (error) { console.warn( - `[PublicationTree] Error adding child ${address} for ${node.address}:`, + `[PublicationTree] Error adding child ${childAddress} for ${node.address}:`, error, ); } @@ -998,18 +1054,30 @@ export class PublicationTree implements AsyncIterable { } #getNodeType(event: NDKEvent): PublicationTreeNodeType { - if ( - event.kind === 30040 && ( - event.tags.some((tag) => tag[0] === "a") || - event.tags.some((tag) => - tag[0] === "e" && tag[1] && /^[0-9a-fA-F]{64}$/.test(tag[1]) - ) - ) - ) { - return PublicationTreeNodeType.Branch; + // AI-NOTE: 2025-01-24 - Show nested 30040s and their zettel kind leaves + // Only 30040 events with children should be branches + // Zettel kinds (30041, 30818, 30023) are always leaves + if (event.kind === 30040) { + // Check if this 30040 has any children (a-tags only, since e-tags are handled separately) + const hasChildren = event.tags.some((tag) => tag[0] === "a"); + + console.debug(`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${event.tags.find(t => t[0] === 'd')?.[1]} - hasChildren: ${hasChildren}, type: ${hasChildren ? 'Branch' : 'Leaf'}`); + + return hasChildren ? PublicationTreeNodeType.Branch : PublicationTreeNodeType.Leaf; + } + + // Zettel kinds are always leaves + if ([30041, 30818, 30023].includes(event.kind)) { + console.debug(`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${event.tags.find(t => t[0] === 'd')?.[1]} - Zettel kind, type: Leaf`); + return PublicationTreeNodeType.Leaf; } - return PublicationTreeNodeType.Leaf; + // For other kinds, check if they have children (a-tags only) + const hasChildren = event.tags.some((tag) => tag[0] === "a"); + + console.debug(`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${event.tags.find(t => t[0] === 'd')?.[1]} - hasChildren: ${hasChildren}, type: ${hasChildren ? 'Branch' : 'Leaf'}`); + + return hasChildren ? PublicationTreeNodeType.Branch : PublicationTreeNodeType.Leaf; } // #endregion From da02c20b82d93b9119cd61f0b98ac77d905f21a9 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 19 Aug 2025 22:19:33 -0500 Subject: [PATCH 92/98] Update Deno lockfile --- deno.lock | 2919 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 2890 insertions(+), 29 deletions(-) diff --git a/deno.lock b/deno.lock index ceb1ed5..201902a 100644 --- a/deno.lock +++ b/deno.lock @@ -1,22 +1,106 @@ { "version": "5", "specifiers": { + "npm:@noble/curves@^1.9.4": "1.9.4", + "npm:@noble/hashes@^1.8.0": "1.8.0", + "npm:@nostr-dev-kit/ndk-cache-dexie@2.6": "2.6.33_nostr-tools@2.15.1__typescript@5.8.3_typescript@5.8.3", + "npm:@nostr-dev-kit/ndk@^2.14.32": "2.14.32_nostr-tools@2.15.1__typescript@5.8.3_typescript@5.8.3", "npm:@playwright/test@^1.54.1": "1.54.1", + "npm:@popperjs/core@2.11": "2.11.8", + "npm:@sveltejs/adapter-auto@^6.0.1": "6.0.1_@sveltejs+kit@2.25.1__@sveltejs+vite-plugin-svelte@6.1.0___svelte@5.36.8____acorn@8.15.0___vite@6.3.5____@types+node@24.0.15____picomatch@4.0.3___@types+node@24.0.15__svelte@5.36.8___acorn@8.15.0__vite@6.3.5___@types+node@24.0.15___picomatch@4.0.3__acorn@8.15.0__@types+node@24.0.15_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@6.3.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@6.3.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15", + "npm:@sveltejs/adapter-node@^5.2.13": "5.2.13_@sveltejs+kit@2.25.1__@sveltejs+vite-plugin-svelte@6.1.0___svelte@5.36.8____acorn@8.15.0___vite@6.3.5____@types+node@24.0.15____picomatch@4.0.3___@types+node@24.0.15__svelte@5.36.8___acorn@8.15.0__vite@6.3.5___@types+node@24.0.15___picomatch@4.0.3__acorn@8.15.0__@types+node@24.0.15_rollup@4.45.1_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@6.3.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@6.3.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15", + "npm:@sveltejs/adapter-static@3": "3.0.8_@sveltejs+kit@2.25.1__@sveltejs+vite-plugin-svelte@6.1.0___svelte@5.36.8____acorn@8.15.0___vite@6.3.5____@types+node@24.0.15____picomatch@4.0.3___@types+node@24.0.15__svelte@5.36.8___acorn@8.15.0__vite@6.3.5___@types+node@24.0.15___picomatch@4.0.3__acorn@8.15.0__@types+node@24.0.15_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@6.3.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@6.3.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15", + "npm:@sveltejs/kit@^2.25.0": "2.25.1_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@6.3.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@6.3.5__@types+node@24.0.15__picomatch@4.0.3_acorn@8.15.0_@types+node@24.0.15", + "npm:@sveltejs/vite-plugin-svelte@^6.1.0": "6.1.0_svelte@5.36.8__acorn@8.15.0_vite@6.3.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15", + "npm:@tailwindcss/forms@0.5": "0.5.10_tailwindcss@3.4.17__postcss@8.5.6", + "npm:@tailwindcss/typography@0.5": "0.5.16_tailwindcss@3.4.17__postcss@8.5.6", "npm:@types/d3@^7.4.3": "7.4.3", "npm:@types/he@1.2": "1.2.3", "npm:@types/mathjax@^0.0.40": "0.0.40", "npm:@types/node@^24.0.15": "24.0.15", "npm:@types/qrcode@^1.5.5": "1.5.5", + "npm:asciidoctor@3.0": "3.0.4_@asciidoctor+core@3.0.4", + "npm:autoprefixer@^10.4.21": "10.4.21_postcss@8.5.6", "npm:bech32@2": "2.0.0", + "npm:d3@^7.9.0": "7.9.0_d3-selection@3.0.0", + "npm:eslint-plugin-svelte@^3.11.0": "3.11.0_eslint@9.31.0_svelte@5.36.8__acorn@8.15.0_postcss@8.5.6", + "npm:flowbite-svelte-icons@2.1": "2.1.1_svelte@5.36.8__acorn@8.15.0_tailwind-merge@3.3.1", + "npm:flowbite-svelte@0.48": "0.48.6_svelte@5.36.8__acorn@8.15.0", + "npm:flowbite@2": "2.5.2", "npm:he@1.2": "1.2.0", "npm:highlight.js@^11.11.1": "11.11.1", "npm:node-emoji@^2.2.0": "2.2.0", + "npm:nostr-tools@2.15": "2.15.1_typescript@5.8.3", "npm:plantuml-encoder@^1.4.0": "1.4.0", "npm:playwright@^1.50.1": "1.54.1", "npm:playwright@^1.54.1": "1.54.1", - "npm:tslib@2.8": "2.8.1" + "npm:postcss-load-config@6": "6.0.1_postcss@8.5.6", + "npm:postcss@^8.5.6": "8.5.6", + "npm:prettier-plugin-svelte@^3.4.0": "3.4.0_prettier@3.6.2_svelte@5.36.8__acorn@8.15.0", + "npm:prettier@^3.6.2": "3.6.2", + "npm:qrcode@^1.5.4": "1.5.4", + "npm:svelte-check@4": "4.3.0_svelte@5.36.8__acorn@8.15.0_typescript@5.8.3", + "npm:svelte@^5.36.8": "5.36.8_acorn@8.15.0", + "npm:tailwind-merge@^3.3.1": "3.3.1", + "npm:tailwindcss@^3.4.17": "3.4.17_postcss@8.5.6", + "npm:tslib@2.8": "2.8.1", + "npm:typescript@^5.8.3": "5.8.3", + "npm:vite@^6.3.5": "6.3.5_@types+node@24.0.15_picomatch@4.0.3", + "npm:vitest@^3.1.3": "3.2.4_@types+node@24.0.15_vite@6.3.5__@types+node@24.0.15__picomatch@4.0.3" }, "npm": { + "@alloc/quick-lru@5.2.0": { + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==" + }, + "@ampproject/remapping@2.3.0": { + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": [ + "@jridgewell/gen-mapping", + "@jridgewell/trace-mapping" + ] + }, + "@asciidoctor/cli@4.0.0_@asciidoctor+core@3.0.4": { + "integrity": "sha512-x2T9gW42921Zd90juEagtbViPZHNP2MWf0+6rJEkOzW7E9m3TGJtz+Guye9J0gwrpZsTMGCpfYMQy1We3X7osg==", + "dependencies": [ + "@asciidoctor/core", + "yargs@17.3.1" + ], + "bin": true + }, + "@asciidoctor/core@3.0.4": { + "integrity": "sha512-41SDMi7iRRBViPe0L6VWFTe55bv6HEOJeRqMj5+E5wB1YPdUPuTucL4UAESPZM6OWmn4t/5qM5LusXomFUVwVQ==", + "dependencies": [ + "@asciidoctor/opal-runtime", + "unxhr" + ] + }, + "@asciidoctor/opal-runtime@3.0.1": { + "integrity": "sha512-iW7ACahOG0zZft4A/4CqDcc7JX+fWRNjV5tFAVkNCzwZD+EnFolPaUOPYt8jzadc0+Bgd80cQTtRMQnaaV1kkg==", + "dependencies": [ + "glob@8.1.0", + "unxhr" + ] + }, + "@babel/helper-string-parser@7.27.1": { + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" + }, + "@babel/helper-validator-identifier@7.27.1": { + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" + }, + "@babel/parser@7.28.0": { + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dependencies": [ + "@babel/types" + ], + "bin": true + }, + "@babel/types@7.28.1": { + "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "dependencies": [ + "@babel/helper-string-parser", + "@babel/helper-validator-identifier" + ] + }, "@esbuild/aix-ppc64@0.25.7": { "integrity": "sha512-uD0kKFHh6ETr8TqEtaAcV+dn/2qnYbH/+8wGEdY70Qf7l1l/jmBUbrmQqwiPKAQE6cOQ7dTj6Xr0HzQDGHyceQ==", "os": ["aix"], @@ -147,6 +231,203 @@ "os": ["win32"], "cpu": ["x64"] }, + "@eslint-community/eslint-utils@4.7.0_eslint@9.31.0": { + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dependencies": [ + "eslint", + "eslint-visitor-keys@3.4.3" + ] + }, + "@eslint-community/regexpp@4.12.1": { + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==" + }, + "@eslint/config-array@0.21.0": { + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dependencies": [ + "@eslint/object-schema", + "debug", + "minimatch@3.1.2" + ] + }, + "@eslint/config-helpers@0.3.0": { + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==" + }, + "@eslint/core@0.15.1": { + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dependencies": [ + "@types/json-schema" + ] + }, + "@eslint/eslintrc@3.3.1": { + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dependencies": [ + "ajv", + "debug", + "espree", + "globals@14.0.0", + "ignore", + "import-fresh", + "js-yaml", + "minimatch@3.1.2", + "strip-json-comments" + ] + }, + "@eslint/js@9.31.0": { + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==" + }, + "@eslint/object-schema@2.1.6": { + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==" + }, + "@eslint/plugin-kit@0.3.3": { + "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "dependencies": [ + "@eslint/core", + "levn" + ] + }, + "@floating-ui/core@1.7.2": { + "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", + "dependencies": [ + "@floating-ui/utils" + ] + }, + "@floating-ui/dom@1.7.2": { + "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", + "dependencies": [ + "@floating-ui/core", + "@floating-ui/utils" + ] + }, + "@floating-ui/utils@0.2.10": { + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" + }, + "@humanfs/core@0.19.1": { + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==" + }, + "@humanfs/node@0.16.6": { + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dependencies": [ + "@humanfs/core", + "@humanwhocodes/retry@0.3.1" + ] + }, + "@humanwhocodes/module-importer@1.0.1": { + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" + }, + "@humanwhocodes/retry@0.3.1": { + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==" + }, + "@humanwhocodes/retry@0.4.3": { + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==" + }, + "@isaacs/cliui@8.0.2": { + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": [ + "string-width@5.1.2", + "string-width-cjs@npm:string-width@4.2.3", + "strip-ansi@7.1.0", + "strip-ansi-cjs@npm:strip-ansi@6.0.1", + "wrap-ansi@8.1.0", + "wrap-ansi-cjs@npm:wrap-ansi@7.0.0" + ] + }, + "@jridgewell/gen-mapping@0.3.12": { + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dependencies": [ + "@jridgewell/sourcemap-codec", + "@jridgewell/trace-mapping" + ] + }, + "@jridgewell/resolve-uri@3.1.2": { + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + }, + "@jridgewell/sourcemap-codec@1.5.4": { + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==" + }, + "@jridgewell/trace-mapping@0.3.29": { + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dependencies": [ + "@jridgewell/resolve-uri", + "@jridgewell/sourcemap-codec" + ] + }, + "@noble/ciphers@0.5.3": { + "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==" + }, + "@noble/curves@1.1.0": { + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dependencies": [ + "@noble/hashes@1.3.1" + ] + }, + "@noble/curves@1.2.0": { + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": [ + "@noble/hashes@1.3.2" + ] + }, + "@noble/curves@1.9.4": { + "integrity": "sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==", + "dependencies": [ + "@noble/hashes@1.8.0" + ] + }, + "@noble/hashes@1.3.1": { + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==" + }, + "@noble/hashes@1.3.2": { + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" + }, + "@noble/hashes@1.8.0": { + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==" + }, + "@noble/secp256k1@2.3.0": { + "integrity": "sha512-0TQed2gcBbIrh7Ccyw+y/uZQvbJwm7Ao4scBUxqpBCcsOlZG0O4KGfjtNAy/li4W8n1xt3dxrwJ0beZ2h2G6Kw==" + }, + "@nodelib/fs.scandir@2.1.5": { + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": [ + "@nodelib/fs.stat", + "run-parallel" + ] + }, + "@nodelib/fs.stat@2.0.5": { + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" + }, + "@nodelib/fs.walk@1.2.8": { + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": [ + "@nodelib/fs.scandir", + "fastq" + ] + }, + "@nostr-dev-kit/ndk-cache-dexie@2.6.33_nostr-tools@2.15.1__typescript@5.8.3_typescript@5.8.3": { + "integrity": "sha512-JzUD5cuJbGQDUXYuW1530vy347Kk3AhdtvPO8tL6kFpV3KzGt/QPZ0SHxcjMhJdf7r6cAIpCEWj9oUlStr0gsg==", + "dependencies": [ + "@nostr-dev-kit/ndk", + "debug", + "dexie", + "nostr-tools", + "typescript-lru-cache" + ] + }, + "@nostr-dev-kit/ndk@2.14.32_nostr-tools@2.15.1__typescript@5.8.3_typescript@5.8.3": { + "integrity": "sha512-LUBO35RCB9/emBYsXNDece7m/WO2rGYR8j4SD0Crb3z8GcKTJq6P8OjpZ6+Kr+sLNo8N0uL07XxtAvEBnp2OqQ==", + "dependencies": [ + "@noble/curves@1.9.4", + "@noble/hashes@1.8.0", + "@noble/secp256k1", + "@scure/base@1.2.6", + "debug", + "light-bolt11-decoder", + "nostr-tools", + "tseep", + "typescript-lru-cache" + ] + }, + "@pkgjs/parseargs@0.11.0": { + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==" + }, "@playwright/test@1.54.1": { "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", "dependencies": [ @@ -154,9 +435,295 @@ ], "bin": true }, + "@polka/url@1.0.0-next.29": { + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==" + }, + "@popperjs/core@2.11.8": { + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" + }, + "@rollup/plugin-commonjs@28.0.6_rollup@4.45.1_picomatch@4.0.3": { + "integrity": "sha512-XSQB1K7FUU5QP+3lOQmVCE3I0FcbbNvmNT4VJSj93iUjayaARrTQeoRdiYQoftAJBLrR9t2agwAd3ekaTgHNlw==", + "dependencies": [ + "@rollup/pluginutils", + "commondir", + "estree-walker@2.0.2", + "fdir", + "is-reference@1.2.1", + "magic-string", + "picomatch@4.0.3", + "rollup" + ], + "optionalPeers": [ + "rollup" + ] + }, + "@rollup/plugin-json@6.1.0_rollup@4.45.1": { + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dependencies": [ + "@rollup/pluginutils", + "rollup" + ], + "optionalPeers": [ + "rollup" + ] + }, + "@rollup/plugin-node-resolve@15.3.1": { + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dependencies": [ + "@rollup/pluginutils", + "@types/resolve", + "deepmerge", + "is-module", + "resolve" + ] + }, + "@rollup/plugin-node-resolve@16.0.1_rollup@4.45.1": { + "integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==", + "dependencies": [ + "@rollup/pluginutils", + "@types/resolve", + "deepmerge", + "is-module", + "resolve", + "rollup" + ], + "optionalPeers": [ + "rollup" + ] + }, + "@rollup/pluginutils@5.2.0_rollup@4.45.1": { + "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", + "dependencies": [ + "@types/estree", + "estree-walker@2.0.2", + "picomatch@4.0.3", + "rollup" + ], + "optionalPeers": [ + "rollup" + ] + }, + "@rollup/rollup-android-arm-eabi@4.45.1": { + "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", + "os": ["android"], + "cpu": ["arm"] + }, + "@rollup/rollup-android-arm64@4.45.1": { + "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@rollup/rollup-darwin-arm64@4.45.1": { + "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@rollup/rollup-darwin-x64@4.45.1": { + "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@rollup/rollup-freebsd-arm64@4.45.1": { + "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@rollup/rollup-freebsd-x64@4.45.1": { + "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@rollup/rollup-linux-arm-gnueabihf@4.45.1": { + "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@rollup/rollup-linux-arm-musleabihf@4.45.1": { + "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@rollup/rollup-linux-arm64-gnu@4.45.1": { + "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@rollup/rollup-linux-arm64-musl@4.45.1": { + "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@rollup/rollup-linux-loongarch64-gnu@4.45.1": { + "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@rollup/rollup-linux-powerpc64le-gnu@4.45.1": { + "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@rollup/rollup-linux-riscv64-gnu@4.45.1": { + "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@rollup/rollup-linux-riscv64-musl@4.45.1": { + "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@rollup/rollup-linux-s390x-gnu@4.45.1": { + "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@rollup/rollup-linux-x64-gnu@4.45.1": { + "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@rollup/rollup-linux-x64-musl@4.45.1": { + "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@rollup/rollup-win32-arm64-msvc@4.45.1": { + "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@rollup/rollup-win32-ia32-msvc@4.45.1": { + "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@rollup/rollup-win32-x64-msvc@4.45.1": { + "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@scure/base@1.1.1": { + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==" + }, + "@scure/base@1.2.6": { + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==" + }, + "@scure/bip32@1.3.1": { + "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", + "dependencies": [ + "@noble/curves@1.1.0", + "@noble/hashes@1.3.2", + "@scure/base@1.1.1" + ] + }, + "@scure/bip39@1.2.1": { + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": [ + "@noble/hashes@1.3.2", + "@scure/base@1.1.1" + ] + }, "@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@6.0.1_@sveltejs+kit@2.25.1__@sveltejs+vite-plugin-svelte@6.1.0___svelte@5.36.8____acorn@8.15.0___vite@6.3.5____@types+node@24.0.15____picomatch@4.0.3___@types+node@24.0.15__svelte@5.36.8___acorn@8.15.0__vite@6.3.5___@types+node@24.0.15___picomatch@4.0.3__acorn@8.15.0__@types+node@24.0.15_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@6.3.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@6.3.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15": { + "integrity": "sha512-mcWud3pYGPWM2Pphdj8G9Qiq24nZ8L4LB7coCUckUEy5Y7wOWGJ/enaZ4AtJTcSm5dNK1rIkBRoqt+ae4zlxcQ==", + "dependencies": [ + "@sveltejs/kit" + ] + }, + "@sveltejs/adapter-node@5.2.13_@sveltejs+kit@2.25.1__@sveltejs+vite-plugin-svelte@6.1.0___svelte@5.36.8____acorn@8.15.0___vite@6.3.5____@types+node@24.0.15____picomatch@4.0.3___@types+node@24.0.15__svelte@5.36.8___acorn@8.15.0__vite@6.3.5___@types+node@24.0.15___picomatch@4.0.3__acorn@8.15.0__@types+node@24.0.15_rollup@4.45.1_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@6.3.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@6.3.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15": { + "integrity": "sha512-yS2TVFmIrxjGhYaV5/iIUrJ3mJl6zjaYn0lBD70vTLnYvJeqf3cjvLXeXCUCuYinhSBoyF4DpfGla49BnIy7sQ==", + "dependencies": [ + "@rollup/plugin-commonjs", + "@rollup/plugin-json", + "@rollup/plugin-node-resolve@16.0.1_rollup@4.45.1", + "@sveltejs/kit", + "rollup" + ] + }, + "@sveltejs/adapter-static@3.0.8_@sveltejs+kit@2.25.1__@sveltejs+vite-plugin-svelte@6.1.0___svelte@5.36.8____acorn@8.15.0___vite@6.3.5____@types+node@24.0.15____picomatch@4.0.3___@types+node@24.0.15__svelte@5.36.8___acorn@8.15.0__vite@6.3.5___@types+node@24.0.15___picomatch@4.0.3__acorn@8.15.0__@types+node@24.0.15_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@6.3.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@6.3.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15": { + "integrity": "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==", + "dependencies": [ + "@sveltejs/kit" + ] + }, + "@sveltejs/kit@2.25.1_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@6.3.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@6.3.5__@types+node@24.0.15__picomatch@4.0.3_acorn@8.15.0_@types+node@24.0.15": { + "integrity": "sha512-8H+fxDEp7Xq6tLFdrGdS5fLu6ONDQQ9DgyjboXpChubuFdfH9QoFX09ypssBpyNkJNZFt9eW3yLmXIc9CesPCA==", + "dependencies": [ + "@sveltejs/acorn-typescript", + "@sveltejs/vite-plugin-svelte", + "@types/cookie", + "acorn@8.15.0", + "cookie", + "devalue", + "esm-env", + "kleur", + "magic-string", + "mrmime", + "sade", + "set-cookie-parser", + "sirv", + "svelte", + "vite" + ], + "bin": true + }, + "@sveltejs/vite-plugin-svelte-inspector@5.0.0_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@6.3.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@6.3.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15": { + "integrity": "sha512-iwQ8Z4ET6ZFSt/gC+tVfcsSBHwsqc6RumSaiLUkAurW3BCpJam65cmHw0oOlDMTO0u+PZi9hilBRYN+LZNHTUQ==", + "dependencies": [ + "@sveltejs/vite-plugin-svelte", + "debug", + "svelte", + "vite" + ] + }, + "@sveltejs/vite-plugin-svelte@6.1.0_svelte@5.36.8__acorn@8.15.0_vite@6.3.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15": { + "integrity": "sha512-+U6lz1wvGEG/BvQyL4z/flyNdQ9xDNv5vrh+vWBWTHaebqT0c9RNggpZTo/XSPoHsSCWBlYaTlRX8pZ9GATXCw==", + "dependencies": [ + "@sveltejs/vite-plugin-svelte-inspector", + "debug", + "deepmerge", + "kleur", + "magic-string", + "svelte", + "vite", + "vitefu" + ] + }, + "@tailwindcss/forms@0.5.10_tailwindcss@3.4.17__postcss@8.5.6": { + "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", + "dependencies": [ + "mini-svg-data-uri", + "tailwindcss" + ] + }, + "@tailwindcss/typography@0.5.16_tailwindcss@3.4.17__postcss@8.5.6": { + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", + "dependencies": [ + "lodash.castarray", + "lodash.isplainobject", + "lodash.merge", + "postcss-selector-parser@6.0.10", + "tailwindcss" + ] + }, + "@types/chai@5.2.2": { + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dependencies": [ + "@types/deep-eql" + ] + }, + "@types/cookie@0.6.0": { + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, "@types/d3-array@3.2.1": { "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" }, @@ -317,12 +884,21 @@ "@types/d3-zoom" ] }, + "@types/deep-eql@4.0.2": { + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==" + }, + "@types/estree@1.0.8": { + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" + }, "@types/geojson@7946.0.16": { "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==" }, "@types/he@1.2.3": { "integrity": "sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==" }, + "@types/json-schema@7.0.15": { + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, "@types/mathjax@0.0.40": { "integrity": "sha512-rHusx08LCg92WJxrsM3SPjvLTSvK5C+gealtSuhKbEOcUZfWlwigaFoPLf6Dfxhg4oryN5qP9Sj7zOQ4HYXINw==" }, @@ -344,62 +920,2134 @@ "@types/node@22.15.15" ] }, - "bech32@2.0.0": { - "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + "@types/resolve@1.20.2": { + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" }, - "char-regex@1.0.2": { - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==" + "@vitest/expect@3.2.4": { + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dependencies": [ + "@types/chai", + "@vitest/spy", + "@vitest/utils", + "chai", + "tinyrainbow" + ] }, - "emojilib@2.4.0": { - "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==" + "@vitest/mocker@3.2.4_vite@6.3.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15": { + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dependencies": [ + "@vitest/spy", + "estree-walker@3.0.3", + "magic-string", + "vite" + ], + "optionalPeers": [ + "vite" + ] }, - "fsevents@2.3.2": { - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "os": ["darwin"], - "scripts": true + "@vitest/pretty-format@3.2.4": { + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dependencies": [ + "tinyrainbow" + ] }, - "he@1.2.0": { - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "bin": true + "@vitest/runner@3.2.4": { + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dependencies": [ + "@vitest/utils", + "pathe", + "strip-literal" + ] }, - "highlight.js@11.11.1": { - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==" + "@vitest/snapshot@3.2.4": { + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dependencies": [ + "@vitest/pretty-format", + "magic-string", + "pathe" + ] }, - "node-emoji@2.2.0": { - "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "@vitest/spy@3.2.4": { + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dependencies": [ - "@sindresorhus/is", - "char-regex", - "emojilib", - "skin-tone" + "tinyspy" ] }, - "plantuml-encoder@1.4.0": { - "integrity": "sha512-sxMwpDw/ySY1WB2CE3+IdMuEcWibJ72DDOsXLkSmEaSzwEUaYBT6DWgOfBiHGCux4q433X6+OEFWjlVqp7gL6g==" + "@vitest/utils@3.2.4": { + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dependencies": [ + "@vitest/pretty-format", + "loupe", + "tinyrainbow" + ] }, - "playwright-core@1.54.1": { + "@yr/monotone-cubic-spline@1.0.3": { + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==" + }, + "a-sync-waterfall@1.0.1": { + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==" + }, + "acorn-jsx@5.3.2_acorn@8.15.0": { + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dependencies": [ + "acorn@8.15.0" + ] + }, + "acorn@7.4.1": { + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "bin": true + }, + "acorn@8.15.0": { + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "bin": true + }, + "ajv@6.12.6": { + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": [ + "fast-deep-equal", + "fast-json-stable-stringify", + "json-schema-traverse", + "uri-js" + ] + }, + "ansi-regex@5.0.1": { + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-regex@6.1.0": { + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==" + }, + "ansi-styles@4.3.0": { + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": [ + "color-convert" + ] + }, + "ansi-styles@6.2.1": { + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" + }, + "any-promise@1.3.0": { + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, + "anymatch@3.1.3": { + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": [ + "normalize-path", + "picomatch@2.3.1" + ] + }, + "apexcharts@3.54.1": { + "integrity": "sha512-E4et0h/J1U3r3EwS/WlqJCQIbepKbp6wGUmaAwJOMjHUP4Ci0gxanLa7FR3okx6p9coi4st6J853/Cb1NP0vpA==", + "dependencies": [ + "@yr/monotone-cubic-spline", + "svg.draggable.js", + "svg.easing.js", + "svg.filter.js", + "svg.pathmorphing.js", + "svg.resize.js", + "svg.select.js@3.0.1" + ] + }, + "arg@5.0.2": { + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "argparse@2.0.1": { + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "aria-query@5.3.2": { + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==" + }, + "asap@2.0.6": { + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "asciidoctor@3.0.4_@asciidoctor+core@3.0.4": { + "integrity": "sha512-hIc0Bx73wePxtic+vWBHOIgMfKSNiCmRz7BBfkyykXATrw20YGd5a3CozCHvqEPH+Wxp5qKD4aBsgtokez8nEA==", + "dependencies": [ + "@asciidoctor/cli", + "@asciidoctor/core", + "ejs", + "handlebars", + "nunjucks", + "pug" + ], + "bin": true + }, + "assert-never@1.4.0": { + "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==" + }, + "assertion-error@2.0.1": { + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==" + }, + "async@3.2.6": { + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "autoprefixer@10.4.21_postcss@8.5.6": { + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dependencies": [ + "browserslist", + "caniuse-lite", + "fraction.js", + "normalize-range", + "picocolors", + "postcss", + "postcss-value-parser" + ], + "bin": true + }, + "axobject-query@4.1.0": { + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==" + }, + "babel-walk@3.0.0-canary-5": { + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "dependencies": [ + "@babel/types" + ] + }, + "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==" + }, + "brace-expansion@1.1.12": { + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dependencies": [ + "balanced-match", + "concat-map" + ] + }, + "brace-expansion@2.0.2": { + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dependencies": [ + "balanced-match" + ] + }, + "braces@3.0.3": { + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": [ + "fill-range" + ] + }, + "browserslist@4.25.1": { + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dependencies": [ + "caniuse-lite", + "electron-to-chromium", + "node-releases", + "update-browserslist-db" + ], + "bin": true + }, + "cac@6.7.14": { + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==" + }, + "call-bind-apply-helpers@1.0.2": { + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": [ + "es-errors", + "function-bind" + ] + }, + "call-bound@1.0.4": { + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": [ + "call-bind-apply-helpers", + "get-intrinsic" + ] + }, + "callsites@3.1.0": { + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "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.30001727": { + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==" + }, + "chai@5.2.1": { + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", + "dependencies": [ + "assertion-error", + "check-error", + "deep-eql", + "loupe", + "pathval" + ] + }, + "chalk@4.1.2": { + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": [ + "ansi-styles@4.3.0", + "supports-color" + ] + }, + "char-regex@1.0.2": { + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==" + }, + "character-parser@2.2.0": { + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "dependencies": [ + "is-regex" + ] + }, + "check-error@2.1.1": { + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==" + }, + "chokidar@3.6.0": { + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": [ + "anymatch", + "braces", + "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": { + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dependencies": [ + "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": [ + "string-width@4.2.3", + "strip-ansi@6.0.1", + "wrap-ansi@7.0.0" + ] + }, + "clsx@2.1.1": { + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, + "color-convert@2.0.1": { + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": [ + "color-name" + ] + }, + "color-name@1.1.4": { + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "commander@4.1.1": { + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" + }, + "commander@5.1.0": { + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" + }, + "commander@7.2.0": { + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + }, + "commondir@1.0.1": { + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" + }, + "concat-map@0.0.1": { + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "constantinople@4.0.1": { + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "dependencies": [ + "@babel/parser", + "@babel/types" + ] + }, + "cookie@0.6.0": { + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" + }, + "cross-spawn@7.0.6": { + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": [ + "path-key", + "shebang-command", + "which" + ] + }, + "cssesc@3.0.0": { + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": true + }, + "d3-array@3.2.4": { + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": [ + "internmap" + ] + }, + "d3-axis@3.0.0": { + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==" + }, + "d3-brush@3.0.0_d3-selection@3.0.0": { + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": [ + "d3-dispatch", + "d3-drag", + "d3-interpolate", + "d3-selection", + "d3-transition" + ] + }, + "d3-chord@3.0.1": { + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": [ + "d3-path" + ] + }, + "d3-color@3.1.0": { + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" + }, + "d3-contour@4.0.2": { + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": [ + "d3-array" + ] + }, + "d3-delaunay@6.0.4": { + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": [ + "delaunator" + ] + }, + "d3-dispatch@3.0.1": { + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" + }, + "d3-drag@3.0.0": { + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": [ + "d3-dispatch", + "d3-selection" + ] + }, + "d3-dsv@3.0.1": { + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": [ + "commander@7.2.0", + "iconv-lite", + "rw" + ], + "bin": true + }, + "d3-ease@3.0.1": { + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" + }, + "d3-fetch@3.0.1": { + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": [ + "d3-dsv" + ] + }, + "d3-force@3.0.0": { + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": [ + "d3-dispatch", + "d3-quadtree", + "d3-timer" + ] + }, + "d3-format@3.1.0": { + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" + }, + "d3-geo@3.1.1": { + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dependencies": [ + "d3-array" + ] + }, + "d3-hierarchy@3.1.2": { + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==" + }, + "d3-interpolate@3.0.1": { + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": [ + "d3-color" + ] + }, + "d3-path@3.1.0": { + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==" + }, + "d3-polygon@3.0.1": { + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==" + }, + "d3-quadtree@3.0.1": { + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==" + }, + "d3-random@3.0.1": { + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==" + }, + "d3-scale-chromatic@3.1.0": { + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dependencies": [ + "d3-color", + "d3-interpolate" + ] + }, + "d3-scale@4.0.2": { + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": [ + "d3-array", + "d3-format", + "d3-interpolate", + "d3-time", + "d3-time-format" + ] + }, + "d3-selection@3.0.0": { + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" + }, + "d3-shape@3.2.0": { + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": [ + "d3-path" + ] + }, + "d3-time-format@4.1.0": { + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": [ + "d3-time" + ] + }, + "d3-time@3.1.0": { + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": [ + "d3-array" + ] + }, + "d3-timer@3.0.1": { + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" + }, + "d3-transition@3.0.1_d3-selection@3.0.0": { + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": [ + "d3-color", + "d3-dispatch", + "d3-ease", + "d3-interpolate", + "d3-selection", + "d3-timer" + ] + }, + "d3-zoom@3.0.0_d3-selection@3.0.0": { + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": [ + "d3-dispatch", + "d3-drag", + "d3-interpolate", + "d3-selection", + "d3-transition" + ] + }, + "d3@7.9.0_d3-selection@3.0.0": { + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dependencies": [ + "d3-array", + "d3-axis", + "d3-brush", + "d3-chord", + "d3-color", + "d3-contour", + "d3-delaunay", + "d3-dispatch", + "d3-drag", + "d3-dsv", + "d3-ease", + "d3-fetch", + "d3-force", + "d3-format", + "d3-geo", + "d3-hierarchy", + "d3-interpolate", + "d3-path", + "d3-polygon", + "d3-quadtree", + "d3-random", + "d3-scale", + "d3-scale-chromatic", + "d3-selection", + "d3-shape", + "d3-time", + "d3-time-format", + "d3-timer", + "d3-transition", + "d3-zoom" + ] + }, + "debug@4.4.1": { + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": [ + "ms" + ] + }, + "decamelize@1.2.0": { + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + }, + "deep-eql@5.0.2": { + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==" + }, + "deep-is@0.1.4": { + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "deepmerge@4.3.1": { + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" + }, + "delaunator@5.0.1": { + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": [ + "robust-predicates" + ] + }, + "devalue@5.1.1": { + "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==" + }, + "dexie@4.0.11": { + "integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==" + }, + "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==" + }, + "doctypes@1.1.0": { + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==" + }, + "dunder-proto@1.0.1": { + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": [ + "call-bind-apply-helpers", + "es-errors", + "gopd" + ] + }, + "eastasianwidth@0.2.0": { + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "ejs@3.1.10": { + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dependencies": [ + "jake" + ], + "bin": true + }, + "electron-to-chromium@1.5.187": { + "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==" + }, + "emoji-regex@8.0.0": { + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "emoji-regex@9.2.2": { + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "emojilib@2.4.0": { + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==" + }, + "es-define-property@1.0.1": { + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors@1.3.0": { + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-module-lexer@1.7.0": { + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==" + }, + "es-object-atoms@1.1.1": { + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": [ + "es-errors" + ] + }, + "esbuild@0.25.7": { + "integrity": "sha512-daJB0q2dmTzo90L9NjRaohhRWrCzYxWNFTjEi72/h+p5DcY3yn4MacWfDakHmaBaDzDiuLJsCh0+6LK/iX+c+Q==", + "optionalDependencies": [ + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-arm64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-arm64", + "@esbuild/openbsd-x64", + "@esbuild/openharmony-arm64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" + ], + "scripts": true, + "bin": true + }, + "escalade@3.2.0": { + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" + }, + "escape-string-regexp@4.0.0": { + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "eslint-plugin-svelte@3.11.0_eslint@9.31.0_svelte@5.36.8__acorn@8.15.0_postcss@8.5.6": { + "integrity": "sha512-KliWlkieHyEa65aQIkRwUFfHzT5Cn4u3BQQsu3KlkJOs7c1u7ryn84EWaOjEzilbKgttT4OfBURA8Uc4JBSQIw==", + "dependencies": [ + "@eslint-community/eslint-utils", + "@jridgewell/sourcemap-codec", + "eslint", + "esutils", + "globals@16.3.0", + "known-css-properties", + "postcss", + "postcss-load-config@3.1.4_postcss@8.5.6", + "postcss-safe-parser", + "semver", + "svelte", + "svelte-eslint-parser" + ], + "optionalPeers": [ + "svelte" + ] + }, + "eslint-scope@8.4.0": { + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dependencies": [ + "esrecurse", + "estraverse" + ] + }, + "eslint-visitor-keys@3.4.3": { + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" + }, + "eslint-visitor-keys@4.2.1": { + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==" + }, + "eslint@9.31.0": { + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "dependencies": [ + "@eslint-community/eslint-utils", + "@eslint-community/regexpp", + "@eslint/config-array", + "@eslint/config-helpers", + "@eslint/core", + "@eslint/eslintrc", + "@eslint/js", + "@eslint/plugin-kit", + "@humanfs/node", + "@humanwhocodes/module-importer", + "@humanwhocodes/retry@0.4.3", + "@types/estree", + "@types/json-schema", + "ajv", + "chalk", + "cross-spawn", + "debug", + "escape-string-regexp", + "eslint-scope", + "eslint-visitor-keys@4.2.1", + "espree", + "esquery", + "esutils", + "fast-deep-equal", + "file-entry-cache", + "find-up@5.0.0", + "glob-parent@6.0.2", + "ignore", + "imurmurhash", + "is-glob", + "json-stable-stringify-without-jsonify", + "lodash.merge", + "minimatch@3.1.2", + "natural-compare", + "optionator" + ], + "bin": true + }, + "esm-env@1.2.2": { + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" + }, + "espree@10.4.0_acorn@8.15.0": { + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dependencies": [ + "acorn@8.15.0", + "acorn-jsx", + "eslint-visitor-keys@4.2.1" + ] + }, + "esquery@1.6.0": { + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dependencies": [ + "estraverse" + ] + }, + "esrap@2.1.0": { + "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", + "dependencies": [ + "@jridgewell/sourcemap-codec" + ] + }, + "esrecurse@4.3.0": { + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": [ + "estraverse" + ] + }, + "estraverse@5.3.0": { + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "estree-walker@2.0.2": { + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "estree-walker@3.0.3": { + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": [ + "@types/estree" + ] + }, + "esutils@2.0.3": { + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "expect-type@1.2.2": { + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==" + }, + "fast-deep-equal@3.1.3": { + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-glob@3.3.3": { + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dependencies": [ + "@nodelib/fs.stat", + "@nodelib/fs.walk", + "glob-parent@5.1.2", + "merge2", + "micromatch" + ] + }, + "fast-json-stable-stringify@2.1.0": { + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein@2.0.6": { + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "fastq@1.19.1": { + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dependencies": [ + "reusify" + ] + }, + "fdir@6.4.6_picomatch@4.0.3": { + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dependencies": [ + "picomatch@4.0.3" + ], + "optionalPeers": [ + "picomatch@4.0.3" + ] + }, + "file-entry-cache@8.0.0": { + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dependencies": [ + "flat-cache" + ] + }, + "filelist@1.0.4": { + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": [ + "minimatch@5.1.6" + ] + }, + "fill-range@7.1.1": { + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": [ + "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@6.0.0", + "path-exists" + ] + }, + "flat-cache@4.0.1": { + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dependencies": [ + "flatted", + "keyv" + ] + }, + "flatted@3.3.3": { + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" + }, + "flowbite-datepicker@1.3.2": { + "integrity": "sha512-6Nfm0MCVX3mpaR7YSCjmEO2GO8CDt6CX8ZpQnGdeu03WUCWtEPQ/uy0PUiNtIJjJZWnX0Cm3H55MOhbD1g+E/g==", + "dependencies": [ + "@rollup/plugin-node-resolve@15.3.1", + "flowbite@2.5.2" + ] + }, + "flowbite-svelte-icons@2.1.1_svelte@5.36.8__acorn@8.15.0_tailwind-merge@3.3.1": { + "integrity": "sha512-VNNMcekjbM1bQEGgbdGsdYR9mRdTj/L0A5ba0P1tiFv5QB9GvbvJMABJoiD80eqpZUkfR2QVOmiZfgCwHicT/Q==", + "dependencies": [ + "svelte", + "tailwind-merge" + ] + }, + "flowbite-svelte@0.48.6_svelte@5.36.8__acorn@8.15.0": { + "integrity": "sha512-/PmeR3ipHHvda8vVY9MZlymaRoJsk8VddEeoLzIygfYwJV68ey8gHuQPC1dq9J6NDCTE5+xOPtBiYUtVjCfvZw==", + "dependencies": [ + "@floating-ui/dom", + "apexcharts", + "flowbite@3.1.2", + "svelte", + "tailwind-merge" + ] + }, + "flowbite@2.5.2": { + "integrity": "sha512-kwFD3n8/YW4EG8GlY3Od9IoKND97kitO+/ejISHSqpn3vw2i5K/+ZI8Jm2V+KC4fGdnfi0XZ+TzYqQb4Q1LshA==", + "dependencies": [ + "@popperjs/core", + "flowbite-datepicker", + "mini-svg-data-uri" + ] + }, + "flowbite@3.1.2": { + "integrity": "sha512-MkwSgbbybCYgMC+go6Da5idEKUFfMqc/AmSjm/2ZbdmvoKf5frLPq/eIhXc9P+rC8t9boZtUXzHDgt5whZ6A/Q==", + "dependencies": [ + "@popperjs/core", + "flowbite-datepicker", + "mini-svg-data-uri", + "postcss" + ] + }, + "foreground-child@3.3.1": { + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dependencies": [ + "cross-spawn", + "signal-exit" + ] + }, + "fraction.js@4.3.7": { + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==" + }, + "fs.realpath@1.0.0": { + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "fsevents@2.3.2": { + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "os": ["darwin"], + "scripts": true + }, + "fsevents@2.3.3": { + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "os": ["darwin"], + "scripts": true + }, + "function-bind@1.1.2": { + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "get-caller-file@2.0.5": { + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-intrinsic@1.3.0": { + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": [ + "call-bind-apply-helpers", + "es-define-property", + "es-errors", + "es-object-atoms", + "function-bind", + "get-proto", + "gopd", + "has-symbols", + "hasown", + "math-intrinsics" + ] + }, + "get-proto@1.0.1": { + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": [ + "dunder-proto", + "es-object-atoms" + ] + }, + "glob-parent@5.1.2": { + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": [ + "is-glob" + ] + }, + "glob-parent@6.0.2": { + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": [ + "is-glob" + ] + }, + "glob@10.4.5": { + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dependencies": [ + "foreground-child", + "jackspeak", + "minimatch@9.0.5", + "minipass", + "package-json-from-dist", + "path-scurry" + ], + "bin": true + }, + "glob@8.1.0": { + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": [ + "fs.realpath", + "inflight", + "inherits", + "minimatch@5.1.6", + "once" + ], + "deprecated": true + }, + "globals@14.0.0": { + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==" + }, + "globals@16.3.0": { + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==" + }, + "gopd@1.2.0": { + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, + "handlebars@4.7.8": { + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dependencies": [ + "minimist", + "neo-async", + "source-map", + "wordwrap" + ], + "optionalDependencies": [ + "uglify-js" + ], + "bin": true + }, + "has-flag@4.0.0": { + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "has-symbols@1.1.0": { + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "has-tostringtag@1.0.2": { + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": [ + "has-symbols" + ] + }, + "hasown@2.0.2": { + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": [ + "function-bind" + ] + }, + "he@1.2.0": { + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": true + }, + "highlight.js@11.11.1": { + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==" + }, + "iconv-lite@0.6.3": { + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": [ + "safer-buffer" + ] + }, + "ignore@5.3.2": { + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" + }, + "import-fresh@3.3.1": { + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dependencies": [ + "parent-module", + "resolve-from" + ] + }, + "imurmurhash@0.1.4": { + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + }, + "inflight@1.0.6": { + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": [ + "once", + "wrappy" + ], + "deprecated": true + }, + "inherits@2.0.4": { + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "internmap@2.0.3": { + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" + }, + "is-binary-path@2.1.0": { + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": [ + "binary-extensions" + ] + }, + "is-core-module@2.16.1": { + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dependencies": [ + "hasown" + ] + }, + "is-expression@4.0.0": { + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "dependencies": [ + "acorn@7.4.1", + "object-assign" + ] + }, + "is-extglob@2.1.1": { + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "is-fullwidth-code-point@3.0.0": { + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob@4.0.3": { + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": [ + "is-extglob" + ] + }, + "is-module@1.0.0": { + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" + }, + "is-number@7.0.0": { + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-promise@2.2.2": { + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, + "is-reference@1.2.1": { + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dependencies": [ + "@types/estree" + ] + }, + "is-reference@3.0.3": { + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dependencies": [ + "@types/estree" + ] + }, + "is-regex@1.2.1": { + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dependencies": [ + "call-bound", + "gopd", + "has-tostringtag", + "hasown" + ] + }, + "isexe@2.0.0": { + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "jackspeak@3.4.3": { + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dependencies": [ + "@isaacs/cliui" + ], + "optionalDependencies": [ + "@pkgjs/parseargs" + ] + }, + "jake@10.9.2": { + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dependencies": [ + "async", + "chalk", + "filelist", + "minimatch@3.1.2" + ], + "bin": true + }, + "jiti@1.21.7": { + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "bin": true + }, + "js-stringify@1.0.2": { + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==" + }, + "js-tokens@9.0.1": { + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==" + }, + "js-yaml@4.1.0": { + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": [ + "argparse" + ], + "bin": true + }, + "json-buffer@3.0.1": { + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "json-schema-traverse@0.4.1": { + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify-without-jsonify@1.0.1": { + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, + "jstransformer@1.0.0": { + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", + "dependencies": [ + "is-promise", + "promise" + ] + }, + "keyv@4.5.4": { + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": [ + "json-buffer" + ] + }, + "kleur@4.1.5": { + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" + }, + "known-css-properties@0.37.0": { + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==" + }, + "levn@0.4.1": { + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": [ + "prelude-ls", + "type-check" + ] + }, + "light-bolt11-decoder@3.2.0": { + "integrity": "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==", + "dependencies": [ + "@scure/base@1.1.1" + ] + }, + "lilconfig@2.1.0": { + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==" + }, + "lilconfig@3.1.3": { + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==" + }, + "lines-and-columns@1.2.4": { + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "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@5.0.0" + ] + }, + "lodash.castarray@4.4.0": { + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==" + }, + "lodash.isplainobject@4.0.6": { + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.merge@4.6.2": { + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "loupe@3.1.4": { + "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==" + }, + "lru-cache@10.4.3": { + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "magic-string@0.30.17": { + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dependencies": [ + "@jridgewell/sourcemap-codec" + ] + }, + "math-intrinsics@1.1.0": { + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "merge2@1.4.1": { + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + }, + "micromatch@4.0.8": { + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": [ + "braces", + "picomatch@2.3.1" + ] + }, + "mini-svg-data-uri@1.4.4": { + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "bin": true + }, + "minimatch@3.1.2": { + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": [ + "brace-expansion@1.1.12" + ] + }, + "minimatch@5.1.6": { + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": [ + "brace-expansion@2.0.2" + ] + }, + "minimatch@9.0.5": { + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": [ + "brace-expansion@2.0.2" + ] + }, + "minimist@1.2.8": { + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "minipass@7.1.2": { + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + }, + "mri@1.2.0": { + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" + }, + "mrmime@2.0.1": { + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==" + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "mz@2.7.0": { + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": [ + "any-promise", + "object-assign", + "thenify-all" + ] + }, + "nanoid@3.3.11": { + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "bin": true + }, + "natural-compare@1.4.0": { + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "neo-async@2.6.2": { + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node-emoji@2.2.0": { + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "dependencies": [ + "@sindresorhus/is", + "char-regex", + "emojilib", + "skin-tone" + ] + }, + "node-releases@2.0.19": { + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" + }, + "normalize-path@3.0.0": { + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "normalize-range@0.1.2": { + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" + }, + "nostr-tools@2.15.1_typescript@5.8.3": { + "integrity": "sha512-LpetHDR9ltnkpJDkva/SONgyKBbsoV+5yLB8DWc0/U3lCWGtoWJw6Nbc2vR2Ai67RIQYrBQeZLyMlhwVZRK/9A==", + "dependencies": [ + "@noble/ciphers", + "@noble/curves@1.2.0", + "@noble/hashes@1.3.1", + "@scure/base@1.1.1", + "@scure/bip32", + "@scure/bip39", + "nostr-wasm", + "typescript" + ], + "optionalPeers": [ + "typescript" + ] + }, + "nostr-wasm@0.1.0": { + "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==" + }, + "nunjucks@3.2.4": { + "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", + "dependencies": [ + "a-sync-waterfall", + "asap", + "commander@5.1.0" + ], + "bin": true + }, + "object-assign@4.1.1": { + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-hash@3.0.0": { + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" + }, + "once@1.4.0": { + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": [ + "wrappy" + ] + }, + "optionator@0.9.4": { + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dependencies": [ + "deep-is", + "fast-levenshtein", + "levn", + "prelude-ls", + "type-check", + "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@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==" + }, + "parent-module@1.0.1": { + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": [ + "callsites" + ] + }, + "path-exists@4.0.0": { + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-key@3.1.1": { + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "path-parse@1.0.7": { + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-scurry@1.11.1": { + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": [ + "lru-cache", + "minipass" + ] + }, + "pathe@2.0.3": { + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" + }, + "pathval@2.0.1": { + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==" + }, + "picocolors@1.1.1": { + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "picomatch@2.3.1": { + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "picomatch@4.0.3": { + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==" + }, + "pify@2.3.0": { + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" + }, + "pirates@4.0.7": { + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==" + }, + "plantuml-encoder@1.4.0": { + "integrity": "sha512-sxMwpDw/ySY1WB2CE3+IdMuEcWibJ72DDOsXLkSmEaSzwEUaYBT6DWgOfBiHGCux4q433X6+OEFWjlVqp7gL6g==" + }, + "playwright-core@1.54.1": { "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", "bin": true }, - "playwright@1.54.1": { - "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", + "playwright@1.54.1": { + "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", + "dependencies": [ + "playwright-core" + ], + "optionalDependencies": [ + "fsevents@2.3.2" + ], + "bin": true + }, + "pngjs@5.0.0": { + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" + }, + "postcss-import@15.1.0_postcss@8.5.6": { + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dependencies": [ + "postcss", + "postcss-value-parser", + "read-cache", + "resolve" + ] + }, + "postcss-js@4.0.1_postcss@8.5.6": { + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dependencies": [ + "camelcase-css", + "postcss" + ] + }, + "postcss-load-config@3.1.4_postcss@8.5.6": { + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dependencies": [ + "lilconfig@2.1.0", + "postcss", + "yaml@1.10.2" + ], + "optionalPeers": [ + "postcss" + ] + }, + "postcss-load-config@4.0.2_postcss@8.5.6": { + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dependencies": [ + "lilconfig@3.1.3", + "postcss", + "yaml@2.8.0" + ], + "optionalPeers": [ + "postcss" + ] + }, + "postcss-load-config@6.0.1_postcss@8.5.6": { + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dependencies": [ + "lilconfig@3.1.3", + "postcss" + ], + "optionalPeers": [ + "postcss" + ] + }, + "postcss-nested@6.2.0_postcss@8.5.6": { + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dependencies": [ + "postcss", + "postcss-selector-parser@6.1.2" + ] + }, + "postcss-safe-parser@7.0.1_postcss@8.5.6": { + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dependencies": [ + "postcss" + ] + }, + "postcss-scss@4.0.9_postcss@8.5.6": { + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dependencies": [ + "postcss" + ] + }, + "postcss-selector-parser@6.0.10": { + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dependencies": [ + "cssesc", + "util-deprecate" + ] + }, + "postcss-selector-parser@6.1.2": { + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dependencies": [ + "cssesc", + "util-deprecate" + ] + }, + "postcss-selector-parser@7.1.0": { + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dependencies": [ + "cssesc", + "util-deprecate" + ] + }, + "postcss-value-parser@4.2.0": { + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "postcss@8.5.6": { + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dependencies": [ + "nanoid", + "picocolors", + "source-map-js" + ] + }, + "prelude-ls@1.2.1": { + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" + }, + "prettier-plugin-svelte@3.4.0_prettier@3.6.2_svelte@5.36.8__acorn@8.15.0": { + "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", + "dependencies": [ + "prettier", + "svelte" + ] + }, + "prettier@3.6.2": { + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "bin": true + }, + "promise@7.3.1": { + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", "dependencies": [ - "playwright-core" + "asap" + ] + }, + "pug-attrs@3.0.0": { + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "dependencies": [ + "constantinople", + "js-stringify", + "pug-runtime" + ] + }, + "pug-code-gen@3.0.3": { + "integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==", + "dependencies": [ + "constantinople", + "doctypes", + "js-stringify", + "pug-attrs", + "pug-error", + "pug-runtime", + "void-elements", + "with" + ] + }, + "pug-error@2.1.0": { + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==" + }, + "pug-filters@4.0.0": { + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", + "dependencies": [ + "constantinople", + "jstransformer", + "pug-error", + "pug-walk", + "resolve" + ] + }, + "pug-lexer@5.0.1": { + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", + "dependencies": [ + "character-parser", + "is-expression", + "pug-error" + ] + }, + "pug-linker@4.0.0": { + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "dependencies": [ + "pug-error", + "pug-walk" + ] + }, + "pug-load@3.0.0": { + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "dependencies": [ + "object-assign", + "pug-walk" + ] + }, + "pug-parser@6.0.0": { + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "dependencies": [ + "pug-error", + "token-stream" + ] + }, + "pug-runtime@3.0.1": { + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==" + }, + "pug-strip-comments@2.0.0": { + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "dependencies": [ + "pug-error" + ] + }, + "pug-walk@2.0.0": { + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==" + }, + "pug@3.0.3": { + "integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==", + "dependencies": [ + "pug-code-gen", + "pug-filters", + "pug-lexer", + "pug-linker", + "pug-load", + "pug-parser", + "pug-runtime", + "pug-strip-comments" + ] + }, + "punycode@2.3.1": { + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + }, + "qrcode@1.5.4": { + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "dependencies": [ + "dijkstrajs", + "pngjs", + "yargs@15.4.1" + ], + "bin": true + }, + "queue-microtask@1.2.3": { + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + }, + "read-cache@1.0.0": { + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dependencies": [ + "pify" + ] + }, + "readdirp@3.6.0": { + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": [ + "picomatch@2.3.1" + ] + }, + "readdirp@4.1.2": { + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==" + }, + "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==" + }, + "resolve@1.22.10": { + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dependencies": [ + "is-core-module", + "path-parse", + "supports-preserve-symlinks-flag" + ], + "bin": true + }, + "reusify@1.1.0": { + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==" + }, + "robust-predicates@3.0.2": { + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, + "rollup@4.45.1": { + "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", + "dependencies": [ + "@types/estree" ], "optionalDependencies": [ - "fsevents" + "@rollup/rollup-android-arm-eabi", + "@rollup/rollup-android-arm64", + "@rollup/rollup-darwin-arm64", + "@rollup/rollup-darwin-x64", + "@rollup/rollup-freebsd-arm64", + "@rollup/rollup-freebsd-x64", + "@rollup/rollup-linux-arm-gnueabihf", + "@rollup/rollup-linux-arm-musleabihf", + "@rollup/rollup-linux-arm64-gnu", + "@rollup/rollup-linux-arm64-musl", + "@rollup/rollup-linux-loongarch64-gnu", + "@rollup/rollup-linux-powerpc64le-gnu", + "@rollup/rollup-linux-riscv64-gnu", + "@rollup/rollup-linux-riscv64-musl", + "@rollup/rollup-linux-s390x-gnu", + "@rollup/rollup-linux-x64-gnu", + "@rollup/rollup-linux-x64-musl", + "@rollup/rollup-win32-arm64-msvc", + "@rollup/rollup-win32-ia32-msvc", + "@rollup/rollup-win32-x64-msvc", + "fsevents@2.3.3" ], "bin": true }, + "run-parallel@1.2.0": { + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dependencies": [ + "queue-microtask" + ] + }, + "rw@1.3.3": { + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "sade@1.8.1": { + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dependencies": [ + "mri" + ] + }, + "safer-buffer@2.1.2": { + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver@7.7.2": { + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "bin": true + }, + "set-blocking@2.0.0": { + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "set-cookie-parser@2.7.1": { + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, + "shebang-command@2.0.0": { + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": [ + "shebang-regex" + ] + }, + "shebang-regex@3.0.0": { + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "siginfo@2.0.0": { + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==" + }, + "signal-exit@4.1.0": { + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + }, + "sirv@3.0.1": { + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", + "dependencies": [ + "@polka/url", + "mrmime", + "totalist" + ] + }, "skin-tone@2.0.0": { "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", "dependencies": [ "unicode-emoji-modifier-base" ] }, + "source-map-js@1.2.1": { + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "source-map@0.6.1": { + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "stackback@0.0.2": { + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==" + }, + "std-env@3.9.0": { + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==" + }, + "string-width@4.2.3": { + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": [ + "emoji-regex@8.0.0", + "is-fullwidth-code-point", + "strip-ansi@6.0.1" + ] + }, + "string-width@5.1.2": { + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": [ + "eastasianwidth", + "emoji-regex@9.2.2", + "strip-ansi@7.1.0" + ] + }, + "strip-ansi@6.0.1": { + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": [ + "ansi-regex@5.0.1" + ] + }, + "strip-ansi@7.1.0": { + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": [ + "ansi-regex@6.1.0" + ] + }, + "strip-json-comments@3.1.1": { + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, + "strip-literal@3.0.0": { + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dependencies": [ + "js-tokens" + ] + }, + "sucrase@3.35.0": { + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dependencies": [ + "@jridgewell/gen-mapping", + "commander@4.1.1", + "glob@10.4.5", + "lines-and-columns", + "mz", + "pirates", + "ts-interface-checker" + ], + "bin": true + }, + "supports-color@7.2.0": { + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": [ + "has-flag" + ] + }, + "supports-preserve-symlinks-flag@1.0.0": { + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "svelte-check@4.3.0_svelte@5.36.8__acorn@8.15.0_typescript@5.8.3": { + "integrity": "sha512-Iz8dFXzBNAM7XlEIsUjUGQhbEE+Pvv9odb9+0+ITTgFWZBGeJRRYqHUUglwe2EkLD5LIsQaAc4IUJyvtKuOO5w==", + "dependencies": [ + "@jridgewell/trace-mapping", + "chokidar@4.0.3", + "fdir", + "picocolors", + "sade", + "svelte", + "typescript" + ], + "bin": true + }, + "svelte-eslint-parser@1.3.0_svelte@5.36.8__acorn@8.15.0_postcss@8.5.6": { + "integrity": "sha512-VCgMHKV7UtOGcGLGNFSbmdm6kEKjtzo5nnpGU/mnx4OsFY6bZ7QwRF5DUx+Hokw5Lvdyo8dpk8B1m8mliomrNg==", + "dependencies": [ + "eslint-scope", + "eslint-visitor-keys@4.2.1", + "espree", + "postcss", + "postcss-scss", + "postcss-selector-parser@7.1.0", + "svelte" + ], + "optionalPeers": [ + "svelte" + ] + }, + "svelte@5.36.8_acorn@8.15.0": { + "integrity": "sha512-8JbZWQu96hMjH/oYQPxXW6taeC6Awl6muGHeZzJTxQx7NGRQ/J9wN1hkzRKLOlSDlbS2igiFg7p5xyTp5uXG3A==", + "dependencies": [ + "@ampproject/remapping", + "@jridgewell/sourcemap-codec", + "@sveltejs/acorn-typescript", + "@types/estree", + "acorn@8.15.0", + "aria-query", + "axobject-query", + "clsx", + "esm-env", + "esrap", + "is-reference@3.0.3", + "locate-character", + "magic-string", + "zimmerframe" + ] + }, + "svg.draggable.js@2.2.2": { + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", + "dependencies": [ + "svg.js" + ] + }, + "svg.easing.js@2.0.0": { + "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", + "dependencies": [ + "svg.js" + ] + }, + "svg.filter.js@2.0.2": { + "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", + "dependencies": [ + "svg.js" + ] + }, + "svg.js@2.7.1": { + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==" + }, + "svg.pathmorphing.js@0.1.3": { + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", + "dependencies": [ + "svg.js" + ] + }, + "svg.resize.js@1.4.3": { + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", + "dependencies": [ + "svg.js", + "svg.select.js@2.1.2" + ] + }, + "svg.select.js@2.1.2": { + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", + "dependencies": [ + "svg.js" + ] + }, + "svg.select.js@3.0.1": { + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", + "dependencies": [ + "svg.js" + ] + }, + "tailwind-merge@3.3.1": { + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==" + }, + "tailwindcss@3.4.17_postcss@8.5.6": { + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dependencies": [ + "@alloc/quick-lru", + "arg", + "chokidar@3.6.0", + "didyoumean", + "dlv", + "fast-glob", + "glob-parent@6.0.2", + "is-glob", + "jiti", + "lilconfig@3.1.3", + "micromatch", + "normalize-path", + "object-hash", + "picocolors", + "postcss", + "postcss-import", + "postcss-js", + "postcss-load-config@4.0.2_postcss@8.5.6", + "postcss-nested", + "postcss-selector-parser@6.1.2", + "resolve", + "sucrase" + ], + "bin": true + }, + "thenify-all@1.6.0": { + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": [ + "thenify" + ] + }, + "thenify@3.3.1": { + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": [ + "any-promise" + ] + }, + "tinybench@2.9.0": { + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==" + }, + "tinyexec@0.3.2": { + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==" + }, + "tinyglobby@0.2.14_picomatch@4.0.3": { + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dependencies": [ + "fdir", + "picomatch@4.0.3" + ] + }, + "tinypool@1.1.1": { + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==" + }, + "tinyrainbow@2.0.0": { + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==" + }, + "tinyspy@4.0.3": { + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==" + }, + "to-regex-range@5.0.1": { + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": [ + "is-number" + ] + }, + "token-stream@1.0.0": { + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==" + }, + "totalist@3.0.1": { + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==" + }, + "ts-interface-checker@0.1.13": { + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + }, + "tseep@1.3.1": { + "integrity": "sha512-ZPtfk1tQnZVyr7BPtbJ93qaAh2lZuIOpTMjhrYa4XctT8xe7t4SAW9LIxrySDuYMsfNNayE51E/WNGrNVgVicQ==" + }, "tslib@2.8.1": { "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, + "type-check@0.4.0": { + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dependencies": [ + "prelude-ls" + ] + }, + "typescript-lru-cache@2.0.0": { + "integrity": "sha512-Jp57Qyy8wXeMkdNuZiglE6v2Cypg13eDA1chHwDG6kq51X7gk4K7P7HaDdzZKCxkegXkVHNcPD0n5aW6OZH3aA==" + }, + "typescript@5.8.3": { + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "bin": true + }, + "uglify-js@3.19.3": { + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "bin": true + }, "undici-types@6.21.0": { "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" }, @@ -408,6 +3056,219 @@ }, "unicode-emoji-modifier-base@1.0.0": { "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==" + }, + "unxhr@1.2.0": { + "integrity": "sha512-6cGpm8NFXPD9QbSNx0cD2giy7teZ6xOkCUH3U89WKVkL9N9rBrWjlCwhR94Re18ZlAop4MOc3WU1M3Hv/bgpIw==" + }, + "update-browserslist-db@1.1.3_browserslist@4.25.1": { + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dependencies": [ + "browserslist", + "escalade", + "picocolors" + ], + "bin": true + }, + "uri-js@4.4.1": { + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": [ + "punycode" + ] + }, + "util-deprecate@1.0.2": { + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "vite-node@3.2.4_@types+node@24.0.15": { + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dependencies": [ + "cac", + "debug", + "es-module-lexer", + "pathe", + "vite" + ], + "bin": true + }, + "vite@6.3.5_@types+node@24.0.15_picomatch@4.0.3": { + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "dependencies": [ + "@types/node@24.0.15", + "esbuild", + "fdir", + "picomatch@4.0.3", + "postcss", + "rollup", + "tinyglobby" + ], + "optionalDependencies": [ + "fsevents@2.3.3" + ], + "optionalPeers": [ + "@types/node@24.0.15" + ], + "bin": true + }, + "vitefu@1.1.1_vite@6.3.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15": { + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "dependencies": [ + "vite" + ], + "optionalPeers": [ + "vite" + ] + }, + "vitest@3.2.4_@types+node@24.0.15_vite@6.3.5__@types+node@24.0.15__picomatch@4.0.3": { + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dependencies": [ + "@types/chai", + "@types/node@24.0.15", + "@vitest/expect", + "@vitest/mocker", + "@vitest/pretty-format", + "@vitest/runner", + "@vitest/snapshot", + "@vitest/spy", + "@vitest/utils", + "chai", + "debug", + "expect-type", + "magic-string", + "pathe", + "picomatch@4.0.3", + "std-env", + "tinybench", + "tinyexec", + "tinyglobby", + "tinypool", + "tinyrainbow", + "vite", + "vite-node", + "why-is-node-running" + ], + "optionalPeers": [ + "@types/node@24.0.15" + ], + "bin": true + }, + "void-elements@3.1.0": { + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" + }, + "which-module@2.0.1": { + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "which@2.0.2": { + "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==", + "dependencies": [ + "@babel/parser", + "@babel/types", + "assert-never", + "babel-walk" + ] + }, + "word-wrap@1.2.5": { + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" + }, + "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": [ + "ansi-styles@4.3.0", + "string-width@4.2.3", + "strip-ansi@6.0.1" + ] + }, + "wrap-ansi@8.1.0": { + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": [ + "ansi-styles@6.2.1", + "string-width@5.1.2", + "strip-ansi@7.1.0" + ] + }, + "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==" + }, + "yaml@1.10.2": { + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + }, + "yaml@2.8.0": { + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "bin": true + }, + "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@7.0.4", + "escalade", + "get-caller-file", + "require-directory", + "string-width@4.2.3", + "y18n@5.0.8", + "yargs-parser@21.1.1" + ] + }, + "yocto-queue@0.1.0": { + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "zimmerframe@1.1.2": { + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==" } }, "redirects": { From 43eddd3ede3fc73b5a70638167f2538d4781fd5c Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 19 Aug 2025 22:20:53 -0500 Subject: [PATCH 93/98] Extract user profile to new interface --- src/lib/components/EventDetails.svelte | 12 ++---------- .../embedded_events/EmbeddedEvent.svelte | 14 +++----------- src/lib/models/user_profile.ts | 12 ++++++++++++ src/lib/snippets/UserSnippets.svelte | 13 +++---------- src/routes/events/+page.svelte | 15 ++------------- 5 files changed, 22 insertions(+), 44 deletions(-) create mode 100644 src/lib/models/user_profile.ts diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index db18355..2f26fe6 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -15,22 +15,14 @@ import ContainingIndexes from "$lib/components/util/ContainingIndexes.svelte"; import Notifications from "$lib/components/Notifications.svelte"; import EmbeddedEvent from "./embedded_events/EmbeddedEvent.svelte"; + import type { UserProfile } from "$lib/models/user_profile"; const { event, profile = null, } = $props<{ event: NDKEvent; - profile?: { - name?: string; - display_name?: string; - about?: string; - picture?: string; - banner?: string; - website?: string; - lud16?: string; - nip05?: string; - } | null; + profile?: UserProfile | null; }>(); let authorDisplayName = $state(undefined); diff --git a/src/lib/components/embedded_events/EmbeddedEvent.svelte b/src/lib/components/embedded_events/EmbeddedEvent.svelte index af455c1..30ef2dd 100644 --- a/src/lib/components/embedded_events/EmbeddedEvent.svelte +++ b/src/lib/components/embedded_events/EmbeddedEvent.svelte @@ -10,7 +10,8 @@ import { nip19 } from "nostr-tools"; import { repostKinds } from "$lib/consts"; import { UserOutline } from "flowbite-svelte-icons"; - + import type { UserProfile } from "$lib/models/user_profile"; + const { nostrIdentifier, nestingLevel = 0, @@ -22,16 +23,7 @@ const ndk = getNdkContext(); let event = $state(null); - let profile = $state<{ - name?: string; - display_name?: string; - about?: string; - picture?: string; - banner?: string; - website?: string; - lud16?: string; - nip05?: string; - } | null>(null); + let profile = $state< UserProfile | null>(null); let loading = $state(true); let error = $state(null); let authorDisplayName = $state(undefined); diff --git a/src/lib/models/user_profile.ts b/src/lib/models/user_profile.ts new file mode 100644 index 0000000..283ff9a --- /dev/null +++ b/src/lib/models/user_profile.ts @@ -0,0 +1,12 @@ +export interface UserProfile { + name?: string; + display_name?: string; + about?: string; + picture?: string; + banner?: string; + website?: string; + lud16?: string; + nip05?: string; + isInUserLists?: boolean; + listKinds?: number[]; +} diff --git a/src/lib/snippets/UserSnippets.svelte b/src/lib/snippets/UserSnippets.svelte index 6e96719..d069c94 100644 --- a/src/lib/snippets/UserSnippets.svelte +++ b/src/lib/snippets/UserSnippets.svelte @@ -5,14 +5,7 @@ toNpub, getUserMetadata, } from "$lib/utils/nostrUtils"; - - // Extend NostrProfile locally to allow display_name for legacy support - type NostrProfileWithLegacy = { - displayName?: string; - display_name?: string; - name?: string; - [key: string]: any; - }; + import type { UserProfile } from "$lib/models/user_profile"; export { userBadge }; @@ -22,13 +15,13 @@ {#if npub} {#if !displayText || displayText.trim().toLowerCase() === "unknown"} {#await getUserMetadata(npub, undefined, false) then profile} - {@const p = profile as NostrProfileWithLegacy} + {@const p = profile as UserProfile} - {/each} -
    +
    + + {/each} +
    {/if} {#if secondOrderResults.length > 0}
    -
    +
    Second-Order Events (References, Replies, Quotes) ({secondOrderResults.length} events) - {#if (searchType === "n" || searchType === "d") && secondOrderResults.length === 100} + {#if (searchType === "n" || searchType === "d") && secondOrderResults.length === 100} +

    + Showing the 100 newest events. More results may be available. +

    + {/if}

    - Showing the 100 newest events. More results may be available. + Events that reference, reply to, highlight, or quote the + original events.

    - {/if} -

    - Events that reference, reply to, highlight, or quote the original - events. -

    -
    - {#each secondOrderResults as result, index} - {@const profileData = (result as any).profileData || parseProfileContent(result)} -
    -
    - {getReferenceType( - result, - originalEventIds, - originalAddresses, - )} -
    - {#if result.kind === 0 && profileData} -
    - {#if profileData.picture} - Profile { - (e.target as HTMLImageElement).style.display = 'none'; - }} - /> - {:else} -
    - - {(profileData.display_name || profileData.name || result.pubkey.slice(0, 1)).toUpperCase()} - + {#if getSummary(result)} +
    + {getSummary(result)}
    {/if} -
    - {#if profileData.display_name || profileData.name} - - {profileData.display_name || profileData.name} - - {/if} - {#if profileData.about} - - {profileData.about} - - {/if} -
    -
    - {:else} - {#if getSummary(result)} -
    - {getSummary(result)} -
    - {/if} - {#if getDeferralNaddr(result)} -
    - Read - { - e.stopPropagation(); - navigateToPublication( - getDeferralNaddr(result) || "", - ); - }} - onkeydown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); + {#if getDeferralNaddr(result)} +
    + Read + { e.stopPropagation(); navigateToPublication( getDeferralNaddr(result) || "", ); - } - }} - tabindex="0" - role="button" + }} + onkeydown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + e.stopPropagation(); + navigateToPublication( + getDeferralNaddr(result) || "", + ); + } + }} + tabindex="0" + role="button" + > + {getDeferralNaddr(result)} + +
    + {/if} + {#if isAddressableEvent(result)} +
    - {getDeferralNaddr(result)} - -
    - {/if} - {#if isAddressableEvent(result)} -
    - -
    - {/if} - {#if result.content} -
    - -
    + +
    + {/if} + {#if result.content} +
    + +
    + {/if} {/if} - {/if} -
    - - {/each} -
    +
    + + {/each} +
    {/if} {#if tTagResults.length > 0}
    -
    +
    Search Results for t-tag: "{searchTerm || - dTagValue?.toLowerCase()}" ({tTagResults.length} events) + (searchType === "t" ? searchValue : "")}" ({tTagResults.length} + events) -

    - Events that are tagged with the t-tag. -

    -
    - {#each tTagResults as result, index} - {@const profileData = (result as any).profileData || parseProfileContent(result)} -
    - {#if result.kind === 0 && profileData} -
    - {#if profileData.picture} - Profile { - (e.target as HTMLImageElement).style.display = 'none'; - }} - /> - {:else} -
    - - {(profileData.display_name || profileData.name || result.pubkey.slice(0, 1)).toUpperCase()} - + {#if getSummary(result)} +
    + {getSummary(result)}
    {/if} -
    - {#if profileData.display_name || profileData.name} - - {profileData.display_name || profileData.name} - - {/if} - {#if profileData.about} - - {profileData.about} - - {/if} -
    -
    - {:else} - {#if getSummary(result)} -
    - {getSummary(result)} -
    - {/if} - {#if getDeferralNaddr(result)} -
    - Read - { - e.stopPropagation(); - navigateToPublication( - getDeferralNaddr(result) || "", - ); - }} - onkeydown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); + {#if getDeferralNaddr(result)} +
    + Read + { e.stopPropagation(); navigateToPublication( getDeferralNaddr(result) || "", ); - } - }} - tabindex="0" - role="button" + }} + onkeydown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + e.stopPropagation(); + navigateToPublication( + getDeferralNaddr(result) || "", + ); + } + }} + tabindex="0" + role="button" + > + {getDeferralNaddr(result)} + +
    + {/if} + {#if isAddressableEvent(result)} +
    - {getDeferralNaddr(result)} - -
    - {/if} - {#if isAddressableEvent(result)} -
    - -
    - {/if} - {#if result.content} -
    - -
    + +
    + {/if} + {#if result.content} +
    + +
    + {/if} {/if} - {/if} -
    - - {/each} -
    +
    + + {/each} +
    {/if} - {#if !event && searchResults.length === 0 && secondOrderResults.length === 0 && tTagResults.length === 0 && !searchValue && !dTagValue && !searchInProgress} + {#if !event && searchResults.length === 0 && secondOrderResults.length === 0 && tTagResults.length === 0 && !searchValue && !searchInProgress}
    - Publish Nostr Event + Publish Nostr Event

    - Create and publish new Nostr events to the network. This form supports various event kinds including: + Create and publish new Nostr events to the network. This form + supports various event kinds including:

    -
      -
    • Kind 30040: Publication indexes that organize AsciiDoc content into structured publications
    • -
    • Kind 30041: Individual section content for publications
    • -
    • Other kinds: Standard Nostr events with custom tags and content
    • +
        +
      • + Kind 30040: Publication indexes that organize AsciiDoc + content into structured publications +
      • +
      • + Kind 30041: Individual section content for publications +
      • +
      • + Other kinds: Standard Nostr events with custom tags + and content +
    @@ -1017,9 +1150,13 @@ {#if showSidePanel && event} -
    +
    - Event Details + Event Details
    - + {#if user?.signedIn}
    - Add Comment + Add Comment
    {:else} From 6d91bb7fc06249c6916df90bd14256c853bce4cf Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 19 Aug 2025 23:44:58 -0500 Subject: [PATCH 97/98] Encode event search queries in URL query string --- src/lib/components/EventSearch.svelte | 55 +++++++++++++++++++++++++-- src/lib/models/search_type.d.ts | 2 +- src/routes/events/+page.svelte | 9 ++++- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/lib/components/EventSearch.svelte b/src/lib/components/EventSearch.svelte index e4faf0e..f4c4310 100644 --- a/src/lib/components/EventSearch.svelte +++ b/src/lib/components/EventSearch.svelte @@ -143,6 +143,54 @@ return; } + // Update URL with search query for all search types + if (clearInput) { + const searchType = getSearchType(query); + if (searchType) { + const { type, term } = searchType; + const encoded = encodeURIComponent(term); + if (type === "d") { + goto(`?d=${encoded}`, { + replaceState: false, + keepFocus: true, + noScroll: true, + }); + } else if (type === "t") { + goto(`?t=${encoded}`, { + replaceState: false, + keepFocus: true, + noScroll: true, + }); + } else if (type === "n") { + goto(`?n=${encoded}`, { + replaceState: false, + keepFocus: true, + noScroll: true, + }); + } else if (type === "nip05") { + goto(`?q=${encodeURIComponent(query)}`, { + replaceState: false, + keepFocus: true, + noScroll: true, + }); + } else if (type === "event") { + goto(`?id=${encoded}`, { + replaceState: false, + keepFocus: true, + noScroll: true, + }); + } + } else { + // No specific search type detected, treat as general search + const encoded = encodeURIComponent(query); + goto(`?q=${encoded}`, { + replaceState: false, + keepFocus: true, + noScroll: true, + }); + } + } + // Handle different search types const searchType = getSearchType(query); if (searchType) { @@ -151,9 +199,7 @@ } // AI-NOTE: 2025-01-24 - If no specific search type is detected, treat as event ID search - if (clearInput) { - navigateToSearch(query, "id"); - } + // URL navigation is now handled in the URL update logic above await handleEventSearch(query); } @@ -210,7 +256,7 @@ if (type === "d") { console.log("EventSearch: Processing d-tag search:", term); - navigateToSearch(term, "d"); + // URL navigation is now handled in handleSearchEvent updateSearchState(false, false, null, null); return; } @@ -222,6 +268,7 @@ if (type === "event") { console.log("EventSearch: Processing event ID search:", term); + // URL navigation is now handled in handleSearchEvent await handleEventSearch(term); return; } diff --git a/src/lib/models/search_type.d.ts b/src/lib/models/search_type.d.ts index db33e7f..b6d448f 100644 --- a/src/lib/models/search_type.d.ts +++ b/src/lib/models/search_type.d.ts @@ -1 +1 @@ -export type SearchType = "id" | "d" | "t" | "n"; +export type SearchType = "id" | "d" | "t" | "n" | "q"; diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index d024149..2d25714 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -179,6 +179,7 @@ const dParam = url.get("d"); const tParam = url.get("t"); const nParam = url.get("n"); + const qParam = url.get("q"); if (idParam) { searchValue = idParam; @@ -192,6 +193,9 @@ } else if (nParam) { searchValue = decodeURIComponent(nParam); searchType = "n"; + } else if (qParam) { + searchValue = decodeURIComponent(qParam); + searchType = "q"; } else { searchValue = null; searchType = null; @@ -200,11 +204,12 @@ // Handle side panel visibility based on search type $effect(() => { - // Close side panel for searches that return multiple results (d-tag, t-tag, name searches) + // Close side panel for searches that return multiple results (d-tag, t-tag, name searches, general searches) if ( searchType === "d" || searchType === "t" || - searchType === "n" + searchType === "n" || + searchType === "q" ) { showSidePanel = false; event = null; From fef9ddaba894847ba8e97599e986424984bba92a Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 20 Aug 2025 00:00:09 -0500 Subject: [PATCH 98/98] Fix a bug where search type failed to change as expected Also add a placeholder generic search query to be used with future fuzzy/full-text/semantic search capabilities. --- src/lib/components/EventSearch.svelte | 203 ++++++++++++++------------ src/routes/events/+page.svelte | 15 +- 2 files changed, 120 insertions(+), 98 deletions(-) diff --git a/src/lib/components/EventSearch.svelte b/src/lib/components/EventSearch.svelte index f4c4310..fc35baf 100644 --- a/src/lib/components/EventSearch.svelte +++ b/src/lib/components/EventSearch.svelte @@ -62,6 +62,20 @@ let searchResultType = $state(null); let isResetting = $state(false); + // Track current search type for internal logic + let currentSearchType = $state(searchType); + let currentSearchValue = $state(searchValue); + + // Sync internal state with props when they change externally + $effect(() => { + if (searchType !== undefined) { + currentSearchType = searchType; + } + if (searchValue !== undefined) { + currentSearchValue = searchValue; + } + }); + // Internal state for cleanup let activeSub: any = null; let currentAbortController: AbortController | null = null; @@ -83,7 +97,7 @@ // Debounced search timeout let searchTimeout: ReturnType | null = null; - // AI-NOTE: 2025-01-24 - Core search handlers extracted for better organization + // AI-NOTE: Core search handlers extracted for better organization async function handleNip05Search(query: string) { try { const foundEvent = await searchNip05(query, ndk); @@ -145,45 +159,46 @@ // Update URL with search query for all search types if (clearInput) { - const searchType = getSearchType(query); - if (searchType) { - const { type, term } = searchType; + const detectedSearchType = getSearchType(query); + if (detectedSearchType) { + const { type, term } = detectedSearchType; const encoded = encodeURIComponent(term); + let newUrl = ""; + if (type === "d") { - goto(`?d=${encoded}`, { - replaceState: false, - keepFocus: true, - noScroll: true, - }); + newUrl = `?d=${encoded}`; + currentSearchType = "d"; + currentSearchValue = term; } else if (type === "t") { - goto(`?t=${encoded}`, { - replaceState: false, - keepFocus: true, - noScroll: true, - }); + newUrl = `?t=${encoded}`; + currentSearchType = "t"; + currentSearchValue = term; } else if (type === "n") { - goto(`?n=${encoded}`, { - replaceState: false, - keepFocus: true, - noScroll: true, - }); + newUrl = `?n=${encoded}`; + currentSearchType = "n"; + currentSearchValue = term; } else if (type === "nip05") { - goto(`?q=${encodeURIComponent(query)}`, { - replaceState: false, - keepFocus: true, - noScroll: true, - }); + newUrl = `?q=${encodeURIComponent(query)}`; + currentSearchType = "q"; + currentSearchValue = query; } else if (type === "event") { - goto(`?id=${encoded}`, { - replaceState: false, - keepFocus: true, - noScroll: true, - }); + newUrl = `?id=${encoded}`; + currentSearchType = "id"; + currentSearchValue = term; } + + goto(newUrl, { + replaceState: false, + keepFocus: true, + noScroll: true, + }); } else { // No specific search type detected, treat as general search const encoded = encodeURIComponent(query); - goto(`?q=${encoded}`, { + const newUrl = `?q=${encoded}`; + currentSearchType = "q"; + currentSearchValue = query; + goto(newUrl, { replaceState: false, keepFocus: true, noScroll: true, @@ -198,12 +213,23 @@ return; } - // AI-NOTE: 2025-01-24 - If no specific search type is detected, treat as event ID search - // URL navigation is now handled in the URL update logic above - await handleEventSearch(query); + // AI-NOTE: If no specific search type is detected, check if it could be an event ID + const trimmedQuery = query.trim(); + if (trimmedQuery && isEventId(trimmedQuery)) { + // Looks like an event ID, treat as event search + await handleEventSearch(query); + } else { + // AI-NOTE: Doesn't look like an event ID, treat as generic search + // The URL update logic above should have set currentSearchType = "q" + // For generic "q" searches, we don't perform actual searches since they're + // unstructured queries. We just update the URL for shareability and show completion + + // TODO: Handle generic "q" searches with a semantic search capability (when available). + updateSearchState(false, true, 0, "q"); + } } - // AI-NOTE: 2025-01-24 - Helper functions for better code organization + // AI-NOTE: Helper functions for better code organization function getSearchType(query: string): { type: string; term: string } | null { const lowerQuery = query.toLowerCase(); @@ -226,22 +252,23 @@ return { type: "nip05", term: query }; } - // AI-NOTE: 2025-01-24 - Detect hex IDs (64-character hex strings with no spaces) + // AI-NOTE: Detect hex IDs (64-character hex strings with no spaces) // These are likely event IDs and should be searched as events const trimmedQuery = query.trim(); if (trimmedQuery && isEventId(trimmedQuery)) { return { type: "event", term: trimmedQuery }; } - // AI-NOTE: 2025-01-24 - Treat plain text searches as profile searches by default - // This allows searching for names like "thebeave" or "TheBeave" without needing n: prefix + // AI-NOTE: Treat plain text searches as generic searches by default + // This allows for flexible searching without assuming it's always a profile search + // Users can still use n: prefix for explicit name/profile searches if ( trimmedQuery && !trimmedQuery.startsWith("nevent") && !trimmedQuery.startsWith("npub") && !trimmedQuery.startsWith("naddr") ) { - return { type: "n", term: trimmedQuery }; + return null; // Let handleSearchEvent treat this as a generic search } return null; @@ -310,15 +337,19 @@ return; } - if (searchValue && searchType) { - if (searchType === "d") { - searchQuery = `d:${searchValue}`; - } else if (searchType === "t") { - searchQuery = `t:${searchValue}`; - } else if (searchType === "n") { - searchQuery = `n:${searchValue}`; + // Use internal state if set (from user actions), otherwise use props + const activeSearchType = currentSearchType ?? searchType; + const activeSearchValue = currentSearchValue ?? searchValue; + + if (activeSearchValue && activeSearchType) { + if (activeSearchType === "d") { + searchQuery = `d:${activeSearchValue}`; + } else if (activeSearchType === "t") { + searchQuery = `t:${activeSearchValue}`; + } else if (activeSearchType === "n") { + searchQuery = `n:${activeSearchValue}`; } else { - searchQuery = searchValue; + searchQuery = activeSearchValue; } } else if (!searchQuery) { searchQuery = ""; @@ -368,30 +399,33 @@ }); $effect(() => { + // Use internal state if set (from user actions), otherwise use props + const activeSearchType = currentSearchType ?? searchType; + const activeSearchValue = currentSearchValue ?? searchValue; + if ( - searchValue && - searchType && + activeSearchValue && + activeSearchType && !searching && !isResetting && - (searchType !== lastProcessedSearchType || - searchValue !== lastProcessedSearchValue) + (activeSearchType !== lastProcessedSearchType || + activeSearchValue !== lastProcessedSearchValue) ) { - console.log("EventSearch: Processing search:", { - searchType, - searchValue, - }); - lastProcessedSearchType = searchType; - lastProcessedSearchValue = searchValue; + + lastProcessedSearchType = activeSearchType; + lastProcessedSearchValue = activeSearchValue; setTimeout(() => { if (!searching && !isResetting) { - if (searchType === "d") { - handleSearchBySubscription("d", searchValue); - } else if (searchType === "t") { - handleSearchBySubscription("t", searchValue); - } else if (searchType === "n") { - handleSearchBySubscription("n", searchValue); + if (activeSearchType === "d") { + handleSearchBySubscription("d", activeSearchValue); + } else if (activeSearchType === "t") { + handleSearchBySubscription("t", activeSearchValue); + } else if (activeSearchType === "n") { + handleSearchBySubscription("n", activeSearchValue); } + // Note: "q" (generic) searches are not processed here since they're + // unstructured queries that don't require actual search execution } }, 100); } @@ -468,6 +502,9 @@ isProcessingSearch = false; currentProcessingSearchValue = null; lastSearchValue = null; + // Reset internal search state + currentSearchType = null; + currentSearchValue = null; updateSearchState(false, false, null, null); cleanupSearch(); @@ -510,16 +547,7 @@ onEventFound(event); } - function navigateToSearch(query: string, paramName: string) { - const encoded = encodeURIComponent(query); - goto(`?${paramName}=${encoded}`, { - replaceState: false, - keepFocus: true, - noScroll: true, - }); - } - - // AI-NOTE: 2025-01-24 - Main subscription search handler with improved error handling + // AI-NOTE: Main subscription search handler with improved error handling async function handleSearchBySubscription( searchType: "d" | "t" | "n", searchTerm: string, @@ -529,7 +557,7 @@ searchTerm, }); - // AI-NOTE: 2025-01-24 - Profile search caching is now handled by centralized searchProfiles function + // AI-NOTE: Profile search caching is now handled by centralized searchProfiles function // No need for separate caching logic here as it's handled in profile_search.ts isResetting = false; @@ -545,7 +573,7 @@ } } - // AI-NOTE: 2025-01-24 - Profile search is now handled by centralized searchProfiles function + // AI-NOTE: Profile search is now handled by centralized searchProfiles function // These functions are no longer needed as profile searches go through subscription_search.ts // which delegates to the centralized profile_search.ts @@ -553,7 +581,7 @@ let retryCount = 0; const maxRetries = 10; // Reduced retry count since we'll use all available relays - // AI-NOTE: 2025-01-24 - Wait for any relays to be available, not just specific types + // AI-NOTE: Wait for any relays to be available, not just specific types // This ensures searches can proceed even if some relay types are not available while (retryCount < maxRetries) { // Check if we have any relays in the NDK pool @@ -565,7 +593,7 @@ retryCount++; } - // AI-NOTE: 2025-01-24 - Don't fail if no relays are available, let the search functions handle fallbacks + // AI-NOTE: Don't fail if no relays are available, let the search functions handle fallbacks // The search functions will use all available relays including fallback relays const poolRelayCount = ndk?.pool?.relays?.size || 0; @@ -616,8 +644,8 @@ updatedResult.eventIds, updatedResult.addresses, updatedResult.searchType, - searchValue || updatedResult.searchTerm, // AI-NOTE: 2025-01-24 - Use original search value for display - false, // AI-NOTE: 2025-01-24 - Second-order update means search is complete + searchValue || updatedResult.searchTerm, // AI-NOTE: Use original search value for display + false, // AI-NOTE: Second-order update means search is complete ); }, onSubscriptionCreated: (sub) => { @@ -649,8 +677,8 @@ result.eventIds, result.addresses, result.searchType, - searchValue || result.searchTerm, // AI-NOTE: 2025-01-24 - Use original search value for display - false, // AI-NOTE: 2025-01-24 - Search is complete + searchValue || result.searchTerm, // AI-NOTE: Use original search value for display + false, // AI-NOTE: Search is complete ); const totalCount = @@ -738,6 +766,9 @@ currentProcessingSearchValue = null; lastSearchValue = null; isWaitingForSearchResult = false; + // Reset internal search state + currentSearchType = null; + currentSearchValue = null; if (searchTimeout) { clearTimeout(searchTimeout); @@ -770,18 +801,6 @@ ? `Search completed. Found 1 ${typeLabel}.` : `Search completed. Found ${searchResultCount} ${countLabel}.`; } - - function getNeventUrl(event: NDKEvent): string { - return neventEncode(event, $activeInboxRelays); - } - - function getNaddrUrl(event: NDKEvent): string { - return naddrEncode(event, $activeInboxRelays); - } - - function getNprofileUrl(pubkey: string): string { - return nprofileEncode(pubkey, $activeInboxRelays); - }
    diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index 2d25714..cdc1428 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -174,12 +174,15 @@ // Use Svelte 5 idiomatic effect to update searchValue and searchType based on URL parameters $effect(() => { - const url = $page.url.searchParams; - const idParam = url.get("id"); - const dParam = url.get("d"); - const tParam = url.get("t"); - const nParam = url.get("n"); - const qParam = url.get("q"); + // Ensure we have the full URL object to trigger reactivity + const url = $page.url; + const searchParams = url.searchParams; + + const idParam = searchParams.get("id"); + const dParam = searchParams.get("d"); + const tParam = searchParams.get("t"); + const nParam = searchParams.get("n"); + const qParam = searchParams.get("q"); if (idParam) { searchValue = idParam;