diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png
index a84f3ac260..6bc9d4696e 100644
Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png differ
diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-primary-filters-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-primary-filters-linux.png
index 8c056bc754..fd60bb3d53 100644
Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-primary-filters-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-primary-filters-linux.png differ
diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unselected-primary-filters-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unselected-primary-filters-linux.png
index 06314b5213..1cff3fea46 100644
Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unselected-primary-filters-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unselected-primary-filters-linux.png differ
diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png
index 313f6a1d14..43cae04270 100644
Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png differ
diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png
index a1106b4dc3..f9417d52da 100644
Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png differ
diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png
index 38b0bf2fff..d5a2d8a4c0 100644
Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png differ
diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png
index 78bca99706..fb8aabd6bf 100644
Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png differ
diff --git a/res/css/views/rooms/RoomListPanel/_RoomListHeaderView.pcss b/res/css/views/rooms/RoomListPanel/_RoomListHeaderView.pcss
index 595f47f9c8..9af934c308 100644
--- a/res/css/views/rooms/RoomListPanel/_RoomListHeaderView.pcss
+++ b/res/css/views/rooms/RoomListPanel/_RoomListHeaderView.pcss
@@ -21,10 +21,6 @@
}
}
- button {
- color: var(--cpd-color-icon-secondary);
- }
-
.mx_SpaceMenu_button {
svg {
transition: transform 0.1s linear;
diff --git a/src/components/views/rooms/NotificationDecoration.tsx b/src/components/views/rooms/NotificationDecoration.tsx
index 4a2f62cb5a..3266445782 100644
--- a/src/components/views/rooms/NotificationDecoration.tsx
+++ b/src/components/views/rooms/NotificationDecoration.tsx
@@ -7,7 +7,7 @@
import React, { type HTMLProps, type JSX } from "react";
import MentionIcon from "@vector-im/compound-design-tokens/assets/web/icons/mention";
-import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error";
+import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid";
import NotificationOffIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications-off-solid";
import { UnreadCounter, Unread } from "@vector-im/compound-web";
diff --git a/src/components/views/rooms/RoomListPanel/RoomListHeaderView.tsx b/src/components/views/rooms/RoomListPanel/RoomListHeaderView.tsx
index e07b886f54..ba4fdc5ca4 100644
--- a/src/components/views/rooms/RoomListPanel/RoomListHeaderView.tsx
+++ b/src/components/views/rooms/RoomListPanel/RoomListHeaderView.tsx
@@ -47,7 +47,7 @@ export function RoomListHeaderView(): JSX.Element {
) : (
vm.createChatRoom(e.nativeEvent)}>
-
+
)}
@@ -76,7 +76,7 @@ function SpaceMenu({ vm }: SpaceMenuProps): JSX.Element {
align="start"
trigger={
-
+
}
>
@@ -135,7 +135,7 @@ function ComposeMenu({ vm }: ComposeMenuProps): JSX.Element {
align="start"
trigger={
-
+
}
>
diff --git a/src/components/views/settings/encryption/ResetIdentityBody.tsx b/src/components/views/settings/encryption/ResetIdentityBody.tsx
new file mode 100644
index 0000000000..f2c339ca4d
--- /dev/null
+++ b/src/components/views/settings/encryption/ResetIdentityBody.tsx
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2024-2025 New Vector 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 { Button, InlineSpinner, VisualList, VisualListItem } from "@vector-im/compound-web";
+import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
+import InfoIcon from "@vector-im/compound-design-tokens/assets/web/icons/info";
+import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid";
+import React, { type JSX, useState, type MouseEventHandler } from "react";
+
+import { _t } from "../../../../languageHandler";
+import { EncryptionCard } from "./EncryptionCard";
+import { uiAuthCallback } from "../../../../CreateCrossSigning";
+import { EncryptionCardButtons } from "./EncryptionCardButtons";
+import { EncryptionCardEmphasisedContent } from "./EncryptionCardEmphasisedContent";
+import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
+
+interface ResetIdentityBodyProps {
+ /**
+ * Called when the identity is reset.
+ */
+ onFinish: MouseEventHandler;
+ /**
+ * Called when the cancel button is clicked.
+ */
+ onCancelClick: () => void;
+
+ /**
+ * The variant of the panel to show. We show more warnings in the 'compromised' variant (no use in showing a user
+ * this warning if they have to reset because they no longer have their key)
+ */
+ variant: ResetIdentityBodyVariant;
+}
+
+/**
+ * "compromised" is shown when the user chooses 'reset' explicitly in settings, usually because they believe their
+ * identity has been compromised.
+ *
+ * "sync_failed" is shown when the user tried to recover their identity but the process failed, probably because
+ * the required information is missing from recovery.
+ *
+ * "forgot" is shown when the user has just forgotten their passphrase.
+ */
+export type ResetIdentityBodyVariant = "compromised" | "forgot" | "sync_failed";
+
+/**
+ * User interface component allowing the user to reset their cryptographic identity.
+ *
+ * Used by {@link ResetIdentityPanel}.
+ */
+export function ResetIdentityBody({ onCancelClick, onFinish, variant }: ResetIdentityBodyProps): JSX.Element {
+ const matrixClient = useMatrixClientContext();
+
+ // After the user clicks "Continue", we disable the button so it can't be
+ // clicked again, and warn the user not to close the window.
+ const [inProgress, setInProgress] = useState(false);
+
+ return (
+
+
+
+
+ {_t("settings|encryption|advanced|breadcrumb_first_description")}
+
+
+ {_t("settings|encryption|advanced|breadcrumb_second_description")}
+
+
+ {_t("settings|encryption|advanced|breadcrumb_third_description")}
+
+
+ {variant === "compromised" && {_t("settings|encryption|advanced|breadcrumb_warning")}}
+
+
+
+ {inProgress ? (
+
+
+ {_t("settings|encryption|advanced|do_not_close_warning")}
+
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+function titleForVariant(variant: ResetIdentityBodyVariant): string {
+ switch (variant) {
+ case "compromised":
+ return _t("settings|encryption|advanced|breadcrumb_title");
+ case "sync_failed":
+ return _t("settings|encryption|advanced|breadcrumb_title_sync_failed");
+
+ default:
+ case "forgot":
+ return _t("settings|encryption|advanced|breadcrumb_title_forgot");
+ }
+}
diff --git a/src/components/views/settings/encryption/ResetIdentityPanel.tsx b/src/components/views/settings/encryption/ResetIdentityPanel.tsx
index f77b6d1138..dc733a1967 100644
--- a/src/components/views/settings/encryption/ResetIdentityPanel.tsx
+++ b/src/components/views/settings/encryption/ResetIdentityPanel.tsx
@@ -5,18 +5,11 @@
* Please see LICENSE files in the repository root for full details.
*/
-import { Breadcrumb, Button, InlineSpinner, VisualList, VisualListItem } from "@vector-im/compound-web";
-import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
-import InfoIcon from "@vector-im/compound-design-tokens/assets/web/icons/info";
-import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid";
-import React, { type JSX, useState, type MouseEventHandler } from "react";
+import { Breadcrumb } from "@vector-im/compound-web";
+import React, { type JSX, type MouseEventHandler } from "react";
import { _t } from "../../../../languageHandler";
-import { EncryptionCard } from "./EncryptionCard";
-import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
-import { uiAuthCallback } from "../../../../CreateCrossSigning";
-import { EncryptionCardButtons } from "./EncryptionCardButtons";
-import { EncryptionCardEmphasisedContent } from "./EncryptionCardEmphasisedContent";
+import { ResetIdentityBody, type ResetIdentityBodyVariant } from "./ResetIdentityBody";
interface ResetIdentityPanelProps {
/**
@@ -29,33 +22,17 @@ interface ResetIdentityPanelProps {
onCancelClick: () => void;
/**
- * The variant of the panel to show. We show more warnings in the 'compromised' variant (no use in showing a user
- * this warning if they have to reset because they no longer have their key)
+ * Which variant of this panel to show.
*/
- variant: ResetIdentityPanelVariant;
+ variant: ResetIdentityBodyVariant;
}
/**
- * "compromised" is shown when the user chooses 'reset' explicitly in settings, usually because they believe their
- * identity has been compromised.
+ * The Encryption Settings panel for resetting the identity of the current user.
*
- * "sync_failed" is shown when the user tried to recover their identity but the process failed, probably because
- * the required information is missing from recovery.
- *
- * "forgot" is shown when the user has just forgotten their passphrase.
- */
-export type ResetIdentityPanelVariant = "compromised" | "forgot" | "sync_failed";
-
-/**
- * The panel for resetting the identity of the current user.
+ * A thin wrapper around {@link ResetIdentityBody}, just adding breadcrumbs.
*/
export function ResetIdentityPanel({ onCancelClick, onFinish, variant }: ResetIdentityPanelProps): JSX.Element {
- const matrixClient = useMatrixClientContext();
-
- // After the user clicks "Continue", we disable the button so it can't be
- // clicked again, and warn the user not to close the window.
- const [inProgress, setInProgress] = useState(false);
-
return (
<>
-
-
-
-
- {_t("settings|encryption|advanced|breadcrumb_first_description")}
-
-
- {_t("settings|encryption|advanced|breadcrumb_second_description")}
-
-
- {_t("settings|encryption|advanced|breadcrumb_third_description")}
-
-
- {variant === "compromised" && {_t("settings|encryption|advanced|breadcrumb_warning")}}
-
-
-
- {inProgress ? (
-
-
- {_t("settings|encryption|advanced|do_not_close_warning")}
-
-
- ) : (
-
- )}
-
-
+
>
);
}
-
-function titleForVariant(variant: ResetIdentityPanelVariant): string {
- switch (variant) {
- case "compromised":
- return _t("settings|encryption|advanced|breadcrumb_title");
- case "sync_failed":
- return _t("settings|encryption|advanced|breadcrumb_title_sync_failed");
-
- default:
- case "forgot":
- return _t("settings|encryption|advanced|breadcrumb_title_forgot");
- }
-}
diff --git a/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx b/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx
index e2c9f443ca..7cd0c1d7b5 100644
--- a/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx
@@ -20,7 +20,8 @@ import SetupEncryptionDialog from "../../../dialogs/security/SetupEncryptionDial
import { SettingsSection } from "../../shared/SettingsSection";
import { SettingsSubheader } from "../../SettingsSubheader";
import { AdvancedPanel } from "../../encryption/AdvancedPanel";
-import { ResetIdentityPanel, type ResetIdentityPanelVariant } from "../../encryption/ResetIdentityPanel";
+import { ResetIdentityPanel } from "../../encryption/ResetIdentityPanel";
+import { type ResetIdentityBodyVariant } from "../../encryption/ResetIdentityBody";
import { RecoveryPanelOutOfSync } from "../../encryption/RecoveryPanelOutOfSync";
import { useTypedEventEmitter } from "../../../../../hooks/useEventEmitter";
import { KeyStoragePanel } from "../../encryption/KeyStoragePanel";
@@ -147,7 +148,7 @@ export function EncryptionUserSettingsTab({ initialState = "loading" }: Props):
* Given what state we want the tab to be in, what variant of the
* ResetIdentityPanel do we need?
*/
-function findResetVariant(state: State): ResetIdentityPanelVariant {
+function findResetVariant(state: State): ResetIdentityBodyVariant {
switch (state) {
case "reset_identity_compromised":
return "compromised";
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 90b9357b98..a93d3964c8 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2127,7 +2127,7 @@
"favourite": "Favourites",
"people": "People",
"rooms": "Rooms",
- "unread": "Unread"
+ "unread": "Unreads"
},
"home_menu_label": "Home options",
"join_public_room_label": "Join public room",
diff --git a/src/utils/Image.ts b/src/utils/Image.ts
index eed70bdf32..ab7c67d477 100644
--- a/src/utils/Image.ts
+++ b/src/utils/Image.ts
@@ -31,13 +31,13 @@ export async function blobIsAnimated(mimeType: string | undefined, blob: Blob):
case "image/webp": {
// Only extended file format WEBP images support animation, so grab the expected data range and verify header.
// Based on https://developers.google.com/speed/webp/docs/riff_container#extended_file_format
- const arr = await blob.slice(0, 17).arrayBuffer();
+ const arr = await blob.slice(0, 21).arrayBuffer();
if (
arrayBufferReadStr(arr, 0, 4) === "RIFF" &&
arrayBufferReadStr(arr, 8, 4) === "WEBP" &&
arrayBufferReadStr(arr, 12, 4) === "VP8X"
) {
- const [flags] = arrayBufferRead(arr, 16, 1);
+ const [flags] = arrayBufferRead(arr, 20, 1);
// Flags: R R I L E X _A_ R (reversed)
const animationFlagMask = 1 << 1;
return (flags & animationFlagMask) != 0;
diff --git a/src/vector/platform/ElectronPlatform.tsx b/src/vector/platform/ElectronPlatform.tsx
index 19fa455168..3be158b18d 100644
--- a/src/vector/platform/ElectronPlatform.tsx
+++ b/src/vector/platform/ElectronPlatform.tsx
@@ -479,7 +479,7 @@ export default class ElectronPlatform extends BasePlatform {
const url = super.getOidcCallbackUrl();
url.protocol = "io.element.desktop";
// Trim the double slash into a single slash to comply with https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
- if (url.href.startsWith(`${url.protocol}://`)) {
+ if (url.href.startsWith(`${url.protocol}//`)) {
url.href = url.href.replace("://", ":/");
}
return url;
diff --git a/test/unit-tests/Image-test.ts b/test/unit-tests/Image-test.ts
index 149ee0ff26..966236bc8a 100644
--- a/test/unit-tests/Image-test.ts
+++ b/test/unit-tests/Image-test.ts
@@ -51,6 +51,13 @@ describe("Image", () => {
expect(await blobIsAnimated("image/webp", img)).toBeFalsy();
});
+ it("Static WEBP in extended file format", async () => {
+ const img = new Blob([
+ fs.readFileSync(path.resolve(__dirname, "images", "static-logo-extended-file-format.webp")),
+ ]);
+ expect(await blobIsAnimated("image/webp", img)).toBeFalsy();
+ });
+
it("Animated PNG", async () => {
const img = new Blob([fs.readFileSync(path.resolve(__dirname, "images", "animated-logo.apng"))]);
expect(await blobIsAnimated("image/png", img)).toBeTruthy();
diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx
index 6ee1f5e3e4..bf92272e08 100644
--- a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx
+++ b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx
@@ -73,7 +73,7 @@ describe("RoomListViewModel", () => {
// should have 4 filters
expect(vm.current.primaryFilters).toHaveLength(4);
// check the order
- for (const [i, name] of ["Unread", "Favourites", "People", "Rooms"].entries()) {
+ for (const [i, name] of ["Unreads", "Favourites", "People", "Rooms"].entries()) {
expect(vm.current.primaryFilters[i].name).toEqual(name);
expect(vm.current.primaryFilters[i].active).toEqual(false);
}
@@ -218,9 +218,13 @@ describe("RoomListViewModel", () => {
[
"Mentions only",
{ secondary: SecondaryFilters.MentionsOnly, filterKey: FilterKey.MentionsFilter },
- "Unread",
+ "Unreads",
+ ],
+ [
+ "Invites only",
+ { secondary: SecondaryFilters.InvitesOnly, filterKey: FilterKey.InvitesFilter },
+ "Unreads",
],
- ["Invites only", { secondary: SecondaryFilters.InvitesOnly, filterKey: FilterKey.InvitesFilter }, "Unread"],
[
"Invites only",
{ secondary: SecondaryFilters.InvitesOnly, filterKey: FilterKey.InvitesFilter },
diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListHeaderView-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListHeaderView-test.tsx.snap
index 731a333f75..5dd6c23699 100644
--- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListHeaderView-test.tsx.snap
+++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListHeaderView-test.tsx.snap
@@ -35,6 +35,7 @@ exports[` compose menu should display the compose menu 1`]
style="--cpd-icon-button-size: 100%;"
>
compose menu should display the compose menu 1`]
style="--cpd-icon-button-size: 100%;"
>
compose menu should not display the compose menu
style="--cpd-icon-button-size: 100%;"
>
compose menu should not display the compose menu
style="--cpd-icon-button-size: 100%;"
>
space menu should display the space menu 1`] = `
style="--cpd-icon-button-size: 100%;"
>
space menu should display the space menu 1`] = `
style="--cpd-icon-button-size: 100%;"
>
space menu should not display the space menu 1`]
style="--cpd-icon-button-size: 100%;"
>
should not render the RoomListSearch component when U
style="--cpd-icon-button-size: 100%;"
>
should not render the RoomListSearch component when U
role="button"
tabindex="0"
>
- Unread
+ Unreads
should render the RoomListSearch component when UICom
style="--cpd-icon-button-size: 100%;"
>
should render the RoomListSearch component when UICom
role="button"
tabindex="0"
>
- Unread
+ Unreads
should render the unset message decoration 1
xmlns="http://www.w3.org/2000/svg"
>
diff --git a/test/unit-tests/components/views/settings/SetIdServer-test.tsx b/test/unit-tests/components/views/settings/SetIdServer-test.tsx
index d92b7c2737..cf8fb45f0d 100644
--- a/test/unit-tests/components/views/settings/SetIdServer-test.tsx
+++ b/test/unit-tests/components/views/settings/SetIdServer-test.tsx
@@ -41,7 +41,7 @@ describe("", () => {
});
it("should allow setting an identity server", async () => {
- const { getByLabelText, getByRole } = render(getComponent());
+ const { getByLabelText, getByRole, findByRole } = render(getComponent());
fetchMock.get("https://identity.example.org/_matrix/identity/v2", {
body: {},
@@ -56,14 +56,14 @@ describe("", () => {
const identServerField = getByLabelText("Enter a new identity server");
await userEvent.type(identServerField, "https://identity.example.org");
await userEvent.click(getByRole("button", { name: "Change" }));
- await userEvent.click(getByRole("button", { name: "Continue" }));
+ await userEvent.click(await findByRole("button", { name: "Continue" }));
});
it("should clear input on cancel", async () => {
- const { getByLabelText, getByRole } = render(getComponent());
+ const { getByLabelText, findByRole } = render(getComponent());
const identServerField = getByLabelText("Enter a new identity server");
await userEvent.type(identServerField, "https://identity.example.org");
- await userEvent.click(getByRole("button", { name: "Reset" }));
+ await userEvent.click(await findByRole("button", { name: "Reset" }));
expect((identServerField as HTMLInputElement).value).toEqual("");
});
diff --git a/test/unit-tests/images/static-logo-extended-file-format.webp b/test/unit-tests/images/static-logo-extended-file-format.webp
new file mode 100644
index 0000000000..bb4364374b
Binary files /dev/null and b/test/unit-tests/images/static-logo-extended-file-format.webp differ