refactor: `<Media />` with improved image source handling and error management

- Refactor image source selection to prioritize proxy and origin URLs based on user preferences.
- Implement error handling to retry loading from alternative sources upon failure.
- Add debug information for successful image loads and errors in development mode.
- Optimize state management for loading and error states.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-06-19 17:30:20 +08:00
parent d71b2e839a
commit 8a65dda8b9
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
1 changed files with 135 additions and 55 deletions

View File

@ -3,7 +3,8 @@ import { getImageProxyUrl } from "@follow/utils/img-proxy"
import { cn } from "@follow/utils/utils"
import { useForceUpdate } from "motion/react"
import type { FC, ImgHTMLAttributes, VideoHTMLAttributes } from "react"
import { memo, use, useMemo, useState } from "react"
import * as React from "react"
import { memo, use, useEffect, useMemo, useRef, useState } from "react"
import { Blurhash, BlurhashCanvas } from "react-blurhash"
import { useEventCallback } from "usehooks-ts"
@ -79,67 +80,117 @@ const MediaImpl: FC<MediaProps> = ({
const finalHeight = height || ctxHeight
const finalWidth = width || ctxWidth
const [currentState, setCurrentState] = useState<"proxy" | "origin" | "error">(() =>
proxy && !preferOrigin ? "proxy" : "origin",
)
// Define the list of available image sources, sorted by priority
const imageSources = useMemo(() => {
if (!src) return []
const [imgSrc, setImgSrc] = useState(() =>
currentState === "proxy" && src
? getImageProxyUrl({
url: src,
width: proxy?.width || 0,
height: proxy?.height || 0,
const sources: Array<{ url: string; type: "proxy" | "origin" }> = []
// Determine priority based on preferences
if (proxy && !preferOrigin) {
// Use proxy first
sources.push(
{
url: getImageProxyUrl({
url: src,
width: proxy.width || 0,
height: proxy.height || 0,
}),
type: "proxy",
},
{ url: src, type: "origin" },
)
} else {
// Use original URL first
sources.push({ url: src, type: "origin" })
if (proxy) {
sources.push({
url: getImageProxyUrl({
url: src,
width: proxy.width || 0,
height: proxy.height || 0,
}),
type: "proxy",
})
: src,
)
const previewImageSrc = useMemo(
() =>
currentState === "proxy" && previewImageUrl
? getImageProxyUrl({
url: previewImageUrl,
width: proxy?.width || 0,
height: proxy?.height || 0,
})
: previewImageUrl,
[currentState, previewImageUrl, proxy?.width, proxy?.height],
)
const [mediaLoadState, setMediaLoadState] = useState<"loading" | "loaded">(() => {
if (imgSrc) {
return isImageLoadedSet.has(imgSrc) ? "loaded" : "loading"
}
return "loading"
})
const errorHandle: React.ReactEventHandler<HTMLImageElement> = useEventCallback(() => {
switch (currentState) {
case "proxy": {
if (imgSrc !== props.src && props.src) {
setImgSrc(props.src)
} else {
setCurrentState("error")
}
break
}
case "origin": {
if (imgSrc === props.src && props.src) {
setImgSrc(
getImageProxyUrl({
url: props.src,
width: proxy?.width || 0,
height: proxy?.height || 0,
}),
)
} else {
setCurrentState("error")
}
break
}
return sources
}, [src, proxy, preferOrigin])
const [currentSourceIndex, setCurrentSourceIndex] = useState(0)
const [isError, setIsError] = useState(false)
const [mediaLoadState, setMediaLoadState] = useState<"loading" | "loaded">("loading")
const currentSource = imageSources[currentSourceIndex]
const imgSrc = currentSource?.url || src
const previewImageSrc = useMemo(() => {
if (!previewImageUrl) return
// Use the same proxy strategy for preview images
if (proxy && currentSource?.type === "proxy") {
return getImageProxyUrl({
url: previewImageUrl,
width: proxy.width || 0,
height: proxy.height || 0,
})
}
return previewImageUrl
}, [previewImageUrl, proxy, currentSource?.type])
// When image source list changes, reset to the first source
const prevImageSources = useRef(imageSources)
useEffect(() => {
if (prevImageSources.current !== imageSources && imageSources.length > 0) {
prevImageSources.current = imageSources
setCurrentSourceIndex(0)
setIsError(false)
}
}, [imageSources])
// When image source changes, reset loading state
const prevImgSrc = useRef(imgSrc)
useEffect(() => {
if (prevImgSrc.current !== imgSrc) {
prevImgSrc.current = imgSrc
setMediaLoadState(imgSrc && isImageLoadedSet.has(imgSrc) ? "loaded" : "loading")
}
}, [imgSrc])
const errorHandle: React.ReactEventHandler<HTMLImageElement> = useEventCallback((e) => {
const nextIndex = currentSourceIndex + 1
if (import.meta.env.DEV) {
console.info(
`[Media Error] Failed to load image source ${currentSourceIndex + 1}/${imageSources.length}`,
{
failedSrc: imgSrc,
originalSrc: src,
error: e,
willRetry: nextIndex < imageSources.length,
nextSource: imageSources[nextIndex]?.url,
},
)
}
if (nextIndex < imageSources.length) {
// Try next available image source
setCurrentSourceIndex(nextIndex)
setMediaLoadState("loading")
} else {
// All sources failed, mark as error state
setIsError(true)
if (import.meta.env.DEV) {
console.error(`[Media Error] All image sources failed for: ${src}`, {
allSources: imageSources,
originalSrc: src,
})
}
}
})
const isError = currentState === "error"
const previewMedia = usePreviewMedia()
const handleClick = useEventCallback((e: React.MouseEvent) => {
e.preventDefault()
@ -166,6 +217,22 @@ const MediaImpl: FC<MediaProps> = ({
setMediaLoadState("loaded")
rest.onLoad?.(e as any)
if (import.meta.env.DEV) {
console.info(`[Media Success] Image loaded successfully`, {
src: imgSrc,
originalSrc: src,
sourceType: currentSource?.type,
sourceIndex: currentSourceIndex + 1,
totalSources: imageSources.length,
dimensions: {
width: e.currentTarget.naturalWidth,
height: e.currentTarget.naturalHeight,
ratio: e.currentTarget.naturalWidth / e.currentTarget.naturalHeight,
},
loadTime: performance.now(),
})
}
if (imgSrc) {
isImageLoadedSet.add(imgSrc)
}
@ -294,6 +361,19 @@ const MediaImpl: FC<MediaProps> = ({
return (
<span
data-state={type !== "video" ? mediaLoadState : undefined}
data-media-debug={
import.meta.env.DEV
? JSON.stringify({
originalSrc: src,
currentSrc: imgSrc,
sourceType: currentSource?.type,
sourceIndex: currentSourceIndex,
totalSources: imageSources.length,
isError,
mediaLoadState,
})
: undefined
}
className={cn("relative overflow-hidden rounded", inline ? "inline" : "block", className)}
style={style}
>