From 8504dd88d3db690bd02d997794c61a19129bbcbc Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 3 Feb 2026 18:58:57 +0100 Subject: [PATCH] bug-fix media --- src/app.css | 11 ++- .../content/MarkdownRenderer.svelte | 65 ++++++++++++------ .../content/MediaAttachments.svelte | 68 ++++++++++++++++++- 3 files changed, 113 insertions(+), 31 deletions(-) diff --git a/src/app.css b/src/app.css index 3852975..1ab1bc6 100644 --- a/src/app.css +++ b/src/app.css @@ -143,15 +143,12 @@ img[src*="emoji" i] { filter: grayscale(100%) sepia(20%) hue-rotate(200deg) saturate(70%) brightness(0.9); } -/* Apply to all images in markdown content */ +/* Content images should be prominent - no grayscale filters */ .markdown-content img, +.markdown-content video, +.markdown-content audio, .anon-content img { - filter: grayscale(100%) sepia(15%) hue-rotate(200deg) saturate(60%) brightness(0.95); -} - -.dark .markdown-content img, -.dark .anon-content img { - filter: grayscale(100%) sepia(20%) hue-rotate(200deg) saturate(70%) brightness(0.9); + filter: none !important; } @media (prefers-reduced-motion: reduce) { diff --git a/src/lib/components/content/MarkdownRenderer.svelte b/src/lib/components/content/MarkdownRenderer.svelte index 4bef69c..da1aee9 100644 --- a/src/lib/components/content/MarkdownRenderer.svelte +++ b/src/lib/components/content/MarkdownRenderer.svelte @@ -52,50 +52,65 @@ .replace(/'/g, '''); } - // Convert plain image URLs to img tags - function convertImageUrls(text: string): string { - // Match image URLs (http/https URLs ending in image extensions) - // Pattern: http(s)://... followed by image extension, optionally with query params - // Don't match URLs that are already in markdown ![alt](url) or HTML tags + // Convert plain media URLs (images, videos, audio) to HTML tags + function convertMediaUrls(text: string): string { + // Match media URLs (http/https URLs ending in media extensions) const imageExtensions = /\.(png|jpg|jpeg|gif|webp|svg|bmp|ico)(\?[^\s<>"']*)?$/i; + const videoExtensions = /\.(mp4|webm|ogg|mov|avi|mkv)(\?[^\s<>"']*)?$/i; + const audioExtensions = /\.(mp3|wav|ogg|flac|aac|m4a)(\?[^\s<>"']*)?$/i; const urlPattern = /https?:\/\/[^\s<>"']+/g; let result = text; - const matches: Array<{ url: string; index: number; endIndex: number }> = []; + const matches: Array<{ url: string; index: number; endIndex: number; type: 'image' | 'video' | 'audio' }> = []; // Find all URLs let match; while ((match = urlPattern.exec(text)) !== null) { const url = match[0]; + const index = match.index; + const endIndex = index + url.length; + + // Check if this URL is already in markdown or HTML + const before = text.substring(Math.max(0, index - 10), index); + const after = text.substring(endIndex, Math.min(text.length, endIndex + 10)); + + // Skip if it's already in markdown or HTML tags + if (before.includes('![') || before.includes('') || after.startsWith('') || after.startsWith('')) { + continue; + } + + // Determine media type if (imageExtensions.test(url)) { - const index = match.index; - const endIndex = index + url.length; - - // Check if this URL is already in markdown or HTML - const before = text.substring(Math.max(0, index - 10), index); - const after = text.substring(endIndex, Math.min(text.length, endIndex + 10)); - - // Skip if it's in markdown image syntax ![alt](url) or already in tag - if (!before.includes('![') && !before.includes('')) { - matches.push({ url, index, endIndex }); - } + matches.push({ url, index, endIndex, type: 'image' }); + } else if (videoExtensions.test(url)) { + matches.push({ url, index, endIndex, type: 'video' }); + } else if (audioExtensions.test(url)) { + matches.push({ url, index, endIndex, type: 'audio' }); } } // Replace from end to start to preserve indices for (let i = matches.length - 1; i >= 0; i--) { - const { url, index, endIndex } = matches[i]; + const { url, index, endIndex, type } = matches[i]; const escapedUrl = escapeHtml(url); - result = result.substring(0, index) + `` + result.substring(endIndex); + + if (type === 'image') { + result = result.substring(0, index) + `` + result.substring(endIndex); + } else if (type === 'video') { + result = result.substring(0, index) + `` + result.substring(endIndex); + } else if (type === 'audio') { + result = result.substring(0, index) + `` + result.substring(endIndex); + } } return result; } - // Process content: replace nostr URIs with HTML span elements and convert image URLs + // Process content: replace nostr URIs with HTML span elements and convert media URLs function processContent(text: string): string { - // First, convert plain image URLs to img tags - let processed = convertImageUrls(text); + // First, convert plain media URLs (images, videos, audio) to HTML tags + let processed = convertMediaUrls(text); // Find all NIP-21 links (nostr:npub, nostr:nprofile, etc.) const links = findNIP21Links(processed); @@ -203,6 +218,8 @@ display: block; visibility: visible !important; opacity: 1 !important; + /* Content images should be prominent - no grayscale filters */ + filter: none !important; } :global(.markdown-content video) { @@ -210,11 +227,15 @@ height: auto; border-radius: 0.25rem; margin: 0.5rem 0; + /* Content videos should be prominent - no grayscale filters */ + filter: none !important; } :global(.markdown-content audio) { width: 100%; margin: 0.5rem 0; + /* Content audio should be prominent - no grayscale filters */ + filter: none !important; } :global(.markdown-content a) { diff --git a/src/lib/components/content/MediaAttachments.svelte b/src/lib/components/content/MediaAttachments.svelte index 9eee66d..51930fd 100644 --- a/src/lib/components/content/MediaAttachments.svelte +++ b/src/lib/components/content/MediaAttachments.svelte @@ -32,6 +32,39 @@ } } + // Check if a URL appears in the content (as plain URL or in markdown) + function isUrlInContent(url: string): boolean { + const normalized = normalizeUrl(url); + const content = event.content.toLowerCase(); + + // Check if URL appears as plain text (with or without protocol) + const urlWithoutProtocol = normalized.replace(/^https?:\/\//i, ''); + if (content.includes(normalized.toLowerCase()) || content.includes(urlWithoutProtocol.toLowerCase())) { + return true; + } + + // Check if URL appears in markdown image syntax ![alt](url) + const markdownImageRegex = /!\[.*?\]\((.*?)\)/gi; + let match; + while ((match = markdownImageRegex.exec(event.content)) !== null) { + const markdownUrl = normalizeUrl(match[1]); + if (markdownUrl === normalized) { + return true; + } + } + + // Check if URL appears in HTML img/video/audio tags + const htmlTagRegex = /<(img|video|audio)[^>]+src=["']([^"']+)["']/gi; + while ((match = htmlTagRegex.exec(event.content)) !== null) { + const htmlUrl = normalizeUrl(match[2]); + if (htmlUrl === normalized) { + return true; + } + } + + return false; + } + function extractMedia(): MediaItem[] { const media: MediaItem[] = []; const seen = new Set(); @@ -50,7 +83,7 @@ } } - // 2. imeta tags (NIP-92) + // 2. imeta tags (NIP-92) - only display if NOT already in content for (const tag of event.tags) { if (tag[0] === 'imeta') { let url: string | undefined; @@ -73,6 +106,11 @@ if (url) { const normalized = normalizeUrl(url); + // Skip if already displayed in content (imeta is just metadata reference) + if (isUrlInContent(url)) { + continue; + } + if (!seen.has(normalized)) { let type: 'image' | 'video' | 'audio' = 'image'; if (mimeType) { @@ -161,28 +199,52 @@ const url = entry.target.getAttribute('data-media-url'); if (url) { loadedMedia.add(url); + // Force reactivity update + loadedMedia = new Set(loadedMedia); observer?.unobserve(entry.target); } } }); }, { - rootMargin: '50px' // Start loading 50px before element is visible + rootMargin: '100px' // Start loading 100px before element is visible } ); + // Observe all existing placeholders + $effect(() => { + if (observer && containerRef) { + const placeholders = containerRef.querySelectorAll('[data-media-url]'); + placeholders.forEach((placeholder) => { + if (observer) { + observer.observe(placeholder); + } + }); + } + }); + return () => { observer?.disconnect(); observer = null; }; }); + let containerRef = $state(null); + // Action to set media ref and observe it function mediaRefAction(node: HTMLElement, url: string) { mediaRefs.set(url, node); // Observe the element when it's added if (observer) { observer.observe(node); + // Also check if it's already visible (in case IntersectionObserver hasn't fired yet) + const rect = node.getBoundingClientRect(); + const isVisible = rect.top < window.innerHeight + 100 && rect.bottom > -100; + if (isVisible) { + loadedMedia.add(url); + loadedMedia = new Set(loadedMedia); + observer.unobserve(node); + } } return { destroy() { @@ -203,6 +265,7 @@ } +
{#if coverImage}
{#if shouldLoad(coverImage.url)} @@ -312,6 +375,7 @@ {/each}
{/if} +