From 434dd7de5e935e9ea01ef5babf6275ab4ee3e566 Mon Sep 17 00:00:00 2001 From: silberengel Date: Sat, 23 Aug 2025 19:45:11 +0200 Subject: [PATCH] display embedded events as hyperlinks --- src/lib/components/CommentViewer.svelte | 9 +- src/lib/components/EventDetails.svelte | 23 ++++- src/lib/components/Notifications.svelte | 13 ++- src/lib/utils/markup/basicMarkupParser.ts | 2 + src/lib/utils/markup/markupServices.ts | 106 ++++++++++++++++++++++ src/routes/events/+page.svelte | 22 ++--- 6 files changed, 154 insertions(+), 21 deletions(-) diff --git a/src/lib/components/CommentViewer.svelte b/src/lib/components/CommentViewer.svelte index 428c18d..a0f55ce 100644 --- a/src/lib/components/CommentViewer.svelte +++ b/src/lib/components/CommentViewer.svelte @@ -6,6 +6,7 @@ import { goto } from "$app/navigation"; import { onMount } from "svelte"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; + import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; const { event } = $props<{ event: NDKEvent }>(); @@ -773,7 +774,9 @@ Comment:
- {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}