diff --git a/src/components/Content/index.tsx b/src/components/Content/index.tsx index d2b3070..ea4a32f 100644 --- a/src/components/Content/index.tsx +++ b/src/components/Content/index.tsx @@ -110,9 +110,11 @@ export default function Content({ } }) - // First pass: find which images appear in content (will be rendered in a single carousel) + // First pass: find which media appears in content (will be rendered in carousels or inline) const mediaInContent = new Set() const imagesInContent: TImetaInfo[] = [] + const videosInContent: TImetaInfo[] = [] + const audioInContent: TImetaInfo[] = [] nodes.forEach((node) => { if (node.type === 'image') { @@ -133,7 +135,20 @@ export default function Content({ } }) } else if (node.type === 'media') { - mediaInContent.add(cleanUrl(node.data)) + const cleanedUrl = cleanUrl(node.data) + mediaInContent.add(cleanedUrl) + const mediaInfo = mediaMap.get(cleanedUrl) + if (mediaInfo) { + if (isVideo(cleanedUrl) || mediaInfo.m?.startsWith('video/')) { + if (!videosInContent.find(v => v.url === cleanedUrl)) { + videosInContent.push(mediaInfo) + } + } else if (isAudio(cleanedUrl) || mediaInfo.m?.startsWith('audio/')) { + if (!audioInContent.find(a => a.url === cleanedUrl)) { + audioInContent.push(mediaInfo) + } + } + } } else if (node.type === 'url') { const cleanedUrl = cleanUrl(node.data) if (isImage(cleanedUrl)) { @@ -142,16 +157,34 @@ export default function Content({ if (!imagesInContent.find(img => img.url === cleanedUrl)) { imagesInContent.push(imageInfo) } + } else if (isVideo(cleanedUrl)) { + mediaInContent.add(cleanedUrl) + const videoInfo = mediaMap.get(cleanedUrl) || { url: cleanedUrl, pubkey: event?.pubkey, m: 'video/*' } + if (!videosInContent.find(v => v.url === cleanedUrl)) { + videosInContent.push(videoInfo) + } + } else if (isAudio(cleanedUrl)) { + mediaInContent.add(cleanedUrl) + const audioInfo = mediaMap.get(cleanedUrl) || { url: cleanedUrl, pubkey: event?.pubkey, m: 'audio/*' } + if (!audioInContent.find(a => a.url === cleanedUrl)) { + audioInContent.push(audioInfo) + } } else if (isMedia(cleanedUrl)) { mediaInContent.add(cleanedUrl) } } }) - // Filter carousel: only show IMAGES that DON'T appear in content + // Filter: only show media that DON'T appear in content (from tags) const carouselImages = extractedMedia.images.filter((img: TImetaInfo) => { return !mediaInContent.has(img.url) }) + const videosFromTags = extractedMedia.videos.filter((video: TImetaInfo) => { + return !mediaInContent.has(video.url) + }) + const audioFromTags = extractedMedia.audio.filter((audio: TImetaInfo) => { + return !mediaInContent.has(audio.url) + }) logger.debug('[Content] Parsed content:', { nodeCount: nodes.length, @@ -193,6 +226,26 @@ export default function Content({ /> )} + {/* Render videos from tags that don't appear in content */} + {videosFromTags.map((video) => ( + + ))} + + {/* Render audio from tags that don't appear in content */} + {audioFromTags.map((audio) => ( + + ))} + {nodes.map((node, index) => { if (node.type === 'text') { return renderRedirectText(node.data, index) diff --git a/src/components/Note/PictureNote.tsx b/src/components/Note/PictureNote.tsx index 7134624..8413ed5 100644 --- a/src/components/Note/PictureNote.tsx +++ b/src/components/Note/PictureNote.tsx @@ -1,29 +1,12 @@ import { Event } from 'nostr-tools' -import { useMemo } from 'react' -import { cleanUrl } from '@/lib/url' -import { useMediaExtraction } from '@/hooks/useMediaExtraction' import Content from '../Content' -import ImageGallery from '../ImageGallery' export default function PictureNote({ event, className }: { event: Event; className?: string }) { - const { images } = useMediaExtraction(event, event.content) - - // Extract cleaned URLs from content to avoid duplicate rendering - const contentUrls = useMemo(() => { - const content = event.content || '' - const urlMatches = content.match(/https?:\/\/[^\s]+/g) || [] - return new Set(urlMatches.map(url => cleanUrl(url))) - }, [event.content]) - - // Images that don't appear in content (from tags only) - const imagesFromTags = useMemo(() => { - return images.filter(img => !contentUrls.has(img.url)) - }, [images, contentUrls]) - + // Content component already handles all image rendering (from content and tags) + // with proper deduplication, so we don't need to add anything extra return (
- {imagesFromTags.length > 0 && }
) } diff --git a/src/components/Note/VideoNote.tsx b/src/components/Note/VideoNote.tsx index 255130e..f31e2d7 100644 --- a/src/components/Note/VideoNote.tsx +++ b/src/components/Note/VideoNote.tsx @@ -1,31 +1,12 @@ import { Event } from 'nostr-tools' -import { useMemo } from 'react' -import { cleanUrl } from '@/lib/url' -import { useMediaExtraction } from '@/hooks/useMediaExtraction' import Content from '../Content' -import MediaPlayer from '../MediaPlayer' export default function VideoNote({ event, className }: { event: Event; className?: string }) { - const { videos } = useMediaExtraction(event, event.content) - - // Extract cleaned URLs from content to avoid duplicate rendering - const contentUrls = useMemo(() => { - const content = event.content || '' - const urlMatches = content.match(/https?:\/\/[^\s]+/g) || [] - return new Set(urlMatches.map(url => cleanUrl(url))) - }, [event.content]) - - // Videos that don't appear in content (from tags only) - const videosFromTags = useMemo(() => { - return videos.filter(video => !contentUrls.has(video.url)) - }, [videos, contentUrls]) - + // Content component already handles all media rendering (from content and tags) + // with proper deduplication, so we don't need to add anything extra return (
- {videosFromTags.map((video) => ( - - ))}
) }