Browse Source

fix image views

imwald
Silberengel 23 hours ago
parent
commit
8164d44336
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 23
      src/components/Image/index.tsx
  4. 27
      src/components/MediaPlayer/index.tsx
  5. 15
      src/services/media-manager.service.ts

4
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "imwald", "name": "imwald",
"version": "23.3.0", "version": "23.3.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "imwald", "name": "imwald",
"version": "23.3.0", "version": "23.3.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@asciidoctor/core": "^3.0.4", "@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "imwald", "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", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true, "private": true,
"type": "module", "type": "module",

23
src/components/Image/index.tsx

@ -1,6 +1,11 @@
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { cn } from '@/lib/utils' 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 { TImetaInfo } from '@/types'
import { blurHashPlaceholderForMediaUrl } from '@/lib/media-placeholder-blurhash' import { blurHashPlaceholderForMediaUrl } from '@/lib/media-placeholder-blurhash'
import { decode } from 'blurhash' import { decode } from 'blurhash'
@ -105,6 +110,8 @@ export default function Image({
const [imageUrl, setImageUrl] = useState(() => resolvePrimalBlossomPlayableUrl(url ?? '')) const [imageUrl, setImageUrl] = useState(() => resolvePrimalBlossomPlayableUrl(url ?? ''))
const [fallbackIndex, setFallbackIndex] = useState(0) const [fallbackIndex, setFallbackIndex] = useState(0)
const loadWatchRef = useRef<number | null>(null) const loadWatchRef = useRef<number | null>(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. // Kept in sync in the reset effect; load-timeout runs only while tap-to-load is actually active.
const wasInitiallyHeldRef = useRef(effectiveHoldUntilClick) const wasInitiallyHeldRef = useRef(effectiveHoldUntilClick)
const imgRef = useRef<HTMLImageElement | null>(null) const imgRef = useRef<HTMLImageElement | null>(null)
@ -157,6 +164,7 @@ export default function Image({
setHasError(false) setHasError(false)
setDisplaySkeleton(true) setDisplaySkeleton(true)
setFallbackIndex(0) setFallbackIndex(0)
triedPrimaryBlossomDirectRef.current = false
clearLoadWatch() clearLoadWatch()
if (!url?.trim()) { if (!url?.trim()) {
setIsLoading(false) setIsLoading(false)
@ -223,6 +231,19 @@ export default function Image({
setImageUrl(resolvePrimalBlossomPlayableUrl(next)) setImageUrl(resolvePrimalBlossomPlayableUrl(next))
return 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) setIsLoading(false)
setDisplaySkeleton(false) setDisplaySkeleton(false)
setHasError(true) setHasError(true)

27
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 { cn } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
@ -61,9 +67,15 @@ export default function MediaPlayer({
const [mediaType, setMediaType] = useState<MediaSurface>(null) const [mediaType, setMediaType] = useState<MediaSurface>(null)
const [probeFailed, setProbeFailed] = useState(false) const [probeFailed, setProbeFailed] = useState(false)
const [embedPainted, setEmbedPainted] = 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 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` — <img> cannot use that, and it // imeta `thumb` / `image` are sometimes the same .mp4 as `url` — <img> cannot use that, and it
// would hide the blurhash placeholder in LazyMediaTapPlaceholder. // would hide the blurhash placeholder in LazyMediaTapPlaceholder.
@ -88,7 +100,8 @@ export default function MediaPlayer({
setEmbedPainted(false) setEmbedPainted(false)
setMediaType(null) setMediaType(null)
setProbeFailed(false) setProbeFailed(false)
}, [playableSrc]) setPreferCanonicalBlossomUrl(false)
}, [src])
useEffect(() => { useEffect(() => {
if (!showEmbed) { if (!showEmbed) {
@ -147,6 +160,12 @@ export default function MediaPlayer({
video.onerror = () => { video.onerror = () => {
if (cancelled) return 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) setProbeFailed(true)
setMediaType(null) setMediaType(null)
} }
@ -158,7 +177,7 @@ export default function MediaPlayer({
} catch { } catch {
setProbeFailed(true) setProbeFailed(true)
} }
}, [playableSrc, showEmbed]) }, [playableSrc, showEmbed, src, preferCanonicalBlossomUrl])
const onEmbedReady = useCallback(() => { const onEmbedReady = useCallback(() => {
if (readyOnceRef.current) return if (readyOnceRef.current) return

15
src/services/media-manager.service.ts

@ -54,12 +54,15 @@ class MediaManagerService {
} }
play(this.currentMedia).catch((error) => { play(this.currentMedia).catch((error) => {
// Don't log expected AbortError when media is interrupted const name = error instanceof Error ? error.name : ''
if (error instanceof Error && error.name === 'AbortError') { const msg = error instanceof Error ? error.message : ''
// This is expected when media is interrupted by pause() or other media // Abort: pause / navigation / another element taking over.
return if (name === 'AbortError') return
} // Autoplay policy (user can still press play); muted autoplay usually avoids this.
// Log other unexpected errors 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 }) logger.error('Error playing media', { error })
this.currentMedia = null this.currentMedia = null
}) })

Loading…
Cancel
Save