Browse Source

bug-fix media

master
Silberengel 1 month ago
parent
commit
8504dd88d3
  1. 11
      src/app.css
  2. 65
      src/lib/components/content/MarkdownRenderer.svelte
  3. 68
      src/lib/components/content/MediaAttachments.svelte

11
src/app.css

@ -143,15 +143,12 @@ img[src*="emoji" i] {
filter: grayscale(100%) sepia(20%) hue-rotate(200deg) saturate(70%) brightness(0.9); 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 img,
.markdown-content video,
.markdown-content audio,
.anon-content img { .anon-content img {
filter: grayscale(100%) sepia(15%) hue-rotate(200deg) saturate(60%) brightness(0.95); filter: none !important;
}
.dark .markdown-content img,
.dark .anon-content img {
filter: grayscale(100%) sepia(20%) hue-rotate(200deg) saturate(70%) brightness(0.9);
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {

65
src/lib/components/content/MarkdownRenderer.svelte

@ -52,50 +52,65 @@
.replace(/'/g, '''); .replace(/'/g, ''');
} }
// Convert plain image URLs to img tags // Convert plain media URLs (images, videos, audio) to HTML tags
function convertImageUrls(text: string): string { function convertMediaUrls(text: string): string {
// Match image URLs (http/https URLs ending in image extensions) // Match media URLs (http/https URLs ending in media 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 <img> tags
const imageExtensions = /\.(png|jpg|jpeg|gif|webp|svg|bmp|ico)(\?[^\s<>"']*)?$/i; 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; const urlPattern = /https?:\/\/[^\s<>"']+/g;
let result = text; 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 // Find all URLs
let match; let match;
while ((match = urlPattern.exec(text)) !== null) { while ((match = urlPattern.exec(text)) !== null) {
const url = match[0]; 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('<img') || before.includes('<video') || before.includes('<audio') ||
after.startsWith('](') || after.startsWith('</img>') || after.startsWith('</video>') || after.startsWith('</audio>')) {
continue;
}
// Determine media type
if (imageExtensions.test(url)) { if (imageExtensions.test(url)) {
const index = match.index; matches.push({ url, index, endIndex, type: 'image' });
const endIndex = index + url.length; } else if (videoExtensions.test(url)) {
matches.push({ url, index, endIndex, type: 'video' });
// Check if this URL is already in markdown or HTML } else if (audioExtensions.test(url)) {
const before = text.substring(Math.max(0, index - 10), index); matches.push({ url, index, endIndex, type: 'audio' });
const after = text.substring(endIndex, Math.min(text.length, endIndex + 10));
// Skip if it's in markdown image syntax ![alt](url) or already in <img> tag
if (!before.includes('![') && !before.includes('<img') && !after.startsWith('](') && !after.startsWith('</img>')) {
matches.push({ url, index, endIndex });
}
} }
} }
// Replace from end to start to preserve indices // Replace from end to start to preserve indices
for (let i = matches.length - 1; i >= 0; i--) { 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); const escapedUrl = escapeHtml(url);
result = result.substring(0, index) + `<img src="${escapedUrl}" alt="" loading="lazy" />` + result.substring(endIndex);
if (type === 'image') {
result = result.substring(0, index) + `<img src="${escapedUrl}" alt="" loading="lazy" />` + result.substring(endIndex);
} else if (type === 'video') {
result = result.substring(0, index) + `<video src="${escapedUrl}" controls preload="none" style="max-width: 100%; max-height: 500px;"></video>` + result.substring(endIndex);
} else if (type === 'audio') {
result = result.substring(0, index) + `<audio src="${escapedUrl}" controls preload="none" style="width: 100%;"></audio>` + result.substring(endIndex);
}
} }
return result; 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 { function processContent(text: string): string {
// First, convert plain image URLs to img tags // First, convert plain media URLs (images, videos, audio) to HTML tags
let processed = convertImageUrls(text); let processed = convertMediaUrls(text);
// Find all NIP-21 links (nostr:npub, nostr:nprofile, etc.) // Find all NIP-21 links (nostr:npub, nostr:nprofile, etc.)
const links = findNIP21Links(processed); const links = findNIP21Links(processed);
@ -203,6 +218,8 @@
display: block; display: block;
visibility: visible !important; visibility: visible !important;
opacity: 1 !important; opacity: 1 !important;
/* Content images should be prominent - no grayscale filters */
filter: none !important;
} }
:global(.markdown-content video) { :global(.markdown-content video) {
@ -210,11 +227,15 @@
height: auto; height: auto;
border-radius: 0.25rem; border-radius: 0.25rem;
margin: 0.5rem 0; margin: 0.5rem 0;
/* Content videos should be prominent - no grayscale filters */
filter: none !important;
} }
:global(.markdown-content audio) { :global(.markdown-content audio) {
width: 100%; width: 100%;
margin: 0.5rem 0; margin: 0.5rem 0;
/* Content audio should be prominent - no grayscale filters */
filter: none !important;
} }
:global(.markdown-content a) { :global(.markdown-content a) {

68
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[] { function extractMedia(): MediaItem[] {
const media: MediaItem[] = []; const media: MediaItem[] = [];
const seen = new Set<string>(); const seen = new Set<string>();
@ -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) { for (const tag of event.tags) {
if (tag[0] === 'imeta') { if (tag[0] === 'imeta') {
let url: string | undefined; let url: string | undefined;
@ -73,6 +106,11 @@
if (url) { if (url) {
const normalized = normalizeUrl(url); const normalized = normalizeUrl(url);
// Skip if already displayed in content (imeta is just metadata reference)
if (isUrlInContent(url)) {
continue;
}
if (!seen.has(normalized)) { if (!seen.has(normalized)) {
let type: 'image' | 'video' | 'audio' = 'image'; let type: 'image' | 'video' | 'audio' = 'image';
if (mimeType) { if (mimeType) {
@ -161,28 +199,52 @@
const url = entry.target.getAttribute('data-media-url'); const url = entry.target.getAttribute('data-media-url');
if (url) { if (url) {
loadedMedia.add(url); loadedMedia.add(url);
// Force reactivity update
loadedMedia = new Set(loadedMedia);
observer?.unobserve(entry.target); 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 () => { return () => {
observer?.disconnect(); observer?.disconnect();
observer = null; observer = null;
}; };
}); });
let containerRef = $state<HTMLElement | null>(null);
// Action to set media ref and observe it // Action to set media ref and observe it
function mediaRefAction(node: HTMLElement, url: string) { function mediaRefAction(node: HTMLElement, url: string) {
mediaRefs.set(url, node); mediaRefs.set(url, node);
// Observe the element when it's added // Observe the element when it's added
if (observer) { if (observer) {
observer.observe(node); 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 { return {
destroy() { destroy() {
@ -203,6 +265,7 @@
} }
</script> </script>
<div bind:this={containerRef}>
{#if coverImage} {#if coverImage}
<div class="cover-image mb-4"> <div class="cover-image mb-4">
{#if shouldLoad(coverImage.url)} {#if shouldLoad(coverImage.url)}
@ -312,6 +375,7 @@
{/each} {/each}
</div> </div>
{/if} {/if}
</div>
<style> <style>
.cover-image { .cover-image {

Loading…
Cancel
Save