From b8ada471fe027a558e060b0ec9dbe6d833399fce Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 23 Aug 2025 18:03:27 +0200 Subject: [PATCH 01/37] remove first-line indent --- src/app.css | 20 ++++++++++++++++++++ src/lib/utils/markup/basicMarkupParser.ts | 2 +- src/lib/utils/markup/embeddedMarkupParser.ts | 2 +- tailwind.config.cjs | 17 +++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/app.css b/src/app.css index c15dc2d..0d1928c 100644 --- a/src/app.css +++ b/src/app.css @@ -588,4 +588,24 @@ .prose-invert p:first-line { font-weight: normal !important; } + + /* Prevent first-line indentation in prose content */ + .prose p, + .prose-sm p, + .prose-invert p { + text-indent: 0 !important; + } + + /* Ensure embedded event content doesn't have unwanted indentation */ + .embedded-event .prose p, + .embedded-event .prose-sm p, + .embedded-event .prose-invert p { + text-indent: 0 !important; + margin: 0 !important; + } + + /* Prevent indentation for paragraphs with no-indent class */ + .no-indent { + text-indent: 0 !important; + } } diff --git a/src/lib/utils/markup/basicMarkupParser.ts b/src/lib/utils/markup/basicMarkupParser.ts index 5fcc376..3526861 100644 --- a/src/lib/utils/markup/basicMarkupParser.ts +++ b/src/lib/utils/markup/basicMarkupParser.ts @@ -245,7 +245,7 @@ export async function parseBasicmarkup(text: string): Promise { ) { return para; } - return `

${para}

`; + return `

${para}

`; }) .join("\n"); diff --git a/src/lib/utils/markup/embeddedMarkupParser.ts b/src/lib/utils/markup/embeddedMarkupParser.ts index 263d8f1..8813c92 100644 --- a/src/lib/utils/markup/embeddedMarkupParser.ts +++ b/src/lib/utils/markup/embeddedMarkupParser.ts @@ -251,7 +251,7 @@ export async function parseEmbeddedMarkup( return para; } - return `

${para}

`; + return `

${para}

`; }) .join("\n"); diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 5bd3b5f..e994125 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -1,5 +1,6 @@ import flowbite from "flowbite/plugin"; import plugin from "tailwindcss/plugin"; +import typography from "@tailwindcss/typography"; /** @type {import('tailwindcss').Config}*/ const config = { @@ -89,11 +90,27 @@ const config = { hueRotate: { 20: "20deg", }, + typography: { + DEFAULT: { + css: { + // Remove first-line indentation + 'p:first-line': { + 'text-indent': '0', + }, + // Ensure paragraphs don't have unwanted indentation + p: { + 'text-indent': '0', + 'margin': '0', + }, + }, + }, + }, }, }, plugins: [ flowbite(), + typography, plugin(function ({ addUtilities, matchUtilities }) { addUtilities({ ".content-visibility-auto": { From 49cf6541731e7ef234e6c8a434151186fd852a1e Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 23 Aug 2025 18:48:29 +0200 Subject: [PATCH 02/37] removed unnecessary embedding --- src/lib/components/CommentViewer.svelte | 10 +++++++--- src/lib/components/EventDetails.svelte | 4 ++-- src/lib/components/Notifications.svelte | 14 +++++++++---- src/routes/events/+page.svelte | 26 ++++++++++++------------- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/lib/components/CommentViewer.svelte b/src/lib/components/CommentViewer.svelte index 46b444a..428c18d 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 "./embedded_events/EmbeddedEvent.svelte"; + const { event } = $props<{ event: NDKEvent }>(); @@ -772,7 +772,9 @@
Comment:
- +
+ {node.event.getMatchingTags("comment")[0]?.[1] || "No comment content"} +
{:else} @@ -812,7 +814,9 @@ {:else} - +
+ {node.event.content || "No content"} +
{/if} diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index 2f26fe6..140577d 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 "./embedded_events/EmbeddedEvent.svelte"; + import type { UserProfile } from "$lib/models/user_profile"; const { @@ -310,7 +310,7 @@ Content:
- + {event.content}
{#if shouldTruncate}
- {node.event.getMatchingTags("comment")[0]?.[1] || "No comment content"} + {#await parseBasicmarkup(node.event.getMatchingTags("comment")[0]?.[1] || "No comment content") then parsed} + {@html parsed} + {/await}
{:else} @@ -815,7 +818,9 @@ {:else}
- {node.event.content || "No content"} + {#await parseBasicmarkup(node.event.content || "No content") then parsed} + {@html parsed} + {/await}
{/if} diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index 140577d..ca9103a 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -13,9 +13,10 @@ 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"; - - import type { UserProfile } from "$lib/models/user_profile"; +import Notifications from "$lib/components/Notifications.svelte"; +import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; + +import type { UserProfile } from "$lib/models/user_profile"; const { event, @@ -28,6 +29,7 @@ let authorDisplayName = $state(undefined); let showFullContent = $state(false); let shouldTruncate = $derived(event.content.length > 250 && !showFullContent); + let parsedContent = $state(""); function getEventTitle(event: NDKEvent): string { // First try to get title from title tag @@ -242,6 +244,19 @@ return ids; } + $effect(() => { + if (event.content) { + parseBasicmarkup(event.content).then((parsed) => { + parsedContent = parsed; + }).catch((error) => { + console.error("Error parsing content:", error); + parsedContent = event.content; + }); + } else { + parsedContent = ""; + } + }); + onMount(() => { function handleInternalLinkClick(event: MouseEvent) { const target = event.target as HTMLElement; @@ -310,7 +325,7 @@ Content:
- {event.content} + {@html parsedContent}
{#if shouldTruncate}
@@ -897,7 +900,9 @@
- {notification.content || "No content"} + {#await parseBasicmarkup(notification.content || "No content") then parsed} + {@html parsed} + {/await}
@@ -934,7 +939,9 @@
Replying to:
- {replyToMessage.content || "No content"} + {#await parseBasicmarkup(replyToMessage.content || "No content") then parsed} + {@html parsed} + {/await}
diff --git a/src/lib/utils/markup/basicMarkupParser.ts b/src/lib/utils/markup/basicMarkupParser.ts index 3526861..02925ec 100644 --- a/src/lib/utils/markup/basicMarkupParser.ts +++ b/src/lib/utils/markup/basicMarkupParser.ts @@ -7,6 +7,7 @@ import { processImageWithReveal, processMediaUrl, processNostrIdentifiersInText, + processAllNostrIdentifiers, processWebSocketUrls, processWikilinks, stripTrackingParams, @@ -251,6 +252,7 @@ export async function parseBasicmarkup(text: string): Promise { // Process Nostr identifiers last processedText = await processNostrIdentifiersInText(processedText); + processedText = processAllNostrIdentifiers(processedText); // Replace wikilinks processedText = processWikilinks(processedText); diff --git a/src/lib/utils/markup/markupServices.ts b/src/lib/utils/markup/markupServices.ts index 3ed8a78..52532f8 100644 --- a/src/lib/utils/markup/markupServices.ts +++ b/src/lib/utils/markup/markupServices.ts @@ -180,6 +180,112 @@ export function processNostrIdentifiersWithEmbeddedEvents( return processedText; } +/** + * Shared service for processing all nostr identifiers (both profiles and events) + * Creates clickable links for all nostr identifiers + */ +export function processAllNostrIdentifiers(text: string): string { + let processedText = text; + + // Pattern for prefixed nostr identifiers (nostr:npub1, nostr:note1, etc.) + // This handles both full identifiers and partial ones that might appear in content + const prefixedNostrPattern = /nostr:(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{20,}/g; + + // Pattern for bare nostr identifiers (npub1, note1, nevent1, naddr1) + // Exclude matches that are part of URLs to avoid breaking existing links + const bareNostrPattern = /(?= 0; i--) { + const match = prefixedMatches[i]; + const [fullMatch] = match; + const matchIndex = match.index ?? 0; + + // Create shortened display text + const identifier = fullMatch.replace('nostr:', ''); + const displayText = `${identifier.slice(0, 8)}...${identifier.slice(-4)}`; + + // Create clickable link + const replacement = `${displayText}`; + + // Replace the match in the text + processedText = processedText.slice(0, matchIndex) + replacement + + processedText.slice(matchIndex + fullMatch.length); + } + + // Process bare nostr identifiers + const bareMatches = Array.from(processedText.matchAll(bareNostrPattern)); + + // Process them in reverse order to avoid index shifting issues + for (let i = bareMatches.length - 1; i >= 0; i--) { + const match = bareMatches[i]; + const [fullMatch] = match; + const matchIndex = match.index ?? 0; + + // Create shortened display text + const displayText = `${fullMatch.slice(0, 8)}...${fullMatch.slice(-4)}`; + + // Create clickable link with nostr: prefix for the href + const replacement = `${displayText}`; + + // Replace the match in the text + processedText = processedText.slice(0, matchIndex) + replacement + + processedText.slice(matchIndex + fullMatch.length); + } + + // Also handle any remaining truncated prefixed identifiers that might be cut off or incomplete + const truncatedPrefixedPattern = /nostr:(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{8,}/g; + const truncatedPrefixedMatches = Array.from(processedText.matchAll(truncatedPrefixedPattern)); + + for (let i = truncatedPrefixedMatches.length - 1; i >= 0; i--) { + const match = truncatedPrefixedMatches[i]; + const [fullMatch] = match; + const matchIndex = match.index ?? 0; + + // Skip if this was already processed by the main pattern + if (fullMatch.length >= 30) continue; // Full identifiers are at least 30 chars + + // Create display text for truncated identifiers + const identifier = fullMatch.replace('nostr:', ''); + const displayText = identifier.length > 12 ? `${identifier.slice(0, 8)}...${identifier.slice(-4)}` : identifier; + + // Create clickable link + const replacement = `${displayText}`; + + // Replace the match in the text + processedText = processedText.slice(0, matchIndex) + replacement + + processedText.slice(matchIndex + fullMatch.length); + } + + // Handle truncated bare identifiers + const truncatedBarePattern = /(?= 0; i--) { + const match = truncatedBareMatches[i]; + const [fullMatch] = match; + const matchIndex = match.index ?? 0; + + // Skip if this was already processed by the main pattern + if (fullMatch.length >= 30) continue; // Full identifiers are at least 30 chars + + // Create display text for truncated identifiers + const displayText = fullMatch.length > 12 ? `${fullMatch.slice(0, 8)}...${fullMatch.slice(-4)}` : fullMatch; + + // Create clickable link + const replacement = `${displayText}`; + + // Replace the match in the text + processedText = processedText.slice(0, matchIndex) + replacement + + processedText.slice(matchIndex + fullMatch.length); + } + + return processedText; +} + /** * Shared service for processing emoji shortcodes */ diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index c358811..287764f 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -21,6 +21,7 @@ import { getEventType } from "$lib/utils/mime"; import ViewPublicationLink from "$lib/components/util/ViewPublicationLink.svelte"; import { checkCommunity } from "$lib/utils/search_utility"; + import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; import { userStore } from "$lib/stores/userStore"; import { @@ -751,10 +752,9 @@
- {result.content.slice(0, 200)} - {#if result.content.length > 200} - ... - {/if} + {#await parseBasicmarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : "")) then parsed} + {@html parsed} + {/await}
{/if} {/if} @@ -938,10 +938,9 @@
- {result.content.slice(0, 200)} - {#if result.content.length > 200} - ... - {/if} + {#await parseBasicmarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : "")) then parsed} + {@html parsed} + {/await}
{/if} {/if} @@ -1111,10 +1110,9 @@
- {result.content.slice(0, 200)} - {#if result.content.length > 200} - ... - {/if} + {#await parseBasicmarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : "")) then parsed} + {@html parsed} + {/await}
{/if} {/if} From 958b16ba23145637a020a7d50ab64012c05fd9fe Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 23 Aug 2025 20:15:33 +0200 Subject: [PATCH 04/37] fixed quote --- src/lib/components/EventDetails.svelte | 69 ++++++++--- src/lib/components/Notifications.svelte | 72 +++++++++++- .../embedded_events/EmbeddedEvent.svelte | 25 +++- src/lib/utils/mime.ts | 12 ++ src/lib/utils/nostrUtils.ts | 2 +- src/routes/events/+page.svelte | 107 ++++++++++++++++-- 6 files changed, 252 insertions(+), 35 deletions(-) diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index ca9103a..61d649a 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -15,6 +15,10 @@ import ContainingIndexes from "$lib/components/util/ContainingIndexes.svelte"; import Notifications from "$lib/components/Notifications.svelte"; import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; +import { repostContent, quotedContent } from "$lib/components/embedded_events/EmbeddedSnippets.svelte"; +import { repostKinds } from "$lib/consts"; +import { getNdkContext } from "$lib/ndk"; +import { processNostrIdentifiers } from "$lib/utils/nostrUtils"; import type { UserProfile } from "$lib/models/user_profile"; @@ -30,6 +34,7 @@ import type { UserProfile } from "$lib/models/user_profile"; let showFullContent = $state(false); let shouldTruncate = $derived(event.content.length > 250 && !showFullContent); let parsedContent = $state(""); + let isRepost = $derived(repostKinds.includes(event.kind) || (event.kind === 1 && event.getMatchingTags("q").length > 0)); function getEventTitle(event: NDKEvent): string { // First try to get title from title tag @@ -246,12 +251,20 @@ import type { UserProfile } from "$lib/models/user_profile"; $effect(() => { if (event.content) { - parseBasicmarkup(event.content).then((parsed) => { - parsedContent = parsed; - }).catch((error) => { - console.error("Error parsing content:", error); + // For kind 6 and 16 reposts, we don't need to parse the content as basic markup since it's JSON + // For quote reposts (kind 1 with q tags), we still need to parse the content for nostr identifiers + if (repostKinds.includes(event.kind)) { parsedContent = event.content; - }); + } else { + // For all other events (including quote reposts), parse the content for nostr identifiers + // Use the proper processNostrIdentifiers function to get display names + processNostrIdentifiers(event.content, getNdkContext()).then((processed) => { + parsedContent = processed; + }).catch((error) => { + console.error("Error parsing content:", error); + parsedContent = event.content; + }); + } } else { parsedContent = ""; } @@ -324,14 +337,44 @@ import type { UserProfile } from "$lib/models/user_profile";
Content:
-
- {@html parsedContent} -
- {#if shouldTruncate} - + {#if isRepost} + + {#if repostKinds.includes(event.kind)} + +
+
+ {event.kind === 6 ? 'Reposted content:' : 'Generic reposted content:'} +
+ {@render repostContent(event.content)} +
+ {:else if event.kind === 1 && event.getMatchingTags("q").length > 0} + +
+
+ Quote repost: +
+ {@render quotedContent(event, [], getNdkContext())} + {#if event.content && event.content.trim()} +
+
+ Added comment: +
+ {@html parsedContent} +
+ {/if} +
+ {/if} + {:else} + +
+ {@html parsedContent} +
+ {#if shouldTruncate} + + {/if} {/if}
diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 7d1703d..e18b971 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -22,6 +22,8 @@ import { formatDate, neventEncode } from "$lib/utils"; import { NDKRelaySetFromNDK } from "$lib/utils/nostrUtils"; import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; + import { repostContent } from "$lib/components/embedded_events/EmbeddedSnippets.svelte"; + import { repostKinds } from "$lib/consts"; import { getNdkContext } from "$lib/ndk"; @@ -819,9 +821,38 @@
- {#await parseBasicmarkup(message.content || "No content") then parsed} - {@html parsed} - {/await} + {#if repostKinds.includes(message.kind)} + +
+
+ {message.kind === 6 ? 'Repost:' : 'Generic repost:'} +
+ {@render repostContent(message.content)} +
+ {:else if message.kind === 1 && message.getMatchingTags("q").length > 0} + +
+
+ Quote repost: +
+ {@render quotedContent(message, publicMessages, ndk)} + {#if message.content && message.content.trim()} +
+
+ Comment: +
+ {#await parseBasicmarkup(message.content.slice(0, 100) + (message.content.length > 100 ? "..." : "")) then parsed} + {@html parsed} + {/await} +
+ {/if} +
+ {:else} + + {#await parseBasicmarkup(message.content || "No content") then parsed} + {@html parsed} + {/await} + {/if}
@@ -900,9 +931,38 @@
- {#await parseBasicmarkup(notification.content || "No content") then parsed} - {@html parsed} - {/await} + {#if repostKinds.includes(notification.kind)} + +
+
+ {notification.kind === 6 ? 'Repost:' : 'Generic repost:'} +
+ {@render repostContent(notification.content)} +
+ {:else if notification.kind === 1 && notification.getMatchingTags("q").length > 0} + +
+
+ Quote repost: +
+ {@render quotedContent(notification, notifications, ndk)} + {#if notification.content && notification.content.trim()} +
+
+ Comment: +
+ {#await parseBasicmarkup(notification.content.slice(0, 100) + (notification.content.length > 100 ? "..." : "")) then parsed} + {@html parsed} + {/await} +
+ {/if} +
+ {:else} + + {#await parseBasicmarkup(notification.content || "No content") then parsed} + {@html parsed} + {/await} + {/if}
diff --git a/src/lib/components/embedded_events/EmbeddedEvent.svelte b/src/lib/components/embedded_events/EmbeddedEvent.svelte index 30ef2dd..005dea8 100644 --- a/src/lib/components/embedded_events/EmbeddedEvent.svelte +++ b/src/lib/components/embedded_events/EmbeddedEvent.svelte @@ -2,7 +2,7 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { fetchEventWithFallback, getUserMetadata, toNpub } from "$lib/utils/nostrUtils"; import { userBadge } from "$lib/snippets/UserSnippets.svelte"; - import { parsedContent } from "$lib/components/embedded_events/EmbeddedSnippets.svelte"; + import { parsedContent, repostContent, quotedContent } from "$lib/components/embedded_events/EmbeddedSnippets.svelte"; import { naddrEncode } from "$lib/utils"; import { activeInboxRelays, getNdkContext } from "$lib/ndk"; import { goto } from "$app/navigation"; @@ -292,14 +292,27 @@ {#if event.kind === 1 || repostKinds.includes(event.kind)}
{#if repostKinds.includes(event.kind)} - +
- Reposted content: + {event.kind === 6 ? 'Reposted content:' : 'Generic reposted content:'}
- {@render parsedContent(event.content.slice(0, 300))} - {#if event.content.length > 300} - ... + {@render repostContent(event.content)} +
+ {:else if event.kind === 1 && event.getMatchingTags("q").length > 0} + +
+
+ Quote repost: +
+ {@render quotedContent(event, [], ndk)} + {#if event.content && event.content.trim()} +
+
+ Added comment: +
+ {@render parsedContent(event.content)} +
{/if}
{:else} diff --git a/src/lib/utils/mime.ts b/src/lib/utils/mime.ts index a8714c3..3b8e416 100644 --- a/src/lib/utils/mime.ts +++ b/src/lib/utils/mime.ts @@ -62,6 +62,18 @@ export function getMimeTags(kind: number): [string, string][] { MTag = ["M", `note/microblog/${replaceability}`]; break; + // Repost (NIP-18) + case 6: + mTag = ["m", "application/json"]; + MTag = ["M", `note/repost/${replaceability}`]; + break; + + // Generic repost (NIP-18) + case 16: + mTag = ["m", "application/json"]; + MTag = ["M", `note/generic-repost/${replaceability}`]; + break; + // Generic reply case 1111: mTag = ["m", "text/plain"]; diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index ef3e8ca..31138c7 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -179,7 +179,7 @@ export async function createProfileLinkWithVerification( /** * Create a note link element */ -function createNoteLink(identifier: string): string { +export function createNoteLink(identifier: string): string { const cleanId = identifier.replace(/^nostr:/, ""); const shortId = `${cleanId.slice(0, 12)}...${cleanId.slice(-8)}`; const escapedId = escapeHtml(cleanId); diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index 287764f..e65e02c 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -22,6 +22,8 @@ import ViewPublicationLink from "$lib/components/util/ViewPublicationLink.svelte"; import { checkCommunity } from "$lib/utils/search_utility"; import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; + import { repostContent, quotedContent } from "$lib/components/embedded_events/EmbeddedSnippets.svelte"; + import { repostKinds } from "$lib/consts"; import { userStore } from "$lib/stores/userStore"; import { @@ -752,9 +754,38 @@
- {#await parseBasicmarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : "")) then parsed} - {@html parsed} - {/await} + {#if repostKinds.includes(result.kind)} + +
+
+ {result.kind === 6 ? 'Repost:' : 'Generic repost:'} +
+ {@render repostContent(result.content)} +
+ {:else if result.kind === 1 && result.getMatchingTags("q").length > 0} + +
+
+ Quote repost: +
+ {@render quotedContent(result, [], ndk)} + {#if result.content && result.content.trim()} +
+
+ Comment: +
+ {#await parseBasicmarkup(result.content.slice(0, 100) + (result.content.length > 100 ? "..." : "")) then parsed} + {@html parsed} + {/await} +
+ {/if} +
+ {:else} + + {#await parseBasicmarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : "")) then parsed} + {@html parsed} + {/await} + {/if}
{/if} {/if} @@ -938,9 +969,38 @@
- {#await parseBasicmarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : "")) then parsed} - {@html parsed} - {/await} + {#if repostKinds.includes(result.kind)} + +
+
+ {result.kind === 6 ? 'Repost:' : 'Generic repost:'} +
+ {@render repostContent(result.content)} +
+ {:else if result.kind === 1 && result.getMatchingTags("q").length > 0} + +
+
+ Quote repost: +
+ {@render quotedContent(result, [], ndk)} + {#if result.content && result.content.trim()} +
+
+ Comment: +
+ {#await parseBasicmarkup(result.content.slice(0, 100) + (result.content.length > 100 ? "..." : "")) then parsed} + {@html parsed} + {/await} +
+ {/if} +
+ {:else} + + {#await parseBasicmarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : "")) then parsed} + {@html parsed} + {/await} + {/if}
{/if} {/if} @@ -1110,9 +1170,38 @@
- {#await parseBasicmarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : "")) then parsed} - {@html parsed} - {/await} + {#if repostKinds.includes(result.kind)} + +
+
+ {result.kind === 6 ? 'Repost:' : 'Generic repost:'} +
+ {@render repostContent(result.content)} +
+ {:else if result.kind === 1 && result.getMatchingTags("q").length > 0} + +
+
+ Quote repost: +
+ {@render quotedContent(result, [], ndk)} + {#if result.content && result.content.trim()} +
+
+ Comment: +
+ {#await parseBasicmarkup(result.content.slice(0, 100) + (result.content.length > 100 ? "..." : "")) then parsed} + {@html parsed} + {/await} +
+ {/if} +
+ {:else} + + {#await parseBasicmarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : "")) then parsed} + {@html parsed} + {/await} + {/if}
{/if} {/if} From 7a79a8f09cd48459c175317217c50c2fff09af3d Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 23 Aug 2025 20:39:50 +0200 Subject: [PATCH 05/37] renders npubs --- src/lib/components/Notifications.svelte | 9 +++--- .../embedded_events/EmbeddedSnippets.svelte | 30 ++++++++++++++++--- src/lib/utils/markup/basicMarkupParser.ts | 1 - 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index e18b971..b463ba6 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -22,6 +22,7 @@ import { formatDate, neventEncode } from "$lib/utils"; import { NDKRelaySetFromNDK } from "$lib/utils/nostrUtils"; import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; + import { processNostrIdentifiers } from "$lib/utils/nostrUtils"; import { repostContent } from "$lib/components/embedded_events/EmbeddedSnippets.svelte"; import { repostKinds } from "$lib/consts"; @@ -841,7 +842,7 @@
Comment:
- {#await parseBasicmarkup(message.content.slice(0, 100) + (message.content.length > 100 ? "..." : "")) then parsed} + {#await processNostrIdentifiers(message.content, ndk) then parsed} {@html parsed} {/await}
@@ -849,7 +850,7 @@ {:else} - {#await parseBasicmarkup(message.content || "No content") then parsed} + {#await processNostrIdentifiers(message.content || "No content", ndk) then parsed} {@html parsed} {/await} {/if} @@ -951,7 +952,7 @@
Comment:
- {#await parseBasicmarkup(notification.content.slice(0, 100) + (notification.content.length > 100 ? "..." : "")) then parsed} + {#await processNostrIdentifiers(notification.content, ndk) then parsed} {@html parsed} {/await} @@ -959,7 +960,7 @@ {:else} - {#await parseBasicmarkup(notification.content || "No content") then parsed} + {#await processNostrIdentifiers(notification.content || "No content", ndk) then parsed} {@html parsed} {/await} {/if} diff --git a/src/lib/components/embedded_events/EmbeddedSnippets.svelte b/src/lib/components/embedded_events/EmbeddedSnippets.svelte index 48f103d..e23c35f 100644 --- a/src/lib/components/embedded_events/EmbeddedSnippets.svelte +++ b/src/lib/components/embedded_events/EmbeddedSnippets.svelte @@ -275,9 +275,20 @@ {#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)} @@ -291,9 +302,20 @@ } })()} {#if nevent} - + {:else}
Quoted message not found. Event ID: {eventId.slice(0, 8)}... diff --git a/src/lib/utils/markup/basicMarkupParser.ts b/src/lib/utils/markup/basicMarkupParser.ts index 02925ec..a7ba2bd 100644 --- a/src/lib/utils/markup/basicMarkupParser.ts +++ b/src/lib/utils/markup/basicMarkupParser.ts @@ -252,7 +252,6 @@ export async function parseBasicmarkup(text: string): Promise { // Process Nostr identifiers last processedText = await processNostrIdentifiersInText(processedText); - processedText = processAllNostrIdentifiers(processedText); // Replace wikilinks processedText = processWikilinks(processedText); From 13ab06e2540ec5484d754b6b47061da4b84bdaf1 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 23 Aug 2025 21:09:21 +0200 Subject: [PATCH 06/37] fixed parsing of notifications --- src/lib/components/Notifications.svelte | 9 ++++----- src/lib/utils/nostrUtils.ts | 21 ++++++++++++++++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index b463ba6..d627de9 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -22,7 +22,6 @@ import { formatDate, neventEncode } from "$lib/utils"; import { NDKRelaySetFromNDK } from "$lib/utils/nostrUtils"; import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; - import { processNostrIdentifiers } from "$lib/utils/nostrUtils"; import { repostContent } from "$lib/components/embedded_events/EmbeddedSnippets.svelte"; import { repostKinds } from "$lib/consts"; @@ -842,7 +841,7 @@
Comment:
- {#await processNostrIdentifiers(message.content, ndk) then parsed} + {#await parseBasicmarkup(message.content) then parsed} {@html parsed} {/await}
@@ -850,7 +849,7 @@ {:else} - {#await processNostrIdentifiers(message.content || "No content", ndk) then parsed} + {#await parseBasicmarkup(message.content || "No content") then parsed} {@html parsed} {/await} {/if} @@ -952,7 +951,7 @@
Comment:
- {#await processNostrIdentifiers(notification.content, ndk) then parsed} + {#await parseBasicmarkup(notification.content) then parsed} {@html parsed} {/await} @@ -960,7 +959,7 @@ {:else} - {#await processNostrIdentifiers(notification.content || "No content", ndk) then parsed} + {#await parseBasicmarkup(notification.content || "No content") then parsed} {@html parsed} {/await} {/if} diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index 31138c7..32072e0 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -191,6 +191,7 @@ export function createNoteLink(identifier: string): string { /** * Process Nostr identifiers in text */ +// AI-NOTE: 2025-01-24 - Enhanced URL detection to prevent processing nostr identifiers that are part of URLs export async function processNostrIdentifiers( content: string, ndk: NDK, @@ -201,7 +202,25 @@ export async function processNostrIdentifiers( function isPartOfUrl(text: string, index: number): boolean { // Look for http(s):// or www. before the match const before = text.slice(Math.max(0, index - 12), index); - return /https?:\/\/$|www\.$/i.test(before); + if (/https?:\/\/$|www\.$/i.test(before)) { + return true; + } + + // Check if the match is part of a larger URL structure + // Look for common URL patterns that might contain nostr identifiers + const beforeContext = text.slice(Math.max(0, index - 50), index); + const afterContext = text.slice(index, Math.min(text.length, index + 50)); + + // Check if there's a URL-like structure around the match + const urlPatterns = [ + /https?:\/\/[^\s]*$/i, // URL starting with http(s):// + /www\.[^\s]*$/i, // URL starting with www. + /[^\s]*\.(com|org|net|io|eu|co|me|app|dev)[^\s]*$/i, // Common TLDs + /[^\s]*\/[^\s]*$/i, // Path-like structures + ]; + + const combinedContext = beforeContext + afterContext; + return urlPatterns.some(pattern => pattern.test(combinedContext)); } // Process profiles (npub and nprofile) From 447740e550fedfb66d9165f69547a964b0a26615 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 23 Aug 2025 21:17:41 +0200 Subject: [PATCH 07/37] added basic markup parsing to content field --- src/lib/components/EventDetails.svelte | 46 ++++++++++++++++--- src/lib/components/cards/ProfileHeader.svelte | 23 +++++++++- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index 61d649a..cdbe669 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -72,10 +72,41 @@ import type { UserProfile } from "$lib/models/user_profile"; return "Untitled"; } + let parsedSummary = $state(""); + let parsedTitle = $state(""); + function getEventSummary(event: NDKEvent): string { return getMatchingTags(event, "summary")[0]?.[1] || ""; } + $effect(() => { + const summary = getEventSummary(event); + if (summary) { + parseBasicmarkup(summary).then((processed) => { + parsedSummary = processed; + }).catch((error) => { + console.error("Error parsing summary:", error); + parsedSummary = summary; + }); + } else { + parsedSummary = ""; + } + }); + + $effect(() => { + const title = getEventTitle(event); + if (title && title !== "Untitled") { + parseBasicmarkup(title).then((processed) => { + parsedTitle = processed; + }).catch((error) => { + console.error("Error parsing title:", error); + parsedTitle = title; + }); + } else { + parsedTitle = title || ""; + } + }); + function getEventTypeDisplay(event: NDKEvent): string { const [mTag, MTag] = getMimeTags(event.kind || 0); return MTag[1].split("/")[1] || `Event Kind ${event.kind}`; @@ -256,9 +287,8 @@ import type { UserProfile } from "$lib/models/user_profile"; if (repostKinds.includes(event.kind)) { parsedContent = event.content; } else { - // For all other events (including quote reposts), parse the content for nostr identifiers - // Use the proper processNostrIdentifiers function to get display names - processNostrIdentifiers(event.content, getNdkContext()).then((processed) => { + // For all other events (including quote reposts), parse the content using basic markup parser + parseBasicmarkup(event.content).then((processed) => { parsedContent = processed; }).catch((error) => { console.error("Error parsing content:", error); @@ -287,9 +317,9 @@ import type { UserProfile } from "$lib/models/user_profile";
- {#if event.kind !== 0 && getEventTitle(event)} + {#if event.kind !== 0 && parsedTitle}

- {getEventTitle(event)} + {@html parsedTitle}

{/if} @@ -321,10 +351,12 @@ import type { UserProfile } from "$lib/models/user_profile"; >
- {#if getEventSummary(event)} + {#if parsedSummary}
Summary: -

{getEventSummary(event)}

+
+ {@html parsedSummary} +
{/if} diff --git a/src/lib/components/cards/ProfileHeader.svelte b/src/lib/components/cards/ProfileHeader.svelte index 7973172..a095434 100644 --- a/src/lib/components/cards/ProfileHeader.svelte +++ b/src/lib/components/cards/ProfileHeader.svelte @@ -17,6 +17,7 @@ import { goto } from "$app/navigation"; import { isPubkeyInUserLists, fetchCurrentUserLists } from "$lib/utils/user_lists"; import { UserOutline } from "flowbite-svelte-icons"; + import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; const { event, @@ -34,6 +35,7 @@ let lnurl = $state(null); let communityStatus = $state(null); let isInUserLists = $state(null); + let parsedAbout = $state(""); onMount(async () => { if (profile?.lud16) { @@ -90,6 +92,19 @@ } }); + $effect(() => { + if (profile?.about) { + parseBasicmarkup(profile.about).then((processed) => { + parsedAbout = processed; + }).catch((error) => { + console.error("Error parsing about:", error); + parsedAbout = profile.about; + }); + } else { + parsedAbout = ""; + } + }); + function navigateToIdentifier(link: string) { goto(link); } @@ -196,10 +211,14 @@
{profile.displayName}
{/if} - {#if profile.about} + {#if parsedAbout}
About:
-
{profile.about}
+
+
+ {@html parsedAbout} +
+
{/if} {#if profile.website} From 40a5afa892ad5ce7cd01a1270d9e0adda50fdce6 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 23 Aug 2025 21:25:42 +0200 Subject: [PATCH 08/37] support display_name and deprecated displayName --- src/lib/models/user_profile.d.ts | 1 + src/lib/snippets/UserSnippets.svelte | 2 +- src/lib/utils/npubCache.ts | 1 + src/lib/utils/search_types.ts | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/models/user_profile.d.ts b/src/lib/models/user_profile.d.ts index 283ff9a..892dda8 100644 --- a/src/lib/models/user_profile.d.ts +++ b/src/lib/models/user_profile.d.ts @@ -1,6 +1,7 @@ export interface UserProfile { name?: string; display_name?: string; + displayName?: string; about?: string; picture?: string; banner?: string; diff --git a/src/lib/snippets/UserSnippets.svelte b/src/lib/snippets/UserSnippets.svelte index d069c94..54c9cf0 100644 --- a/src/lib/snippets/UserSnippets.svelte +++ b/src/lib/snippets/UserSnippets.svelte @@ -21,7 +21,7 @@ class="npub-badge bg-transparent border-none p-0 underline cursor-pointer" onclick={() => goto(`/events?id=${npub}`)} > - @{p.display_name || + @{p.displayName || p.display_name || p.name || npub.slice(0, 8) + "..." + npub.slice(-4)} diff --git a/src/lib/utils/npubCache.ts b/src/lib/utils/npubCache.ts index bc50d7b..1b3f3d9 100644 --- a/src/lib/utils/npubCache.ts +++ b/src/lib/utils/npubCache.ts @@ -124,6 +124,7 @@ class UnifiedProfileCache { const metadata: NostrProfile = { name: profile?.name || fallback.name, displayName: profile?.displayName || profile?.display_name, + display_name: profile?.display_name || profile?.displayName, // AI-NOTE: 2025-01-24 - Added for compatibility nip05: profile?.nip05, picture: profile?.picture || profile?.image, about: profile?.about, diff --git a/src/lib/utils/search_types.ts b/src/lib/utils/search_types.ts index 46da61e..dc336d0 100644 --- a/src/lib/utils/search_types.ts +++ b/src/lib/utils/search_types.ts @@ -20,6 +20,7 @@ export interface Filter { export interface NostrProfile { name?: string; displayName?: string; + display_name?: string; // AI-NOTE: 2025-01-24 - Added for compatibility with existing code nip05?: string; picture?: string; about?: string; From 9fff4fb6269b4fdefb3971cbf15b9345a80dd204 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 23 Aug 2025 21:39:26 +0200 Subject: [PATCH 09/37] fixed npub display in notifications and eventdetails --- src/lib/components/EventDetails.svelte | 2 +- src/lib/components/Notifications.svelte | 10 +++++----- src/lib/snippets/UserSnippets.svelte | 9 ++++++--- src/lib/utils/markup/basicMarkupParser.ts | 5 +++-- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index cdbe669..2979b0d 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -288,7 +288,7 @@ import type { UserProfile } from "$lib/models/user_profile"; parsedContent = event.content; } else { // For all other events (including quote reposts), parse the content using basic markup parser - parseBasicmarkup(event.content).then((processed) => { + parseBasicmarkup(event.content, getNdkContext()).then((processed) => { parsedContent = processed; }).catch((error) => { console.error("Error parsing content:", error); diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index d627de9..3466450 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -841,7 +841,7 @@
Comment:
- {#await parseBasicmarkup(message.content) then parsed} + {#await parseBasicmarkup(message.content, ndk) then parsed} {@html parsed} {/await} @@ -849,7 +849,7 @@ {:else} - {#await parseBasicmarkup(message.content || "No content") then parsed} + {#await parseBasicmarkup(message.content || "No content", ndk) then parsed} {@html parsed} {/await} {/if} @@ -951,7 +951,7 @@
Comment:
- {#await parseBasicmarkup(notification.content) then parsed} + {#await parseBasicmarkup(notification.content, ndk) then parsed} {@html parsed} {/await} @@ -959,7 +959,7 @@ {:else} - {#await parseBasicmarkup(notification.content || "No content") then parsed} + {#await parseBasicmarkup(notification.content || "No content", ndk) then parsed} {@html parsed} {/await} {/if} @@ -999,7 +999,7 @@
Replying to:
- {#await parseBasicmarkup(replyToMessage.content || "No content") then parsed} + {#await parseBasicmarkup(replyToMessage.content || "No content", ndk) then parsed} {@html parsed} {/await}
diff --git a/src/lib/snippets/UserSnippets.svelte b/src/lib/snippets/UserSnippets.svelte index 54c9cf0..497a650 100644 --- a/src/lib/snippets/UserSnippets.svelte +++ b/src/lib/snippets/UserSnippets.svelte @@ -6,6 +6,7 @@ getUserMetadata, } from "$lib/utils/nostrUtils"; import type { UserProfile } from "$lib/models/user_profile"; + import { getNdkContext } from "$lib/ndk"; export { userBadge }; @@ -14,8 +15,9 @@ {@const npub = toNpub(identifier)} {#if npub} {#if !displayText || displayText.trim().toLowerCase() === "unknown"} - {#await getUserMetadata(npub, undefined, false) then profile} + {#await getUserMetadata(npub, getNdkContext(), false) then profile} {@const p = profile as UserProfile} + {@const debugInfo = console.log("Profile data for", npub, ":", p)}
{/if} {:else} - {#await parseBasicmarkup(message.content || "No content", ndk) then parsed} - {@html parsed} - {/await} + {@render basicMarkup(message.content || "No content", ndk)} {/if} @@ -951,17 +946,13 @@
Comment:
- {#await parseBasicmarkup(notification.content, ndk) then parsed} - {@html parsed} - {/await} + {@render basicMarkup(notification.content, ndk)} {/if} {:else} - {#await parseBasicmarkup(notification.content || "No content", ndk) then parsed} - {@html parsed} - {/await} + {@render basicMarkup(notification.content || "No content", ndk)} {/if} @@ -999,9 +990,7 @@
Replying to:
- {#await parseBasicmarkup(replyToMessage.content || "No content", ndk) then parsed} - {@html parsed} - {/await} + {@render basicMarkup(replyToMessage.content || "No content", ndk)}
diff --git a/src/lib/components/cards/ProfileHeader.svelte b/src/lib/components/cards/ProfileHeader.svelte index a095434..d58c13c 100644 --- a/src/lib/components/cards/ProfileHeader.svelte +++ b/src/lib/components/cards/ProfileHeader.svelte @@ -17,7 +17,8 @@ import { goto } from "$app/navigation"; import { isPubkeyInUserLists, fetchCurrentUserLists } from "$lib/utils/user_lists"; import { UserOutline } from "flowbite-svelte-icons"; - import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; + import { basicMarkup } from "$lib/snippets/MarkupSnippets.svelte"; + import { getNdkContext } from "$lib/ndk"; const { event, @@ -31,11 +32,12 @@ communityStatusMap?: Record; }>(); + const ndk = getNdkContext(); + let lnModalOpen = $state(false); let lnurl = $state(null); let communityStatus = $state(null); let isInUserLists = $state(null); - let parsedAbout = $state(""); onMount(async () => { if (profile?.lud16) { @@ -92,19 +94,6 @@ } }); - $effect(() => { - if (profile?.about) { - parseBasicmarkup(profile.about).then((processed) => { - parsedAbout = processed; - }).catch((error) => { - console.error("Error parsing about:", error); - parsedAbout = profile.about; - }); - } else { - parsedAbout = ""; - } - }); - function navigateToIdentifier(link: string) { goto(link); } @@ -211,12 +200,12 @@
{profile.displayName}
{/if} - {#if parsedAbout} + {#if profile.about}
About:
- {@html parsedAbout} + {@render basicMarkup(profile.about, ndk)}
diff --git a/src/lib/components/embedded_events/EmbeddedEvent.svelte b/src/lib/components/embedded_events/EmbeddedEvent.svelte index 005dea8..9b30919 100644 --- a/src/lib/components/embedded_events/EmbeddedEvent.svelte +++ b/src/lib/components/embedded_events/EmbeddedEvent.svelte @@ -2,7 +2,7 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { fetchEventWithFallback, getUserMetadata, toNpub } from "$lib/utils/nostrUtils"; import { userBadge } from "$lib/snippets/UserSnippets.svelte"; - import { parsedContent, repostContent, quotedContent } from "$lib/components/embedded_events/EmbeddedSnippets.svelte"; + import { parsedContent, repostContent, quotedContent } from "$lib/snippets/EmbeddedSnippets.svelte"; import { naddrEncode } from "$lib/utils"; import { activeInboxRelays, getNdkContext } from "$lib/ndk"; import { goto } from "$app/navigation"; diff --git a/src/lib/components/embedded_events/EmbeddedSnippets.svelte b/src/lib/snippets/EmbeddedSnippets.svelte similarity index 99% rename from src/lib/components/embedded_events/EmbeddedSnippets.svelte rename to src/lib/snippets/EmbeddedSnippets.svelte index e23c35f..8245e74 100644 --- a/src/lib/components/embedded_events/EmbeddedSnippets.svelte +++ b/src/lib/snippets/EmbeddedSnippets.svelte @@ -7,7 +7,7 @@ import { buildCompleteRelaySet } from "$lib/utils/relay_management"; import { nip19 } from "nostr-tools"; import { parseEmbeddedMarkup } from "$lib/utils/markup/embeddedMarkupParser"; - import type NDK from "@nostr-dev-kit/ndk"; + import type NDK from "@nostr-dev-kit/ndk"; export { parsedContent, diff --git a/src/lib/snippets/MarkupSnippets.svelte b/src/lib/snippets/MarkupSnippets.svelte new file mode 100644 index 0000000..eb08603 --- /dev/null +++ b/src/lib/snippets/MarkupSnippets.svelte @@ -0,0 +1,14 @@ + + +{#snippet basicMarkup(text: string, ndk?: NDK)} + {#await parseBasicMarkup(text, ndk) then parsed} + {@html parsed} + {:catch error: Error} +
Error processing markup: {error.message}
+ {/await} +{/snippet} diff --git a/src/lib/utils/markup/advancedMarkupParser.ts b/src/lib/utils/markup/advancedMarkupParser.ts index 3a4816c..63da62a 100644 --- a/src/lib/utils/markup/advancedMarkupParser.ts +++ b/src/lib/utils/markup/advancedMarkupParser.ts @@ -1,4 +1,4 @@ -import { parseBasicmarkup } from "./basicMarkupParser.ts"; +import { parseBasicMarkup } from "./basicMarkupParser.ts"; import hljs from "highlight.js"; import "highlight.js/lib/common"; // Import common languages import "highlight.js/styles/github-dark.css"; // Dark theme only @@ -444,7 +444,7 @@ export async function parseAdvancedmarkup(text: string): Promise { // 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 + // Blockquotes are now processed only by parseBasicMarkup to avoid double-processing conflicts processedText = processTables(processedText); processedText = processHeadings(processedText); processedText = processHorizontalRules(processedText); @@ -454,7 +454,7 @@ export async function parseAdvancedmarkup(text: string): Promise { // Step 6: Process basic markup (which will also handle Nostr identifiers) // This includes paragraphs, inline code, links, lists, etc. - processedText = await parseBasicmarkup(processedText); + processedText = await parseBasicMarkup(processedText); // Step 7: Restore code blocks processedText = restoreCodeBlocks(processedText, blocks); diff --git a/src/lib/utils/markup/basicMarkupParser.ts b/src/lib/utils/markup/basicMarkupParser.ts index 4b90151..6c7611f 100644 --- a/src/lib/utils/markup/basicMarkupParser.ts +++ b/src/lib/utils/markup/basicMarkupParser.ts @@ -8,7 +8,6 @@ import { processImageWithReveal, processMediaUrl, processNostrIdentifiersInText, - processAllNostrIdentifiers, processWebSocketUrls, processWikilinks, stripTrackingParams, @@ -217,7 +216,7 @@ function processBasicFormatting(content: string): string { return processedText; } -export async function parseBasicmarkup(text: string, ndk?: NDK): Promise { +export async function parseBasicMarkup(text: string, ndk?: NDK): Promise { if (!text) return ""; try { @@ -258,10 +257,7 @@ export async function parseBasicmarkup(text: string, ndk?: NDK): Promise processedText = processWikilinks(processedText); return processedText; - } catch (e: unknown) { - console.error("Error in parseBasicmarkup:", e); - return `
Error processing markup: ${ - (e as Error)?.message ?? "Unknown error" - }
`; + } catch (e) { + throw new Error(`Error in parseBasicMarkup: ${e}`); } } diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index b17c08c..7a0bd52 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -21,10 +21,8 @@ import { getEventType } from "$lib/utils/mime"; import ViewPublicationLink from "$lib/components/util/ViewPublicationLink.svelte"; import { checkCommunity } from "$lib/utils/search_utility"; - import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; - import { repostContent, quotedContent } from "$lib/components/embedded_events/EmbeddedSnippets.svelte"; + import { repostContent, quotedContent } from "$lib/snippets/EmbeddedSnippets.svelte"; import { repostKinds } from "$lib/consts"; - import { userStore } from "$lib/stores/userStore"; import { fetchCurrentUserLists, @@ -34,6 +32,7 @@ import type { UserProfile } from "$lib/models/user_profile"; import type { SearchType } from "$lib/models/search_type"; import { clearAllCaches } from "$lib/utils/cache_manager"; + import { basicMarkup } from "$lib/snippets/MarkupSnippets.svelte"; // AI-NOTE: 2025-01-24 - Add cache clearing function for testing second-order search // This can be called from browser console: window.clearCache() @@ -796,17 +795,13 @@
Comment:
- {#await parseBasicmarkup(result.content.slice(0, 100) + (result.content.length > 100 ? "..." : "")) then parsed} - {@html parsed} - {/await} + {@render basicMarkup(result.content.slice(0, 100) + (result.content.length > 100 ? "..." : ""), ndk)} {/if} {:else} - {#await parseBasicmarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : "")) then parsed} - {@html parsed} - {/await} + {@render basicMarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : ""), ndk)} {/if} {/if} @@ -1011,17 +1006,13 @@
Comment:
- {#await parseBasicmarkup(result.content.slice(0, 100) + (result.content.length > 100 ? "..." : "")) then parsed} - {@html parsed} - {/await} + {@render basicMarkup(result.content.slice(0, 100) + (result.content.length > 100 ? "..." : ""), ndk)} {/if} {:else} - {#await parseBasicmarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : "")) then parsed} - {@html parsed} - {/await} + {@render basicMarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : ""), ndk)} {/if} {/if} @@ -1212,17 +1203,13 @@
Comment:
- {#await parseBasicmarkup(result.content.slice(0, 100) + (result.content.length > 100 ? "..." : "")) then parsed} - {@html parsed} - {/await} + {@render basicMarkup(result.content.slice(0, 100) + (result.content.length > 100 ? "..." : ""), ndk)} {/if} {:else} - {#await parseBasicmarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : "")) then parsed} - {@html parsed} - {/await} + {@render basicMarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : ""), ndk)} {/if} {/if} From 77781a350fcee6772fd6afdcc4129378e8988c22 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 23 Aug 2025 21:00:07 -0500 Subject: [PATCH 15/37] Add contextual note for AI to encourage use of `basicMarkup` snippet --- src/lib/snippets/MarkupSnippets.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/snippets/MarkupSnippets.svelte b/src/lib/snippets/MarkupSnippets.svelte index eb08603..ec1127d 100644 --- a/src/lib/snippets/MarkupSnippets.svelte +++ b/src/lib/snippets/MarkupSnippets.svelte @@ -5,6 +5,8 @@ export { basicMarkup }; + {#snippet basicMarkup(text: string, ndk?: NDK)} {#await parseBasicMarkup(text, ndk) then parsed} {@html parsed} From 19c874c2ed9809da8461ca786ea61b5a0d72322b Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 23 Aug 2025 21:09:59 -0500 Subject: [PATCH 16/37] Add NDK as parameter `getContext` doesn't work in snippets --- src/lib/components/EventDetails.svelte | 3 ++- src/lib/components/cards/BlogHeader.svelte | 7 +++++-- src/lib/components/cards/ProfileHeader.svelte | 2 ++ .../components/embedded_events/EmbeddedEvent.svelte | 1 + .../publications/PublicationHeader.svelte | 6 ++++-- src/lib/components/util/ArticleNav.svelte | 5 ++++- src/lib/components/util/CardActions.svelte | 8 +++++--- src/lib/components/util/Details.svelte | 13 ++++++++----- src/lib/snippets/UserSnippets.svelte | 9 ++++----- src/routes/about/+page.svelte | 6 +++++- src/routes/contact/+page.svelte | 1 + src/routes/events/+page.svelte | 3 +++ 12 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index c06123e..12d830c 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -285,6 +285,7 @@ >Author: {@render userBadge( toNpub(event.pubkey) as string, profile?.display_name || undefined, + ndk, )} {:else} @@ -336,7 +337,7 @@
Quote repost:
- {@render quotedContent(event, [], getNdkContext())} + {@render quotedContent(event, [], ndk)} {#if content}
diff --git a/src/lib/components/cards/BlogHeader.svelte b/src/lib/components/cards/BlogHeader.svelte index ff218fe..3df794a 100644 --- a/src/lib/components/cards/BlogHeader.svelte +++ b/src/lib/components/cards/BlogHeader.svelte @@ -9,7 +9,8 @@ import { getMatchingTags } from "$lib/utils/nostrUtils"; import LazyImage from "$components/util/LazyImage.svelte"; import { generateDarkPastelColor } from "$lib/utils/image_utils"; - + import { getNdkContext } from "$lib/ndk"; + const { rootId, event, @@ -22,6 +23,8 @@ active: boolean; }>(); + const ndk = getNdkContext(); + let title: string = $derived(event.getMatchingTags("title")[0]?.[1]); let author: string = $derived( getMatchingTags(event, "author")[0]?.[1] ?? "unknown", @@ -59,7 +62,7 @@
- {@render userBadge(authorPubkey, author)} + {@render userBadge(authorPubkey, author, ndk)} {publishedAt()}
diff --git a/src/lib/components/cards/ProfileHeader.svelte b/src/lib/components/cards/ProfileHeader.svelte index d58c13c..a1fc210 100644 --- a/src/lib/components/cards/ProfileHeader.svelte +++ b/src/lib/components/cards/ProfileHeader.svelte @@ -145,6 +145,7 @@ profile.display_name || profile.name || event.pubkey, + ndk, )}
{#if communityStatus === true} @@ -277,6 +278,7 @@ {@render userBadge( toNpub(event.pubkey) as string, profile?.displayName || profile.name || event.pubkey, + ndk, )}

{profile.lud16}

diff --git a/src/lib/components/embedded_events/EmbeddedEvent.svelte b/src/lib/components/embedded_events/EmbeddedEvent.svelte index 9b30919..7b0c9a6 100644 --- a/src/lib/components/embedded_events/EmbeddedEvent.svelte +++ b/src/lib/components/embedded_events/EmbeddedEvent.svelte @@ -261,6 +261,7 @@ {@render userBadge( toNpub(event.pubkey) as string, authorDisplayName, + ndk, )} {:else} diff --git a/src/lib/components/publications/PublicationHeader.svelte b/src/lib/components/publications/PublicationHeader.svelte index 5cab792..db296cd 100644 --- a/src/lib/components/publications/PublicationHeader.svelte +++ b/src/lib/components/publications/PublicationHeader.svelte @@ -1,7 +1,7 @@ -{#snippet userBadge(identifier: string, displayText: string | undefined)} +{#snippet userBadge(identifier: string, displayText: string | undefined, ndk?: NDK)} {@const npub = toNpub(identifier)} {#if npub} {#if !displayText || displayText.trim().toLowerCase() === "unknown"} - {#await getUserMetadata(npub, getNdkContext(), false) then profile} + {#await getUserMetadata(npub, ndk, false) then profile} {@const p = profile as UserProfile} - {@const debugInfo = console.log("Profile data for", npub, ":", p)} {/each}
diff --git a/src/lib/navigator/EventNetwork/index.svelte b/src/lib/navigator/EventNetwork/index.svelte index 98ad74a..aea8c8a 100644 --- a/src/lib/navigator/EventNetwork/index.svelte +++ b/src/lib/navigator/EventNetwork/index.svelte @@ -168,6 +168,7 @@ let totalPersonCount = $state(0); let displayedPersonCount = $state(0); let hasInitializedPersons = $state(false); + let hasInitializedTags = $state(false); // Update dimensions when container changes @@ -298,9 +299,22 @@ label: n.title, count: n.connectedNodes?.length || 0, color: getTagAnchorColor(n.tagType || ""), + value: `${n.tagType}-${n.title}`, // Use the correct tag ID format for toggling })); + + // Auto-disable all tag anchors by default (only on first time showing) + if (!hasInitializedTags && tagAnchors.length > 0) { + tagAnchorInfo.forEach(anchor => { + disabledTags.add(anchor.value); + }); + hasInitializedTags = true; + } } else { tagAnchorInfo = []; + // Reset initialization flag when tag anchors are hidden + if (hasInitializedTags && tagAnchorInfo.length === 0) { + hasInitializedTags = false; + } } // Add person nodes if enabled From 83e11192f0a640a2f58ed39ec3cf03345d173033 Mon Sep 17 00:00:00 2001 From: limina1 Date: Sun, 24 Aug 2025 09:40:40 -0400 Subject: [PATCH 32/37] fix tag ID consistency and auto-disable for all tag types - Use tag.value consistently instead of reconstructing tag.type-tag.label - Fix auto-disable to work for ALL tag types (hashtags, authors, event refs, titles, summaries) - Each tag type now maintains separate initialization state --- src/lib/navigator/EventNetwork/Legend.svelte | 7 +++---- src/lib/navigator/EventNetwork/index.svelte | 14 +++++++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/lib/navigator/EventNetwork/Legend.svelte b/src/lib/navigator/EventNetwork/Legend.svelte index 8dd5bb7..e955b90 100644 --- a/src/lib/navigator/EventNetwork/Legend.svelte +++ b/src/lib/navigator/EventNetwork/Legend.svelte @@ -344,12 +344,11 @@
{#each sortedAnchors as tag} - {@const tagId = `${tag.type}-${tag.label}`} - {@const isDisabled = disabledTags.has(tagId)} + {@const isDisabled = disabledTags.has(tag.value)} diff --git a/src/lib/components/util/Profile.svelte b/src/lib/components/util/Profile.svelte index b9ce5f7..a869095 100644 --- a/src/lib/components/util/Profile.svelte +++ b/src/lib/components/util/Profile.svelte @@ -502,7 +502,7 @@ > View profile + />View notifications From fb7419a508f7ae3f641dd42adf80832f1bc72ae0 Mon Sep 17 00:00:00 2001 From: silberengel Date: Mon, 25 Aug 2025 21:14:43 +0200 Subject: [PATCH 34/37] Fixed npub display on Asciidoc --- src/lib/components/publications/PublicationSection.svelte | 4 +++- src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts | 4 +++- src/lib/utils/markup/asciidoctorPostProcessor.ts | 4 +++- src/routes/my-notes/+page.svelte | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/lib/components/publications/PublicationSection.svelte b/src/lib/components/publications/PublicationSection.svelte index 2b9aace..1379e27 100644 --- a/src/lib/components/publications/PublicationSection.svelte +++ b/src/lib/components/publications/PublicationSection.svelte @@ -12,6 +12,7 @@ import type { TableOfContents as TocType } from "./table_of_contents.svelte"; import { postProcessAdvancedAsciidoctorHtml } from "$lib/utils/markup/advancedAsciidoctorPostProcessor"; import { parseAdvancedmarkup } from "$lib/utils/markup/advancedMarkupParser"; + import NDK from "@nostr-dev-kit/ndk"; let { address, @@ -30,6 +31,7 @@ } = $props(); const asciidoctor: Asciidoctor = getContext("asciidoctor"); + const ndk: NDK = getContext("ndk"); let leafEvent: Promise = $derived.by( async () => await publicationTree.getEvent(address), @@ -62,7 +64,7 @@ } else { // For 30041 and 30818 events, use Asciidoctor (AsciiDoc) const converted = asciidoctor.convert(content); - const processed = await postProcessAdvancedAsciidoctorHtml(converted.toString()); + const processed = await postProcessAdvancedAsciidoctorHtml(converted.toString(), ndk); return processed; } }); diff --git a/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts b/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts index 2cde13e..14b2344 100644 --- a/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts +++ b/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts @@ -1,3 +1,4 @@ +import NDK from "@nostr-dev-kit/ndk"; import { postProcessAsciidoctorHtml } from "./asciidoctorPostProcessor.ts"; import plantumlEncoder from "plantuml-encoder"; @@ -10,11 +11,12 @@ import plantumlEncoder from "plantuml-encoder"; */ export async function postProcessAdvancedAsciidoctorHtml( html: string, + ndk?: NDK, ): Promise { if (!html) return html; try { // First apply the basic post-processing (wikilinks, nostr addresses) - let processedHtml = await postProcessAsciidoctorHtml(html); + let processedHtml = await postProcessAsciidoctorHtml(html, ndk); // Unified math block processing processedHtml = fixAllMathBlocks(processedHtml); // Process PlantUML blocks diff --git a/src/lib/utils/markup/asciidoctorPostProcessor.ts b/src/lib/utils/markup/asciidoctorPostProcessor.ts index 090ae94..f90dc42 100644 --- a/src/lib/utils/markup/asciidoctorPostProcessor.ts +++ b/src/lib/utils/markup/asciidoctorPostProcessor.ts @@ -1,3 +1,4 @@ +import NDK from "@nostr-dev-kit/ndk"; import { processAsciiDocAnchors, processImageWithReveal, @@ -54,6 +55,7 @@ function fixStemBlocks(html: string): string { */ export async function postProcessAsciidoctorHtml( html: string, + ndk?: NDK, ): Promise { if (!html) return html; @@ -63,7 +65,7 @@ export async function postProcessAsciidoctorHtml( // Then process wikilinks in [[...]] format (if any remain) processedHtml = processWikilinks(processedHtml); // Then process nostr addresses (but not those already in links) - processedHtml = await processNostrIdentifiersInText(processedHtml); + processedHtml = await processNostrIdentifiersInText(processedHtml, ndk); processedHtml = fixStemBlocks(processedHtml); // Fix math blocks for MathJax // Process image blocks to add reveal/enlarge functionality processedHtml = processImageBlocks(processedHtml); diff --git a/src/routes/my-notes/+page.svelte b/src/routes/my-notes/+page.svelte index 9e7dfa4..c8d645e 100644 --- a/src/routes/my-notes/+page.svelte +++ b/src/routes/my-notes/+page.svelte @@ -65,6 +65,7 @@ }); renderedContent[event.id] = await postProcessAsciidoctorHtml( html as string, + ndk, ); } // Collect unique tags by type From aaaf3300f79ffd87748c3294b4eec2df500780e8 Mon Sep 17 00:00:00 2001 From: silberengel Date: Mon, 25 Aug 2025 21:45:50 +0200 Subject: [PATCH 35/37] sped up comment threads --- src/lib/components/CommentViewer.svelte | 202 +++++++++++------------- src/lib/components/EventDetails.svelte | 9 +- src/routes/events/+page.svelte | 6 + 3 files changed, 102 insertions(+), 115 deletions(-) diff --git a/src/lib/components/CommentViewer.svelte b/src/lib/components/CommentViewer.svelte index 900b64b..2c5dcde 100644 --- a/src/lib/components/CommentViewer.svelte +++ b/src/lib/components/CommentViewer.svelte @@ -24,6 +24,8 @@ let error = $state(null); let profiles = $state(new Map()); let activeSub: any = null; + let isFetching = $state(false); // Track if we're currently fetching to prevent duplicate fetches + let retryCount = $state(0); // Track retry attempts for failed fetches interface CommentNode { event: NDKEvent; @@ -64,9 +66,17 @@ async function fetchComments() { if (!event?.id) return; + // AI-NOTE: Prevent duplicate fetches for the same event + if (isFetching) { + console.log(`[CommentViewer] Already fetching comments, skipping`); + return; + } + + isFetching = true; loading = true; error = null; comments = []; + retryCount = 0; // Reset retry count for new event console.log(`[CommentViewer] Fetching comments for event: ${event.id}`); console.log(`[CommentViewer] Event kind: ${event.kind}`); @@ -98,67 +108,26 @@ console.log(`[CommentViewer] Event address for NIP-22: ${eventAddress}`); - // Use more targeted filters to reduce noise - const filters = [ - // Primary filter: events that explicitly reference our target via e-tags - { - kinds: [1, 1111, 9802], - "#e": [event.id], - limit: 50, - } - ]; - - // 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); + // AI-NOTE: Use a single comprehensive filter to ensure all comments are found + // Multiple filters can cause issues with NDK subscription handling + const filter = { + kinds: [1, 1111, 9802], + "#e": [event.id], + limit: 100, // Increased limit to ensure we get all comments + }; - // 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}`); + console.log(`[CommentViewer] Setting up subscription with filter:`, filter); + console.log(`[CommentViewer] Target event ID: ${event.id}`); + console.log(`[CommentViewer] Event address: ${eventAddress}`); - // Get all available relays for a more comprehensive search - // Use the full NDK pool relays instead of just active relays + // Use the full NDK pool relays for comprehensive search const ndkPoolRelays = Array.from(ndk.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 = ndk.subscribe(filters); - - // Also try a direct search for the specific comment we're looking for - console.log(`[CommentViewer] Also searching for specific comment: 64173a81c2a8e26342d4a75d3def804da8644377bde99cfdfeaf189dff87f942`); - const specificCommentSub = ndk.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}`); - }); + // Subscribe with single filter + activeSub = ndk.subscribe(filter); - specificCommentSub.on("eose", () => { - console.log(`[CommentViewer] Specific comment search EOSE`); - specificCommentSub.stop(); - }); + const timeout = setTimeout(() => { console.log(`[CommentViewer] Subscription timeout - no comments found`); @@ -167,6 +136,7 @@ activeSub = null; } loading = false; + isFetching = false; }, 10000); activeSub.on("event", (commentEvent: NDKEvent) => { @@ -175,12 +145,6 @@ 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 = ""; @@ -211,7 +175,9 @@ if (referencesTarget) { console.log(`[CommentViewer] Comment references target event via ${referenceMethod} - adding to comments`); - comments = [...comments, commentEvent]; + // AI-NOTE: Use immutable update to prevent UI flashing + const newComments = [...comments, commentEvent]; + comments = newComments; fetchProfile(commentEvent.pubkey); // Fetch nested replies for this comment @@ -234,6 +200,8 @@ activeSub = null; } loading = false; + isFetching = false; + retryCount = 0; // Reset retry count on successful fetch // Pre-fetch all profiles after comments are loaded preFetchAllProfiles(); @@ -243,9 +211,15 @@ fetchNestedReplies(comment.id); }); - // AI-NOTE: Test for comments if none were found - if (comments.length === 0) { - testForComments(); + // AI-NOTE: If no comments found and we haven't retried too many times, try again + if (comments.length === 0 && retryCount < 2) { + console.log(`[CommentViewer] No comments found, retrying... (attempt ${retryCount + 1})`); + retryCount++; + setTimeout(() => { + if (!isFetching) { + fetchComments(); + } + }, 2000); // Wait 2 seconds before retry } }); @@ -258,12 +232,14 @@ } error = "Error fetching comments"; loading = false; + isFetching = false; }); } catch (err) { console.error(`[CommentViewer] Error setting up subscription:`, err); error = "Error setting up subscription"; loading = false; + isFetching = false; } } @@ -285,51 +261,7 @@ console.log(`[CommentViewer] Pre-fetching complete`); } - // AI-NOTE: 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 = ndk.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}, 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", () => { - 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[] { @@ -433,15 +365,53 @@ // Derived value for threaded comments let threadedComments = $derived(buildCommentThread(comments)); - // Fetch comments when event changes + // AI-NOTE: Comment feed update issue when navigating via e-tags + // When clicking e-tags in EventDetails, the comment feed sometimes doesn't update properly + // This can manifest as: + // 1. Empty comment feed even when comments exist + // 2. Flash between nested and flat thread views + // 3. Delayed comment loading + // + // Potential causes: + // - Race condition between event prop change and comment fetching + // - Subscription cleanup timing issues + // - Nested reply fetching interfering with main comment display + // - Relay availability or timeout issues + // + // TODO: Consider adding a small delay before fetching comments to ensure + // the event prop has fully settled, or implement a more robust state + // management system for comment fetching $effect(() => { if (event?.id) { console.log(`[CommentViewer] Event changed, fetching comments for:`, event.id, `kind:`, event.kind); + + // AI-NOTE: Clean up previous subscription and reset state if (activeSub) { activeSub.stop(); activeSub = null; } - fetchComments(); + + // Reset state for new event + comments = []; + profiles = new Map(); + nestedReplyIds = new Set(); + isFetchingNestedReplies = false; + retryCount = 0; + + // AI-NOTE: Add small delay to prevent race conditions during navigation + setTimeout(() => { + if (event?.id && !isFetching) { // Double-check we're not already fetching + fetchComments(); + } + }, 100); + } else { + // Clear state when no event + comments = []; + profiles = new Map(); + nestedReplyIds = new Set(); + isFetchingNestedReplies = false; + isFetching = false; + retryCount = 0; } }); @@ -483,7 +453,9 @@ if (referencesTarget && !comments.some(c => c.id === nestedEvent.id)) { console.log(`[CommentViewer] Adding nested reply to comments`); - comments = [...comments, nestedEvent]; + // AI-NOTE: Use immutable update to prevent UI flashing + const newComments = [...comments, nestedEvent]; + comments = newComments; fetchProfile(nestedEvent.pubkey); // Recursively fetch replies to this nested reply @@ -524,7 +496,9 @@ if (referencesTarget && !comments.some(c => c.id === nip22Event.id)) { console.log(`[CommentViewer] Adding NIP-22 nested reply to comments`); - comments = [...comments, nip22Event]; + // AI-NOTE: Use immutable update to prevent UI flashing + const newComments = [...comments, nip22Event]; + comments = newComments; fetchProfile(nip22Event.pubkey); // Recursively fetch replies to this nested reply diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index f4530b2..a7040fe 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -457,7 +457,14 @@ const tTag = tagInfo.gotoValue!.substring(2); goto(`/events?t=${encodeURIComponent(tTag)}`); } else if (/^[0-9a-fA-F]{64}$/.test(tagInfo.gotoValue!)) { - // For hex event IDs - use navigateToEvent + // AI-NOTE: E-tag navigation may cause comment feed update issues + // When navigating to a new event via e-tag, the CommentViewer component + // may experience timing issues that result in: + // - Empty comment feeds even when comments exist + // - UI flashing between different thread views + // - Delayed comment loading + // This is likely due to race conditions between event prop changes + // and comment fetching in the CommentViewer component. navigateToEvent(tagInfo.gotoValue!); } else { // For other cases, try direct navigation diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index 72ba009..0183eb6 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -80,6 +80,12 @@ // Get NDK context during component initialization const ndk = getNdkContext(); + // AI-NOTE: Event navigation and comment feed update issue + // When navigating to events via e-tags, the CommentViewer component may experience + // timing issues that cause comment feed problems. This function is called when + // a new event is found, and it triggers the CommentViewer to update. + // The CommentViewer has been updated with better state management to handle + // these race conditions. function handleEventFound(newEvent: NDKEvent) { event = newEvent; showSidePanel = true; From 3972958a9149b54bf073a20a4a92042e6b2f3a24 Mon Sep 17 00:00:00 2001 From: silberengel Date: Mon, 25 Aug 2025 22:12:38 +0200 Subject: [PATCH 36/37] Sped up notifications and reduced relay calls. --- src/lib/components/Notifications.svelte | 275 +++++++++++++++++++++--- 1 file changed, 243 insertions(+), 32 deletions(-) diff --git a/src/lib/components/Notifications.svelte b/src/lib/components/Notifications.svelte index 5b987a0..37e210a 100644 --- a/src/lib/components/Notifications.svelte +++ b/src/lib/components/Notifications.svelte @@ -56,6 +56,16 @@ let authorProfiles = $state>(new Map()); let filteredByUser = $state(null); + // AI-NOTE: Client-side pagination - fetch once, paginate locally + let allToMeNotifications = $state([]); // All fetched "to-me" notifications + let allFromMeNotifications = $state([]); // All fetched "from-me" notifications + let allPublicMessages = $state([]); // All fetched public messages + let currentPage = $state(1); + let itemsPerPage = 20; // Show 20 items per page + let hasFetchedToMe = $state(false); // Track if we've already fetched "to-me" data + let hasFetchedFromMe = $state(false); // Track if we've already fetched "from-me" data + let hasFetchedPublic = $state(false); // Track if we've already fetched public messages + // New Message Modal state let showNewMessageModal = $state(false); let newMessageContent = $state(""); @@ -461,10 +471,52 @@ } } - // AI-NOTE: Simplified notification fetching + // AI-NOTE: Client-side pagination calculations + let paginatedNotifications = $derived.by(() => { + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const currentNotifications = notificationMode === "to-me" ? allToMeNotifications : allFromMeNotifications; + return currentNotifications.slice(startIndex, endIndex); + }); + + let paginatedPublicMessages = $derived.by(() => { + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + return allPublicMessages.slice(startIndex, endIndex); + }); + + let totalPages = $derived.by(() => { + let totalItems = 0; + if (notificationMode === "public-messages") { + totalItems = allPublicMessages.length; + } else if (notificationMode === "to-me") { + totalItems = allToMeNotifications.length; + } else { + totalItems = allFromMeNotifications.length; + } + return Math.ceil(totalItems / itemsPerPage); + }); + + let hasNextPage = $derived.by(() => currentPage < totalPages); + let hasPreviousPage = $derived.by(() => currentPage > 1); + + // AI-NOTE: Optimized notification fetching - fetch once, paginate locally async function fetchNotifications() { - if (!$userStore.pubkey || !isOwnProfile) return; + if (!$userStore.pubkey || !isOwnProfile || isFetching) return; + // Check if we've already fetched data for this specific mode + if (notificationMode === "to-me" && hasFetchedToMe && allToMeNotifications.length > 0) { + currentPage = 1; + notifications = paginatedNotifications; + return; + } + if (notificationMode === "from-me" && hasFetchedFromMe && allFromMeNotifications.length > 0) { + currentPage = 1; + notifications = paginatedNotifications; + return; + } + + isFetching = true; loading = true; error = null; @@ -483,7 +535,7 @@ ? { "#p": [$userStore.pubkey] } : { authors: [$userStore.pubkey] } ), - limit: 100, + limit: 500, // Fetch more data once to avoid multiple relay calls }; const ndkRelaySet = NDKRelaySetFromNDK.fromRelayUrls(relays, ndk); @@ -501,22 +553,41 @@ } }); - notifications = filteredEvents - .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) - .slice(0, 100); + const sortedEvents = filteredEvents + .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)); + + // Store in the appropriate array based on mode + if (notificationMode === "to-me") { + allToMeNotifications = sortedEvents; + hasFetchedToMe = true; + } else { + allFromMeNotifications = sortedEvents; + hasFetchedFromMe = true; + } - authorProfiles = await fetchAuthorProfiles(notifications, ndk); + // Set current page to 1 and update displayed notifications + currentPage = 1; + notifications = paginatedNotifications; + + // Load profiles in background + authorProfiles = await fetchAuthorProfiles(sortedEvents, ndk); } catch (err) { console.error("[Notifications] Error fetching notifications:", err); error = err instanceof Error ? err.message : "Failed to fetch notifications"; } finally { loading = false; + isFetching = false; } } - // AI-NOTE: Simplified public messages fetching - only kind 24 messages + // AI-NOTE: Optimized public messages fetching - fetch once, paginate locally async function fetchPublicMessages() { - if (!$userStore.pubkey || !isOwnProfile) return; + if (!$userStore.pubkey || !isOwnProfile || isFetching) return; + + // Only fetch if we haven't already fetched data for this mode + if (hasFetchedPublic && allPublicMessages.length > 0) { + return; + } loading = true; error = null; @@ -526,6 +597,9 @@ const userStoreValue = get(userStore); const user = userStoreValue.signedIn && userStoreValue.pubkey ? ndk.getUser({ pubkey: userStoreValue.pubkey }) : null; + + // AI-NOTE: Cache relay set to prevent excessive calls + console.log("[PublicMessages] Building relay set for public messages..."); const relaySet = await buildCompleteRelaySet(ndk, user); const relays = [...relaySet.inboxRelays, ...relaySet.outboxRelays]; if (relays.length === 0) throw new Error("No relays available"); @@ -534,8 +608,8 @@ // Fetch only kind 24 messages const [messagesEvents, userMessagesEvents] = await Promise.all([ - 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) + ndk.fetchEvents({ kinds: [24 as any], "#p": [$userStore.pubkey], limit: 500 }, undefined, ndkRelaySet), + ndk.fetchEvents({ kinds: [24 as any], authors: [$userStore.pubkey], limit: 500 }, undefined, ndkRelaySet) ]); const allMessages = [ @@ -543,24 +617,75 @@ ...Array.from(userMessagesEvents) ]; - // Deduplicate and filter + // Deduplicate and sort const uniqueMessages = allMessages.filter((event, index, self) => index === self.findIndex(e => e.id === event.id) ); - publicMessages = uniqueMessages - .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) - .slice(0, 200); + allPublicMessages = uniqueMessages + .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)); - authorProfiles = await fetchAuthorProfiles(publicMessages, ndk); + // Set current page to 1 and update displayed messages + currentPage = 1; + publicMessages = paginatedPublicMessages; + hasFetchedPublic = true; + + // Load profiles in background + authorProfiles = await fetchAuthorProfiles(allPublicMessages, ndk); } catch (err) { console.error("[PublicMessages] Error fetching public messages:", err); error = err instanceof Error ? err.message : "Failed to fetch public messages"; } finally { loading = false; + isFetching = false; + } + } + + // AI-NOTE: Pagination navigation functions + function nextPage() { + if (hasNextPage) { + currentPage++; + updateDisplayedItems(); } } + function previousPage() { + if (hasPreviousPage) { + currentPage--; + updateDisplayedItems(); + } + } + + function goToPage(page: number) { + if (page >= 1 && page <= totalPages) { + currentPage = page; + updateDisplayedItems(); + } + } + + // AI-NOTE: Update displayed items based on current page + function updateDisplayedItems() { + if (notificationMode === "public-messages") { + publicMessages = paginatedPublicMessages; + } else { + notifications = paginatedNotifications; + } + } + + // AI-NOTE: Reset pagination when mode changes + function resetPagination() { + currentPage = 1; + hasFetchedToMe = false; + hasFetchedFromMe = false; + hasFetchedPublic = false; + allToMeNotifications = []; + allFromMeNotifications = []; + allPublicMessages = []; + notifications = []; + publicMessages = []; + authorProfiles.clear(); + } + // Check if user is viewing their own profile $effect(() => { if ($userStore.signedIn && $userStore.pubkey && event.pubkey) { @@ -570,20 +695,62 @@ } }); - // Fetch notifications when viewing own profile or when mode changes + // AI-NOTE: Track previous state to prevent unnecessary refetches + let previousMode = $state<"to-me" | "from-me" | "public-messages" | null>(null); + let previousPubkey = $state(null); + let previousIsOwnProfile = $state(false); + let isFetching = $state(false); // Guard against concurrent fetches + + // Fetch notifications when viewing own profile or when mode changes - with guards $effect(() => { - if (isOwnProfile && $userStore.pubkey && $userStore.signedIn) { - if (notificationMode === "public-messages") { - fetchPublicMessages(); - } else { - fetchNotifications(); + const currentMode = notificationMode; + const currentPubkey = $userStore.pubkey; + const currentIsOwnProfile = isOwnProfile; + + // Only proceed if something actually changed and we're not already fetching + if (currentIsOwnProfile && currentPubkey && $userStore.signedIn && !isFetching) { + if (previousMode !== currentMode || previousPubkey !== currentPubkey || previousIsOwnProfile !== currentIsOwnProfile) { + console.log("[Notifications] Mode or user changed, fetching data..."); + + // Reset pagination when mode changes + if (currentMode === "public-messages" && !hasFetchedPublic) { + resetPagination(); + fetchPublicMessages(); + } else if (currentMode !== "public-messages" && + ((currentMode === "to-me" && !hasFetchedToMe) || + (currentMode === "from-me" && !hasFetchedFromMe))) { + resetPagination(); + fetchNotifications(); + } else { + // Mode changed but we have data - just update displayed items + currentPage = 1; + updateDisplayedItems(); + } + + // Update previous state + previousMode = currentMode; + previousPubkey = currentPubkey; + previousIsOwnProfile = currentIsOwnProfile; } - } else { + } else if ((previousIsOwnProfile || previousPubkey) && !currentIsOwnProfile) { // Clear notifications when user logs out or is not viewing own profile - notifications = []; - publicMessages = []; - authorProfiles.clear(); + console.log("[Notifications] User logged out, clearing data..."); + resetPagination(); + previousMode = null; + previousPubkey = null; + previousIsOwnProfile = false; + } + }); + + // AI-NOTE: Update displayed items when page changes - debounced + let pageUpdateTimeout: ReturnType | null = null; + $effect(() => { + if (pageUpdateTimeout) { + clearTimeout(pageUpdateTimeout); } + pageUpdateTimeout = setTimeout(() => { + updateDisplayedItems(); + }, 50); }); // AI-NOTE: Refactored to avoid blocking $effect with async operations @@ -860,9 +1027,31 @@ {/each}
- {#if filteredMessages.length > 100} -
- Showing 100 of {filteredMessages.length} messages {filteredByUser ? `(filtered)` : ''}. Scroll to see more. + + {#if totalPages > 1} +
+
+ Page {currentPage} of {totalPages} ({allPublicMessages.length} total messages) +
+
+ + + {currentPage} / {totalPages} + + +
{/if}
@@ -964,9 +1153,31 @@ {/each} - {#if notifications.length > 100} -
- Showing 100 of {notifications.length} notifications {notificationMode === "to-me" ? "received" : "sent"}. Scroll to see more. + + {#if totalPages > 1} +
+
+ Page {currentPage} of {totalPages} ({notificationMode === "to-me" ? allToMeNotifications.length : allFromMeNotifications.length} total notifications) +
+
+ + + {currentPage} / {totalPages} + + +
{/if}
From 2c545f148f54452f161c9eb272c1d57dc364d01a Mon Sep 17 00:00:00 2001 From: silberengel Date: Mon, 25 Aug 2025 22:23:28 +0200 Subject: [PATCH 37/37] Add missing 'q' tag to event tag list --- src/lib/components/EventDetails.svelte | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index a7040fe..c20d483 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -189,6 +189,31 @@ text: `t:${tag[1]}`, gotoValue: `t:${tag[1]}`, }; + } else if (tag[0] === "q" && tag.length > 1) { + // 'q' tags are quoted events - navigate to the quoted event + 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 { + text: `q:${tag[1]}`, + gotoValue: nevent, + }; + } catch (error) { + console.warn("Failed to encode nevent for q tag:", tag[1], error); + return { text: `q:${tag[1]}` }; + } + } else { + console.warn("Invalid event ID in q tag:", tag[1]); + return { text: `q:${tag[1]}` }; + } } return { text: `${tag[0]}:${tag[1]}` }; }