Browse Source

display embedded events as hyperlinks

master
silberengel 7 months ago
parent
commit
434dd7de5e
  1. 9
      src/lib/components/CommentViewer.svelte
  2. 23
      src/lib/components/EventDetails.svelte
  3. 13
      src/lib/components/Notifications.svelte
  4. 2
      src/lib/utils/markup/basicMarkupParser.ts
  5. 106
      src/lib/utils/markup/markupServices.ts
  6. 22
      src/routes/events/+page.svelte

9
src/lib/components/CommentViewer.svelte

@ -6,6 +6,7 @@ @@ -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 @@ @@ -773,7 +774,9 @@
<span class="font-medium">Comment:</span>
</div>
<div class="text-sm text-gray-700 dark:text-gray-300">
{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}
</div>
</div>
{:else}
@ -815,7 +818,9 @@ @@ -815,7 +818,9 @@
{:else}
<!-- Regular comment content -->
<div class="text-sm text-gray-700 dark:text-gray-300">
{node.event.content || "No content"}
{#await parseBasicmarkup(node.event.content || "No content") then parsed}
{@html parsed}
{/await}
</div>
{/if}
</div>

23
src/lib/components/EventDetails.svelte

@ -13,9 +13,10 @@ @@ -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 @@ @@ -28,6 +29,7 @@
let authorDisplayName = $state<string | undefined>(undefined);
let showFullContent = $state(false);
let shouldTruncate = $derived(event.content.length > 250 && !showFullContent);
let parsedContent = $state<string>("");
function getEventTitle(event: NDKEvent): string {
// First try to get title from title tag
@ -242,6 +244,19 @@ @@ -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 @@ @@ -310,7 +325,7 @@
<span class="text-gray-700 dark:text-gray-300 font-semibold">Content:</span>
<div class="prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100 break-words overflow-wrap-anywhere min-w-0">
<div class={shouldTruncate ? 'max-h-32 overflow-hidden' : ''}>
{event.content}
{@html parsedContent}
</div>
{#if shouldTruncate}
<button

13
src/lib/components/Notifications.svelte

@ -21,6 +21,7 @@ @@ -21,6 +21,7 @@
import { buildCompleteRelaySet } from "$lib/utils/relay_management";
import { formatDate, neventEncode } from "$lib/utils";
import { NDKRelaySetFromNDK } from "$lib/utils/nostrUtils";
import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser";
import { getNdkContext } from "$lib/ndk";
@ -818,7 +819,9 @@ @@ -818,7 +819,9 @@
<div class="text-sm text-gray-800 dark:text-gray-200 mb-2 leading-relaxed">
<div class="px-2">
<div class="text-sm text-gray-700 dark:text-gray-300">
{message.content || "No content"}
{#await parseBasicmarkup(message.content || "No content") then parsed}
{@html parsed}
{/await}
</div>
</div>
</div>
@ -897,7 +900,9 @@ @@ -897,7 +900,9 @@
<div class="text-sm text-gray-800 dark:text-gray-200 mb-2 leading-relaxed">
<div class="px-2">
<div class="text-sm text-gray-700 dark:text-gray-300">
{notification.content || "No content"}
{#await parseBasicmarkup(notification.content || "No content") then parsed}
{@html parsed}
{/await}
</div>
</div>
</div>
@ -934,7 +939,9 @@ @@ -934,7 +939,9 @@
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Replying to:</div>
<div class="text-sm text-gray-800 dark:text-gray-200">
<div class="text-sm text-gray-700 dark:text-gray-300">
{replyToMessage.content || "No content"}
{#await parseBasicmarkup(replyToMessage.content || "No content") then parsed}
{@html parsed}
{/await}
</div>
</div>
</div>

2
src/lib/utils/markup/basicMarkupParser.ts

@ -7,6 +7,7 @@ import { @@ -7,6 +7,7 @@ import {
processImageWithReveal,
processMediaUrl,
processNostrIdentifiersInText,
processAllNostrIdentifiers,
processWebSocketUrls,
processWikilinks,
stripTrackingParams,
@ -251,6 +252,7 @@ export async function parseBasicmarkup(text: string): Promise<string> { @@ -251,6 +252,7 @@ export async function parseBasicmarkup(text: string): Promise<string> {
// Process Nostr identifiers last
processedText = await processNostrIdentifiersInText(processedText);
processedText = processAllNostrIdentifiers(processedText);
// Replace wikilinks
processedText = processWikilinks(processedText);

106
src/lib/utils/markup/markupServices.ts

@ -180,6 +180,112 @@ export function processNostrIdentifiersWithEmbeddedEvents( @@ -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 = /(?<!https?:\/\/[^\s]*)(?<!wss?:\/\/[^\s]*)(?<!nostr:)(npub1|note1|nevent1|naddr1)[a-zA-Z0-9]{20,}/g;
// Process prefixed nostr identifiers first
const prefixedMatches = Array.from(processedText.matchAll(prefixedNostrPattern));
// Process them in reverse order to avoid index shifting issues
for (let i = prefixedMatches.length - 1; i >= 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 = `<a href="/events?id=${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="${fullMatch}">${displayText}</a>`;
// 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 = `<a href="/events?id=nostr:${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="nostr:${fullMatch}">${displayText}</a>`;
// 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 = `<a href="/events?id=${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="${fullMatch}">${displayText}</a>`;
// Replace the match in the text
processedText = processedText.slice(0, matchIndex) + replacement +
processedText.slice(matchIndex + fullMatch.length);
}
// Handle truncated bare identifiers
const truncatedBarePattern = /(?<!https?:\/\/[^\s]*)(?<!wss?:\/\/[^\s]*)(?<!nostr:)(npub1|note1|nevent1|naddr1)[a-zA-Z0-9]{8,}/g;
const truncatedBareMatches = Array.from(processedText.matchAll(truncatedBarePattern));
for (let i = truncatedBareMatches.length - 1; i >= 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 = `<a href="/events?id=nostr:${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="nostr:${fullMatch}">${displayText}</a>`;
// 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
*/

22
src/routes/events/+page.svelte

@ -21,6 +21,7 @@ @@ -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 @@ @@ -751,10 +752,9 @@
<div
class="text-sm text-gray-800 dark:text-gray-200 mt-1 line-clamp-2 break-words"
>
{result.content.slice(0, 200)}
{#if result.content.length > 200}
<span class="text-gray-500 dark:text-gray-400">...</span>
{/if}
{#await parseBasicmarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : "")) then parsed}
{@html parsed}
{/await}
</div>
{/if}
{/if}
@ -938,10 +938,9 @@ @@ -938,10 +938,9 @@
<div
class="text-sm text-gray-800 dark:text-gray-200 mt-1 line-clamp-2 break-words"
>
{result.content.slice(0, 200)}
{#if result.content.length > 200}
<span class="text-gray-500 dark:text-gray-400">...</span>
{/if}
{#await parseBasicmarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : "")) then parsed}
{@html parsed}
{/await}
</div>
{/if}
{/if}
@ -1111,10 +1110,9 @@ @@ -1111,10 +1110,9 @@
<div
class="text-sm text-gray-800 dark:text-gray-200 mt-1 line-clamp-2 break-words"
>
{result.content.slice(0, 200)}
{#if result.content.length > 200}
<span class="text-gray-500 dark:text-gray-400">...</span>
{/if}
{#await parseBasicmarkup(result.content.slice(0, 200) + (result.content.length > 200 ? "..." : "")) then parsed}
{@html parsed}
{/await}
</div>
{/if}
{/if}

Loading…
Cancel
Save