From 4b9382f888e1e37830aebb0baaaf756f9991f9f8 Mon Sep 17 00:00:00 2001 From: Florian D Date: Thu, 13 Feb 2025 11:53:51 +0100 Subject: [PATCH 01/53] New room list: hide favourites and people meta spaces (#29241) * feat(new room list)!: hide Favourites and People meta spaces when the new room list is enabled * test(space store): add testcase for new labs flag * feat(quick settings): hide pin to sidebar and more options and add extra margin --- res/css/structures/_QuickSettingsButton.pcss | 6 ++ .../tabs/user/SidebarUserSettingsTab.tsx | 65 +++++++++------ .../views/spaces/QuickSettingsButton.tsx | 83 +++++++++++-------- src/stores/spaces/SpaceStore.ts | 18 +++- test/unit-tests/stores/SpaceStore-test.ts | 11 +++ 5 files changed, 120 insertions(+), 63 deletions(-) diff --git a/res/css/structures/_QuickSettingsButton.pcss b/res/css/structures/_QuickSettingsButton.pcss index 44e0ded064..52aa2377ac 100644 --- a/res/css/structures/_QuickSettingsButton.pcss +++ b/res/css/structures/_QuickSettingsButton.pcss @@ -104,6 +104,12 @@ Please see LICENSE files in the repository root for full details. } } +.mx_QuickSettingsButton_ContextMenuWrapper_new_room_list { + .mx_QuickThemeSwitcher { + margin-top: var(--cpd-space-2x); + } +} + .mx_QuickSettingsButton_icon { // TODO remove when all icons have fill=currentColor * { diff --git a/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx index 067ec9a124..ccf717e881 100644 --- a/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx @@ -72,6 +72,9 @@ const SidebarUserSettingsTab: React.FC = () => { PosthogTrackers.trackInteraction("WebSettingsSidebarTabSpacesCheckbox", event, 1); }; + // "Favourites" and "People" meta spaces are not available in the new room list + const newRoomListEnabled = useSettingValue("feature_new_room_list"); + return ( @@ -109,33 +112,43 @@ const SidebarUserSettingsTab: React.FC = () => { - - - - {_t("common|favourites")} - - - {_t("settings|sidebar|metaspaces_favourites_description")} - - + {!newRoomListEnabled && ( + <> + + + + {_t("common|favourites")} + + + {_t("settings|sidebar|metaspaces_favourites_description")} + + - - - - {_t("common|people")} - - - {_t("settings|sidebar|metaspaces_people_description")} - - + + + + {_t("common|people")} + + + {_t("settings|sidebar|metaspaces_people_description")} + + + + )} )} -

- - {_t("quick_settings|metaspace_section")} -

- - - - {_t("common|favourites")} - - - - {_t("common|people")} - - { - closeMenu(); - defaultDispatcher.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Sidebar, - }); - }} - > - - {_t("quick_settings|sidebar_settings")} - + {!newRoomListEnabled && ( + <> +

+ + {_t("quick_settings|metaspace_section")} +

+ + + {_t("common|favourites")} + + + + {_t("common|people")} + + { + closeMenu(); + defaultDispatcher.dispatch({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Sidebar, + }); + }} + > + + {_t("quick_settings|sidebar_settings")} + + + )} ); diff --git a/src/stores/spaces/SpaceStore.ts b/src/stores/spaces/SpaceStore.ts index 13179f8c86..8e8b4cc273 100644 --- a/src/stores/spaces/SpaceStore.ts +++ b/src/stores/spaces/SpaceStore.ts @@ -162,6 +162,20 @@ export class SpaceStoreClass extends AsyncStoreWithClient { SettingsStore.monitorSetting("feature_dynamic_room_predecessors", null); } + /** + * Get the order of meta spaces to display in the space panel. + * + * This accessor should be removed when the "feature_new_room_list" labs flag is removed. + * "People" and "Favourites" will be removed from the "metaSpaceOrder" array and this filter will no longer be needed. + * @private + */ + private get metaSpaceOrder(): MetaSpace[] { + if (!SettingsStore.getValue("feature_new_room_list")) return metaSpaceOrder; + + // People and Favourites are not shown when the new room list is enabled + return metaSpaceOrder.filter((space) => space !== MetaSpace.People && space !== MetaSpace.Favourites); + } + public get invitedSpaces(): Room[] { return Array.from(this._invitedSpaces); } @@ -1164,7 +1178,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const oldMetaSpaces = this._enabledMetaSpaces; const enabledMetaSpaces = SettingsStore.getValue("Spaces.enabledMetaSpaces"); - this._enabledMetaSpaces = metaSpaceOrder.filter((k) => enabledMetaSpaces[k]); + this._enabledMetaSpaces = this.metaSpaceOrder.filter((k) => enabledMetaSpaces[k]); this._allRoomsInHome = SettingsStore.getValue("Spaces.allRoomsInHome"); this.sendUserProperties(); @@ -1278,7 +1292,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { case "Spaces.enabledMetaSpaces": { const newValue = SettingsStore.getValue("Spaces.enabledMetaSpaces"); - const enabledMetaSpaces = metaSpaceOrder.filter((k) => newValue[k]); + const enabledMetaSpaces = this.metaSpaceOrder.filter((k) => newValue[k]); if (arrayHasDiff(this._enabledMetaSpaces, enabledMetaSpaces)) { const hadPeopleOrHomeEnabled = this.enabledMetaSpaces.some((s) => { return s === MetaSpace.Home || s === MetaSpace.People; diff --git a/test/unit-tests/stores/SpaceStore-test.ts b/test/unit-tests/stores/SpaceStore-test.ts index 9855b264b5..7471ae118b 100644 --- a/test/unit-tests/stores/SpaceStore-test.ts +++ b/test/unit-tests/stores/SpaceStore-test.ts @@ -141,6 +141,8 @@ describe("SpaceStore", () => { }); afterEach(async () => { + // Disable the new room list feature flag + await SettingsStore.setValue("feature_new_room_list", null, SettingLevel.DEVICE, false); await testUtils.resetAsyncStoreWithClient(store); }); @@ -1391,6 +1393,15 @@ describe("SpaceStore", () => { removeListener(); }); + it("Favourites and People meta spaces should not be returned when the feature_new_room_list labs flag is enabled", async () => { + // Enable the new room list + await SettingsStore.setValue("feature_new_room_list", null, SettingLevel.DEVICE, true); + + await run(); + // Favourites and People meta spaces should not be returned + expect(SpaceStore.instance.enabledMetaSpaces).toStrictEqual([MetaSpace.Home, MetaSpace.Orphans]); + }); + describe("when feature_dynamic_room_predecessors is not enabled", () => { beforeAll(() => { jest.spyOn(SettingsStore, "getValue").mockImplementation( From 85f80b1d0ab463320dadc1c052ad9d6fa9871eb2 Mon Sep 17 00:00:00 2001 From: Florian D Date: Thu, 13 Feb 2025 16:18:41 +0100 Subject: [PATCH 02/53] Replace `focus_room_filter` dispatch by `Action.OpenSpotlight` (#29259) * refactor(room search): replace `focus_room_filter` dispatch by `Action.OpenSpotlight` * test(LoggedInView): add test to Ctrl+k shortcut --- src/components/structures/LoggedInView.tsx | 4 +--- src/components/structures/RoomSearch.tsx | 17 ----------------- .../components/structures/LoggedInView-test.tsx | 8 ++++++++ 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 3422598503..5969dd1ccb 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -501,9 +501,7 @@ class LoggedInView extends React.Component { handled = true; break; case KeyBindingAction.FilterRooms: - dis.dispatch({ - action: "focus_room_filter", - }); + dis.fire(Action.OpenSpotlight); handled = true; break; case KeyBindingAction.ToggleUserMenu: diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 05731c79a0..439c1e47fe 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -11,7 +11,6 @@ import * as React from "react"; import { ALTERNATE_KEY_NAME } from "../../accessibility/KeyboardShortcuts"; import defaultDispatcher from "../../dispatcher/dispatcher"; -import { type ActionPayload } from "../../dispatcher/payloads"; import { IS_MAC, Key } from "../../Keyboard"; import { _t } from "../../languageHandler"; import AccessibleButton from "../views/elements/AccessibleButton"; @@ -22,26 +21,10 @@ interface IProps { } export default class RoomSearch extends React.PureComponent { - private dispatcherRef?: string; - - public componentDidMount(): void { - this.dispatcherRef = defaultDispatcher.register(this.onAction); - } - - public componentWillUnmount(): void { - defaultDispatcher.unregister(this.dispatcherRef); - } - private openSpotlight(): void { defaultDispatcher.fire(Action.OpenSpotlight); } - private onAction = (payload: ActionPayload): void => { - if (payload.action === "focus_room_filter") { - this.openSpotlight(); - } - }; - public render(): React.ReactNode { const classes = classNames( { diff --git a/test/unit-tests/components/structures/LoggedInView-test.tsx b/test/unit-tests/components/structures/LoggedInView-test.tsx index cddbf312f6..beca86ddcf 100644 --- a/test/unit-tests/components/structures/LoggedInView-test.tsx +++ b/test/unit-tests/components/structures/LoggedInView-test.tsx @@ -421,6 +421,14 @@ describe("", () => { expect(defaultDispatcher.dispatch).not.toHaveBeenCalledWith({ action: Action.ViewHomePage }); }); + it("should open spotlight when Ctrl+k is fired", async () => { + jest.spyOn(defaultDispatcher, "fire"); + + getComponent(); + await userEvent.keyboard("{Control>}k{/Control}"); + expect(defaultDispatcher.fire).toHaveBeenCalledWith(Action.OpenSpotlight); + }); + describe("timezone updates", () => { const userTimezone = "Europe/London"; const originalController = SETTINGS["userTimezonePublish"].controller; From 2abd5342c274b6da11de1d1a44ada65d18f13df8 Mon Sep 17 00:00:00 2001 From: Florian D Date: Thu, 13 Feb 2025 16:49:09 +0100 Subject: [PATCH 03/53] New room list: add search section (#29251) * feat(new room list): move `RoomListView` to its own folder and add styling * feat(new room list): add search section * test(new room list): add tests for `RoomListSearch` * test(new room list): add tests for `RoomListView` * test(e2e): add method to close notification toast to `ElementAppPage` * test(e2e): add tests for the search section * test(e2e): add tests for the room list view * refactor: use Flex component * fix: loop icon size in search button * refactor: remove `focus_room_filter` listener --- .../room-list-view/room-list-search.spec.ts | 53 +++++++ .../room-list-view/room-list-view.spec.ts | 34 +++++ .../security-user-settings-tab.spec.ts | 8 +- playwright/pages/ElementAppPage.ts | 11 ++ .../search-section-linux.png | Bin 0 -> 3970 bytes .../room-list-view-linux.png | Bin 0 -> 6399 bytes res/css/_components.pcss | 2 + .../rooms/RoomListView/_RoomListSearch.pcss | 39 +++++ .../rooms/RoomListView/_RoomListView.pcss | 12 ++ src/components/structures/LeftPanel.tsx | 2 +- src/components/views/rooms/RoomListView.tsx | 14 -- .../rooms/RoomListView/RoomListSearch.tsx | 69 +++++++++ .../views/rooms/RoomListView/RoomListView.tsx | 33 ++++ .../views/rooms/RoomListView/index.ts | 8 + .../RoomListView/RoomListSearch-test.tsx | 84 +++++++++++ .../rooms/RoomListView/RoomListView-test.tsx | 43 ++++++ .../RoomListSearch-test.tsx.snap | 142 ++++++++++++++++++ .../__snapshots__/RoomListView-test.tsx.snap | 76 ++++++++++ 18 files changed, 609 insertions(+), 21 deletions(-) create mode 100644 playwright/e2e/left-panel/room-list-view/room-list-search.spec.ts create mode 100644 playwright/e2e/left-panel/room-list-view/room-list-view.spec.ts create mode 100644 playwright/snapshots/left-panel/room-list-view/room-list-search.spec.ts/search-section-linux.png create mode 100644 playwright/snapshots/left-panel/room-list-view/room-list-view.spec.ts/room-list-view-linux.png create mode 100644 res/css/views/rooms/RoomListView/_RoomListSearch.pcss create mode 100644 res/css/views/rooms/RoomListView/_RoomListView.pcss delete mode 100644 src/components/views/rooms/RoomListView.tsx create mode 100644 src/components/views/rooms/RoomListView/RoomListSearch.tsx create mode 100644 src/components/views/rooms/RoomListView/RoomListView.tsx create mode 100644 src/components/views/rooms/RoomListView/index.ts create mode 100644 test/unit-tests/components/views/rooms/RoomListView/RoomListSearch-test.tsx create mode 100644 test/unit-tests/components/views/rooms/RoomListView/RoomListView-test.tsx create mode 100644 test/unit-tests/components/views/rooms/RoomListView/__snapshots__/RoomListSearch-test.tsx.snap create mode 100644 test/unit-tests/components/views/rooms/RoomListView/__snapshots__/RoomListView-test.tsx.snap diff --git a/playwright/e2e/left-panel/room-list-view/room-list-search.spec.ts b/playwright/e2e/left-panel/room-list-view/room-list-search.spec.ts new file mode 100644 index 0000000000..028503f622 --- /dev/null +++ b/playwright/e2e/left-panel/room-list-view/room-list-search.spec.ts @@ -0,0 +1,53 @@ +/* + * Copyright 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 { type Page } from "@playwright/test"; + +import { test, expect } from "../../../element-web-test"; + +test.describe("Search section of the room list", () => { + test.use({ + labsFlags: ["feature_new_room_list"], + }); + + /** + * Get the search section of the room list + * @param page + */ + function getSearchSection(page: Page) { + return page.getByRole("search"); + } + + test.beforeEach(async ({ page, app, user }) => { + // The notification toast is displayed above the search section + await app.closeNotificationToast(); + }); + + test("should render the search section", { tag: "@screenshot" }, async ({ page, app, user }) => { + const searchSection = getSearchSection(page); + // exact=false to ignore the shortcut which is related to the OS + await expect(searchSection.getByRole("button", { name: "Search", exact: false })).toBeVisible(); + await expect(searchSection).toMatchScreenshot("search-section.png"); + }); + + test("should open the spotlight when the search button is clicked", async ({ page, app, user }) => { + const searchSection = getSearchSection(page); + await searchSection.getByRole("button", { name: "Search", exact: false }).click(); + // The spotlight should be displayed + await expect(page.getByRole("dialog", { name: "Search Dialog" })).toBeVisible(); + }); + + test("should open the room directory when the search button is clicked", async ({ page, app, user }) => { + const searchSection = getSearchSection(page); + await searchSection.getByRole("button", { name: "Explore rooms" }).click(); + const dialog = page.getByRole("dialog", { name: "Search Dialog" }); + // The room directory should be displayed + await expect(dialog).toBeVisible(); + // The public room filter should be displayed + await expect(dialog.getByText("Public rooms")).toBeVisible(); + }); +}); diff --git a/playwright/e2e/left-panel/room-list-view/room-list-view.spec.ts b/playwright/e2e/left-panel/room-list-view/room-list-view.spec.ts new file mode 100644 index 0000000000..7cd5122e8a --- /dev/null +++ b/playwright/e2e/left-panel/room-list-view/room-list-view.spec.ts @@ -0,0 +1,34 @@ +/* + * Copyright 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 { type Page } from "@playwright/test"; + +import { test, expect } from "../../../element-web-test"; + +test.describe("Search section of the room list", () => { + test.use({ + labsFlags: ["feature_new_room_list"], + }); + + /** + * Get the room list view + * @param page + */ + function getRoomListView(page: Page) { + return page.getByTestId("room-list-view"); + } + + test.beforeEach(async ({ page, app, user }) => { + // The notification toast is displayed above the search section + await app.closeNotificationToast(); + }); + + test("should render the room list view", { tag: "@screenshot" }, async ({ page, app, user }) => { + const roomListView = getRoomListView(page); + await expect(roomListView).toMatchScreenshot("room-list-view.png"); + }); +}); diff --git a/playwright/e2e/settings/security-user-settings-tab.spec.ts b/playwright/e2e/settings/security-user-settings-tab.spec.ts index b723d1398f..9b9439796d 100644 --- a/playwright/e2e/settings/security-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/security-user-settings-tab.spec.ts @@ -25,13 +25,9 @@ test.describe("Security user settings tab", () => { }, }); - test.beforeEach(async ({ page, user }) => { + test.beforeEach(async ({ page, app, user }) => { // Dismiss "Notification" toast - await page - .locator(".mx_Toast_toast", { hasText: "Notifications" }) - .getByRole("button", { name: "Dismiss" }) - .click(); - + await app.closeNotificationToast(); await page.locator(".mx_Toast_buttons").getByRole("button", { name: "Yes" }).click(); // Allow analytics }); diff --git a/playwright/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts index d530c75b54..15b475a5d1 100644 --- a/playwright/pages/ElementAppPage.ts +++ b/playwright/pages/ElementAppPage.ts @@ -202,4 +202,15 @@ export class ElementAppPage { } return this.page.locator(`id=${labelledById ?? describedById}`); } + + /** + * Close the notification toast + */ + public closeNotificationToast(): Promise { + // Dismiss "Notification" toast + return this.page + .locator(".mx_Toast_toast", { hasText: "Notifications" }) + .getByRole("button", { name: "Dismiss" }) + .click(); + } } diff --git a/playwright/snapshots/left-panel/room-list-view/room-list-search.spec.ts/search-section-linux.png b/playwright/snapshots/left-panel/room-list-view/room-list-search.spec.ts/search-section-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..6c2684e84964c41f2614af19fe003ca9c2518f73 GIT binary patch literal 3970 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 { return (
- +
); diff --git a/src/components/views/rooms/RoomListView.tsx b/src/components/views/rooms/RoomListView.tsx deleted file mode 100644 index c5f593decf..0000000000 --- a/src/components/views/rooms/RoomListView.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/* -Copyright 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 React from "react"; - -type IProps = unknown; - -export const RoomListView: React.FC = (props: IProps) => { - return
New Room List
; -}; diff --git a/src/components/views/rooms/RoomListView/RoomListSearch.tsx b/src/components/views/rooms/RoomListView/RoomListSearch.tsx new file mode 100644 index 0000000000..415e817ad9 --- /dev/null +++ b/src/components/views/rooms/RoomListView/RoomListSearch.tsx @@ -0,0 +1,69 @@ +/* + * Copyright 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 React, { type JSX } from "react"; +import { Button } from "@vector-im/compound-web"; +import ExploreIcon from "@vector-im/compound-design-tokens/assets/web/icons/explore"; +import SearchIcon from "@vector-im/compound-design-tokens/assets/web/icons/search"; + +import { IS_MAC, Key } from "../../../../Keyboard"; +import { _t } from "../../../../languageHandler"; +import { ALTERNATE_KEY_NAME } from "../../../../accessibility/KeyboardShortcuts"; +import { shouldShowComponent } from "../../../../customisations/helpers/UIComponents"; +import { UIComponent } from "../../../../settings/UIFeature"; +import { MetaSpace } from "../../../../stores/spaces"; +import { Action } from "../../../../dispatcher/actions"; +import PosthogTrackers from "../../../../PosthogTrackers"; +import defaultDispatcher from "../../../../dispatcher/dispatcher"; +import { Flex } from "../../../utils/Flex"; + +type RoomListSearchProps = { + /** + * Current active space + * The explore button is only displayed in the Home meta space + */ + activeSpace: string; +}; + +/** + * A search component to be displayed at the top of the room list + * The `Explore` button is displayed only in the Home meta space and when UIComponent.ExploreRooms is enabled. + */ +export function RoomListSearch({ activeSpace }: RoomListSearchProps): JSX.Element { + const displayExploreButton = activeSpace === MetaSpace.Home && shouldShowComponent(UIComponent.ExploreRooms); + + return ( + + + {displayExploreButton && ( + + + + +`; + +exports[` should hide the explore button when UIComponent.ExploreRooms is disabled 1`] = ` + + + +`; + +exports[` should hide the explore button when the active space is not MetaSpace.Home 1`] = ` + + + +`; diff --git a/test/unit-tests/components/views/rooms/RoomListView/__snapshots__/RoomListView-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListView/__snapshots__/RoomListView-test.tsx.snap new file mode 100644 index 0000000000..4ddc9ac5ec --- /dev/null +++ b/test/unit-tests/components/views/rooms/RoomListView/__snapshots__/RoomListView-test.tsx.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should not render the RoomListSearch component when UIComponent.FilterContainer is at false 1`] = ` + +
+ +`; + +exports[` should render the RoomListSearch component when UIComponent.FilterContainer is at true 1`] = ` + +
+ +
+
+`; From f9a85d37fa49e95351e54584f25e1ce56fd95d13 Mon Sep 17 00:00:00 2001 From: ElementRobot Date: Fri, 14 Feb 2025 07:14:47 +0100 Subject: [PATCH 04/53] [create-pull-request] automated change (#29253) Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com> --- playwright/testcontainers/synapse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 6445d3483a..94ecb557a4 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -25,7 +25,7 @@ import { type HomeserverContainer, type StartedHomeserverContainer } from "./Hom import { type StartedMatrixAuthenticationServiceContainer } from "./mas.ts"; import { Api, ClientServerApi, type Verb } from "../plugins/utils/api.ts"; -const TAG = "develop@sha256:56456456f52cb3b9d23a3e5e889e5a2e908784f0459d4bf759835be87e7e5888"; +const TAG = "develop@sha256:dfacd4d40994c77eb478fc5773913a38fbf07d593421a5410c5dafb8330ddd13"; const DEFAULT_CONFIG = { server_name: "localhost", From c47ce59478177f9f9464683583e9fb8d759b7e75 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 14 Feb 2025 10:58:20 +0000 Subject: [PATCH 05/53] Render reason for invite rejection. (#29257) * Render reason for invite rejection. * Add test * extra test --- src/TextForEvent.tsx | 5 +++- src/i18n/strings/en_EN.json | 1 + test/unit-tests/TextForEvent-test.ts | 43 ++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index d319114c8e..b63e5b2a00 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -191,7 +191,10 @@ function textForMemberEvent( case KnownMembership.Leave: if (ev.getSender() === ev.getStateKey()) { if (prevContent.membership === KnownMembership.Invite) { - return () => _t("timeline|m.room.member|reject_invite", { targetName }); + return () => + reason + ? _t("timeline|m.room.member|reject_invite_reason", { targetName, reason }) + : _t("timeline|m.room.member|reject_invite", { targetName }); } else { return () => reason diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b2903fce85..d88d29008e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3458,6 +3458,7 @@ "left_reason": "%(targetName)s left the room: %(reason)s", "no_change": "%(senderName)s made no change", "reject_invite": "%(targetName)s rejected the invitation", + "reject_invite_reason": "%(targetName)s rejected the invitation: %(reason)s", "remove_avatar": "%(senderName)s removed their profile picture", "remove_name": "%(senderName)s removed their display name (%(oldDisplayName)s)", "set_avatar": "%(senderName)s set a profile picture", diff --git a/test/unit-tests/TextForEvent-test.ts b/test/unit-tests/TextForEvent-test.ts index 7fc1a61b0a..a505d6510f 100644 --- a/test/unit-tests/TextForEvent-test.ts +++ b/test/unit-tests/TextForEvent-test.ts @@ -519,6 +519,49 @@ describe("TextForEvent", () => { ), ).toMatchInlineSnapshot(`"Andy changed their display name and profile picture"`); }); + + it("should handle rejected invites", () => { + expect( + textForEvent( + new MatrixEvent({ + type: "m.room.member", + sender: "@a:foo", + content: { + membership: KnownMembership.Leave, + }, + unsigned: { + prev_content: { + membership: KnownMembership.Invite, + }, + }, + state_key: "@a:foo", + }), + mockClient, + ), + ).toMatchInlineSnapshot(`"Member rejected the invitation"`); + }); + + it("should handle rejected invites with a reason", () => { + expect( + textForEvent( + new MatrixEvent({ + type: "m.room.member", + sender: "@a:foo", + content: { + membership: KnownMembership.Leave, + reason: "I don't want to be in this room.", + }, + unsigned: { + prev_content: { + membership: KnownMembership.Invite, + }, + }, + state_key: "@a:foo", + }), + mockClient, + ), + ).toMatchInlineSnapshot(`"Member rejected the invitation: I don't want to be in this room."`); + }); }); describe("textForJoinRulesEvent()", () => { From 09db599fe0d0ffd56b78b96856ba58b131e188b0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:59:02 +0000 Subject: [PATCH 06/53] Minor cleanups to `initialiseDehydration` (#29261) * dehydration: fix documentation * initialiseDehydration: improve name ... to make it clearer that it does nothing if dehydration is disabled * initialiseDehydration: remove dependency on MatrixClientPeg We're trying to move away from relying on `MatrixClientPeg` everywhere, and this is a particularly easy win. --- src/MatrixClientPeg.ts | 4 ++-- .../dialogs/security/CreateSecretStorageDialog.tsx | 4 ++-- src/stores/SetupEncryptionStore.ts | 4 ++-- src/utils/device/dehydration.ts | 11 ++++++----- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 81a73ffccc..9682b41800 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -41,7 +41,7 @@ import PlatformPeg from "./PlatformPeg"; import { formatList } from "./utils/FormattingUtils"; import SdkConfig from "./SdkConfig"; import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts"; -import { initialiseDehydration } from "./utils/device/dehydration"; +import { initialiseDehydrationIfEnabled } from "./utils/device/dehydration"; export interface IMatrixClientCreds { homeserverUrl: string; @@ -347,7 +347,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { // is a new login, we will start dehydration after Secret Storage is // unlocked. try { - await initialiseDehydration({ onlyIfKeyCached: true, rehydrate: false }, this.matrixClient); + await initialiseDehydrationIfEnabled(this.matrixClient, { onlyIfKeyCached: true, rehydrate: false }); } catch (e) { // We may get an error dehydrating, such as if cross-signing and // SSSS are not set up yet. Just log the error and continue. diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 14ad5d3039..daefa267bc 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -37,7 +37,7 @@ import Spinner from "../../../../components/views/elements/Spinner"; import InteractiveAuthDialog from "../../../../components/views/dialogs/InteractiveAuthDialog"; import { type IValidationResult } from "../../../../components/views/elements/Validation"; import PassphraseConfirmField from "../../../../components/views/auth/PassphraseConfirmField"; -import { initialiseDehydration } from "../../../../utils/device/dehydration"; +import { initialiseDehydrationIfEnabled } from "../../../../utils/device/dehydration"; // I made a mistake while converting this and it has to be fixed! enum Phase { @@ -273,7 +273,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { - client = client || MatrixClientPeg.safeGet(); +export async function initialiseDehydrationIfEnabled( + client: MatrixClient, + opts: StartDehydrationOpts = {}, +): Promise { const crypto = client.getCrypto(); if (await deviceDehydrationEnabled(client, crypto)) { logger.log("Device dehydration enabled"); From f822653d656f3568ca3832102719c77e35d79152 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:08:59 +0000 Subject: [PATCH 07/53] Dehydration: enable dehydrated device on "Set up recovery" (#29265) * playwright/dehydration: update check The old "Security & Privacy" tab is going away, so we need a new way to check for dehydrated device existence. * Dehydration: enable dehydrated device on "Set up recovery" Clicking "Set up recovery" should set up a dehydrated device, if that feature is enabled. Fixes #29135 * Empty commit ... to wake up the CLA bot --- playwright/e2e/crypto/dehydration.spec.ts | 49 ++++++++++++++++--- .../settings/encryption/ChangeRecoveryKey.tsx | 10 ++-- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts index ca6485d88e..472ee39493 100644 --- a/playwright/e2e/crypto/dehydration.spec.ts +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -10,6 +10,7 @@ import { test, expect } from "../../element-web-test"; import { isDendrite } from "../../plugins/homeserver/dendrite"; import { completeCreateSecretStorageDialog, createBot, logIntoElement } from "./utils.ts"; import { type Client } from "../../pages/client.ts"; +import { type ElementAppPage } from "../../pages/ElementAppPage.ts"; const NAME = "Alice"; @@ -49,13 +50,7 @@ test.describe("Dehydration", () => { await completeCreateSecretStorageDialog(page); - // Open the settings again - await app.settings.openUserSettings("Security & Privacy"); - - // The Security tab should indicate that there is a dehydrated device present - await expect(securityTab.getByText("Offline device enabled")).toBeVisible(); - - await app.settings.closeDialog(); + await expectDehydratedDeviceEnabled(app); // the dehydrated device gets created with the name "Dehydrated // device". We want to make sure that it is not visible as a normal @@ -64,6 +59,33 @@ test.describe("Dehydration", () => { await expect(sessionsTab.getByText("Dehydrated device")).not.toBeVisible(); }); + test("'Set up recovery' creates dehydrated device", async ({ app, credentials, page }) => { + await logIntoElement(page, credentials); + + const settingsDialogLocator = await app.settings.openUserSettings("Encryption"); + await settingsDialogLocator.getByRole("button", { name: "Set up recovery" }).click(); + + // First it displays an informative panel about the recovery key + await expect(settingsDialogLocator.getByRole("heading", { name: "Set up recovery" })).toBeVisible(); + await settingsDialogLocator.getByRole("button", { name: "Continue" }).click(); + + // Next, it displays the new recovery key. We click on the copy button. + await expect(settingsDialogLocator.getByText("Save your recovery key somewhere safe")).toBeVisible(); + await settingsDialogLocator.getByRole("button", { name: "Copy" }).click(); + const recoveryKey = await app.getClipboard(); + await settingsDialogLocator.getByRole("button", { name: "Continue" }).click(); + + await expect( + settingsDialogLocator.getByText("Enter your recovery key to confirm", { exact: true }), + ).toBeVisible(); + await settingsDialogLocator.getByRole("textbox").fill(recoveryKey); + await settingsDialogLocator.getByRole("button", { name: "Finish set up" }).click(); + + await app.settings.closeDialog(); + + await expectDehydratedDeviceEnabled(app); + }); + test("Reset recovery key during login re-creates dehydrated device", async ({ page, homeserver, @@ -109,3 +131,16 @@ async function getDehydratedDeviceIds(client: Client): Promise { ); }); } + +/** Wait for our user to have a dehydrated device */ +async function expectDehydratedDeviceEnabled(app: ElementAppPage): Promise { + // It might be nice to do this via the UI, but currently this info is not exposed via the UI. + // + // Note we might have to wait for the device list to be refreshed, so we wrap in `expect.poll`. + await expect + .poll(async () => { + const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client); + return dehydratedDeviceIds.length; + }) + .toEqual(1); +} diff --git a/src/components/views/settings/encryption/ChangeRecoveryKey.tsx b/src/components/views/settings/encryption/ChangeRecoveryKey.tsx index 979b4bee04..b149b396e1 100644 --- a/src/components/views/settings/encryption/ChangeRecoveryKey.tsx +++ b/src/components/views/settings/encryption/ChangeRecoveryKey.tsx @@ -26,6 +26,7 @@ import { EncryptionCard } from "./EncryptionCard"; import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; import { useAsyncMemo } from "../../../../hooks/useAsyncMemo"; import { copyPlaintext } from "../../../../utils/strings"; +import { initialiseDehydrationIfEnabled } from "../../../../utils/device/dehydration.ts"; import { withSecretStorageKeyCache } from "../../../../SecurityManager"; /** @@ -122,12 +123,13 @@ export function ChangeRecoveryKey({ try { // We need to enable the cache to avoid to prompt the user to enter the new key // when we will try to access the secret storage during the bootstrap - await withSecretStorageKeyCache(() => - crypto.bootstrapSecretStorage({ + await withSecretStorageKeyCache(async () => { + await crypto.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey: async () => recoveryKey, - }), - ); + }); + await initialiseDehydrationIfEnabled(matrixClient, { createNewKey: true }); + }); onFinish(); } catch (e) { logger.error("Failed to bootstrap secret storage", e); From 6dbc3b489a52674301308cb68772cf03407fad96 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 14 Feb 2025 16:23:30 +0000 Subject: [PATCH 08/53] Grow member list search when resizing the right panel (#29267) --- res/css/views/rooms/_MemberListHeaderView.pcss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/res/css/views/rooms/_MemberListHeaderView.pcss b/res/css/views/rooms/_MemberListHeaderView.pcss index 326cf84dd6..314bd74062 100644 --- a/res/css/views/rooms/_MemberListHeaderView.pcss +++ b/res/css/views/rooms/_MemberListHeaderView.pcss @@ -16,6 +16,7 @@ Please see LICENSE files in the repository root for full details. .mx_MemberListHeaderView_invite_small { margin-left: var(--cpd-space-3x); + margin-right: var(--cpd-space-4x); } .mx_MemberListHeaderView_invite_large { @@ -33,5 +34,7 @@ Please see LICENSE files in the repository root for full details. .mx_MemberListHeaderView_search { width: 240px; + flex-grow: 1; + margin-left: var(--cpd-space-4x); } } From a36553336781a3b0da258dc6c53f3805808b6a59 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 14 Feb 2025 16:44:34 +0000 Subject: [PATCH 09/53] ChangeRecoveryKey: error handling (#29262) * CreateSecretStorageDialog: error handling I'm fed up with setup operations in EW failing silently. Rather than leaving the user with a mysteriously broken client, let's at least tell them that something has gone wrong, so that they can report the issue and we can investigate. Obviously, showing an unactionable Error dialog is a last resort: ideally, we should handle the error ourselves, or give the user actionable steps to resolve the problem. But that takes significant design and engineering. Just swallowing errors is the worst of all possible options. * Fix typo in test name * Improve test coverage --- .../settings/encryption/ChangeRecoveryKey.tsx | 4 +-- src/utils/ErrorUtils.tsx | 27 +++++++++++++++ .../encryption/ChangeRecoveryKey-test.tsx | 34 ++++++++++++++++++- .../ChangeRecoveryKey-test.tsx.snap | 8 ++--- 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/components/views/settings/encryption/ChangeRecoveryKey.tsx b/src/components/views/settings/encryption/ChangeRecoveryKey.tsx index b149b396e1..866ea52d1d 100644 --- a/src/components/views/settings/encryption/ChangeRecoveryKey.tsx +++ b/src/components/views/settings/encryption/ChangeRecoveryKey.tsx @@ -19,7 +19,6 @@ import { } from "@vector-im/compound-web"; import CopyIcon from "@vector-im/compound-design-tokens/assets/web/icons/copy"; import KeyIcon from "@vector-im/compound-design-tokens/assets/web/icons/key-solid"; -import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../../languageHandler"; import { EncryptionCard } from "./EncryptionCard"; @@ -28,6 +27,7 @@ import { useAsyncMemo } from "../../../../hooks/useAsyncMemo"; import { copyPlaintext } from "../../../../utils/strings"; import { initialiseDehydrationIfEnabled } from "../../../../utils/device/dehydration.ts"; import { withSecretStorageKeyCache } from "../../../../SecurityManager"; +import { logErrorAndShowErrorDialog } from "../../../../utils/ErrorUtils.tsx"; /** * The possible states of the component. @@ -132,7 +132,7 @@ export function ChangeRecoveryKey({ }); onFinish(); } catch (e) { - logger.error("Failed to bootstrap secret storage", e); + logErrorAndShowErrorDialog("Failed to set up secret storage", e); } }} submitButtonLabel={ diff --git a/src/utils/ErrorUtils.tsx b/src/utils/ErrorUtils.tsx index 38ba489c7f..2772350a0c 100644 --- a/src/utils/ErrorUtils.tsx +++ b/src/utils/ErrorUtils.tsx @@ -8,11 +8,14 @@ Please see LICENSE files in the repository root for full details. import React, { type ReactNode } from "react"; import { MatrixError, ConnectionError } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; import { _t, _td, lookupString, type Tags, type TranslatedString, type TranslationKey } from "../languageHandler"; import SdkConfig from "../SdkConfig"; import { type ValidatedServerConfig } from "./ValidatedServerConfig"; import ExternalLink from "../components/views/elements/ExternalLink"; +import Modal from "../Modal.tsx"; +import ErrorDialog from "../components/views/dialogs/ErrorDialog.tsx"; export const resourceLimitStrings = { "monthly_active_user": _td("error|mau"), @@ -191,3 +194,27 @@ export function messageForConnectionError( return errorText; } + +/** + * Utility for handling unexpected errors: pops up the error dialog. + * + * Example usage: + * ``` + * try { + * /// complicated operation + * } catch (e) { + * logErrorAndShowErrorDialog("Failed complicated operation", e); + * } + * ``` + * + * This isn't particularly intended to be pretty; rather it lets the user know that *something* has gone wrong so that + * they can report a bug. The general idea is that it's better to let the user know of a failure, even if they + * can't do anything about it, than it is to fail silently with the appearance of success. + * + * @param title - Title for the error dialog. + * @param error - The thrown error. Becomes the content of the error dialog. + */ +export function logErrorAndShowErrorDialog(title: string, error: any): void { + logger.error(`${title}:`, error); + Modal.createDialog(ErrorDialog, { title, description: `${error}` }); +} diff --git a/test/unit-tests/components/views/settings/encryption/ChangeRecoveryKey-test.tsx b/test/unit-tests/components/views/settings/encryption/ChangeRecoveryKey-test.tsx index 783aaf701c..706efb2b90 100644 --- a/test/unit-tests/components/views/settings/encryption/ChangeRecoveryKey-test.tsx +++ b/test/unit-tests/components/views/settings/encryption/ChangeRecoveryKey-test.tsx @@ -9,6 +9,7 @@ import React from "react"; import { render, screen, waitFor } from "jest-matrix-react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import userEvent from "@testing-library/user-event"; +import { mocked } from "jest-mock"; import { ChangeRecoveryKey } from "../../../../../../src/components/views/settings/encryption/ChangeRecoveryKey"; import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils"; @@ -18,6 +19,10 @@ jest.mock("../../../../../../src/utils/strings", () => ({ copyPlaintext: jest.fn(), })); +afterEach(() => { + jest.restoreAllMocks(); +}); + describe("", () => { let matrixClient: MatrixClient; @@ -36,7 +41,7 @@ describe("", () => { ); } - describe("flow to setup a recovery key", () => { + describe("flow to set up a recovery key", () => { it("should display information about the recovery key", async () => { const user = userEvent.setup(); @@ -107,6 +112,33 @@ describe("", () => { await user.click(finishButton); expect(onFinish).toHaveBeenCalledWith(); }); + + it("should display errors from bootstrapSecretStorage", async () => { + const consoleErrorSpy = jest.spyOn(console, "error").mockReturnValue(undefined); + mocked(matrixClient.getCrypto()!).bootstrapSecretStorage.mockRejectedValue(new Error("can't bootstrap")); + + const user = userEvent.setup(); + renderComponent(false); + + // Display the recovery key to save + await waitFor(() => user.click(screen.getByRole("button", { name: "Continue" }))); + // Display the form to confirm the recovery key + await waitFor(() => user.click(screen.getByRole("button", { name: "Continue" }))); + + await waitFor(() => expect(screen.getByText("Enter your recovery key to confirm")).toBeInTheDocument()); + + const finishButton = screen.getByRole("button", { name: "Finish set up" }); + const input = screen.getByRole("textbox"); + await userEvent.type(input, "encoded private key"); + await user.click(finishButton); + + await screen.findByText("Failed to set up secret storage"); + await screen.findByText("Error: can't bootstrap"); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Failed to set up secret storage:", + new Error("can't bootstrap"), + ); + }); }); describe("flow to change the recovery key", () => { diff --git a/test/unit-tests/components/views/settings/encryption/__snapshots__/ChangeRecoveryKey-test.tsx.snap b/test/unit-tests/components/views/settings/encryption/__snapshots__/ChangeRecoveryKey-test.tsx.snap index 0719c6cc53..882476080e 100644 --- a/test/unit-tests/components/views/settings/encryption/__snapshots__/ChangeRecoveryKey-test.tsx.snap +++ b/test/unit-tests/components/views/settings/encryption/__snapshots__/ChangeRecoveryKey-test.tsx.snap @@ -160,7 +160,7 @@ exports[` flow to change the recovery key should display th `; -exports[` flow to setup a recovery key should ask the user to enter the recovery key 1`] = ` +exports[` flow to set up a recovery key should ask the user to enter the recovery key 1`] = `
-
+ -
+ ); } @@ -349,12 +350,12 @@ function KeyForm({ onCancelClick, onSubmit, recoveryKey, submitButtonLabel }: Ke {_t("settings|encryption|recovery|enter_key_error")} )} -
+ -
+ ); } diff --git a/src/components/views/settings/encryption/EncryptionCardButtons.tsx b/src/components/views/settings/encryption/EncryptionCardButtons.tsx new file mode 100644 index 0000000000..a098f73ce1 --- /dev/null +++ b/src/components/views/settings/encryption/EncryptionCardButtons.tsx @@ -0,0 +1,16 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type PropsWithChildren } from "react"; + +/** + * A component to present action buttons at the bottom of an {@link EncryptionCard} + * (mostly as somewhere for the common CSS to live). + */ +export function EncryptionCardButtons({ children }: PropsWithChildren): JSX.Element { + return
{children}
; +} diff --git a/src/components/views/settings/encryption/ResetIdentityPanel.tsx b/src/components/views/settings/encryption/ResetIdentityPanel.tsx index 40475b2ad1..c445ce1d8c 100644 --- a/src/components/views/settings/encryption/ResetIdentityPanel.tsx +++ b/src/components/views/settings/encryption/ResetIdentityPanel.tsx @@ -15,6 +15,7 @@ import { _t } from "../../../../languageHandler"; import { EncryptionCard } from "./EncryptionCard"; import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; import { uiAuthCallback } from "../../../../CreateCrossSigning"; +import { EncryptionCardButtons } from "./EncryptionCardButtons"; interface ResetIdentityPanelProps { /** @@ -74,7 +75,7 @@ export function ResetIdentityPanel({ onCancelClick, onFinish, variant }: ResetId {variant === "compromised" && {_t("settings|encryption|advanced|breadcrumb_warning")}} -
+ -
+ ); diff --git a/test/unit-tests/components/views/settings/encryption/__snapshots__/ChangeRecoveryKey-test.tsx.snap b/test/unit-tests/components/views/settings/encryption/__snapshots__/ChangeRecoveryKey-test.tsx.snap index 882476080e..aab0ea55f1 100644 --- a/test/unit-tests/components/views/settings/encryption/__snapshots__/ChangeRecoveryKey-test.tsx.snap +++ b/test/unit-tests/components/views/settings/encryption/__snapshots__/ChangeRecoveryKey-test.tsx.snap @@ -135,7 +135,7 @@ exports[` flow to change the recovery key should display th From d8904a6e56d5b545bc4c30b56d8ad097665c8e8d Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 21 Feb 2025 13:45:52 +0000 Subject: [PATCH 45/53] getByRole is slow on large trees, use getByText (#29331) --- .../components/views/settings/AddRemoveThreepids-test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit-tests/components/views/settings/AddRemoveThreepids-test.tsx b/test/unit-tests/components/views/settings/AddRemoveThreepids-test.tsx index 5685f1eb8f..6d3d3de692 100644 --- a/test/unit-tests/components/views/settings/AddRemoveThreepids-test.tsx +++ b/test/unit-tests/components/views/settings/AddRemoveThreepids-test.tsx @@ -172,7 +172,7 @@ describe("AddRemoveThreepids", () => { const countryDropdown = await screen.findByRole("button", { name: /Country Dropdown/ }); await userEvent.click(countryDropdown); - const gbOption = screen.getByRole("option", { name: "🇬🇧 United Kingdom (+44)" }); + const gbOption = screen.getByText("United Kingdom (+44)"); await userEvent.click(gbOption); const input = screen.getByRole("textbox", { name: "Phone Number" }); @@ -511,7 +511,7 @@ describe("AddRemoveThreepids", () => { const countryDropdown = screen.getByRole("button", { name: /Country Dropdown/ }); await userEvent.click(countryDropdown); - const gbOption = screen.getByRole("option", { name: "🇬🇧 United Kingdom (+44)" }); + const gbOption = screen.getByText("United Kingdom (+44)"); await userEvent.click(gbOption); const input = screen.getByRole("textbox", { name: "Phone Number" }); From 0cbc6f99d06b8daf12c0185e8a211796b5c57e4b Mon Sep 17 00:00:00 2001 From: David Langley Date: Sat, 22 Feb 2025 00:16:33 +0000 Subject: [PATCH 46/53] Set language explicitly (#29332) --- .../components/views/elements/SyntaxHighlight-test.tsx | 6 ++++-- .../elements/__snapshots__/SyntaxHighlight-test.tsx.snap | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/test/unit-tests/components/views/elements/SyntaxHighlight-test.tsx b/test/unit-tests/components/views/elements/SyntaxHighlight-test.tsx index 5983e7b0f2..3d442a6f4d 100644 --- a/test/unit-tests/components/views/elements/SyntaxHighlight-test.tsx +++ b/test/unit-tests/components/views/elements/SyntaxHighlight-test.tsx @@ -12,8 +12,10 @@ import SyntaxHighlight from "../../../../../src/components/views/elements/Syntax describe("", () => { it("renders", async () => { - const { container } = render(console.log("Hello, World!");); - await waitFor(() => expect(container.querySelector(".language-arcade")).toBeTruthy()); + const { container } = render( + console.log("Hello, World!");, + ); + await waitFor(() => expect(container.querySelector(".language-javascript")).toBeTruthy()); expect(container).toMatchSnapshot(); }); diff --git a/test/unit-tests/components/views/elements/__snapshots__/SyntaxHighlight-test.tsx.snap b/test/unit-tests/components/views/elements/__snapshots__/SyntaxHighlight-test.tsx.snap index e7ad9c057b..9068995c59 100644 --- a/test/unit-tests/components/views/elements/__snapshots__/SyntaxHighlight-test.tsx.snap +++ b/test/unit-tests/components/views/elements/__snapshots__/SyntaxHighlight-test.tsx.snap @@ -3,17 +3,17 @@ exports[` renders 1`] = `
     
       
         console
       
       .
       
         log
       

From 27c0e97e44c1df848bc5e924fe0a2d5c8d2acc1d Mon Sep 17 00:00:00 2001
From: David Langley 
Date: Sat, 22 Feb 2025 00:17:13 +0000
Subject: [PATCH 47/53] Fix test that doesn't make sense in
 usePublicRoomDirectory-test.tsx (#29335)

* remove test that doesn't make sense

* Actually let's fix the test rather than remove it

* Add comment, remove consoles.
---
 .../hooks/usePublicRoomDirectory-test.tsx     | 22 +++++++++----------
 1 file changed, 10 insertions(+), 12 deletions(-)

diff --git a/test/unit-tests/hooks/usePublicRoomDirectory-test.tsx b/test/unit-tests/hooks/usePublicRoomDirectory-test.tsx
index cc35a49bf7..5c1b2a8ac4 100644
--- a/test/unit-tests/hooks/usePublicRoomDirectory-test.tsx
+++ b/test/unit-tests/hooks/usePublicRoomDirectory-test.tsx
@@ -27,17 +27,15 @@ describe("usePublicRoomDirectory", () => {
         cli.getDomain = () => "matrix.org";
         cli.getThirdpartyProtocols = () => Promise.resolve({});
         cli.publicRooms = ({ filter }: IRoomDirectoryOptions) => {
-            const chunk = filter?.generic_search_term
-                ? [
-                      {
-                          room_id: "hello world!",
-                          name: filter.generic_search_term,
-                          world_readable: true,
-                          guest_can_join: true,
-                          num_joined_members: 1,
-                      },
-                  ]
-                : [];
+            const chunk = [
+                {
+                    room_id: "hello world!",
+                    name: filter?.generic_search_term ?? "", // If the query is "" no filter is applied(an is undefined here), in keeping with the pattern let's call the room ""
+                    world_readable: true,
+                    guest_can_join: true,
+                    num_joined_members: 1,
+                },
+            ];
             return Promise.resolve({
                 chunk,
                 total_room_count_estimate: 1,
@@ -67,7 +65,7 @@ describe("usePublicRoomDirectory", () => {
     });
 
     it("should work with empty queries", async () => {
-        const query = "ROOM NAME";
+        const query = "";
         const { result } = render();
 
         act(() => {

From 99b9eee86e1f46148725f0956cc6270de1e6b3d5 Mon Sep 17 00:00:00 2001
From: ElementRobot 
Date: Sat, 22 Feb 2025 07:14:13 +0100
Subject: [PATCH 48/53] [create-pull-request] automated change (#29338)

Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
---
 playwright/testcontainers/synapse.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts
index 82d567185d..13250c86d9 100644
--- a/playwright/testcontainers/synapse.ts
+++ b/playwright/testcontainers/synapse.ts
@@ -25,7 +25,7 @@ import { type HomeserverContainer, type StartedHomeserverContainer } from "./Hom
 import { type StartedMatrixAuthenticationServiceContainer } from "./mas.ts";
 import { Api, ClientServerApi, type Verb } from "../plugins/utils/api.ts";
 
-const TAG = "develop@sha256:cdf9ad343d8d9cae7ae37078dd3a07e8ec88fba30ba014e767bab1f752eb9752";
+const TAG = "develop@sha256:8d1c531cf6010b63142a04e1b138a60720946fa131ad404813232f02db4ce7ba";
 
 const DEFAULT_CONFIG = {
     server_name: "localhost",

From 37136ecf46218c0425e3b985f87b9f2d327bbdae Mon Sep 17 00:00:00 2001
From: ElementRobot 
Date: Mon, 24 Feb 2025 07:21:38 +0100
Subject: [PATCH 49/53] [create-pull-request] automated change (#29342)

Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
---
 src/i18n/strings/el.json    |  7 +++++++
 src/i18n/strings/nb_NO.json | 17 +++++++++++------
 src/i18n/strings/pl.json    |  8 +++++++-
 src/i18n/strings/uk.json    |  7 ++++++-
 4 files changed, 31 insertions(+), 8 deletions(-)

diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json
index 84b8fc4d44..0f3ed2c5d4 100644
--- a/src/i18n/strings/el.json
+++ b/src/i18n/strings/el.json
@@ -2212,13 +2212,18 @@
             "ignore_users_empty": "Δεν έχετε χρήστες που έχετε αγνοήσει.",
             "ignore_users_section": "Χρήστες που αγνοήθηκαν",
             "import_megolm_keys": "Εισαγωγή κλειδιών E2E",
+            "key_backup_active": "Αυτή η συνεδρία δημιουργεί αντίγραφα ασφαλείας των κλειδιών σου.",
+            "key_backup_active_version": "Ενεργή έκδοση αντιγράφων ασφαλείας:",
             "key_backup_active_version_none": "Κανένα",
             "key_backup_algorithm": "Αλγόριθμος:",
+            "key_backup_can_be_restored": "Αυτό το αντίγραφο ασφαλείας μπορεί να επαναφερθεί σε αυτήν την περίοδο λειτουργίας",
             "key_backup_complete": "Δημιουργήθηκαν αντίγραφα ασφαλείας όλων των κλειδιών",
             "key_backup_connect": "Συνδέστε αυτήν την συνεδρία με το αντίγραφο ασφαλείας κλειδιού",
             "key_backup_connect_prompt": "Συνδέστε αυτήν την συνεδρία με το αντίγραφο ασφαλείας κλειδιού πριν αποσυνδεθείτε για να αποφύγετε την απώλεια κλειδιών που μπορεί να υπάρχουν μόνο σε αυτήν την συνεδρία.",
+            "key_backup_in_progress": "Δημιουργία αντιγράφων %(sessionsRemaining)s κλειδιών...",
             "key_backup_inactive": "Αυτή η συνεδρία δεν δημιουργεί αντίγραφα ασφαλείας των κλειδιών σας, αλλά έχετε ένα υπάρχον αντίγραφο ασφαλείας από το οποίο μπορείτε να επαναφέρετε και να προσθέσετε στη συνέχεια.",
             "key_backup_inactive_warning": "Δεν δημιουργούνται αντίγραφα ασφαλείας των κλειδιών σας από αυτήν την συνεδρία.",
+            "key_backup_latest_version": "Τελευταία έκδοση αντιγράφων ασφαλείας στο διακομιστή:",
             "message_search_disable_warning": "Εάν απενεργοποιηθεί, τα μηνύματα από κρυπτογραφημένα δωμάτια δε θα εμφανίζονται στα αποτελέσματα αναζήτησης.",
             "message_search_disabled": "Αποθηκεύστε με ασφάλεια κρυπτογραφημένα μηνύματα τοπικά για να εμφανίζονται στα αποτελέσματα αναζήτησης.",
             "message_search_enabled": {
@@ -2237,6 +2242,7 @@
             "message_search_space_used": "Χώρος που χρησιμοποιείται:",
             "message_search_unsupported": "Λείπουν ορισμένα στοιχεία από το %(brand)s που απαιτούνται για την ασφαλή αποθήκευση κρυπτογραφημένων μηνυμάτων τοπικά. Εάν θέλετε να πειραματιστείτε με αυτό το χαρακτηριστικό, δημιουργήστε μια προσαρμοσμένη %(brand)s επιφάνεια εργασίαςμε προσθήκη στοιχείων αναζήτησης.",
             "message_search_unsupported_web": "Το %(brand)s δεν μπορεί να αποθηκεύσει με ασφάλεια κρυπτογραφημένα μηνύματα τοπικά ενώ εκτελείται σε πρόγραμμα περιήγησης ιστού. Χρησιμοποιήστε την %(brand)s Επιφάνεια εργασίας για να εμφανίζονται κρυπτογραφημένα μηνύματα στα αποτελέσματα αναζήτησης.",
+            "record_session_details": "Κατέγραψε το όνομα του πελάτη, την έκδοση και τη διεύθυνση URL για να αναγνωρίζεις τις συνεδρίες πιο εύκολα στον διαχειριστή συνεδρίας",
             "restore_key_backup": "Επαναφορά από Αντίγραφο ασφαλείας",
             "secret_storage_not_ready": "δεν είναι έτοιμο",
             "secret_storage_ready": "έτοιμο",
@@ -2305,6 +2311,7 @@
             "auto_gain_control": "Αυτόματος έλεγχος gain",
             "connection_section": "Σύνδεση",
             "echo_cancellation": "Ακύρωση ηχούς",
+            "enable_fallback_ice_server": "Να επιτρέπεται ο εναλλακτικής διακομιστής υποβοήθησης κλήσης (%(server)s )",
             "enable_fallback_ice_server_description": "Ισχύει μόνο εάν ο οικιακός διακομιστής σου δεν προσφέρει ένα. Η διεύθυνση IP σου θα κοινοποιηθεί κατά τη διάρκεια μιας κλήσης.",
             "mirror_local_feed": "Αντικατοπτρίστε την τοπική ροή βίντεο",
             "missing_permissions_prompt": "Λείπουν δικαιώματα πολυμέσων, κάντε κλικ στο κουμπί παρακάτω για να αιτηθείτε.",
diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json
index c361dcdbe7..f20b4e1af6 100644
--- a/src/i18n/strings/nb_NO.json
+++ b/src/i18n/strings/nb_NO.json
@@ -80,12 +80,14 @@
         "maximise": "Maksimer",
         "mention": "Nevn",
         "minimise": "Minimer",
+        "new_message": "Ny melding",
         "new_room": "Nytt rom",
         "new_video_room": "Nytt videorom",
         "next": "Neste",
         "no": "Nei",
         "ok": "OK",
         "open": "Åpne",
+        "open_menu": "Åpne meny",
         "pause": "Pause",
         "pin": "Fest",
         "play": "Spill av",
@@ -1242,6 +1244,7 @@
         "change": "Bytt ut identitetstjener",
         "change_prompt": "Koble fra identitetsserveren  og koble til  i stedet?",
         "change_server_prompt": "Hvis du ikke ønsker å bruke  til å oppdage og bli oppdaget av eksisterende kontakter som du kjenner, skriv inn en annen identitetstjener nedenfor.",
+        "changed": "Identitetsserveren din har blitt endret",
         "checking": "Sjekker tjeneren",
         "description_connected": "Du bruker for øyeblikket  til å oppdage og bli oppdaget av eksisterende kontakter du kjenner. Du kan endre identitetsserveren din nedenfor.",
         "description_disconnected": "Du bruker for øyeblikket ikke en identitetsserver. For å oppdage og bli oppdaget av eksisterende kontakter du kjenner, legg til en nedenfor.",
@@ -3621,18 +3624,19 @@
                 "ble invitert %(count)s ganger": "other"
             },
             "joined": {
-                "%(oneUser)s ble med %(count)s ganger": "other",
-                "%(oneUser)s ble med": "one"
+                "one": "%(oneUser)sble med",
+                "other": "%(oneUser)sble med %(count)s ganger"
             },
             "joined_and_left": {
                 "%(oneUser)sble med og forlot igjen": "one"
             },
             "joined_and_left_multiple": {
-                "%(severalUsers)sble med og forlot igjen": "one"
+                "one": "%(severalUsers)sble med og forlot",
+                "other": "%(severalUsers)sble med og forlot %(count)s ganger"
             },
             "joined_multiple": {
-                "%(severalUsers)s ble med %(count)s ganger": "other",
-                "%(severalUsers)s ble med": "one"
+                "one": "%(severalUsers)sble med",
+                "other": "%(severalUsers)sble med %(count)s ganger"
             },
             "kicked": {
                 "one": "ble fjernet",
@@ -3675,7 +3679,8 @@
                 "other": "%(severalUsers)sfjernet %(count)s meldinger"
             },
             "rejected_invite": {
-                "%(oneUser)savslo invitasjonen sin": "one"
+                "one": "%(oneUser)savviste invitasjonen",
+                "other": "%(oneUser)savviste invitasjonen deres %(count)s ganger"
             },
             "rejected_invite_multiple": {
                 "one": "%(severalUsers)savviste invitasjonene deres",
diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json
index 5c9eb18c56..24b026098b 100644
--- a/src/i18n/strings/pl.json
+++ b/src/i18n/strings/pl.json
@@ -80,12 +80,14 @@
         "maximise": "Maksymalizuj",
         "mention": "Wzmianka",
         "minimise": "Minimalizuj",
+        "new_message": "Nowa wiadomość",
         "new_room": "Nowy pokój",
         "new_video_room": "Nowy pokój wideo",
         "next": "Dalej",
         "no": "Nie",
         "ok": "OK",
         "open": "Otwórz",
+        "open_menu": "Otwórz menu",
         "pause": "Wstrzymaj",
         "pin": "Przypnij",
         "play": "Odtwórz",
@@ -1251,6 +1253,7 @@
         "change": "Zmień serwer tożsamości",
         "change_prompt": "Rozłączyć się z bieżącym serwerem tożsamości  i połączyć się z ?",
         "change_server_prompt": "Jeżeli nie chcesz używać  do odnajdywania i bycia odnajdywanym przez osoby, które znasz, wpisz inny serwer tożsamości poniżej.",
+        "changed": "Twój serwer tożsamości został zmieniony",
         "checking": "Sprawdzanie serwera",
         "description_connected": "Używasz , aby odnajdywać i móc być odnajdywanym przez istniejące kontakty, które znasz. Możesz zmienić serwer tożsamości poniżej.",
         "description_disconnected": "Nie używasz serwera tożsamości. Aby odkrywać i być odkrywanym przez istniejące kontakty które znasz, dodaj jeden poniżej.",
@@ -1294,7 +1297,9 @@
         "title": "%(brand)s nie wspiera tej przeglądarki",
         "use_desktop_heading": "Zamiast tego użyj %(brand)s Desktop",
         "use_mobile_heading": "Zamiast tego użyj %(brand)s Mobile",
-        "use_mobile_heading_after_desktop": "lub skorzystaj z naszej aplikacji mobilnej"
+        "use_mobile_heading_after_desktop": "lub skorzystaj z naszej aplikacji mobilnej",
+        "windows_64bit": "Windows (64-bit)",
+        "windows_arm_64bit": "Windows (ARM 64-bit)"
     },
     "info_tooltip_title": "Informacje",
     "integration_manager": {
@@ -3471,6 +3476,7 @@
             "left_reason": "%(targetName)s opuścił pokój: %(reason)s",
             "no_change": "%(senderName)s nie dokonał żadnych zmian",
             "reject_invite": "%(targetName)s odrzucił zaproszenie",
+            "reject_invite_reason": "%(targetName)s odrzucił zaproszenie: %(reason)s",
             "remove_avatar": "%(senderName)s usunął swoje zdjęcie profilowe",
             "remove_name": "%(senderName)s usunął swoją widoczną nazwę (%(oldDisplayName)s)",
             "set_avatar": "%(senderName)s ustawił zdjęcie profilowe",
diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json
index 98e8a2af2a..33bd47ea3c 100644
--- a/src/i18n/strings/uk.json
+++ b/src/i18n/strings/uk.json
@@ -80,12 +80,14 @@
         "maximise": "Розгорнути",
         "mention": "Згадати",
         "minimise": "Згорнути",
+        "new_message": "Нове повідомлення",
         "new_room": "Нова кімната",
         "new_video_room": "Нова відеокімната",
         "next": "Далі",
         "no": "НІ",
         "ok": "Гаразд",
         "open": "Відкрити",
+        "open_menu": "Відкрити меню",
         "pause": "Призупинити",
         "pin": "Кнопка",
         "play": "Відтворити",
@@ -1244,6 +1246,7 @@
         "change": "Змінити сервер ідентифікації",
         "change_prompt": "Від'єднатися від сервера ідентифікації  й натомість під'єднатися до ?",
         "change_server_prompt": "Якщо ви не бажаєте використовувати , щоб знаходити наявні контакти й щоб вони вас знаходили, введіть інший сервер ідентифікації нижче.",
+        "changed": "Ваш сервер ідентифікації змінено",
         "checking": "Перевірка сервера",
         "description_connected": "Зараз  дозволяє вам знаходити контакти, а контактам вас. Можете змінити сервер ідентифікації нижче.",
         "description_disconnected": "Зараз ви не використовуєте сервер ідентифікації. Щоб знайти наявні контакти й вони могли знайти вас, додайте його нижче.",
@@ -1286,7 +1289,9 @@
         "title": "Непідтримуваний браузер",
         "use_desktop_heading": "Натомість використовуйте %(brand)s для комп'ютерів",
         "use_mobile_heading": "Натомість використовуйте %(brand)s для мобільних",
-        "use_mobile_heading_after_desktop": "Або скористайтеся нашим мобільним застосунком"
+        "use_mobile_heading_after_desktop": "Або скористайтеся нашим мобільним застосунком",
+        "windows_64bit": "Windows (64-розрядна)",
+        "windows_arm_64bit": "Windows (64-розрядна версія ARM)"
     },
     "info_tooltip_title": "Відомості",
     "integration_manager": {

From 7d94fa9b038136445c6b818f83ba127f48549127 Mon Sep 17 00:00:00 2001
From: Florian Duros 
Date: Mon, 24 Feb 2025 16:51:50 +0100
Subject: [PATCH 50/53] New room list: add compose menu for spaces in header
 (#29347)

* feat(new room list): add compose menu in header for spaces

* test(new room list): add tests for space

* test(e2e new room list): update space test

* chore: formatting and reuse type var
---
 .../room-list-view/room-list-header.spec.ts   |  2 +-
 .../roomlist/RoomListHeaderViewModel.tsx      | 37 ++++++++++++-------
 .../roomlist/RoomListHeaderViewModel-test.tsx | 28 ++++++++++++--
 3 files changed, 49 insertions(+), 18 deletions(-)

diff --git a/playwright/e2e/left-panel/room-list-view/room-list-header.spec.ts b/playwright/e2e/left-panel/room-list-view/room-list-header.spec.ts
index bda87b0462..2f4ef6f001 100644
--- a/playwright/e2e/left-panel/room-list-view/room-list-header.spec.ts
+++ b/playwright/e2e/left-panel/room-list-view/room-list-header.spec.ts
@@ -53,6 +53,6 @@ test.describe("Header section of the room list", () => {
 
         const roomListHeader = getHeaderSection(page);
         await expect(roomListHeader.getByRole("heading", { name: "MySpace" })).toBeVisible();
-        await expect(roomListHeader.getByRole("button", { name: "Add" })).not.toBeVisible();
+        await expect(roomListHeader.getByRole("button", { name: "Add" })).toBeVisible();
     });
 });
diff --git a/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx b/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx
index 3284933a9b..a37384b7fa 100644
--- a/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx
+++ b/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx
@@ -23,6 +23,7 @@ import {
     UPDATE_SELECTED_SPACE,
 } from "../../../stores/spaces";
 import SpaceStore from "../../../stores/spaces/SpaceStore";
+import { showCreateNewRoom } from "../../../utils/space";
 
 /**
  * Hook to get the active space and its title.
@@ -55,7 +56,7 @@ export interface RoomListHeaderViewState {
     title: string;
     /**
      * Whether to display the compose menu
-     * True if the user can create rooms and is not in a Space
+     * True if the user can create rooms
      */
     displayComposeMenu: boolean;
     /**
@@ -84,15 +85,13 @@ export interface RoomListHeaderViewState {
 
 /**
  * View model for the RoomListHeader.
- * The actions don't work when called in a space yet.
  */
 export function useRoomListHeaderViewModel(): RoomListHeaderViewState {
     const { activeSpace, title } = useSpace();
 
     const canCreateRoom = shouldShowComponent(UIComponent.CreateRooms);
     const canCreateVideoRoom = useFeatureEnabled("feature_video_rooms");
-    // Temporary: don't display the compose menu when in a Space
-    const displayComposeMenu = canCreateRoom && !activeSpace;
+    const displayComposeMenu = canCreateRoom;
 
     /* Actions */
 
@@ -101,20 +100,30 @@ export function useRoomListHeaderViewModel(): RoomListHeaderViewState {
         PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateChatItem", e);
     }, []);
 
-    const createRoom = useCallback((e: Event) => {
-        defaultDispatcher.fire(Action.CreateRoom);
-        PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
-    }, []);
+    const createRoom = useCallback(
+        (e: Event) => {
+            if (activeSpace) {
+                showCreateNewRoom(activeSpace);
+            } else {
+                defaultDispatcher.fire(Action.CreateRoom);
+            }
+            PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
+        },
+        [activeSpace],
+    );
 
     const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
-    const createVideoRoom = useCallback(
-        () =>
+    const createVideoRoom = useCallback(() => {
+        const type = elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo;
+        if (activeSpace) {
+            showCreateNewRoom(activeSpace, type);
+        } else {
             defaultDispatcher.dispatch({
                 action: Action.CreateRoom,
-                type: elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
-            }),
-        [elementCallVideoRoomsEnabled],
-    );
+                type,
+            });
+        }
+    }, [activeSpace, elementCallVideoRoomsEnabled]);
 
     return {
         title,
diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListHeaderViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListHeaderViewModel-test.tsx
index 95a594e3b6..fe96e26861 100644
--- a/test/unit-tests/components/viewmodels/roomlist/RoomListHeaderViewModel-test.tsx
+++ b/test/unit-tests/components/viewmodels/roomlist/RoomListHeaderViewModel-test.tsx
@@ -6,7 +6,7 @@
  */
 
 import { renderHook } from "jest-matrix-react";
-import { type MatrixClient, RoomType } from "matrix-js-sdk/src/matrix";
+import { type MatrixClient, type Room, RoomType } from "matrix-js-sdk/src/matrix";
 import { mocked } from "jest-mock";
 
 import { useRoomListHeaderViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListHeaderViewModel";
@@ -16,16 +16,23 @@ import { shouldShowComponent } from "../../../../../src/customisations/helpers/U
 import SettingsStore from "../../../../../src/settings/SettingsStore";
 import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
 import { Action } from "../../../../../src/dispatcher/actions";
+import { showCreateNewRoom } from "../../../../../src/utils/space";
 
 jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({
     shouldShowComponent: jest.fn(),
 }));
 
+jest.mock("../../../../../src/utils/space", () => ({
+    showCreateNewRoom: jest.fn(),
+}));
+
 describe("useRoomListHeaderViewModel", () => {
     let matrixClient: MatrixClient;
+    let space: Room;
 
     beforeEach(() => {
         matrixClient = stubClient();
+        space = mkStubRoom("spaceId", "spaceName", matrixClient);
     });
 
     afterEach(() => {
@@ -39,8 +46,7 @@ describe("useRoomListHeaderViewModel", () => {
         });
 
         it("should return the current space name as title", () => {
-            const room = mkStubRoom("spaceId", "spaceName", matrixClient);
-            jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(room);
+            jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(space);
             const { result } = renderHook(() => useRoomListHeaderViewModel());
 
             expect(result.current.title).toStrictEqual("spaceName");
@@ -81,6 +87,14 @@ describe("useRoomListHeaderViewModel", () => {
         expect(spy).toHaveBeenCalledWith(Action.CreateRoom);
     });
 
+    it("should call showCreateNewRoom when createRoom is called in a space", () => {
+        jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(space);
+        const { result } = renderHook(() => useRoomListHeaderViewModel());
+        result.current.createRoom(new Event("click"));
+
+        expect(showCreateNewRoom).toHaveBeenCalledWith(space);
+    });
+
     it("should fire Action.CreateRoom with RoomType.UnstableCall when createVideoRoom is called and feature_element_call_video_rooms is enabled", () => {
         jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
         const spy = jest.spyOn(defaultDispatcher, "dispatch");
@@ -98,4 +112,12 @@ describe("useRoomListHeaderViewModel", () => {
 
         expect(spy).toHaveBeenCalledWith({ action: Action.CreateRoom, type: RoomType.ElementVideo });
     });
+
+    it("should call showCreateNewRoom when createVideoRoom is called in a space", () => {
+        jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(space);
+        const { result } = renderHook(() => useRoomListHeaderViewModel());
+        result.current.createVideoRoom();
+
+        expect(showCreateNewRoom).toHaveBeenCalledWith(space, RoomType.ElementVideo);
+    });
 });

From 8ef84349b5b74d4c2b9edceee376e6a1df56ec92 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 24 Feb 2025 16:54:57 +0000
Subject: [PATCH 51/53] Dynamically load Element Web modules in Docker
 entrypoint (#29346)

* Dynamically load Element Web modules in Docker entrypoint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Drop environment for PR runs

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 .github/workflows/docker.yaml                 | 37 ++++++++++++++++++-
 Dockerfile                                    |  8 +++-
 .../18-load-element-modules.sh                | 34 +++++++++++++++++
 docker/nginx-templates/default.conf.template  |  4 ++
 docs/install.md                               | 12 ++++++
 5 files changed, 93 insertions(+), 2 deletions(-)
 create mode 100755 docker/docker-entrypoint.d/18-load-element-modules.sh

diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
index a6b54cabd5..56b91c750e 100644
--- a/.github/workflows/docker.yaml
+++ b/.github/workflows/docker.yaml
@@ -3,6 +3,7 @@ on:
     workflow_dispatch: {}
     push:
         tags: [v*]
+    pull_request: {}
     schedule:
         # This job can take a while, and we have usage limits, so just publish develop only twice a day
         - cron: "0 7/12 * * *"
@@ -12,10 +13,12 @@ jobs:
     buildx:
         name: Docker Buildx
         runs-on: ubuntu-24.04
-        environment: dockerhub
+        environment: ${{ github.event_name != 'pull_request' && 'dockerhub' || '' }}
         permissions:
             id-token: write # needed for signing the images with GitHub OIDC Token
             packages: write # needed for publishing packages to GHCR
+        env:
+            TEST_TAG: vectorim/element-web:test
         steps:
             - uses: actions/checkout@v4
               with:
@@ -23,6 +26,7 @@ jobs:
 
             - name: Install Cosign
               uses: sigstore/cosign-installer@c56c2d3e59e4281cc41dea2217323ba5694b171e # v3
+              if: github.event_name != 'pull_request'
 
             - name: Set up QEMU
               uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 # v3
@@ -34,20 +38,48 @@ jobs:
 
             - name: Login to Docker Hub
               uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
+              if: github.event_name != 'pull_request'
               with:
                   username: ${{ secrets.DOCKERHUB_USERNAME }}
                   password: ${{ secrets.DOCKERHUB_TOKEN }}
 
             - name: Login to GitHub Container Registry
               uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
+              if: github.event_name != 'pull_request'
               with:
                   registry: ghcr.io
                   username: ${{ github.repository_owner }}
                   password: ${{ secrets.GITHUB_TOKEN }}
 
+            - name: Build and load
+              uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
+              with:
+                  context: .
+                  load: true
+                  tags: ${{ env.TEST_TAG }}
+
+            - name: Test the image
+              run: |
+                  # Make a fake module to test the image
+                  MODULE_PATH="modules/module_name/index.js"
+                  mkdir -p $(dirname $MODULE_PATH)
+                  echo 'alert("Testing");' > $MODULE_PATH
+
+                  # Spin up a container of the image
+                  CONTAINER_ID=$(docker run --rm -dp 80:80 -v $(pwd)/modules:/tmp/element-web-modules ${{ env.TEST_TAG }})
+
+                  # Run some smoke tests
+                  wget --retry-connrefused --tries=5 -q --wait=3 --spider http://localhost:80/modules/module_name/index.js
+                  MODULE_1=$(curl http://localhost:80/config.json | jq -r .modules[0])
+                  test "$MODULE_1" = "/${MODULE_PATH}"
+
+                  # Clean up
+                  docker stop "$CONTAINER_ID"
+
             - name: Docker meta
               id: meta
               uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
+              if: github.event_name != 'pull_request'
               with:
                   images: |
                       vectorim/element-web
@@ -61,6 +93,7 @@ jobs:
             - name: Build and push
               id: build-and-push
               uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
+              if: github.event_name != 'pull_request'
               with:
                   context: .
                   push: true
@@ -72,6 +105,7 @@ jobs:
               env:
                   DIGEST: ${{ steps.build-and-push.outputs.digest }}
                   TAGS: ${{ steps.meta.outputs.tags }}
+              if: github.event_name != 'pull_request'
               run: |
                   images=""
                   for tag in ${TAGS}; do
@@ -81,6 +115,7 @@ jobs:
 
             - name: Update repo description
               uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4
+              if: github.event_name != 'pull_request'
               continue-on-error: true
               with:
                   username: ${{ secrets.DOCKERHUB_USERNAME }}
diff --git a/Dockerfile b/Dockerfile
index 93d7c676d9..dd8be21932 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,3 +1,5 @@
+# syntax=docker.io/docker/dockerfile:1.7-labs
+
 # Builder
 FROM --platform=$BUILDPLATFORM node:22-bullseye AS builder
 
@@ -8,7 +10,7 @@ ARG JS_SDK_BRANCH="master"
 
 WORKDIR /src
 
-COPY . /src
+COPY --exclude=docker . /src
 RUN /src/scripts/docker-link-repos.sh
 RUN yarn --network-timeout=200000 install
 RUN /src/scripts/docker-package.sh
@@ -19,11 +21,15 @@ RUN cp /src/config.sample.json /src/webapp/config.json
 # App
 FROM nginx:alpine-slim
 
+# Install jq and moreutils for sponge, both used by our entrypoints
+RUN apk add jq moreutils
+
 COPY --from=builder /src/webapp /app
 
 # Override default nginx config. Templates in `/etc/nginx/templates` are passed
 # through `envsubst` by the nginx docker image entry point.
 COPY /docker/nginx-templates/* /etc/nginx/templates/
+COPY /docker/docker-entrypoint.d/* /docker-entrypoint.d/
 
 # Tell nginx to put its pidfile elsewhere, so it can run as non-root
 RUN sed -i -e 's,/var/run/nginx.pid,/tmp/nginx.pid,' /etc/nginx/nginx.conf
diff --git a/docker/docker-entrypoint.d/18-load-element-modules.sh b/docker/docker-entrypoint.d/18-load-element-modules.sh
new file mode 100755
index 0000000000..15c0cb6086
--- /dev/null
+++ b/docker/docker-entrypoint.d/18-load-element-modules.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+
+# Loads modules from `/tmp/element-web-modules` into config.json's `modules` field
+
+set -e
+
+entrypoint_log() {
+    if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then
+        echo "$@"
+    fi
+}
+
+# Copy these config files as a base
+mkdir /tmp/element-web-config
+cp /app/config*.json /tmp/element-web-config/
+
+# If there are modules to be loaded
+if [ -d "/tmp/element-web-modules" ]; then
+    cd /tmp/element-web-modules
+
+    for MODULE in *
+    do
+        # If the module has a package.json, use its main field as the entrypoint
+        ENTRYPOINT="index.js"
+        if [ -f "/tmp/element-web-modules/$MODULE/package.json" ]; then
+            ENTRYPOINT=$(jq -r '.main' "/tmp/element-web-modules/$MODULE/package.json")
+        fi
+
+        entrypoint_log "Loading module $MODULE with entrypoint $ENTRYPOINT"
+
+        # Append the module to the config
+        jq ".modules += [\"/modules/$MODULE/$ENTRYPOINT\"]" /tmp/element-web-config/config.json | sponge /tmp/element-web-config/config.json
+    done
+fi
diff --git a/docker/nginx-templates/default.conf.template b/docker/nginx-templates/default.conf.template
index 06f33e08dd..53870038b6 100644
--- a/docker/nginx-templates/default.conf.template
+++ b/docker/nginx-templates/default.conf.template
@@ -18,8 +18,12 @@ server {
     }
     # covers config.json and config.hostname.json requests as it is prefix.
     location /config {
+        root /tmp/element-web-config;
         add_header Cache-Control "no-cache";
     }
+    location /modules {
+        alias /tmp/element-web-modules;
+    }
     # redirect server error pages to the static page /50x.html
     #
     error_page   500 502 503 504  /50x.html;
diff --git a/docs/install.md b/docs/install.md
index f6bd98611c..743ae0a93a 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -66,6 +66,18 @@ on other runtimes may require root privileges. To resolve this, either run the
 image as root (`docker run --user 0`) or, better, change the port that nginx
 listens on via the `ELEMENT_WEB_PORT` environment variable.
 
+[Element Web Modules](https://github.com/element-hq/element-modules/tree/main/packages/element-web-module-api) can be dynamically loaded
+by being made available (e.g. via bind mount) in a directory within `/tmp/element-web-modules/`.
+The default entrypoint will be index.js in that directory but can be overridden if a package.json file is found with a `main` directive.
+These modules will be presented in a `/modules` subdirectory within the webroot, and automatically added to the config.json `modules` field.
+
+If you wish to use docker in read-only mode,
+you should follow the [upstream instructions](https://hub.docker.com/_/nginx#:~:text=Running%20nginx%20in%20read%2Donly%20mode)
+but additionally include the following directories:
+
+- /tmp/element-web-config/
+- /etc/nginx/conf.d/
+
 The behaviour of the docker image can be customised via the following
 environment variables:
 

From efc6149a8b3362c01b93f52e76e5c4ae8cbcb65c Mon Sep 17 00:00:00 2001
From: Florian Duros 
Date: Mon, 24 Feb 2025 18:08:12 +0100
Subject: [PATCH 52/53] Update `@vector-im/compound-design-tokens` &
 `@vector-im/compound-web` (#29307)

* chore: update `@vector-im/compound-design-tokens` & `@vector-im/compound-web` to last version

* chore: use `error-solid` icon instead of `error`

* chore: update jest snapshot

* fix: `AccessibleButton` lint
---
 package.json                                  |   4 +-
 res/css/structures/_SpaceHierarchy.pcss       |   2 +-
 .../context_menus/_MessageContextMenu.pcss    |   2 +-
 .../security/_AccessSecretStorageDialog.pcss  |   4 +-
 res/css/views/elements/_InfoTooltip.pcss      |   2 +-
 src/components/views/auth/LoginWithQRFlow.tsx |   2 +-
 .../views/elements/AccessibleButton.tsx       |   2 +-
 .../views/right_panel/RoomSummaryCard.tsx     |   2 +-
 .../MemberList/tiles/common/E2EIconView.tsx   |   2 +-
 .../views/rooms/RoomHeader/RoomHeader.tsx     |   2 +-
 .../views/settings/SettingsSubheader.tsx      |   2 +-
 .../encryption/ResetIdentityPanel.tsx         |   2 +-
 .../__snapshots__/ErrorView-test.tsx.snap     |  32 +-
 .../__snapshots__/FilePanel-test.tsx.snap     |  14 +-
 .../__snapshots__/MessagePanel-test.tsx.snap  |   2 +-
 .../__snapshots__/RoomView-test.tsx.snap      | 248 ++++----
 .../SpaceHierarchy-test.tsx.snap              |  10 +-
 .../__snapshots__/ThreadPanel-test.tsx.snap   |   8 +-
 ...teractiveAuthEntryComponents-test.tsx.snap |  10 +-
 .../DecoratedRoomAvatar-test.tsx.snap         |   4 +-
 .../__snapshots__/RoomAvatar-test.tsx.snap    |   6 +-
 .../__snapshots__/BeaconMarker-test.tsx.snap  |   2 +-
 .../BeaconViewDialog-test.tsx.snap            |   4 +-
 .../__snapshots__/DialogSidebar-test.tsx.snap |   6 +-
 .../ConfirmUserActionDialog-test.tsx.snap     |   2 +-
 ...nageRestrictedJoinRuleDialog-test.tsx.snap |   2 +-
 .../__snapshots__/ShareDialog-test.tsx.snap   |  40 +-
 .../UnpinAllDialog-test.tsx.snap              |   6 +-
 .../UserSettingsDialog-test.tsx.snap          |  34 +-
 .../__snapshots__/AppTile-test.tsx.snap       |  42 +-
 .../__snapshots__/FacePile-test.tsx.snap      |   4 +-
 .../FilterDropdown-test.tsx.snap              |   2 +-
 .../elements/__snapshots__/Pill-test.tsx.snap |  18 +-
 .../__snapshots__/RoomFacePile-test.tsx.snap  |   4 +-
 .../LocationViewDialog-test.tsx.snap          |   6 +-
 .../__snapshots__/MapError-test.tsx.snap      |   6 +-
 .../__snapshots__/Marker-test.tsx.snap        |   2 +-
 .../__snapshots__/SmartMarker-test.tsx.snap   |   4 +-
 .../__snapshots__/ZoomButtons-test.tsx.snap   |   4 +-
 .../views/messages/TextualBody-test.tsx       |   4 +-
 .../DecryptionFailureBody-test.tsx.snap       |   2 +-
 .../__snapshots__/MBeaconBody-test.tsx.snap   |   2 +-
 .../__snapshots__/MFileBody-test.tsx.snap     |   4 +-
 .../__snapshots__/MLocationBody-test.tsx.snap |   4 +-
 .../PinnedMessageBadge-test.tsx.snap          |   2 +-
 .../__snapshots__/TextualBody-test.tsx.snap   |  22 +-
 .../__snapshots__/PollHistory-test.tsx.snap   |   2 +-
 .../__snapshots__/BaseCard-test.tsx.snap      |   8 +-
 .../ExtensionsCard-test.tsx.snap              |  44 +-
 .../PinnedMessagesCard-test.tsx.snap          |  68 +--
 .../RoomSummaryCard-test.tsx.snap             | 574 +++++++++---------
 .../__snapshots__/UserInfo-test.tsx.snap      | 170 +++---
 .../UrlPreviewSettings-test.tsx.snap          |   4 +-
 .../EventTileThreadToolbar-test.tsx.snap      |   2 +-
 .../__snapshots__/RoomHeader-test.tsx.snap    |  28 +-
 .../VideoRoomChatButton-test.tsx.snap         |   6 +-
 .../RoomListHeaderView-test.tsx.snap          |  13 +-
 .../RoomListSearch-test.tsx.snap              |  16 +-
 .../__snapshots__/RoomListView-test.tsx.snap  |  21 +-
 .../PinnedEventTile-test.tsx.snap             | 100 +--
 .../PinnedMessageBanner-test.tsx.snap         |  24 +-
 .../ReadReceiptGroup-test.tsx.snap            |  10 +-
 .../RoomPreviewBar-test.tsx.snap              |  12 +-
 .../__snapshots__/RoomTile-test.tsx.snap      |   8 +-
 .../ThirdPartyMemberInfo-test.tsx.snap        |  24 +-
 .../MemberTileView-test.tsx.snap              |  14 +-
 .../PresenceIconView-test.tsx.snap            |  10 +-
 .../FormattingButtons-test.tsx.snap           |  20 +-
 .../LayoutSwitcher-test.tsx.snap              |  64 +-
 .../PowerLevelSelector-test.tsx.snap          |   4 +-
 .../__snapshots__/SetIdServer-test.tsx.snap   |  10 +-
 .../SettingsHeader-test.tsx.snap              |   4 +-
 .../SettingsSubheader-test.tsx.snap           |   6 +-
 .../ThemeChoicePanel-test.tsx.snap            | 250 ++++----
 .../CurrentDeviceSection-test.tsx.snap        |  10 +-
 .../DeviceExpandDetailsButton-test.tsx.snap   |   4 +-
 .../LoginWithQRFlow-test.tsx.snap             | 100 +--
 .../__snapshots__/AdvancedPanel-test.tsx.snap |  42 +-
 .../ChangeRecoveryKey-test.tsx.snap           | 170 +++---
 .../EncryptionCard-test.tsx.snap              |   6 +-
 .../__snapshots__/RecoveryPanel-test.tsx.snap |  18 +-
 .../RecoveryPanelOutOfSync-test.tsx.snap      |  10 +-
 .../ResetIdentityPanel-test.tsx.snap          |  96 +--
 .../Notifications2-test.tsx.snap              |  18 +-
 .../PeopleRoomSettingsTab-test.tsx.snap       |  14 +-
 .../AppearanceUserSettingsTab-test.tsx.snap   | 110 ++--
 .../EncryptionUserSettingsTab-test.tsx.snap   |  66 +-
 .../SecurityUserSettingsTab-test.tsx.snap     |  10 +-
 .../SessionManagerTab-test.tsx.snap           |   8 +-
 .../SidebarUserSettingsTab-test.tsx.snap      |  20 +-
 .../AddExistingToSpaceDialog-test.tsx.snap    |   4 +-
 .../__snapshots__/SpacePanel-test.tsx.snap    |   8 +-
 .../ThreadsActivityCentre-test.tsx.snap       | 106 ++--
 .../__snapshots__/GenericToast-test.tsx.snap  |   8 +-
 .../VerificationRequestToast-test.tsx.snap    |   8 +-
 .../UnverifiedSessionToast-test.tsx.snap      |   6 +-
 .../__snapshots__/HTMLExport-test.ts.snap     |   6 +-
 .../vector/__snapshots__/init-test.ts.snap    |  34 +-
 yarn.lock                                     |  23 +-
 99 files changed, 1498 insertions(+), 1513 deletions(-)

diff --git a/package.json b/package.json
index a3390c11c0..2c37e3111b 100644
--- a/package.json
+++ b/package.json
@@ -91,8 +91,8 @@
         "@sentry/browser": "^9.0.0",
         "@types/png-chunks-extract": "^1.0.2",
         "@types/react-virtualized": "^9.21.30",
-        "@vector-im/compound-design-tokens": "^3.0.0",
-        "@vector-im/compound-web": "^7.6.1",
+        "@vector-im/compound-design-tokens": "^4.0.0",
+        "@vector-im/compound-web": "^7.6.4",
         "@vector-im/matrix-wysiwyg": "2.38.0",
         "@zxcvbn-ts/core": "^3.0.4",
         "@zxcvbn-ts/language-common": "^3.0.4",
diff --git a/res/css/structures/_SpaceHierarchy.pcss b/res/css/structures/_SpaceHierarchy.pcss
index 31dad9413f..02f39a0b72 100644
--- a/res/css/structures/_SpaceHierarchy.pcss
+++ b/res/css/structures/_SpaceHierarchy.pcss
@@ -77,7 +77,7 @@ Please see LICENSE files in the repository root for full details.
             height: 16px;
             width: 16px;
             left: 0;
-            background-image: url("@vector-im/compound-design-tokens/icons/error.svg");
+            background-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg");
             background-size: cover;
             background-repeat: no-repeat;
         }
diff --git a/res/css/views/context_menus/_MessageContextMenu.pcss b/res/css/views/context_menus/_MessageContextMenu.pcss
index f365c4a293..9fc454f328 100644
--- a/res/css/views/context_menus/_MessageContextMenu.pcss
+++ b/res/css/views/context_menus/_MessageContextMenu.pcss
@@ -29,7 +29,7 @@ Please see LICENSE files in the repository root for full details.
     }
 
     .mx_MessageContextMenu_iconReport::before {
-        mask-image: url("@vector-im/compound-design-tokens/icons/error.svg");
+        mask-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg");
     }
 
     .mx_MessageContextMenu_iconLink::before {
diff --git a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss
index da71b4462b..83b9fe96b4 100644
--- a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss
+++ b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss
@@ -21,7 +21,7 @@ Please see LICENSE files in the repository root for full details.
 
         &.mx_AccessSecretStorageDialog_resetBadge::before {
             /* The image isn't capable of masking, so we use a background instead. */
-            background-image: url("@vector-im/compound-design-tokens/icons/error.svg");
+            background-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg");
             background-size: 24px;
             background-color: transparent;
         }
@@ -120,7 +120,7 @@ Please see LICENSE files in the repository root for full details.
                         width: 16px;
                         left: 0;
                         top: 2px; /* alignment */
-                        background-image: url("@vector-im/compound-design-tokens/icons/error.svg");
+                        background-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg");
                         background-size: contain;
                     }
 
diff --git a/res/css/views/elements/_InfoTooltip.pcss b/res/css/views/elements/_InfoTooltip.pcss
index 5229b7d9f5..a214f0bf83 100644
--- a/res/css/views/elements/_InfoTooltip.pcss
+++ b/res/css/views/elements/_InfoTooltip.pcss
@@ -29,5 +29,5 @@ Please see LICENSE files in the repository root for full details.
 }
 
 .mx_InfoTooltip_icon_warning::before {
-    mask-image: url("@vector-im/compound-design-tokens/icons/error.svg");
+    mask-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg");
 }
diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx
index 588e599098..b246013421 100644
--- a/src/components/views/auth/LoginWithQRFlow.tsx
+++ b/src/components/views/auth/LoginWithQRFlow.tsx
@@ -10,7 +10,7 @@ import React, { createRef, type ReactNode } from "react";
 import { ClientRendezvousFailureReason, MSC4108FailureReason } from "matrix-js-sdk/src/rendezvous";
 import ChevronLeftIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-left";
 import CheckCircleSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/check-circle-solid";
-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 { Heading, MFAInput, Text } from "@vector-im/compound-web";
 import classNames from "classnames";
 import { QrCodeIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx
index efb6e5f939..a07e40c1d7 100644
--- a/src/components/views/elements/AccessibleButton.tsx
+++ b/src/components/views/elements/AccessibleButton.tsx
@@ -80,7 +80,7 @@ type Props = {
     /**
      * The tooltip to show on hover or focus.
      */
-    title?: TooltipProps["label"];
+    title?: string;
     /**
      * The caption is a secondary text displayed under the `title` of the tooltip.
      * Only valid when used in conjunction with `title`.
diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx
index 8c32c21f0f..2d3453feba 100644
--- a/src/components/views/right_panel/RoomSummaryCard.tsx
+++ b/src/components/views/right_panel/RoomSummaryCard.tsx
@@ -35,7 +35,7 @@ import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin";
 import LockIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-solid";
 import LockOffIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-off";
 import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public";
-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 ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down";
 import { EventType, JoinRule, type Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
 
diff --git a/src/components/views/rooms/MemberList/tiles/common/E2EIconView.tsx b/src/components/views/rooms/MemberList/tiles/common/E2EIconView.tsx
index a1f37e5185..c25d40fc58 100644
--- a/src/components/views/rooms/MemberList/tiles/common/E2EIconView.tsx
+++ b/src/components/views/rooms/MemberList/tiles/common/E2EIconView.tsx
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
 import React from "react";
 import { Tooltip } from "@vector-im/compound-web";
 import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified";
-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 { _t } from "../../../../../../languageHandler";
 import { E2EStatus } from "../../../../../../utils/ShieldUtils";
diff --git a/src/components/views/rooms/RoomHeader/RoomHeader.tsx b/src/components/views/rooms/RoomHeader/RoomHeader.tsx
index 964295f0be..dbd8c4d073 100644
--- a/src/components/views/rooms/RoomHeader/RoomHeader.tsx
+++ b/src/components/views/rooms/RoomHeader/RoomHeader.tsx
@@ -15,7 +15,7 @@ import ThreadsIcon from "@vector-im/compound-design-tokens/assets/web/icons/thre
 import RoomInfoIcon from "@vector-im/compound-design-tokens/assets/web/icons/info-solid";
 import NotificationsIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications-solid";
 import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified";
-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 PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public";
 import { JoinRule, type Room } from "matrix-js-sdk/src/matrix";
 import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
diff --git a/src/components/views/settings/SettingsSubheader.tsx b/src/components/views/settings/SettingsSubheader.tsx
index e3e6eae555..158728f530 100644
--- a/src/components/views/settings/SettingsSubheader.tsx
+++ b/src/components/views/settings/SettingsSubheader.tsx
@@ -7,7 +7,7 @@
 
 import React, { type JSX } from "react";
 import CheckCircleIcon from "@vector-im/compound-design-tokens/assets/web/icons/check-circle-solid";
-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 classNames from "classnames";
 
 interface SettingsSubheaderProps {
diff --git a/src/components/views/settings/encryption/ResetIdentityPanel.tsx b/src/components/views/settings/encryption/ResetIdentityPanel.tsx
index 7b3b84c6b8..747b22fd59 100644
--- a/src/components/views/settings/encryption/ResetIdentityPanel.tsx
+++ b/src/components/views/settings/encryption/ResetIdentityPanel.tsx
@@ -8,7 +8,7 @@
 import { Breadcrumb, Button, 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";
+import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid";
 import React, { type MouseEventHandler } from "react";
 
 import { _t } from "../../../../languageHandler";
diff --git a/test/unit-tests/async-components/structures/__snapshots__/ErrorView-test.tsx.snap b/test/unit-tests/async-components/structures/__snapshots__/ErrorView-test.tsx.snap
index 16e94cdeee..469ef5db5d 100644
--- a/test/unit-tests/async-components/structures/__snapshots__/ErrorView-test.tsx.snap
+++ b/test/unit-tests/async-components/structures/__snapshots__/ErrorView-test.tsx.snap
@@ -15,17 +15,17 @@ exports[` should match snapshot 1`] = `
       class="mx_ErrorView_container"
     >
       

TITLE

MSG1

MSG2

@@ -49,17 +49,17 @@ exports[` should match snapshot 1`] = ` class="mx_ErrorView_container" >

Element does not support this browser

Element uses some browser features which are not available in your current browser. Try updating this browser if you're not using the latest version and try again.

For the best experience, use @@ -102,7 +102,7 @@ exports[` should match snapshot 1`] = ` style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x);" >

Or use our mobile app

diff --git a/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap index 6a58ac9811..d8a1e2302d 100644 --- a/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap @@ -12,7 +12,7 @@ exports[`FilePanel renders empty state 1`] = ` class="mx_BaseCard_header_title" >

Files @@ -20,14 +20,14 @@ exports[`FilePanel renders empty state 1`] = `

@@ -263,13 +263,13 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
@@ -563,13 +563,13 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
@@ -940,13 +940,13 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
@@ -1325,13 +1325,13 @@ exports[`RoomView should not display the timeline when the room encryption is lo
0
@@ -1497,7 +1497,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
0
@@ -1878,7 +1878,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
0
@@ -2025,7 +2025,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = ` class="mx_BaseCard_header_title" >

Chat @@ -2033,14 +2033,14 @@ exports[`RoomView video rooms should render joined video room view 1`] = `

@@ -41,7 +41,7 @@ exports[` renders sidebar correctly with beacons 1`] = ` class="mx_BeaconListItem" > renders sidebar correctly without beacons 1`] = ` xmlns="http://www.w3.org/2000/svg" >
diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/ConfirmUserActionDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/ConfirmUserActionDialog-test.tsx.snap index bf0fc31443..d48b62c57d 100644 --- a/test/unit-tests/components/views/dialogs/__snapshots__/ConfirmUserActionDialog-test.tsx.snap +++ b/test/unit-tests/components/views/dialogs/__snapshots__/ConfirmUserActionDialog-test.tsx.snap @@ -35,7 +35,7 @@ exports[`ConfirmUserActionDialog renders 1`] = ` class="mx_ConfirmUserActionDialog_avatar" > should list spaces which are not par
Make sure that you really want to remove all pinned messages. This action can’t be undone. @@ -32,7 +32,7 @@ exports[` should render 1`] = ` class="mx_UnpinAllDialog_buttons" >
@@ -161,7 +161,7 @@ exports[`AppTile for a pinned widget should render 1`] = ` xmlns="http://www.w3.org/2000/svg" >
@@ -182,7 +182,7 @@ exports[`AppTile for a pinned widget should render 1`] = ` xmlns="http://www.w3.org/2000/svg" >
@@ -213,7 +213,7 @@ exports[`AppTile for a pinned widget should render permission request 1`] = `
@@ -273,7 +273,7 @@ exports[`AppTile for a pinned widget should render permission request 1`] = ` xmlns="http://www.w3.org/2000/svg" >
@@ -294,7 +294,7 @@ exports[`AppTile for a pinned widget should render permission request 1`] = ` xmlns="http://www.w3.org/2000/svg" >
@@ -316,7 +316,7 @@ exports[`AppTile for a pinned widget should render permission request 1`] = `
@@ -404,7 +404,7 @@ exports[`AppTile preserves non-persisted widget on container move 1`] = `
@@ -464,7 +464,7 @@ exports[`AppTile preserves non-persisted widget on container move 1`] = ` xmlns="http://www.w3.org/2000/svg" >
@@ -485,7 +485,7 @@ exports[`AppTile preserves non-persisted widget on container move 1`] = ` xmlns="http://www.w3.org/2000/svg" >
diff --git a/test/unit-tests/components/views/elements/__snapshots__/FacePile-test.tsx.snap b/test/unit-tests/components/views/elements/__snapshots__/FacePile-test.tsx.snap index 2956bed066..2e923be862 100644 --- a/test/unit-tests/components/views/elements/__snapshots__/FacePile-test.tsx.snap +++ b/test/unit-tests/components/views/elements/__snapshots__/FacePile-test.tsx.snap @@ -9,10 +9,10 @@ exports[` renders with a tooltip 1`] = ` tabindex="0" >
renders dropdown options in menu 1`] = ` xmlns="http://www.w3.org/2000/svg" > should render the expected pill for @room 1`] = ` >
@@ -67,7 +67,7 @@ exports[` renders map correctly 1`] = ` xmlns="http://www.w3.org/2000/svg" >
diff --git a/test/unit-tests/components/views/location/__snapshots__/MapError-test.tsx.snap b/test/unit-tests/components/views/location/__snapshots__/MapError-test.tsx.snap index 238097d995..f0c4749153 100644 --- a/test/unit-tests/components/views/location/__snapshots__/MapError-test.tsx.snap +++ b/test/unit-tests/components/views/location/__snapshots__/MapError-test.tsx.snap @@ -15,7 +15,7 @@ exports[` applies class when isMinimised is truthy 1`] = ` xmlns="http://www.w3.org/2000/svg" >

renders correctly for MapStyleUrlNotConfigured 1`] = ` xmlns="http://www.w3.org/2000/svg" >

renders correctly for MapStyleUrlNotReachable 1`] = ` xmlns="http://www.w3.org/2000/svg" >

renders with location icon when no room member 1`] = ` xmlns="http://www.w3.org/2000/svg" >

diff --git a/test/unit-tests/components/views/location/__snapshots__/SmartMarker-test.tsx.snap b/test/unit-tests/components/views/location/__snapshots__/SmartMarker-test.tsx.snap index f2b3e4cc8b..f1663bb751 100644 --- a/test/unit-tests/components/views/location/__snapshots__/SmartMarker-test.tsx.snap +++ b/test/unit-tests/components/views/location/__snapshots__/SmartMarker-test.tsx.snap @@ -18,7 +18,7 @@ exports[` creates a marker on mount 1`] = ` xmlns="http://www.w3.org/2000/svg" >
@@ -45,7 +45,7 @@ exports[` removes marker on unmount 1`] = ` xmlns="http://www.w3.org/2000/svg" >
diff --git a/test/unit-tests/components/views/location/__snapshots__/ZoomButtons-test.tsx.snap b/test/unit-tests/components/views/location/__snapshots__/ZoomButtons-test.tsx.snap index 4aa18b2f1e..02131a43d7 100644 --- a/test/unit-tests/components/views/location/__snapshots__/ZoomButtons-test.tsx.snap +++ b/test/unit-tests/components/views/location/__snapshots__/ZoomButtons-test.tsx.snap @@ -21,7 +21,7 @@ exports[` renders buttons 1`] = ` xmlns="http://www.w3.org/2000/svg" >
@@ -41,7 +41,7 @@ exports[` renders buttons 1`] = ` xmlns="http://www.w3.org/2000/svg" >
diff --git a/test/unit-tests/components/views/messages/TextualBody-test.tsx b/test/unit-tests/components/views/messages/TextualBody-test.tsx index a10e4bebe0..753534a93f 100644 --- a/test/unit-tests/components/views/messages/TextualBody-test.tsx +++ b/test/unit-tests/components/views/messages/TextualBody-test.tsx @@ -186,7 +186,7 @@ describe("", () => { const { container } = getComponent({ mxEvent: ev }); const content = container.querySelector(".mx_EventTile_body"); expect(content.innerHTML).toMatchInlineSnapshot( - `"Chat with Member"`, + `"Chat with Member"`, ); }); @@ -204,7 +204,7 @@ describe("", () => { const { container } = getComponent({ mxEvent: ev }); const content = container.querySelector(".mx_EventTile_body"); expect(content.innerHTML).toMatchInlineSnapshot( - `"Visit #room:example.com"`, + `"Visit #room:example.com"`, ); }); diff --git a/test/unit-tests/components/views/messages/__snapshots__/DecryptionFailureBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/DecryptionFailureBody-test.tsx.snap index a3b9fb205f..599a7719d5 100644 --- a/test/unit-tests/components/views/messages/__snapshots__/DecryptionFailureBody-test.tsx.snap +++ b/test/unit-tests/components/views/messages/__snapshots__/DecryptionFailureBody-test.tsx.snap @@ -35,7 +35,7 @@ exports[`DecryptionFailureBody should handle messages from users who change iden xmlns="http://www.w3.org/2000/svg" > Sender's verified identity has changed diff --git a/test/unit-tests/components/views/messages/__snapshots__/MBeaconBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/MBeaconBody-test.tsx.snap index de31628ec3..14007bd61a 100644 --- a/test/unit-tests/components/views/messages/__snapshots__/MBeaconBody-test.tsx.snap +++ b/test/unit-tests/components/views/messages/__snapshots__/MBeaconBody-test.tsx.snap @@ -14,7 +14,7 @@ exports[` when map display is not configured renders maps unavail xmlns="http://www.w3.org/2000/svg" >

should show a download button in file rendering type 1`] = class="mx_MFileBody_download" > should show a download button in file rendering type 1`] = xmlns="http://www.w3.org/2000/svg" > Download diff --git a/test/unit-tests/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap index 7b919b5326..48dc54241f 100644 --- a/test/unit-tests/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap +++ b/test/unit-tests/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap @@ -58,7 +58,7 @@ exports[`MLocationBody without error renders map correctly 1`] = xmlns="http://www.w3.org/2000/svg" > @@ -92,7 +92,7 @@ exports[`MLocationBody without error renders marker correctly fo class="mx_Marker_border" > Pinned message diff --git a/test/unit-tests/components/views/messages/__snapshots__/TextualBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/TextualBody-test.tsx.snap index 1ea245acf0..44672397ac 100644 --- a/test/unit-tests/components/views/messages/__snapshots__/TextualBody-test.tsx.snap +++ b/test/unit-tests/components/views/messages/__snapshots__/TextualBody-test.tsx.snap @@ -86,7 +86,7 @@ exports[` renders formatted m.text correctly pills appear for an Message from Member"`; +exports[` renders plain-text m.text correctly should pillify a permalink to a message in the same room with the label »Message from Member« 1`] = `"Visit Message from Member"`; -exports[` renders plain-text m.text correctly should pillify a permalink to an event in another room with the label »Message in Room 2« 1`] = `"Visit Message in Room 2"`; +exports[` renders plain-text m.text correctly should pillify a permalink to an event in another room with the label »Message in Room 2« 1`] = `"Visit Message in Room 2"`; exports[` renders plain-text m.text correctly should pillify a permalink to an unknown message in the same room with the label »Message« 1`] = `
renders plain-text m.text correctly should pillify a pe xmlns="http://www.w3.org/2000/svg" > Poll detail navigates back to poll list from detail vie xmlns="http://www.w3.org/2000/svg" > Active polls diff --git a/test/unit-tests/components/views/right_panel/__snapshots__/BaseCard-test.tsx.snap b/test/unit-tests/components/views/right_panel/__snapshots__/BaseCard-test.tsx.snap index 21ec64e9b3..dc85fba985 100644 --- a/test/unit-tests/components/views/right_panel/__snapshots__/BaseCard-test.tsx.snap +++ b/test/unit-tests/components/views/right_panel/__snapshots__/BaseCard-test.tsx.snap @@ -12,7 +12,7 @@ exports[` should close when clicking X button 1`] = ` class="mx_BaseCard_header_title" >

Heading text @@ -20,14 +20,14 @@ exports[` should close when clicking X button 1`] = `

message case AskToJoin renders the corresponding mes

message case AskToJoin renders the corresponding mes

with an invite with an invited email when client has

with an invite without an invited email for a dm roo

with an invite without an invited email for a non-dm

should render invite 1`] = ` class="mx_BaseCard_header_title" >

Profile @@ -20,14 +20,14 @@ exports[` should render invite 1`] = ` @@ -39,7 +39,7 @@ exports[`FormattingButtons renders in german 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -58,7 +58,7 @@ exports[`FormattingButtons renders in german 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -77,7 +77,7 @@ exports[`FormattingButtons renders in german 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -96,7 +96,7 @@ exports[`FormattingButtons renders in german 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -115,7 +115,7 @@ exports[`FormattingButtons renders in german 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -134,7 +134,7 @@ exports[`FormattingButtons renders in german 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -153,7 +153,7 @@ exports[`FormattingButtons renders in german 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -172,7 +172,7 @@ exports[`FormattingButtons renders in german 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -191,7 +191,7 @@ exports[`FormattingButtons renders in german 1`] = ` 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 2aa08adb94..e8ab3ccd2f 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 @@ -19,25 +19,25 @@ exports[` should render 1`] = ` class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" >

  1. flow to change the recovery key should display th
  2. Change recovery key @@ -61,7 +61,7 @@ exports[` flow to change the recovery key should display th class="mx_EncryptionCard_header" >
    flow to change the recovery key should display th xmlns="http://www.w3.org/2000/svg" >

    Change recovery key?

    @@ -89,32 +89,32 @@ exports[` flow to change the recovery key should display th class="mx_KeyPanel" > Recovery key
    encoded private key Do not share this with anyone!
    1. flow to set up a recovery key should ask the user
    2. Set up recovery @@ -221,7 +221,7 @@ exports[` flow to set up a recovery key should ask the user class="mx_EncryptionCard_header" >
      flow to set up a recovery key should ask the user xmlns="http://www.w3.org/2000/svg" >

      Enter your recovery key to confirm

      @@ -246,19 +246,19 @@ exports[` flow to set up a recovery key should ask the user
After clicking continue, we’ll generate a recovery key for you. @@ -542,7 +542,7 @@ exports[` flow to set up a recovery key should display info class="mx_EncryptionCard_buttons" >
  1. flow to set up a recovery key should display the
  2. Set up recovery @@ -625,7 +625,7 @@ exports[` flow to set up a recovery key should display the class="mx_EncryptionCard_header" >
    flow to set up a recovery key should display the xmlns="http://www.w3.org/2000/svg" >

    Save your recovery key somewhere safe

    @@ -653,32 +653,32 @@ exports[` flow to set up a recovery key should display the class="mx_KeyPanel" > Recovery key
    encoded private key Do not share this with anyone!
  1. should display the 'forgot recovery key' variant
  2. Reset encryption @@ -61,7 +61,7 @@ exports[` should display the 'forgot recovery key' variant class="mx_EncryptionCard_header" >
    should display the 'forgot recovery key' variant xmlns="http://www.w3.org/2000/svg" >

    Forgot your recovery key? You’ll need to reset your identity.

    @@ -87,14 +87,14 @@ exports[` should display the 'forgot recovery key' variant style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);" >