+ {getQuotedPreview()}
+ {getQuotedKindInfo()}
+
+ {:else}
+ Quoted event not found
+ {/if}
+ diff --git a/src/lib/components/content/EmbeddedEventBlurb.svelte b/src/lib/components/content/EmbeddedEventBlurb.svelte new file mode 100644 index 0000000..9953389 --- /dev/null +++ b/src/lib/components/content/EmbeddedEventBlurb.svelte @@ -0,0 +1,277 @@ + + +
+ + diff --git a/src/lib/components/content/MarkdownRenderer.svelte b/src/lib/components/content/MarkdownRenderer.svelte index f1e5c20..bffdb6f 100644 --- a/src/lib/components/content/MarkdownRenderer.svelte +++ b/src/lib/components/content/MarkdownRenderer.svelte @@ -16,7 +16,9 @@ import { getCachedMarkdown, cacheMarkdown } from '../../services/cache/markdown-cache.js'; import EmbeddedEvent from './EmbeddedEvent.svelte'; + import EmbeddedEventBlurb from './EmbeddedEventBlurb.svelte'; let mountingEmbeddedEvents = $state(false); // Guard for mounting + let mountingEmbeddedBlurbs = $state(false); // Guard for mounting blurbs interface Props { content: string; @@ -425,7 +427,7 @@ link.parsed.type === 'note' || link.parsed.type === 'nevent' || link.parsed.type === 'naddr' ); - // Replace event links with HTML div elements (for block-level display) + // Replace event links with HTML div elements for embedded blurbs (block-level display) // Process from end to start to preserve indices for (let i = eventLinks.length - 1; i >= 0; i--) { const link = eventLinks[i]; @@ -434,8 +436,8 @@ if (eventId && isValidNostrId(eventId)) { // Escape event ID to prevent XSS const escapedEventId = escapeHtml(eventId); - // Create a div element for embedded event cards (block-level) - const div = ``; + // Create a div element for embedded event blurbs (block-level, styled differently) + const div = ``; processed = processed.slice(0, link.start) + div + @@ -859,7 +861,65 @@ } } - // Mount EmbeddedEvent components after rendering + // Mount EmbeddedEventBlurb components after rendering (for nostr: links in content) + function mountEmbeddedBlurbs() { + if (!containerRef || mountingEmbeddedBlurbs) return; + + // Find all event blurb placeholders and mount EmbeddedEventBlurb components + const placeholders = containerRef.querySelectorAll('[data-nostr-event-blurb]:not([data-mounted])'); + + if (placeholders.length > 0) { + mountingEmbeddedBlurbs = true; + try { + // Validate event IDs before mounting to prevent invalid fetches + const validPlaceholders: Element[] = []; + placeholders.forEach((placeholder) => { + const eventId = placeholder.getAttribute('data-event-id'); + // Use strict validation to prevent invalid fetches + if (eventId && isValidNostrId(eventId)) { + validPlaceholders.push(placeholder); + } else if (eventId) { + // Invalid event ID - mark as mounted to prevent retries + placeholder.setAttribute('data-mounted', 'true'); + placeholder.textContent = ''; // Don't show invalid IDs + console.debug('Skipping invalid event ID in MarkdownRenderer:', eventId); + } + }); + + if (validPlaceholders.length > 0) { + console.debug(`Mounting ${validPlaceholders.length} EmbeddedEventBlurb components`); + + validPlaceholders.forEach((placeholder) => { + const eventId = placeholder.getAttribute('data-event-id'); + if (eventId) { + placeholder.setAttribute('data-mounted', 'true'); + + try { + // Clear and mount component + placeholder.innerHTML = ''; + // Mount EmbeddedEventBlurb component - it will decode and fetch the event + const instance = mountComponent(placeholder as HTMLElement, EmbeddedEventBlurb as any, { eventId }); + + if (!instance) { + console.warn('EmbeddedEventBlurb mount returned null', { eventId }); + // Fallback: show the event ID + placeholder.textContent = eventId.slice(0, 20) + '...'; + } + } catch (error) { + console.error('Error mounting EmbeddedEventBlurb:', error, { eventId }); + // Show fallback + placeholder.textContent = eventId.slice(0, 20) + '...'; + } + } + }); + } + } finally { + mountingEmbeddedBlurbs = false; + } + } + } + + // Mount EmbeddedEvent components after rendering (for other embedded events) function mountEmbeddedEvents() { if (!containerRef || mountingEmbeddedEvents) return; @@ -985,6 +1045,7 @@ }); mountProfileBadges(); + mountEmbeddedBlurbs(); mountEmbeddedEvents(); }, 150); @@ -1008,6 +1069,7 @@ } mutationDebounceTimeout = setTimeout(() => { mountProfileBadges(); + mountEmbeddedBlurbs(); mountEmbeddedEvents(); mutationDebounceTimeout = null; }, 300); // 300ms debounce diff --git a/src/lib/components/content/MediaAttachments.svelte b/src/lib/components/content/MediaAttachments.svelte index 01c4ed5..5c30c2e 100644 --- a/src/lib/components/content/MediaAttachments.svelte +++ b/src/lib/components/content/MediaAttachments.svelte @@ -4,9 +4,17 @@ interface Props { event: NostrEvent; forceRender?: boolean; // If true, always render media even if URL is in content (for media kinds) + onMediaClick?: (url: string, event: MouseEvent) => void; // Optional callback when media is clicked } - let { event, forceRender = false }: Props = $props(); + let { event, forceRender = false, onMediaClick }: Props = $props(); + + function handleMediaClick(e: MouseEvent, url: string) { + e.stopPropagation(); // Don't trigger parent click handlers + if (onMediaClick) { + onMediaClick(url, e); + } + } interface MediaItem { url: string; @@ -15,6 +23,7 @@ width?: number; height?: number; size?: number; + alt?: string; // Alt text for images source: 'image-tag' | 'imeta' | 'file-tag' | 'content'; } @@ -91,6 +100,7 @@ let mimeType: string | undefined; let width: number | undefined; let height: number | undefined; + let alt: string | undefined; for (let i = 1; i < tag.length; i++) { const item = tag[i]; @@ -102,6 +112,8 @@ width = parseInt(item.substring(2).trim(), 10); } else if (item.startsWith('y ')) { height = parseInt(item.substring(2).trim(), 10); + } else if (item.startsWith('alt ')) { + alt = item.substring(4).trim(); } } @@ -126,6 +138,7 @@ mimeType, width, height, + alt, source: 'imeta' }); seen.add(normalized); @@ -133,6 +146,13 @@ } } } + + // Also check for standalone alt tag (fallback if not in imeta) + const altTag = event.tags.find(t => t[0] === 'alt' && t[1]); + if (altTag && altTag[1] && media.length > 0 && !media[0].alt) { + // Apply alt text to first media item if it doesn't have one + media[0].alt = altTag[1]; + } // 3. file tags (NIP-94) for (const tag of event.tags) { @@ -208,13 +228,33 @@Loading preview...
@@ -1123,6 +1140,10 @@