diff --git a/src/components/Content/index.tsx b/src/components/Content/index.tsx index dd56e66..1c994ce 100644 --- a/src/components/Content/index.tsx +++ b/src/components/Content/index.tsx @@ -9,6 +9,7 @@ import { EmbeddedWebsocketUrlParser, parseContent } from '@/lib/content-parser' +import logger from '@/lib/logger' import { getImetaInfosFromEvent } from '@/lib/event' import { getEmojiInfosFromEmojiTags, getImetaInfoFromImetaTag } from '@/lib/tag' import { cn } from '@/lib/utils' @@ -119,6 +120,7 @@ export default function Content({ } let imageIndex = 0 + logger.debug('[Content] Parsed content:', { nodeCount: nodes.length, allImages: allImages.length, nodes: nodes.map(n => ({ type: n.type, data: Array.isArray(n.data) ? n.data.length : n.data })) }) return (
{nodes.map((node, index) => { @@ -129,6 +131,7 @@ export default function Content({ const start = imageIndex const end = imageIndex + (Array.isArray(node.data) ? node.data.length : 1) imageIndex = end + logger.debug('[Content] Creating ImageGallery:', { nodeType: node.type, start, end, totalImages: allImages.length, nodeData: Array.isArray(node.data) ? node.data.length : node.data }) return ( { event.stopPropagation() event.preventDefault() - setIndex(start + current) + const newIndex = start + current + logger.debug('[ImageGallery] Click:', { start, current, newIndex, totalImages: images.length, displayImages: displayImages.length }) + setIndex(newIndex) } const displayImages = images.slice(start, end) @@ -62,7 +65,7 @@ export default function ImageGallery({ imageContent = ( +
{displayImages.map((image, i) => ( +
{displayImages.map((image, i) => ( +
{imageContent} {index >= 0 && createPortal(
e.stopPropagation()}> ({ - src: url, - alt: alt || url - }))} + slides={(() => { + const slides = images.map(({ url, alt }) => ({ + src: url, + alt: alt || url + })) + logger.debug('[ImageGallery] Lightbox slides:', { index, slidesCount: slides.length, slides }) + return slides + })()} plugins={[Zoom]} open={index >= 0} close={() => setIndex(-1)} @@ -121,6 +128,9 @@ export default function ImageGallery({ styles={{ toolbar: { paddingTop: '2.25rem' } }} + carousel={{ + finite: false + }} />
, document.body diff --git a/src/components/Note/LongFormArticle/index.tsx b/src/components/Note/LongFormArticle/index.tsx index 67ffe2e..2738620 100644 --- a/src/components/Note/LongFormArticle/index.tsx +++ b/src/components/Note/LongFormArticle/index.tsx @@ -78,9 +78,9 @@ export default function LongFormArticle({ img: (props) => ( ) @@ -101,7 +101,7 @@ export default function LongFormArticle({ {metadata.image && ( )} )} @@ -67,7 +67,7 @@ export default function LongFormArticlePreview({ {metadata.image && autoLoadMedia && ( )} diff --git a/src/components/UniversalContent/SimpleContent.tsx b/src/components/UniversalContent/SimpleContent.tsx index 501453b..cf33c75 100644 --- a/src/components/UniversalContent/SimpleContent.tsx +++ b/src/components/UniversalContent/SimpleContent.tsx @@ -1,13 +1,14 @@ import { useMemo } from 'react' import { cleanUrl } from '@/lib/url' import { getImetaInfosFromEvent } from '@/lib/event' +import logger from '@/lib/logger' import { Event } from 'nostr-tools' import Markdown from 'react-markdown' import remarkGfm from 'remark-gfm' import { remarkNostr } from '../Note/LongFormArticle/remarkNostr' import NostrNode from '../Note/LongFormArticle/NostrNode' import { cn } from '@/lib/utils' -import ImageWithLightbox from '../ImageWithLightbox' +import ImageGallery from '../ImageGallery' import MediaPlayer from '../MediaPlayer' interface SimpleContentProps { @@ -75,107 +76,139 @@ export default function SimpleContent({ const markdownLines: string[] = [] let key = 0 + // Extract all image URLs from content + const imageUrls: string[] = [] lines.forEach((line) => { - // Check if line contains an image URL const imageMatch = line.match(/(https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|heic|svg))/i) - if (imageMatch) { - const imageUrl = imageMatch[1] - const imageInfo = imetaInfos.find((info) => info.url === imageUrl) - const imageData = imageInfo || { url: imageUrl, pubkey: event?.pubkey } - - elements.push( -
- -
- ) - - // Add the rest of the line as text if there's anything else - const beforeImage = line.substring(0, imageMatch.index).trim() - const afterImage = line.substring(imageMatch.index! + imageUrl.length).trim() - - if (beforeImage || afterImage) { - markdownLines.push(beforeImage + afterImage) + imageUrls.push(imageMatch[1]) + } + }) + + // Extract all video URLs from content + const videoUrls: string[] = [] + lines.forEach((line) => { + const videoMatch = line.match(/(https?:\/\/[^\s]+\.(mp4|webm|ogg|avi|mov|wmv|flv|mkv|m4v))/i) + if (videoMatch) { + videoUrls.push(videoMatch[1]) + } + }) + + // Get all unique images - prioritize imeta tags, then add content images that aren't in imeta + const allImageInfos = [...imetaInfos] // Start with imeta images + const processedUrls = new Set(imetaInfos.map(info => info.url)) + + // Add content images that aren't already in imeta + imageUrls.forEach(url => { + if (!processedUrls.has(url)) { + allImageInfos.push({ url: url, pubkey: event?.pubkey }) + processedUrls.add(url) + } + }) + + // Get all unique videos - prioritize imeta tags, then add content videos that aren't in imeta + const allVideoInfos = imetaInfos.filter(info => { + // Check if the imeta info is a video by looking at the URL extension + const url = info.url + const extension = url.split('.').pop()?.toLowerCase() + return extension && ['mp4', 'webm', 'ogg', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'm4v'].includes(extension) + }) + + const processedVideoUrls = new Set(allVideoInfos.map(info => { + try { + return new URL(cleanUrl(info.url)).href + } catch { + return cleanUrl(info.url) + } + })) + + // Add content videos that aren't already in imeta + videoUrls.forEach(url => { + const cleanedUrl = (() => { + try { + return cleanUrl(url) + } catch { + return url } - } else { - // Check if line contains a video URL - const videoMatch = line.match(/(https?:\/\/[^\s]+\.(mp4|webm|ogg|avi|mov|wmv|flv|mkv|m4v))/i) - - if (videoMatch) { - const originalVideoUrl = videoMatch[1] - // Clean the video URL to remove tracking parameters - const cleanedVideoUrl = (() => { - try { - return cleanUrl(originalVideoUrl) - } catch { - return originalVideoUrl - } - })() - - // Check if this video URL is already handled by imeta tags - const normalizedVideoUrl = (() => { - try { - return new URL(cleanedVideoUrl).href - } catch { - return cleanedVideoUrl - } - })() - - if (!imetaVideoUrls.includes(normalizedVideoUrl)) { - elements.push( -
- -
- ) - } - - // Add the rest of the line as text if there's anything else - const beforeVideo = line.substring(0, videoMatch.index).trim() - const afterVideo = line.substring(videoMatch.index! + originalVideoUrl.length).trim() - - if (beforeVideo || afterVideo) { - markdownLines.push(beforeVideo + afterVideo) - } - } else { - // Regular text line - add to markdown processing - markdownLines.push(line) + })() + + const normalizedUrl = (() => { + try { + return new URL(cleanedUrl).href + } catch { + return cleanedUrl } + })() + + if (!processedVideoUrls.has(normalizedUrl)) { + allVideoInfos.push({ url: url, pubkey: event?.pubkey }) + processedVideoUrls.add(normalizedUrl) } }) - // Add imeta videos to the elements - imetaInfos - .filter(info => { - // Check if the imeta info is a video by looking at the URL extension - const url = info.url - const extension = url.split('.').pop()?.toLowerCase() + logger.debug('[SimpleContent] Processing content:', { + totalLines: lines.length, + imetaImages: imetaInfos.length, + contentImages: imageUrls.length, + totalUniqueImages: allImageInfos.length, + imetaVideos: imetaInfos.filter(info => { + const extension = info.url.split('.').pop()?.toLowerCase() return extension && ['mp4', 'webm', 'ogg', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'm4v'].includes(extension) + }).length, + contentVideos: videoUrls.length, + totalUniqueVideos: allVideoInfos.length + }) + + // If we have images, create a single ImageGallery for all of them + if (allImageInfos.length > 0) { + logger.debug('[SimpleContent] Creating ImageGallery with all unique images:', { + count: allImageInfos.length, + urls: allImageInfos.map(i => i.url) }) - .forEach(videoInfo => { - // Clean the imeta video URL to remove tracking parameters - const cleanedVideoUrl = (() => { - try { - return cleanUrl(videoInfo.url) - } catch { - return videoInfo.url - } - })() - - elements.push( -
- -
- ) - }) + + elements.push( +
+ +
+ ) + } + + // Add all unique videos to elements + allVideoInfos.forEach(videoInfo => { + const cleanedVideoUrl = (() => { + try { + return cleanUrl(videoInfo.url) + } catch { + return videoInfo.url + } + })() + + elements.push( +
+ +
+ ) + }) + + // Process lines for text content (excluding images and videos) + lines.forEach((line) => { + // Skip lines that contain images or videos (already processed above) + const hasImage = line.match(/(https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|heic|svg))/i) + const hasVideo = line.match(/(https?:\/\/[^\s]+\.(mp4|webm|ogg|avi|mov|wmv|flv|mkv|m4v))/i) + + if (hasImage || hasVideo) { + return // Skip this line as it's already processed + } + + // Regular text line - add to markdown processing + markdownLines.push(line) + }) return { markdownContent: markdownLines.join('\n'), diff --git a/src/components/WebPreview/index.tsx b/src/components/WebPreview/index.tsx index 432a4be..950fb95 100644 --- a/src/components/WebPreview/index.tsx +++ b/src/components/WebPreview/index.tsx @@ -35,7 +35,7 @@ export default function WebPreview({ url, className }: { url: string; className? window.open(url, '_blank') }} > - +
{hostname}
{title}
@@ -55,7 +55,7 @@ export default function WebPreview({ url, className }: { url: string; className? {image && ( )} diff --git a/src/pages/secondary/NotePage/index.tsx b/src/pages/secondary/NotePage/index.tsx index 3c65db7..dcaf3d4 100644 --- a/src/pages/secondary/NotePage/index.tsx +++ b/src/pages/secondary/NotePage/index.tsx @@ -109,7 +109,7 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false }: { id?: string; return ( -
+
{rootITag && } {rootEventId && rootEventId !== parentEventId && (
-
+