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 @@ @@ -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",

2
package.json

@ -1,6 +1,6 @@ @@ -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",

23
src/components/Image/index.tsx

@ -1,6 +1,11 @@ @@ -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({ @@ -105,6 +110,8 @@ export default function Image({
const [imageUrl, setImageUrl] = useState(() => resolvePrimalBlossomPlayableUrl(url ?? ''))
const [fallbackIndex, setFallbackIndex] = useState(0)
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.
const wasInitiallyHeldRef = useRef(effectiveHoldUntilClick)
const imgRef = useRef<HTMLImageElement | null>(null)
@ -157,6 +164,7 @@ export default function Image({ @@ -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({ @@ -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)

27
src/components/MediaPlayer/index.tsx

@ -1,4 +1,10 @@ @@ -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({ @@ -61,9 +67,15 @@ export default function MediaPlayer({
const [mediaType, setMediaType] = useState<MediaSurface>(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` — <img> cannot use that, and it
// would hide the blurhash placeholder in LazyMediaTapPlaceholder.
@ -88,7 +100,8 @@ export default function MediaPlayer({ @@ -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({ @@ -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({ @@ -158,7 +177,7 @@ export default function MediaPlayer({
} catch {
setProbeFailed(true)
}
}, [playableSrc, showEmbed])
}, [playableSrc, showEmbed, src, preferCanonicalBlossomUrl])
const onEmbedReady = useCallback(() => {
if (readyOnceRef.current) return

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

@ -54,12 +54,15 @@ class MediaManagerService { @@ -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
})

Loading…
Cancel
Save