diff --git a/src/components/ImageGallery/index.tsx b/src/components/ImageGallery/index.tsx index f15a0b3a..45e7ea01 100644 --- a/src/components/ImageGallery/index.tsx +++ b/src/components/ImageGallery/index.tsx @@ -1,5 +1,4 @@ import { randomString } from '@/lib/random' -import { preferBlossomPrimalDisplayUrl } from '@/lib/url' import { cn } from '@/lib/utils' import logger from '@/lib/logger' import { useContentPolicy } from '@/providers/ContentPolicyProvider' @@ -7,8 +6,10 @@ import modalManager from '@/services/modal-manager.service' import { TImetaInfo } from '@/types' import { ReactNode, useEffect, useMemo, useState } from 'react' import { createPortal } from 'react-dom' +import { lightboxSlideFromImeta } from '@/lib/lightbox-slides' import Lightbox from 'yet-another-react-lightbox' import Captions from 'yet-another-react-lightbox/plugins/captions' +import Video from 'yet-another-react-lightbox/plugins/video' import Zoom from 'yet-another-react-lightbox/plugins/zoom' import 'yet-another-react-lightbox/plugins/captions.css' import Image from '../Image' @@ -118,15 +119,11 @@ export default function ImageGallery({ { - const slides = images.map(({ url, alt }) => ({ - src: preferBlossomPrimalDisplayUrl(url), - alt: alt || url, - title: alt || undefined - })) + const slides = images.map((img) => lightboxSlideFromImeta(img)) logger.debug('[ImageGallery] Lightbox slides:', { index, slidesCount: slides.length, slides }) return slides })()} - plugins={[Zoom, Captions]} + plugins={[Video, Zoom, Captions]} open={index >= 0} close={() => setIndex(-1)} controller={{ diff --git a/src/components/ImageWithLightbox/index.tsx b/src/components/ImageWithLightbox/index.tsx index e07eadb2..cd36a6a5 100644 --- a/src/components/ImageWithLightbox/index.tsx +++ b/src/components/ImageWithLightbox/index.tsx @@ -5,10 +5,11 @@ import modalManager from '@/services/modal-manager.service' import { TImetaInfo } from '@/types' import { useEffect, useMemo, useState } from 'react' import { createPortal } from 'react-dom' -import { preferBlossomPrimalDisplayUrl } from '@/lib/url' +import { lightboxSlideFromImeta } from '@/lib/lightbox-slides' import { useTranslation } from 'react-i18next' import Lightbox from 'yet-another-react-lightbox' import Captions from 'yet-another-react-lightbox/plugins/captions' +import Video from 'yet-another-react-lightbox/plugins/video' import Zoom from 'yet-another-react-lightbox/plugins/zoom' import 'yet-another-react-lightbox/plugins/captions.css' import Image from '../Image' @@ -91,14 +92,8 @@ export default function ImageWithLightbox({ > = 0} close={() => setIndex(-1)} controller={{ diff --git a/src/components/MediaPlayer/index.tsx b/src/components/MediaPlayer/index.tsx index a8879476..521e4afe 100644 --- a/src/components/MediaPlayer/index.tsx +++ b/src/components/MediaPlayer/index.tsx @@ -50,8 +50,8 @@ export default function MediaPlayer({ return } - // Matroska is video-first for feeds; avoids waiting on metadata probe (codec support still browser-dependent). - if (extension === 'mkv') { + // Matroska / Ogg Theora: treat as video first (codec support is browser-dependent). + if (extension === 'mkv' || extension === 'ogv') { setMediaType('video') return } diff --git a/src/components/NormalFeed/index.tsx b/src/components/NormalFeed/index.tsx index 849b51ee..668f076f 100644 --- a/src/components/NormalFeed/index.tsx +++ b/src/components/NormalFeed/index.tsx @@ -127,7 +127,7 @@ const NormalFeed = forwardRef { const seenUrls = new Set() - const images: Array<{ url: string; alt?: string }> = [] + const images: Array<{ url: string; alt?: string; m?: string; image?: string }> = [] // Add images from extractedMedia extractedMedia.images.forEach(img => { const cleaned = cleanUrl(img.url) if (cleaned && !seenUrls.has(cleaned)) { seenUrls.add(cleaned) - images.push({ url: img.url, alt: img.alt }) + images.push({ url: img.url, alt: img.alt, m: img.m, image: img.image }) } }) @@ -2131,11 +2132,8 @@ export default function AsciidocArticle({ > ({ - src: preferBlossomPrimalDisplayUrl(url), - alt: alt || url - }))} - plugins={[Zoom]} + slides={allImages.map((img) => lightboxSlideFromImeta(img))} + plugins={[Video, Zoom]} open={lightboxIndex >= 0} close={() => setLightboxIndex(-1)} controller={{ diff --git a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx index a13596ee..7767e6cf 100644 --- a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx +++ b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx @@ -16,8 +16,7 @@ import { isAudio, isWebsocketUrl, isPseudoNostrHttpsUrl, - isSafeMediaUrl, - preferBlossomPrimalDisplayUrl + isSafeMediaUrl } from '@/lib/url' import { getHttpUrlFromITags, getImetaInfosFromEvent } from '@/lib/event' import { canonicalizeRssArticleUrl } from '@/lib/rss-article' @@ -31,7 +30,9 @@ import { TEmoji, TImetaInfo } from '@/types' import { emojis, shortcodeToEmoji } from '@tiptap/extension-emoji' import React, { useMemo, useState, useCallback, useEffect, useRef } from 'react' import { createPortal } from 'react-dom' +import { lightboxSlideFromImeta } from '@/lib/lightbox-slides' import Lightbox from 'yet-another-react-lightbox' +import Video from 'yet-another-react-lightbox/plugins/video' import Zoom from 'yet-another-react-lightbox/plugins/zoom' import CalendarEventContent from '@/components/CalendarEventContent' import { EmbeddedNote, EmbeddedMention, HttpNostrAwareUrl } from '@/components/Embedded' @@ -4544,14 +4545,14 @@ export default function MarkdownArticle({ // Get all images for gallery (deduplicated) const allImages = useMemo(() => { const seenUrls = new Set() - const images: Array<{ url: string; alt?: string }> = [] + const images: Array> = [] // Add images from extractedMedia extractedMedia.images.forEach(img => { const cleaned = cleanUrl(img.url) if (cleaned && !seenUrls.has(cleaned)) { seenUrls.add(cleaned) - images.push({ url: img.url, alt: img.alt }) + images.push({ url: img.url, alt: img.alt, m: img.m, image: img.image }) } }) @@ -5093,11 +5094,8 @@ export default function MarkdownArticle({ > ({ - src: preferBlossomPrimalDisplayUrl(url), - alt: alt || url - }))} - plugins={[Zoom]} + slides={allImages.map((img) => lightboxSlideFromImeta(img))} + plugins={[Video, Zoom]} open={lightboxOpen} close={() => setLightboxOpen(false)} on={{ diff --git a/src/components/PostEditor/PostContent.tsx b/src/components/PostEditor/PostContent.tsx index fb4a0770..2a347f2c 100644 --- a/src/components/PostEditor/PostContent.tsx +++ b/src/components/PostEditor/PostContent.tsx @@ -73,6 +73,7 @@ import { Laugh } from 'lucide-react' import { fileLooksLikeUploadableMedia } from '@/lib/compress-upload-media' +import { nip94PairsToImetaTag } from '@/lib/upload-nip94-imeta' import { getMediaKindFromFile } from '@/lib/media-kind-detection' import { hasPrivateRelays, getPrivateRelayUrls } from '@/lib/private-relays' import mediaUpload from '@/services/media-upload.service' @@ -1492,7 +1493,20 @@ export default function PostContent({ const handleUploadCompressPhase = useCallback((file: File, phase: 'compressing' | 'uploading') => { setUploadProgresses((prev) => - prev.map((row) => (row.file === file ? { ...row, phase } : row)) + prev.map((row) => + row.file === file + ? { ...row, phase, progress: phase === 'uploading' ? 0 : row.progress } + : row + ) + ) + }, []) + + const handleUploadCompressProgress = useCallback((file: File, percent: number) => { + const p = Math.max(0, Math.min(100, Math.round(percent))) + setUploadProgresses((prev) => + prev.map((row) => + row.file === file && row.phase === 'compressing' ? { ...row, progress: p } : row + ) ) }, []) @@ -1556,9 +1570,7 @@ export default function PostContent({ const handleUploadProgress = (file: File, progress: number) => { setUploadProgresses((prev) => prev.map((item) => - item.file === file - ? { ...item, progress, phase: progress > 0 ? 'uploading' : item.phase } - : item + item.file === file ? { ...item, progress } : item ) ) } @@ -1648,8 +1660,8 @@ export default function PostContent({ let newImetaTag: string[] if (imetaTag) { newImetaTag = imetaTag - } else if (tags && tags.length > 0 && tags[0]) { - newImetaTag = tags[0] + } else if (tags && tags.length > 0) { + newImetaTag = nip94PairsToImetaTag(tags) } else { // Create a basic imeta tag if none exists newImetaTag = ['imeta', `url ${url}`] @@ -1692,7 +1704,7 @@ export default function PostContent({ if (imetaTag) { setMediaImetaTags([imetaTag]) } else if (tags && tags.length > 0) { - setMediaImetaTags(tags) + setMediaImetaTags([nip94PairsToImetaTag(tags)]) } else { const basicImetaTag: string[] = ['imeta', `url ${url}`] // Update MIME type based on selected kind @@ -1835,7 +1847,7 @@ export default function PostContent({ if (imetaTag) { setMediaImetaTags([imetaTag]) } else if (tags && tags.length > 0) { - setMediaImetaTags(tags) + setMediaImetaTags([nip94PairsToImetaTag(tags)]) } else { const basicImetaTag: string[] = ['imeta', `url ${url}`] // For webm/ogg/mp3/m4a files uploaded via microphone, ensure MIME type is set to audio/* @@ -2782,6 +2794,7 @@ export default function PostContent({ onUploadEnd={handleUploadEnd} onUploadSuccess={handleMediaUploadSuccess} onUploadCompressPhase={handleUploadCompressPhase} + onUploadCompressProgress={handleUploadCompressProgress} kind={getDeterminedKind} highlightData={isHighlight ? highlightData : undefined} pollCreateData={isPoll ? pollCreateData : undefined} @@ -3055,7 +3068,7 @@ export default function PostContent({ : t('Uploading to media server…')}
- {phase === 'compressing' ? ( + {phase === 'compressing' && progress <= 0 ? (
)}
@@ -3110,6 +3125,7 @@ export default function PostContent({ onUploadEnd={handleUploadEnd} onProgress={handleUploadProgress} onUploadCompressPhase={handleUploadCompressPhase} + onUploadCompressProgress={handleUploadCompressProgress} accept="image/*,audio/*,video/*,.mkv,.mka,video/x-matroska,audio/x-matroska" className="sr-only" > @@ -3126,6 +3142,7 @@ export default function PostContent({ onUploadEnd={handleUploadEnd} onProgress={handleUploadProgress} onUploadCompressPhase={handleUploadCompressPhase} + onUploadCompressProgress={handleUploadCompressProgress} accept="audio/*,.mka,audio/x-matroska" >