From 0a659e174cd331185b76699ae9d659482a0dc38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BA=B3=E8=A5=BF=E5=A6=B2=20=C2=B7=20Nahida?= Date: Sun, 17 Aug 2025 13:40:41 +0800 Subject: [PATCH] feat(message): prioritize thumbnail loading with fallback to original image When viewing messages, thumbnails are loaded first, if loading fails, the original image is loaded as a fallback. --- src/app/components/RenderMessageContent.tsx | 21 +++++++++++++++++- .../message/content/ImageContent.tsx | 22 ++++++++++++++----- src/types/matrix/common.ts | 1 + 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/app/components/RenderMessageContent.tsx b/src/app/components/RenderMessageContent.tsx index 2457e5e3..99155bfa 100644 --- a/src/app/components/RenderMessageContent.tsx +++ b/src/app/components/RenderMessageContent.tsx @@ -184,13 +184,32 @@ export function RenderMessageContent({ } if (msgType === MsgType.Image) { + const content: IImageContent = getContent(); + const width = content?.info?.w; + const height = content?.info?.h; + let thumbnail: { + imgWidth: number, + imgHeight: number, + resizeMethod: string + } | undefined; + if (width && height) { + const scale = (width > height ? width : height) / 800; + if (width > 800 || height > 800 && scale > 1) { + thumbnail = { + imgWidth: width / scale, + imgHeight: height / scale, + resizeMethod: "scale", + } + } + } return ( <> ( } renderViewer={(p) => } diff --git a/src/app/components/message/content/ImageContent.tsx b/src/app/components/message/content/ImageContent.tsx index cc0c0c91..87de7dda 100644 --- a/src/app/components/message/content/ImageContent.tsx +++ b/src/app/components/message/content/ImageContent.tsx @@ -55,6 +55,9 @@ export type ImageContentProps = { autoPlay?: boolean; markedAsSpoiler?: boolean; spoilerReason?: string; + imgWidth?: number; + imgHeight?: number; + resizeMethod?: string; renderViewer: (props: RenderViewerProps) => ReactNode; renderImage: (props: RenderImageProps) => ReactNode; }; @@ -70,6 +73,9 @@ export const ImageContent = as<'div', ImageContentProps>( autoPlay, markedAsSpoiler, spoilerReason, + imgWidth, + imgHeight, + resizeMethod, renderViewer, renderImage, ...props @@ -84,10 +90,11 @@ export const ImageContent = as<'div', ImageContentProps>( const [error, setError] = useState(false); const [viewer, setViewer] = useState(false); const [blurred, setBlurred] = useState(markedAsSpoiler ?? false); + const [useThumbnail, setUseThumbnail] = useState(imgWidth && imgHeight && resizeMethod); const [srcState, loadSrc] = useAsyncCallback( useCallback(async () => { - const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; + const mediaUrl = (useThumbnail ? mxcUrlToHttp(mx, url, useAuthentication) : mxcUrlToHttp(mx, url, useAuthentication, imgWidth, imgHeight, resizeMethod)) ?? url; if (encInfo) { const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType ?? FALLBACK_MIMETYPE, encInfo) @@ -95,15 +102,20 @@ export const ImageContent = as<'div', ImageContentProps>( return URL.createObjectURL(fileContent); } return mediaUrl; - }, [mx, url, useAuthentication, mimeType, encInfo]) + }, [mx, url, useAuthentication, mimeType, encInfo, useThumbnail, imgWidth, imgHeight, resizeMethod]) ); const handleLoad = () => { setLoad(true); }; const handleError = () => { - setLoad(false); - setError(true); + if (useThumbnail) { + setUseThumbnail(false); + loadSrc(); + } else { + setLoad(false); + setError(true); + } }; const handleRetry = () => { @@ -134,7 +146,7 @@ export const ImageContent = as<'div', ImageContentProps>( onContextMenu={(evt: any) => evt.stopPropagation()} > {renderViewer({ - src: srcState.data, + src: useThumbnail ? (mxcUrlToHttp(mx, url, useAuthentication) ?? url) : srcState.data, alt: body, requestClose: () => setViewer(false), })} diff --git a/src/types/matrix/common.ts b/src/types/matrix/common.ts index 210c711f..6c95309a 100644 --- a/src/types/matrix/common.ts +++ b/src/types/matrix/common.ts @@ -49,6 +49,7 @@ export type IImageContent = { url?: string; info?: IImageInfo & IThumbnailContent; file?: IEncryptedFile; + info?: { h: number, w: number }; [MATRIX_SPOILER_PROPERTY_NAME]?: boolean; [MATRIX_SPOILER_REASON_PROPERTY_NAME]?: string; };