Browse Source

standardized renderig of videos, images, and audiofiles

imwald
Silberengel 4 months ago
parent
commit
b7f69ff3f0
  1. 59
      src/components/Content/index.tsx
  2. 21
      src/components/Note/PictureNote.tsx
  3. 23
      src/components/Note/VideoNote.tsx

59
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<string>() const mediaInContent = new Set<string>()
const imagesInContent: TImetaInfo[] = [] const imagesInContent: TImetaInfo[] = []
const videosInContent: TImetaInfo[] = []
const audioInContent: TImetaInfo[] = []
nodes.forEach((node) => { nodes.forEach((node) => {
if (node.type === 'image') { if (node.type === 'image') {
@ -133,7 +135,20 @@ export default function Content({
} }
}) })
} else if (node.type === 'media') { } 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') { } else if (node.type === 'url') {
const cleanedUrl = cleanUrl(node.data) const cleanedUrl = cleanUrl(node.data)
if (isImage(cleanedUrl)) { if (isImage(cleanedUrl)) {
@ -142,16 +157,34 @@ export default function Content({
if (!imagesInContent.find(img => img.url === cleanedUrl)) { if (!imagesInContent.find(img => img.url === cleanedUrl)) {
imagesInContent.push(imageInfo) 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)) { } else if (isMedia(cleanedUrl)) {
mediaInContent.add(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) => { const carouselImages = extractedMedia.images.filter((img: TImetaInfo) => {
return !mediaInContent.has(img.url) 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:', { logger.debug('[Content] Parsed content:', {
nodeCount: nodes.length, nodeCount: nodes.length,
@ -193,6 +226,26 @@ export default function Content({
/> />
)} )}
{/* Render videos from tags that don't appear in content */}
{videosFromTags.map((video) => (
<MediaPlayer
key={`tag-video-${video.url}`}
src={video.url}
className="mt-2"
mustLoad={mustLoadMedia}
/>
))}
{/* Render audio from tags that don't appear in content */}
{audioFromTags.map((audio) => (
<MediaPlayer
key={`tag-audio-${audio.url}`}
src={audio.url}
className="mt-2"
mustLoad={mustLoadMedia}
/>
))}
{nodes.map((node, index) => { {nodes.map((node, index) => {
if (node.type === 'text') { if (node.type === 'text') {
return renderRedirectText(node.data, index) return renderRedirectText(node.data, index)

21
src/components/Note/PictureNote.tsx

@ -1,29 +1,12 @@
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import { cleanUrl } from '@/lib/url'
import { useMediaExtraction } from '@/hooks/useMediaExtraction'
import Content from '../Content' import Content from '../Content'
import ImageGallery from '../ImageGallery'
export default function PictureNote({ event, className }: { event: Event; className?: string }) { export default function PictureNote({ event, className }: { event: Event; className?: string }) {
const { images } = useMediaExtraction(event, event.content) // Content component already handles all image rendering (from content and tags)
// with proper deduplication, so we don't need to add anything extra
// 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])
return ( return (
<div className={className}> <div className={className}>
<Content event={event} /> <Content event={event} />
{imagesFromTags.length > 0 && <ImageGallery images={imagesFromTags} className="mt-2" />}
</div> </div>
) )
} }

23
src/components/Note/VideoNote.tsx

@ -1,31 +1,12 @@
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import { cleanUrl } from '@/lib/url'
import { useMediaExtraction } from '@/hooks/useMediaExtraction'
import Content from '../Content' import Content from '../Content'
import MediaPlayer from '../MediaPlayer'
export default function VideoNote({ event, className }: { event: Event; className?: string }) { export default function VideoNote({ event, className }: { event: Event; className?: string }) {
const { videos } = useMediaExtraction(event, event.content) // Content component already handles all media rendering (from content and tags)
// with proper deduplication, so we don't need to add anything extra
// 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])
return ( return (
<div className={className}> <div className={className}>
<Content event={event} /> <Content event={event} />
{videosFromTags.map((video) => (
<MediaPlayer src={video.url} key={video.url} className="mt-2" mustLoad={false} />
))}
</div> </div>
) )
} }

Loading…
Cancel
Save