From 45ab53673727fbf7ec609d3e5040eb11e238c376 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 1 Dec 2025 16:00:09 +0000 Subject: [PATCH] Fix handling of SVGs (#31359) * Fix handling of SVGs 1. Ensure we always include thumbnails for them 2. Show `m.file` handler if we cannot render the SVG 3. When opening ImageView use svg thumbnail if the SVG cannot be rendered Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix UploadConfirmDialog choking under React devmode Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/ContentMessages.ts | 2 +- .../views/dialogs/UploadConfirmDialog.tsx | 41 +++-- src/components/views/messages/MImageBody.tsx | 21 ++- src/utils/DecryptFile.ts | 3 +- src/utils/MediaEventHelper.ts | 7 +- src/utils/blobs.ts | 14 +- .../dialogs/UploadConfirmDialog-test.tsx | 27 ++++ .../UploadConfirmDialog-test.tsx.snap | 80 ++++++++++ .../views/messages/MImageBody-test.tsx | 75 +++++++++- .../__snapshots__/MImageBody-test.tsx.snap | 141 ++++++++++++++++++ 10 files changed, 389 insertions(+), 22 deletions(-) create mode 100644 test/unit-tests/components/views/dialogs/UploadConfirmDialog-test.tsx create mode 100644 test/unit-tests/components/views/dialogs/__snapshots__/UploadConfirmDialog-test.tsx.snap diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts index ebfc23d43a..ec68f854fe 100644 --- a/src/ContentMessages.ts +++ b/src/ContentMessages.ts @@ -140,7 +140,7 @@ const IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT = 0.1; // 10% // and videos tend to be much larger. // Image mime types for which to always include a thumbnail for even if it is larger than the input for wider support. -const ALWAYS_INCLUDE_THUMBNAIL = ["image/avif", "image/webp"]; +const ALWAYS_INCLUDE_THUMBNAIL = ["image/avif", "image/webp", "image/svg+xml"]; /** * Read the metadata for an image file and create and upload a thumbnail of the image. diff --git a/src/components/views/dialogs/UploadConfirmDialog.tsx b/src/components/views/dialogs/UploadConfirmDialog.tsx index 36a05733a3..820b047c38 100644 --- a/src/components/views/dialogs/UploadConfirmDialog.tsx +++ b/src/components/views/dialogs/UploadConfirmDialog.tsx @@ -11,7 +11,6 @@ import React, { type JSX } from "react"; import { FilesIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; -import { getBlobSafeMimeType } from "../../../utils/blobs"; import BaseDialog from "./BaseDialog"; import DialogButtons from "../elements/DialogButtons"; import { fileSize } from "../../../utils/FileUtils"; @@ -23,10 +22,11 @@ interface IProps { onFinished: (uploadConfirmed: boolean, uploadAll?: boolean) => void; } -export default class UploadConfirmDialog extends React.Component { - private readonly objectUrl: string; - private readonly mimeType: string; +interface IState { + objectUrl?: string; +} +export default class UploadConfirmDialog extends React.Component { public static defaultProps: Partial = { totalFiles: 1, currentIndex: 0, @@ -35,15 +35,22 @@ export default class UploadConfirmDialog extends React.Component { public constructor(props: IProps) { super(props); - // Create a fresh `Blob` for previewing (even though `File` already is - // one) so we can adjust the MIME type if needed. - this.mimeType = getBlobSafeMimeType(props.file.type); - const blob = new Blob([props.file], { type: this.mimeType }); - this.objectUrl = URL.createObjectURL(blob); + this.state = {}; + } + + public componentDidMount(): void { + if (this.props.file.type.startsWith("image/") || this.props.file.type.startsWith("video/")) { + this.setState({ + // We do not filter the mimetype using getBlobSafeMimeType here as if the user is uploading the file + // themselves they should be trusting it enough to open/load it, and it will be rendered into a hidden + // canvas for thumbnail generation anyway + objectUrl: URL.createObjectURL(this.props.file), + }); + } } public componentWillUnmount(): void { - if (this.objectUrl) URL.revokeObjectURL(this.objectUrl); + if (this.state.objectUrl) URL.revokeObjectURL(this.state.objectUrl); } private onCancelClick = (): void => { @@ -70,17 +77,23 @@ export default class UploadConfirmDialog extends React.Component { } const fileId = `mx-uploadconfirmdialog-${this.props.file.name}`; + const mimeType = this.props.file.type; + let preview: JSX.Element | undefined; let placeholder: JSX.Element | undefined; - if (this.mimeType.startsWith("image/")) { + if (mimeType.startsWith("image/")) { preview = ( - + ); - } else if (this.mimeType.startsWith("video/")) { + } else if (mimeType.startsWith("video/")) { preview = (