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>
This commit is contained in:
Michael Telatynski
2025-12-01 16:00:09 +00:00
committed by GitHub
parent afa186cdf4
commit 45ab536737
10 changed files with 389 additions and 22 deletions

View File

@@ -0,0 +1,27 @@
/*
Copyright 2025 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "jest-matrix-react";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import UploadConfirmDialog from "../../../../../src/components/views/dialogs/UploadConfirmDialog.tsx";
describe("<UploadConfirmDialog />", () => {
it("should display image preview", () => {
const url = "blob:null/1234-5678-9101-1121";
jest.spyOn(URL, "createObjectURL").mockReturnValue(url);
const file = new File([secureRandomString(1024 * 124)], "image.png", { type: "image/png" });
const { asFragment, getByRole } = render(
<UploadConfirmDialog file={file} currentIndex={0} totalFiles={1} onFinished={jest.fn()} />,
);
expect(getByRole("img")).toHaveAttribute("src", url);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,80 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<UploadConfirmDialog /> should display image preview 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_UploadConfirmDialog"
data-focus-lock-disabled="false"
role="dialog"
tabindex="-1"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Upload files
</h1>
</div>
<div
id="mx_Dialog_content"
>
<div
class="mx_UploadConfirmDialog_previewOuter"
>
<div
class="mx_UploadConfirmDialog_previewInner"
>
<div>
<img
aria-labelledby="mx-uploadconfirmdialog-image.png"
class="mx_UploadConfirmDialog_imagePreview"
src="blob:null/1234-5678-9101-1121"
/>
</div>
<div
id="mx-uploadconfirmdialog-image.png"
>
image.png (124 KB)
</div>
</div>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
Upload
</button>
</span>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from "jest-matrix-react";
import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved, within } from "jest-matrix-react";
import { EventType, getHttpUriForMxc, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import fetchMock from "fetch-mock-jest";
import encrypt from "matrix-encrypt-attachment";
@@ -70,6 +70,7 @@ describe("<MImageBody/>", () => {
info: {
w: 40,
h: 50,
mimetype: "image/png",
},
file: {
url: "mxc://server/encrypted-image",
@@ -304,4 +305,76 @@ describe("<MImageBody/>", () => {
expect(container.querySelector(".mx_MImageBody_banner")).toHaveTextContent("...alt for a test image");
});
it("should render MFileBody for svg with no thumbnail", async () => {
const event = new MatrixEvent({
room_id: "!room:server",
sender: senderUserId,
type: EventType.RoomMessage,
content: {
info: {
w: 40,
h: 50,
mimetype: "image/svg+xml",
},
file: {
url: "mxc://server/encrypted-svg",
},
},
});
const { container, asFragment } = render(
<MImageBody {...props} mxEvent={event} mediaEventHelper={new MediaEventHelper(event)} />,
withClientContextRenderOptions(cli),
);
expect(container.querySelector(".mx_MFileBody")).toHaveTextContent("Attachment");
expect(asFragment()).toMatchSnapshot();
});
it("should open ImageView using thumbnail for encrypted svg", async () => {
const url = "https://server/_matrix/media/v3/download/server/encrypted-svg";
fetchMock.getOnce(url, { status: 200 });
const thumbUrl = "https://server/_matrix/media/v3/download/server/svg-thumbnail";
fetchMock.getOnce(thumbUrl, { status: 200 });
const event = new MatrixEvent({
room_id: "!room:server",
sender: senderUserId,
type: EventType.RoomMessage,
origin_server_ts: 1234567890,
content: {
info: {
w: 40,
h: 50,
mimetype: "image/svg+xml",
thumbnail_file: {
url: "mxc://server/svg-thumbnail",
},
thumbnail_info: { mimetype: "image/png" },
},
file: {
url: "mxc://server/encrypted-svg",
},
},
});
const mediaEventHelper = new MediaEventHelper(event);
mediaEventHelper.thumbnailUrl["prom"] = Promise.resolve(thumbUrl);
mediaEventHelper.sourceUrl["prom"] = Promise.resolve(url);
const { findByRole } = render(
<MImageBody {...props} mxEvent={event} mediaEventHelper={mediaEventHelper} />,
withClientContextRenderOptions(cli),
);
fireEvent.click(await findByRole("link"));
const dialog = await screen.findByRole("dialog");
await expect(within(dialog).findByRole("img")).resolves.toHaveAttribute(
"src",
"https://server/_matrix/media/v3/download/server/svg-thumbnail",
);
expect(dialog).toMatchSnapshot();
});
});

View File

@@ -47,6 +47,147 @@ exports[`<MImageBody/> should generate a thumbnail if one isn't included for ani
</div>
`;
exports[`<MImageBody/> should open ImageView using thumbnail for encrypted svg 1`] = `
<div
aria-label="Image view"
class="mx_ImageView"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_ImageView_panel"
>
<div
class="mx_ImageView_info_wrapper"
>
<button
aria-label="Profile picture"
aria-live="off"
class="_avatar_1qbcf_8 mx_BaseAvatar mx_Dialog_nonDialogButton _avatar-imageless_1qbcf_52"
data-color="2"
data-testid="avatar-img"
data-type="round"
role="button"
style="--cpd-avatar-size: 32px;"
>
o
</button>
<div
class="mx_ImageView_info"
>
<div
class="mx_ImageView_info_sender"
>
@other_use:server
</div>
<a
aria-live="off"
class="mx_MessageTimestamp"
href="https://matrix.to/#/!room:server/undefined"
>
Thu, Jan 15, 1970, 06:56
</a>
</div>
</div>
<div
class="mx_ImageView_title"
>
Image
</div>
<div
class="mx_ImageView_toolbar"
>
<div
aria-label="Zoom out"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_zoomOut"
role="button"
tabindex="0"
/>
<div
aria-label="Zoom in"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_zoomIn"
role="button"
tabindex="0"
/>
<div
aria-label="Rotate Left"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_rotateCCW"
role="button"
tabindex="0"
/>
<div
aria-label="Rotate Right"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_rotateCW"
role="button"
tabindex="0"
/>
<div
aria-label="Download"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_download"
role="button"
tabindex="0"
/>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_more"
role="button"
tabindex="0"
/>
<div
aria-label="Close"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_close"
role="button"
tabindex="0"
/>
</div>
</div>
<div
class="mx_ImageView_image_wrapper"
>
<img
alt="Attachment"
class="mx_ImageView_image "
draggable="true"
src="https://server/_matrix/media/v3/download/server/svg-thumbnail"
style="transform: translateX(-512px)
translateY(NaNpx)
scale(0)
rotate(0deg); cursor: zoom-out;"
/>
</div>
</div>
`;
exports[`<MImageBody/> should render MFileBody for svg with no thumbnail 1`] = `
<DocumentFragment>
<span
class="mx_MFileBody"
>
<div
class="mx_AccessibleButton mx_MediaBody mx_MFileBody_info"
role="button"
tabindex="0"
>
<span
class="mx_MFileBody_info_icon"
/>
<span
aria-labelledby="_r_0_"
tabindex="0"
>
<span
class="mx_MFileBody_info_filename"
>
Attachment
</span>
</span>
</div>
</span>
</DocumentFragment>
`;
exports[`<MImageBody/> should show a thumbnail while image is being downloaded 1`] = `
<div>
<div