From 8164d44336d7592e80d49e65cc21dfb8746a1e21 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 2 May 2026 18:16:07 +0200 Subject: [PATCH] fix image views --- package-lock.json | 4 ++-- package.json | 2 +- src/components/Image/index.tsx | 23 ++++++++++++++++++++++- src/components/MediaPlayer/index.tsx | 27 +++++++++++++++++++++++---- src/services/media-manager.service.ts | 15 +++++++++------ 5 files changed, 57 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0aa94a11..05122e66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "imwald", - "version": "23.3.0", + "version": "23.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "imwald", - "version": "23.3.0", + "version": "23.3.1", "license": "MIT", "dependencies": { "@asciidoctor/core": "^3.0.4", diff --git a/package.json b/package.json index 98f8005a..f2d93de3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "imwald", - "version": "23.3.0", + "version": "23.3.1", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "private": true, "type": "module", diff --git a/src/components/Image/index.tsx b/src/components/Image/index.tsx index 5f21aa7b..0a153c64 100644 --- a/src/components/Image/index.tsx +++ b/src/components/Image/index.tsx @@ -1,6 +1,11 @@ import { Skeleton } from '@/components/ui/skeleton' import { cn } from '@/lib/utils' -import { isRenderableMediaUrl, isSafeMediaUrl, resolvePrimalBlossomPlayableUrl } from '@/lib/url' +import { + isRenderableMediaUrl, + isSafeMediaUrl, + primalR2aMirrorForBlossomPrimalUrl, + resolvePrimalBlossomPlayableUrl +} from '@/lib/url' import { TImetaInfo } from '@/types' import { blurHashPlaceholderForMediaUrl } from '@/lib/media-placeholder-blurhash' import { decode } from 'blurhash' @@ -105,6 +110,8 @@ export default function Image({ const [imageUrl, setImageUrl] = useState(() => resolvePrimalBlossomPlayableUrl(url ?? '')) const [fallbackIndex, setFallbackIndex] = useState(0) const loadWatchRef = useRef(null) + /** After r2a + imeta fallbacks fail, try `url` on blossom.primal.net once (see handleError). */ + const triedPrimaryBlossomDirectRef = useRef(false) // Kept in sync in the reset effect; load-timeout runs only while tap-to-load is actually active. const wasInitiallyHeldRef = useRef(effectiveHoldUntilClick) const imgRef = useRef(null) @@ -157,6 +164,7 @@ export default function Image({ setHasError(false) setDisplaySkeleton(true) setFallbackIndex(0) + triedPrimaryBlossomDirectRef.current = false clearLoadWatch() if (!url?.trim()) { setIsLoading(false) @@ -223,6 +231,19 @@ export default function Image({ setImageUrl(resolvePrimalBlossomPlayableUrl(next)) return } + // r2a mirror sometimes 404s while blossom.primal.net still serves (redirect chain). Retry canonical URL once. + const primary = (url ?? '').trim() + const mirrorOfPrimary = primary ? primalR2aMirrorForBlossomPrimalUrl(primary) : null + if ( + mirrorOfPrimary && + primary !== mirrorOfPrimary && + !triedPrimaryBlossomDirectRef.current + ) { + triedPrimaryBlossomDirectRef.current = true + loadSettledRef.current = false + setImageUrl(primary) + return + } setIsLoading(false) setDisplaySkeleton(false) setHasError(true) diff --git a/src/components/MediaPlayer/index.tsx b/src/components/MediaPlayer/index.tsx index d16b2478..39e64b62 100644 --- a/src/components/MediaPlayer/index.tsx +++ b/src/components/MediaPlayer/index.tsx @@ -1,4 +1,10 @@ -import { isHlsPlaylistUrl, isImage, isZapStreamWatchPageUrl, resolvePrimalBlossomPlayableUrl } from '@/lib/url' +import { + isHlsPlaylistUrl, + isImage, + isZapStreamWatchPageUrl, + primalR2aMirrorForBlossomPrimalUrl, + resolvePrimalBlossomPlayableUrl +} from '@/lib/url' import { cn } from '@/lib/utils' import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' @@ -61,9 +67,15 @@ export default function MediaPlayer({ const [mediaType, setMediaType] = useState(null) const [probeFailed, setProbeFailed] = useState(false) const [embedPainted, setEmbedPainted] = useState(false) + /** After r2a mirror fails, retry metadata probe with canonical blossom.primal.net URL once. */ + const [preferCanonicalBlossomUrl, setPreferCanonicalBlossomUrl] = useState(false) const readyOnceRef = useRef(false) - const playableSrc = useMemo(() => resolvePrimalBlossomPlayableUrl(src), [src]) + const playableSrc = useMemo(() => { + const raw = src.trim() + if (preferCanonicalBlossomUrl) return raw + return resolvePrimalBlossomPlayableUrl(src) + }, [src, preferCanonicalBlossomUrl]) // imeta `thumb` / `image` are sometimes the same .mp4 as `url` — cannot use that, and it // would hide the blurhash placeholder in LazyMediaTapPlaceholder. @@ -88,7 +100,8 @@ export default function MediaPlayer({ setEmbedPainted(false) setMediaType(null) setProbeFailed(false) - }, [playableSrc]) + setPreferCanonicalBlossomUrl(false) + }, [src]) useEffect(() => { if (!showEmbed) { @@ -147,6 +160,12 @@ export default function MediaPlayer({ video.onerror = () => { if (cancelled) return + const raw = src.trim() + const mirror = raw ? primalR2aMirrorForBlossomPrimalUrl(raw) : null + if (mirror && playableSrc === mirror && raw !== mirror && !preferCanonicalBlossomUrl) { + setPreferCanonicalBlossomUrl(true) + return + } setProbeFailed(true) setMediaType(null) } @@ -158,7 +177,7 @@ export default function MediaPlayer({ } catch { setProbeFailed(true) } - }, [playableSrc, showEmbed]) + }, [playableSrc, showEmbed, src, preferCanonicalBlossomUrl]) const onEmbedReady = useCallback(() => { if (readyOnceRef.current) return diff --git a/src/services/media-manager.service.ts b/src/services/media-manager.service.ts index b97cd78b..55591e7a 100644 --- a/src/services/media-manager.service.ts +++ b/src/services/media-manager.service.ts @@ -54,12 +54,15 @@ class MediaManagerService { } play(this.currentMedia).catch((error) => { - // Don't log expected AbortError when media is interrupted - if (error instanceof Error && error.name === 'AbortError') { - // This is expected when media is interrupted by pause() or other media - return - } - // Log other unexpected errors + const name = error instanceof Error ? error.name : '' + const msg = error instanceof Error ? error.message : '' + // Abort: pause / navigation / another element taking over. + if (name === 'AbortError') return + // Autoplay policy (user can still press play); muted autoplay usually avoids this. + if (name === 'NotAllowedError') return + // Codec / empty resource — surface elsewhere (video onerror); play() adds noise only. + if (name === 'NotSupportedError') return + if (/play\(\) request was interrupted|The operation was aborted/i.test(msg)) return logger.error('Error playing media', { error }) this.currentMedia = null })