-
+
{pollEvent.question.text}
{formattedDate}
diff --git a/src/createRoom.ts b/src/createRoom.ts
index 8e39260921..e819bf39af 100644
--- a/src/createRoom.ts
+++ b/src/createRoom.ts
@@ -316,9 +316,6 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
return Promise.reject(err);
}
})
- .finally(function () {
- if (modal) modal.close();
- })
.then(async (res): Promise
=> {
roomId = res.room_id;
@@ -340,6 +337,9 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
if (opts.dmUserId) await Rooms.setDMRoom(client, roomId, opts.dmUserId);
})
+ .finally(function () {
+ if (modal) modal.close();
+ })
.then(() => {
if (opts.parentSpace) {
return SpaceStore.instance.addRoomToSpace(
diff --git a/src/utils/DecryptFile.ts b/src/utils/DecryptFile.ts
index baf2acafc1..b159b526d0 100644
--- a/src/utils/DecryptFile.ts
+++ b/src/utils/DecryptFile.ts
@@ -64,8 +64,7 @@ export async function decryptFile(file?: EncryptedFile, info?: MediaEventInfo):
// they introduce XSS attacks if the Blob URI is viewed directly in the
// browser (e.g. by copying the URI into a new tab or window.)
// See warning at top of file.
- let mimetype = info?.mimetype ? info.mimetype.split(";")[0].trim() : "";
- mimetype = getBlobSafeMimeType(mimetype);
+ const mimetype = getBlobSafeMimeType(info?.mimetype?.split(";")[0].trim() ?? "");
return new Blob([dataArray], { type: mimetype });
} catch (e) {
diff --git a/src/utils/MediaEventHelper.ts b/src/utils/MediaEventHelper.ts
index 308d85b2fb..837f049398 100644
--- a/src/utils/MediaEventHelper.ts
+++ b/src/utils/MediaEventHelper.ts
@@ -14,6 +14,7 @@ import { LazyValue } from "./LazyValue";
import { type Media, mediaFromContent } from "../customisations/Media";
import { decryptFile } from "./DecryptFile";
import { type IDestroyable } from "./IDestroyable";
+import { getBlobSafeMimeType } from "./blobs.ts";
// TODO: We should consider caching the blobs. https://github.com/vector-im/element-web/issues/17192
@@ -82,7 +83,7 @@ export class MediaEventHelper implements IDestroyable {
.downloadSource()
.then((r) => r.blob())
// Set the mime type from the event info on the blob
- .then((blob) => blob.slice(0, blob.size, content.info?.mimetype ?? blob.type))
+ .then((blob) => blob.slice(0, blob.size, getBlobSafeMimeType(content.info?.mimetype ?? blob.type)))
);
};
@@ -107,7 +108,9 @@ export class MediaEventHelper implements IDestroyable {
fetch(thumbnailHttp)
.then((r) => r.blob())
// Set the mime type from the event info on the blob
- .then((blob) => blob.slice(0, blob.size, content.info?.thumbnail_info?.mimetype ?? blob.type))
+ .then((blob) =>
+ blob.slice(0, blob.size, getBlobSafeMimeType(content.info?.thumbnail_info?.mimetype ?? blob.type)),
+ )
);
};
diff --git a/src/utils/blobs.ts b/src/utils/blobs.ts
index 70ba54a030..b9ee316a1f 100644
--- a/src/utils/blobs.ts
+++ b/src/utils/blobs.ts
@@ -66,8 +66,20 @@ const ALLOWED_BLOB_MIMETYPES = [
"audio/x-flac",
];
+/**
+ * Checks whether the given mime type is in the allowed mimetype list
+ * @param mimetype - the mimetype to check
+ */
+export function isMimeTypeAllowed(mimetype: string): boolean {
+ return ALLOWED_BLOB_MIMETYPES.includes(mimetype);
+}
+
+/**
+ * Returns the input mimetype if it is allowed, `application/octet-stream` otherwise
+ * @param mimetype - the mimetype to check
+ */
export function getBlobSafeMimeType(mimetype: string): string {
- if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) {
+ if (!isMimeTypeAllowed(mimetype)) {
return "application/octet-stream";
}
return mimetype;
diff --git a/test/unit-tests/components/views/dialogs/UploadConfirmDialog-test.tsx b/test/unit-tests/components/views/dialogs/UploadConfirmDialog-test.tsx
new file mode 100644
index 0000000000..43b3f912ac
--- /dev/null
+++ b/test/unit-tests/components/views/dialogs/UploadConfirmDialog-test.tsx
@@ -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("", () => {
+ 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(
+ ,
+ );
+
+ expect(getByRole("img")).toHaveAttribute("src", url);
+ expect(asFragment()).toMatchSnapshot();
+ });
+});
diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/UploadConfirmDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/UploadConfirmDialog-test.tsx.snap
new file mode 100644
index 0000000000..689167e46c
--- /dev/null
+++ b/test/unit-tests/components/views/dialogs/__snapshots__/UploadConfirmDialog-test.tsx.snap
@@ -0,0 +1,80 @@
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
+
+exports[` should display image preview 1`] = `
+
+
+
+
+
+
+
+
+

+
+
+ image.png (124 KB)
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/test/unit-tests/components/views/messages/MImageBody-test.tsx b/test/unit-tests/components/views/messages/MImageBody-test.tsx
index aab468cdd2..41d89ff505 100644
--- a/test/unit-tests/components/views/messages/MImageBody-test.tsx
+++ b/test/unit-tests/components/views/messages/MImageBody-test.tsx
@@ -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("", () => {
info: {
w: 40,
h: 50,
+ mimetype: "image/png",
},
file: {
url: "mxc://server/encrypted-image",
@@ -304,4 +305,76 @@ describe("", () => {
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(
+ ,
+ 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(
+ ,
+ 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();
+ });
});
diff --git a/test/unit-tests/components/views/messages/__snapshots__/MImageBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/MImageBody-test.tsx.snap
index dfa5c9d27d..4ab00419d1 100644
--- a/test/unit-tests/components/views/messages/__snapshots__/MImageBody-test.tsx.snap
+++ b/test/unit-tests/components/views/messages/__snapshots__/MImageBody-test.tsx.snap
@@ -47,6 +47,147 @@ exports[` should generate a thumbnail if one isn't included for ani
`;
+exports[`
when poll start event does not exist in current timeline fetches the related poll start event and displays a poll tile 1`] = `
-
+ fill="currentColor"
+ height="1em"
+ viewBox="0 0 24 24"
+ width="1em"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+
+
+
`;
diff --git a/test/unit-tests/components/views/polls/pollHistory/__snapshots__/PollHistory-test.tsx.snap b/test/unit-tests/components/views/polls/pollHistory/__snapshots__/PollHistory-test.tsx.snap
index daa3e64211..948c29fe2f 100644
--- a/test/unit-tests/components/views/polls/pollHistory/__snapshots__/PollHistory-test.tsx.snap
+++ b/test/unit-tests/components/views/polls/pollHistory/__snapshots__/PollHistory-test.tsx.snap
@@ -97,9 +97,18 @@ exports[`
renders a list of active polls when there are polls in
02/02/23
-
+ fill="currentColor"
+ height="1em"
+ viewBox="0 0 24 24"
+ width="1em"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+
+
@@ -122,9 +131,18 @@ exports[` renders a list of active polls when there are polls in
02/02/23
-
+ fill="currentColor"
+ height="1em"
+ viewBox="0 0 24 24"
+ width="1em"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+
+
diff --git a/test/unit-tests/components/views/polls/pollHistory/__snapshots__/PollListItem-test.tsx.snap b/test/unit-tests/components/views/polls/pollHistory/__snapshots__/PollListItem-test.tsx.snap
index 9db0dd9da3..558e4ef98a 100644
--- a/test/unit-tests/components/views/polls/pollHistory/__snapshots__/PollListItem-test.tsx.snap
+++ b/test/unit-tests/components/views/polls/pollHistory/__snapshots__/PollListItem-test.tsx.snap
@@ -16,9 +16,18 @@ exports[` renders a poll 1`] = `
01/01/70
-
+ fill="currentColor"
+ height="1em"
+ viewBox="0 0 24 24"
+ width="1em"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+
+
diff --git a/test/unit-tests/components/views/polls/pollHistory/__snapshots__/PollListItemEnded-test.tsx.snap b/test/unit-tests/components/views/polls/pollHistory/__snapshots__/PollListItemEnded-test.tsx.snap
index fb3968be94..635ef0e140 100644
--- a/test/unit-tests/components/views/polls/pollHistory/__snapshots__/PollListItemEnded-test.tsx.snap
+++ b/test/unit-tests/components/views/polls/pollHistory/__snapshots__/PollListItemEnded-test.tsx.snap
@@ -16,9 +16,21 @@ exports[` renders a poll with no responses 1`] = `
-
+ fill="currentColor"
+ height="1em"
+ viewBox="0 0 24 24"
+ width="1em"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+
+
+
diff --git a/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap b/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap
index 0404c983b0..557ac3f688 100644
--- a/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap
+++ b/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap
@@ -121,7 +121,19 @@ exports[` should render 1`] = `
role="button"
tabindex="0"
>
-
+
should render 1`] = `
role="button"
tabindex="0"
>
-
+