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