Compare commits

...

19 Commits

Author SHA1 Message Date
Andy Balaam
ff3f069122 Provide a labs flag for encrypted state events (MSC3414) (#31513)
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
Co-authored-by: Skye Elliot <actuallyori@gmail.com>
2025-12-18 14:45:52 +00:00
Florian Duros
6f0369e623 Fix shared component screenshot update (#31568)
* build: fix shared component screenshot update

The yarn command `test:storybook:update` was running twice
`playwright-sceenshots`. However this script is using ryuk to delete
remaining containers/etc and ryuk does the cleanup after 50sec of idle.
So on the script second call, ryuk container was still running and the
script failed.

This PR introduces a shell script to install the dependencies and to run
the tests in the same playrwright-screenshots call.

* Update packages/shared-components/scripts/storybook-screenshot-update.sh

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update packages/shared-components/scripts/storybook-screenshot-update.sh

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* doc: fix duplicated documentation after github commit

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2025-12-18 10:44:17 +00:00
Florian Duros
7a69ab8be4 Fix act warning in member list test (#31570)
* test: fix act warning in member list

Wrapping rerender in act

* refactor: remove extra inline function
2025-12-17 17:08:00 +00:00
Skye Elliot
4da149e56f Update prop type & documentation for HistoryVisibleBanner and VM. (#31545)
* docs: Update documentation for HistoryVisibleBanner and VM.

* docs: Improve documentation for `HistoryVisibleBanner`, second pass.

* docs: Move documentation to prop types over FC.

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* fix: Simplify type for `HistoryVisibleBannerViewModel` `threadId`.

* docs: Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2025-12-17 16:04:53 +00:00
Michael Telatynski
e696f92bd3 Switch to Compound icons in more places (#31560)
* Switch to Compound icons in ServerPicker

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

* Switch to Compound ask-to-join icon

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

* Switch to Compound invite icon

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

* Switch to Compound pin icon

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

* Switch to Compound Spinner

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

* Update snapshots

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

* Fix ServerPicker icon rendering

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

* Update screenshots

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

* Update screenshot

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

* Replace search-inset.svg with Compound Message icon

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-17 11:17:11 +00:00
ElementRobot
48c360f688 [create-pull-request] automated change (#31554)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-12-17 10:57:57 +00:00
RiotRobot
3ee50c59f8 Reset matrix-js-sdk back to develop branch 2025-12-16 14:54:45 +00:00
RiotRobot
ba2386ae41 Merge branch 'master' into develop 2025-12-16 14:53:41 +00:00
RiotRobot
fab2997107 v1.12.7 2025-12-16 14:49:39 +00:00
Michael Telatynski
60ef5d880c [Backport staging] Amend e2e normal icon from lock-solid to info (#31559)
* Amend e2e normal icon from lock-solid to info (#31555)

* Amend e2e normal icon from lock-solid to info

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

* Update screenshot

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

---------

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

(cherry picked from commit 773662e018)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update screenshot

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 14:16:43 +00:00
RiotRobot
c486299deb Upgrade dependency to matrix-js-sdk@39.4.0 2025-12-16 13:52:36 +00:00
ElementRobot
3fb0f65735 Fix CSS specificity causing icon issues in e2e verification (#31542) (#31548)
(cherry picked from commit 0cfaeaa3a7)

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-15 16:39:44 +00:00
RiotRobot
154baba303 v1.12.7-rc.2 2025-12-12 15:47:29 +00:00
ElementRobot
8d918e3b16 Fix e2e icons in CompleteSecurity & SetupEncryptionBody (#31521) (#31522)
* Fix e2e icons in CompleteSecurity & SetupEncryptionBody



* Tests



* Prevent screenshot clash between tests



---------


(cherry picked from commit 7b024f956d)

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-11 17:21:53 +00:00
RiotRobot
011b975d3f v1.12.7-rc.1 2025-12-11 13:43:28 +00:00
ElementRobot
00954542f3 Remove an extra paragraph in advanced room settings (#31500) (#31511)
This was introduced (presumably accidentally) in
[#30169](https://github.com/element-hq/element-web/pull/30169/files#diff-89268874351e08a327e47b0a7e1d4e916e1ad8dc4be8b4a3f1ef67f3f83a5bc9R459)

(cherry picked from commit 3d80e607ce)

Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
2025-12-11 11:01:17 +00:00
ElementRobot
1cf9e546fd Don't show the key storage out of sync toast when backup disabled (#31506) (#31507)
(cherry picked from commit 4bd8eeb17a)

Co-authored-by: Hubert Chathi <hubert@uhoreg.ca>
2025-12-10 18:14:32 +00:00
RiotRobot
cf25d5e258 v1.12.7-rc.0 2025-12-09 15:01:14 +00:00
RiotRobot
86a38cd2d2 Upgrade dependency to matrix-js-sdk@39.4.0-rc.0 2025-12-09 14:53:31 +00:00
114 changed files with 1481 additions and 289 deletions

View File

@@ -1,3 +1,52 @@
Changes in [1.12.7](https://github.com/element-hq/element-web/releases/tag/v1.12.7) (2025-12-16)
================================================================================================
## ✨ Features
* Replace legacy icons with compound ([#31424](https://github.com/element-hq/element-web/pull/31424)). Contributed by @t3chguy.
* Update polls UX to match EX Mobile and improve accessibility ([#31245](https://github.com/element-hq/element-web/pull/31245)). Contributed by @langleyd.
* Add option to enable read receipt and marker when user interact with UI ([#31353](https://github.com/element-hq/element-web/pull/31353)). Contributed by @florianduros.
* Introduce a hook to auto dispose view models ([#31178](https://github.com/element-hq/element-web/pull/31178)). Contributed by @MidhunSureshR.
* Update settings toggles to use consistent design across app. ([#30169](https://github.com/element-hq/element-web/pull/30169)). Contributed by @Half-Shot.
* Add ability to the room view to hide widgets ([#31400](https://github.com/element-hq/element-web/pull/31400)). Contributed by @langleyd.
* call: Pass the echo cancellation and noise suppression settings to EC ([#31317](https://github.com/element-hq/element-web/pull/31317)). Contributed by @BillCarsonFr.
* Tweak rendering of icons for a11y ([#31358](https://github.com/element-hq/element-web/pull/31358)). Contributed by @t3chguy.
* Implement new `renderNotificationDecoration` from module API ([#31389](https://github.com/element-hq/element-web/pull/31389)). Contributed by @MidhunSureshR.
* Replace more icons with compound ([#31381](https://github.com/element-hq/element-web/pull/31381)). Contributed by @t3chguy.
* Replace more icons with compound ([#31378](https://github.com/element-hq/element-web/pull/31378)). Contributed by @t3chguy.
* `<Banner/>`: Hide `Dismiss` button if `onClose` handler is not provided. ([#31362](https://github.com/element-hq/element-web/pull/31362)). Contributed by @kaylendog.
* Replace batch of legacy icons with compound design tokens ([#31360](https://github.com/element-hq/element-web/pull/31360)). Contributed by @t3chguy.
* MSC4380: Invite blocking ([#31268](https://github.com/element-hq/element-web/pull/31268)). Contributed by @richvdh.
* Tweak rendering of icons for accessibility ([#31346](https://github.com/element-hq/element-web/pull/31346)). Contributed by @t3chguy.
* Implement a shared `Banner` component. ([#31266](https://github.com/element-hq/element-web/pull/31266)). Contributed by @kaylendog.
* Allow the Login screen to use the dark theme ([#31293](https://github.com/element-hq/element-web/pull/31293)). Contributed by @richvdh.
## 🐛 Bug Fixes
* [Backport staging] Amend e2e normal icon from lock-solid to info ([#31559](https://github.com/element-hq/element-web/pull/31559)). Contributed by @t3chguy.
* [Backport staging] Fix CSS specificity causing icon issues in e2e verification ([#31548](https://github.com/element-hq/element-web/pull/31548)). Contributed by @RiotRobot.
* [Backport staging] Fix e2e icons in CompleteSecurity \& SetupEncryptionBody ([#31522](https://github.com/element-hq/element-web/pull/31522)). Contributed by @RiotRobot.
* [Backport staging] Remove an extra paragraph in advanced room settings ([#31511](https://github.com/element-hq/element-web/pull/31511)). Contributed by @RiotRobot.
* [Backport staging] Don't show the key storage out of sync toast when backup disabled ([#31507](https://github.com/element-hq/element-web/pull/31507)). Contributed by @RiotRobot.
* Fix composer button visibility in contrast colour mode ([#31255](https://github.com/element-hq/element-web/pull/31255)). Contributed by @t3chguy.
* Ensure correct room version is used and permissions are appropriately sert when creating rooms ([#31464](https://github.com/element-hq/element-web/pull/31464)). Contributed by @Half-Shot.
* Fix e2e icon rendering ([#31454](https://github.com/element-hq/element-web/pull/31454)). Contributed by @t3chguy.
* EventIndexer: ensure we add initial checkpoints when the db is first opened ([#31448](https://github.com/element-hq/element-web/pull/31448)). Contributed by @richvdh.
* Fix `/join <alias>` command failing due to race condition ([#31433](https://github.com/element-hq/element-web/pull/31433)). Contributed by @MidhunSureshR.
* MessageEventIndexDialog: distinguish indexed rooms ([#31436](https://github.com/element-hq/element-web/pull/31436)). Contributed by @richvdh.
* Move `EditInPlace` out of `Form` (Fixes: reloading EW on EC url update) ([#31434](https://github.com/element-hq/element-web/pull/31434)). Contributed by @toger5.
* Fixes issue where cursor would jump to the beginning of the input field after converting Japanese text and pressing Tab ([#31432](https://github.com/element-hq/element-web/pull/31432)). Contributed by @shinaoka.
* Fix widgets getting stuck in loading states ([#31314](https://github.com/element-hq/element-web/pull/31314)). Contributed by @robintown.
* Room list: fix room options remaining on room item after mouse leaving ([#31414](https://github.com/element-hq/element-web/pull/31414)). Contributed by @florianduros.
* Make `RoomList.showMessagePreview` configurable by `config.json` ([#31419](https://github.com/element-hq/element-web/pull/31419)). Contributed by @florianduros.
* Fix bug which caused app not to load correctly when `force_verification` is enabled ([#31265](https://github.com/element-hq/element-web/pull/31265)). Contributed by @richvdh.
* Room list: display the menu option on the room list item when clicked/opened ([#31380](https://github.com/element-hq/element-web/pull/31380)). Contributed by @florianduros.
* Fix handling of SVGs ([#31359](https://github.com/element-hq/element-web/pull/31359)). Contributed by @t3chguy.
* Fix word wrapping in expanded left panel buttons ([#31377](https://github.com/element-hq/element-web/pull/31377)). Contributed by @t3chguy.
* Fix aspect ratio on error view background ([#31361](https://github.com/element-hq/element-web/pull/31361)). Contributed by @t3chguy.
* Fix failure to request persistent storage perms ([#31299](https://github.com/element-hq/element-web/pull/31299)). Contributed by @richvdh.
* Fix calls sometimes not knowing that they're presented ([#31313](https://github.com/element-hq/element-web/pull/31313)). Contributed by @robintown.
Changes in [1.12.6](https://github.com/element-hq/element-web/releases/tag/v1.12.6) (2025-12-03) Changes in [1.12.6](https://github.com/element-hq/element-web/releases/tag/v1.12.6) (2025-12-03)
================================================================================================ ================================================================================================
This release fixes a bug where 1:1 calling was incorrectly not available if no Element Call focus was set. This release fixes a bug where 1:1 calling was incorrectly not available if no Element Call focus was set.

View File

@@ -112,3 +112,25 @@ Enables knock feature for rooms. This allows users to ask to join a room.
## New room list (`feature_new_room_list`) [In Development] ## New room list (`feature_new_room_list`) [In Development]
Enable the new room list that is currently in development. Enable the new room list that is currently in development.
## Exclude insecure devices when sending/receiving messages (`feature_exclude_insecure_devices`)
Do not send or receive messages to/from devices that are not properly verified. Users with unverified devices will not
receive your messages at all on those devices, and if they send messages, you will not be able to read them, but you
will be aware that a message exists.
## Share encrypted history with new members (`feature_share_history_on_invite`) [In Development]
When inviting users to an encrypted room with shared history (i.e. a room with the "Who can read history?" setting set
to "Members only (since the point in time of selecting this option)"), send the keys for previous messages to the
invitee so they can read them.
Both the inviter and the invitee must set this labs flag, before the invitation is sent.
## Encrypted state events (MSC4362) (`feature_msc4362_encrypted_state_events`)
Encrypt most of the state events in the room, including the room name and topic.
WARNING: this means that users joining a room who do not have access to its history will not be able to see the name or
topic of the room, or any other room state information. It also means the room name and topic are not available before
joining a room.

View File

@@ -1,6 +1,6 @@
{ {
"name": "element-web", "name": "element-web",
"version": "1.12.6", "version": "1.12.7",
"description": "Element: the future of secure communication", "description": "Element: the future of secure communication",
"author": "New Vector Ltd.", "author": "New Vector Ltd.",
"repository": { "repository": {

View File

@@ -43,7 +43,7 @@
"lint:types": "tsc --noEmit --jsx react", "lint:types": "tsc --noEmit --jsx react",
"test:storybook": "test-storybook --url http://localhost:6007/", "test:storybook": "test-storybook --url http://localhost:6007/",
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" \"yarn storybook --no-open\" \"wait-on tcp:6007 && yarn test-storybook --url http://localhost:6007/ --ci --maxWorkers=2\"", "test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" \"yarn storybook --no-open\" \"wait-on tcp:6007 && yarn test-storybook --url http://localhost:6007/ --ci --maxWorkers=2\"",
"test:storybook:update": "playwright-screenshots --entrypoint yarn --with-node-modules && playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot" "test:storybook:update": "playwright-screenshots --entrypoint /work/scripts/storybook-screenshot-update.sh --with-node-modules"
}, },
"resolutions": { "resolutions": {
"playwright": "1.57.0" "playwright": "1.57.0"

View File

@@ -0,0 +1,32 @@
#!/bin/bash
#
# Update storybook screenshots
#
# This script should be used as the entrypoint parameter of the `playwright-screenshots` script. It
# installs the yarn dependencies, and then runs `test-storybook` to update the storybook screenshots.
#
# It expects to find a storybook instance running at :6007 on the host machine. It also requires that
# `playwright-screenshots` is given the `--with-node-modules` parameter.
#
# Example:
#
# test-storybook --url http://localhost:6007/
# playwright-screenshots --entrypoint /work/scripts/storybook-screenshot-update.sh --with-node-modules
#
#
# Note: even though this script is small, it is important because the alternative is running
# `playwright-screenshots` twice in quick succession (once to do `yarn install`, a second to do the
# actual updates): and that fails, because running `playwright-screenshots` without actually starting
# Testcontainers leaves a ryuk container hanging around for up to 60s, which will block the second
# invocation.
set -e
# First install dependencies. We have to do this within the playwright container rather than the host,
# because we have which must be built for the right architecture (and some environments use a VM
# to run docker containers, meaning that things inside a container use a different architecture than
# those on the host).
yarn
# Now run the screenshot update
/work/node_modules/.bin/test-storybook --url http://host.docker.internal:6007/ --updateSnapshot

View File

@@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { type Page } from "playwright-core";
import { SettingLevel } from "../../../src/settings/SettingLevel"; import { SettingLevel } from "../../../src/settings/SettingLevel";
import { UIFeature } from "../../../src/settings/UIFeature"; import { UIFeature } from "../../../src/settings/UIFeature";
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
@@ -110,4 +112,107 @@ test.describe("Create Room", () => {
await expect(header).toContainText(name); await expect(header).toContainText(name);
}); });
}); });
test.describe("when the encrypted state labs flag is turned off", () => {
test.use({ labsFlags: [] });
test("creates a room without encrypted state", { tag: "@screenshot" }, async ({ page, user: _user }) => {
// When we start to create a room
await page.getByRole("button", { name: "New conversation", exact: true }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill(name);
// Then there is no Encrypt state events button
await expect(page.getByRole("checkbox", { name: "Encrypt state events" })).not.toBeVisible();
// And when we create the room
await page.getByRole("button", { name: "Create room" }).click();
// Then we created a normal encrypted room, without encrypted state
await expect(page.getByText("Encryption enabled")).toBeVisible();
await expect(page.getByText("State encryption enabled")).not.toBeVisible();
// And the room name state event is not encrypted
await viewSourceOnRoomNameEvent(page);
await expect(page.getByText("Original event source")).toBeVisible();
await expect(page.getByText("Decrypted event source")).not.toBeVisible();
}); });
});
test.describe("when the encrypted state labs flag is turned on", () => {
test.use({ labsFlags: ["feature_msc4362_encrypted_state_events"] });
test(
"creates a room with encrypted state if we check the box",
{ tag: "@screenshot" },
async ({ page, user: _user }) => {
// Given we check the Encrypted State checkbox
await page.getByRole("button", { name: "New conversation", exact: true }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
await expect(page.getByRole("switch", { name: "Enable end-to-end encryption" })).toBeChecked();
await page.getByRole("switch", { name: "Encrypt state events" }).click();
await expect(page.getByRole("switch", { name: "Encrypt state events" })).toBeChecked();
// When we create a room
await page.getByRole("textbox", { name: "Name" }).fill(name);
await page.getByRole("button", { name: "Create room" }).click();
// Then we created an encrypted state room
await expect(page.getByText("State encryption enabled")).toBeVisible();
// And it has the correct name
await expect(page.getByTestId("timeline").getByRole("heading", { name })).toBeVisible();
// And the room name state event is encrypted
await viewSourceOnRoomNameEvent(page);
await expect(page.getByText("Decrypted event source")).toBeVisible();
},
);
test(
"creates a room without encrypted state if we don't check the box",
{ tag: "@screenshot" },
async ({ page, user: _user }) => {
// Given we did not check the Encrypted State checkbox
await page.getByRole("button", { name: "New conversation", exact: true }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
await expect(page.getByRole("switch", { name: "Enable end-to-end encryption" })).toBeChecked();
// And it is off by default
await expect(page.getByRole("switch", { name: "Encrypt state events" })).not.toBeChecked();
// When we create a room
await page.getByRole("textbox", { name: "Name" }).fill(name);
await page.getByRole("button", { name: "Create room" }).click();
// Then we created a normal encrypted room, without encrypted state
await expect(page.getByText("Encryption enabled")).toBeVisible();
await expect(page.getByText("State encryption enabled")).not.toBeVisible();
// And it has the correct name
await expect(page.getByTestId("timeline").getByRole("heading", { name })).toBeVisible();
// And the room name state event is not encrypted
await viewSourceOnRoomNameEvent(page);
await expect(page.getByText("Original event source")).toBeVisible();
await expect(page.getByText("Decrypted event source")).not.toBeVisible();
},
);
});
});
async function viewSourceOnRoomNameEvent(page: Page) {
await page
.getByRole("listitem")
.filter({ hasText: "created and configured the room" })
.getByRole("button", { name: "expand" })
.click();
await page
.getByRole("listitem")
.filter({ hasText: "changed the room name to" })
.getByRole("button", { name: "Options" })
.click();
await page.getByRole("menuitem", { name: "View source" }).click();
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 984 KiB

After

Width:  |  Height:  |  Size: 984 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

View File

@@ -10,7 +10,7 @@ import {
type StartedPostgreSqlContainer, type StartedPostgreSqlContainer,
} from "@element-hq/element-web-playwright-common/lib/testcontainers"; } from "@element-hq/element-web-playwright-common/lib/testcontainers";
const TAG = "main@sha256:1ffa26f3d7b1e7481e10ec23bbb65afc0394a1f0416462601b8ef5b0eaf9aced"; const TAG = "main@sha256:2c5966c2ff06458ac5cbae959f12e19d30e3ebb63c641d31ec1ae08abccb9c6d";
/** /**
* MatrixAuthenticationServiceContainer which freezes the docker digest to * MatrixAuthenticationServiceContainer which freezes the docker digest to

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers"; import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
const TAG = "develop@sha256:a2790ff0be7d8da93e26c09bcfedded2f5410affac87065cfe11309a85b4c728"; const TAG = "develop@sha256:7c3dce1d2b44fdc4b1494c5b8f4792018733ad323f823b88aac30c883d09fb35";
/** /**
* SynapseContainer which freezes the docker digest to stabilise tests, * SynapseContainer which freezes the docker digest to stabilise tests,

View File

@@ -162,7 +162,7 @@ Please see LICENSE files in the repository root for full details.
width: 16px; width: 16px;
background: var(--cpd-color-icon-on-solid-primary); background: var(--cpd-color-icon-on-solid-primary);
mask-size: 16px; mask-size: 16px;
mask-image: url("$(res)/img/element-icons/room/invite.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/user-add.svg");
} }
} }
@@ -232,7 +232,7 @@ Please see LICENSE files in the repository root for full details.
text-decoration: underline; text-decoration: underline;
&::before { &::before {
mask-image: url("$(res)/img/element-icons/room/invite.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/user-add.svg");
background-color: var(--cpd-color-icon-primary); background-color: var(--cpd-color-icon-primary);
} }
} }

View File

@@ -237,7 +237,7 @@ Please see LICENSE files in the repository root for full details.
font-size: $font-15px; font-size: $font-15px;
line-height: $font-24px; line-height: $font-24px;
.mx_InlineSpinner img { .mx_InlineSpinner svg {
vertical-align: sub; vertical-align: sub;
margin-right: 5px; margin-right: 5px;
} }

View File

@@ -454,7 +454,7 @@ Please see LICENSE files in the repository root for full details.
} }
.mx_SpotlightDialog_searchMessages::before { .mx_SpotlightDialog_searchMessages::before {
mask-image: url("$(res)/img/element-icons/room/search-inset.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/chat.svg");
} }
.mx_SpotlightDialog_otherSearches_messageSearchText { .mx_SpotlightDialog_otherSearches_messageSearchText {

View File

@@ -7,14 +7,10 @@ Please see LICENSE files in the repository root for full details.
.mx_InlineSpinner { .mx_InlineSpinner {
display: inline; display: inline;
}
.mx_InlineSpinner img, svg {
.mx_InlineSpinner_icon {
margin: 0px 6px; margin: 0px 6px;
vertical-align: -3px; vertical-align: -3px;
display: inline-block;
} }
.mx_InlineSpinner_icon {
display: inline-block !important; /* Override regular mx_Spinner_icon */
} }

View File

@@ -25,13 +25,11 @@ Please see LICENSE files in the repository root for full details.
.mx_ServerPicker_help { .mx_ServerPicker_help {
width: 20px; width: 20px;
height: 20px; height: 20px;
background-color: $icon-button-color;
border-radius: 10px; border-radius: 10px;
grid-column: 2; grid-column: 2;
grid-row: 1; grid-row: 1;
margin-left: auto; margin-left: auto;
text-align: center; text-align: center;
color: #ffffff;
font-size: 16px; font-size: 16px;
position: relative; position: relative;
@@ -45,8 +43,8 @@ Please see LICENSE files in the repository root for full details.
mask-position: center; mask-position: center;
mask-size: contain; mask-size: contain;
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-image: url("$(res)/img/element-icons/i.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/info.svg");
background: #ffffff; background: $icon-button-color;
} }
} }

View File

@@ -19,39 +19,3 @@ Please see LICENSE files in the repository root for full details.
.mx_MatrixChat_middlePanel .mx_Spinner { .mx_MatrixChat_middlePanel .mx_Spinner {
height: auto; height: auto;
} }
@keyframes spin {
from {
transform: rotateZ(0deg);
}
to {
transform: rotateZ(360deg);
}
}
.mx_Spinner_icon {
background-color: $quinary-content;
mask: url("$(res)/img/spinner/spinner-background.svg");
mask-size: 100%;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
&::before {
background-color: $secondary-content;
mask: url("$(res)/img/spinner/spinner-foreground.svg");
mask-size: 100%;
width: 100%;
height: 100%;
content: "";
display: flex;
animation: 1s linear spin infinite;
}
}

View File

@@ -89,7 +89,7 @@ Please see LICENSE files in the repository root for full details.
right: 8px; right: 8px;
&::before { &::before {
mask-image: url("$(res)/img/element-icons/room/pin-upright.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/pin-solid.svg");
} }
} }

View File

@@ -40,7 +40,7 @@ Please see LICENSE files in the repository root for full details.
} }
.mx_NewRoomIntro_inviteButton::before { .mx_NewRoomIntro_inviteButton::before {
mask-image: url("$(res)/img/element-icons/room/invite.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/user-add.svg");
} }
} }

View File

@@ -56,8 +56,8 @@ Please see LICENSE files in the repository root for full details.
} }
&.mx_NotificationBadge_knocked { &.mx_NotificationBadge_knocked {
mask-image: url("$(res)/img/element-icons/ask-to-join.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/ask-to-join.svg");
width: 12px; width: 16px;
height: 16px; height: 16px;
} }

View File

@@ -66,7 +66,7 @@ Please see LICENSE files in the repository root for full details.
&.mx_LegacyCallViewHeader_button_pin { &.mx_LegacyCallViewHeader_button_pin {
&::before { &::before {
mask-image: url("$(res)/img/element-icons/room/pin-upright.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/pin-solid.svg");
} }
} }

View File

@@ -1 +0,0 @@
<svg width="12" height="16" xmlns="http://www.w3.org/2000/svg"><path d="M6.806 16c-1.211 0-2.242-.336-3.091-1.008-.85-.673-1.458-1.48-1.826-2.425L.217 8.25c-.206-.533-.265-.93-.179-1.192.087-.26.282-.391.585-.391.4 0 .714.12.941.358.227.239.411.542.552.908l.714 1.884c.01.033.097.094.26.183h.227V2.2c0-.233.084-.436.251-.608a.8.8 0 011.177 0 .858.858 0 01.244.608v5.467c0 .089.032.166.097.233a.307.307 0 00.454 0 .324.324 0 00.098-.233v-6.8c0-.234.084-.436.251-.609A.8.8 0 016.482 0a.8.8 0 01.592.258.843.843 0 01.252.609v6.8c0 .089.032.166.097.233a.307.307 0 00.455 0 .324.324 0 00.097-.233v-5.8c0-.234.084-.436.252-.609A.8.8 0 018.819 1a.8.8 0 01.592.258.843.843 0 01.252.609v5.8c0 .089.032.166.097.233a.307.307 0 00.455 0 .324.324 0 00.097-.233v-3.8c0-.234.084-.436.252-.609A.8.8 0 0111.156 3a.8.8 0 01.592.258.843.843 0 01.252.609v6.966c0 1.523-.495 2.764-1.485 3.725C9.525 15.52 8.289 16 6.806 16z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 932 B

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 10C12.8284 10 13.5 9.32843 13.5 8.5C13.5 7.67157 12.8284 7 12 7C11.1716 7 10.5 7.67157 10.5 8.5C10.5 9.32843 11.1716 10 12 10ZM11 13C10.4477 13 10 12.5523 10 12C10 11.4477 10.4477 11 11 11H12C12.5523 11 13 11.4477 13 12V15.5H13.5C14.0523 15.5 14.5 15.9477 14.5 16.5C14.5 17.0523 14.0523 17.5 13.5 17.5H12C11.4477 17.5 11 17.0523 11 16.5L11 13Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 516 B

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="-0.4 1 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.1001 9C18.7779 9 18.5168 8.73883 18.5168 8.41667V6.08333H16.1834C15.8613 6.08333 15.6001 5.82217 15.6001 5.5C15.6001 5.17783 15.8613 4.91667 16.1834 4.91667H18.5168V2.58333C18.5168 2.26117 18.7779 2 19.1001 2C19.4223 2 19.6834 2.26117 19.6834 2.58333V4.91667H22.0168C22.3389 4.91667 22.6001 5.17783 22.6001 5.5C22.6001 5.82217 22.3389 6.08333 22.0168 6.08333H19.6834V8.41667C19.6834 8.73883 19.4223 9 19.1001 9ZM19.6001 11C20.0669 11 20.5212 10.9467 20.9574 10.8458C21.1161 11.5383 21.2 12.2594 21.2 13C21.2 16.1409 19.6917 18.9294 17.3598 20.6808V20.6807C16.0014 21.7011 14.3635 22.3695 12.5815 22.5505C12.2588 22.5832 11.9314 22.6 11.6 22.6C6.29807 22.6 2 18.302 2 13C2 7.69809 6.29807 3.40002 11.6 3.40002C12.3407 3.40002 13.0618 3.48391 13.7543 3.64268C13.6534 4.07884 13.6001 4.53319 13.6001 5C13.6001 8.31371 16.2864 11 19.6001 11ZM11.5999 20.68C13.6754 20.68 15.5585 19.8567 16.9407 18.5189C16.0859 16.4086 14.0167 14.92 11.5998 14.92C9.18298 14.92 7.11378 16.4086 6.25901 18.5189C7.64115 19.8567 9.52436 20.68 11.5999 20.68ZM11.7426 7.41172C10.3168 7.54168 9.2 8.74043 9.2 10.2C9.2 11.7464 10.4536 13 12 13C13.0308 13 13.9315 12.443 14.4176 11.6135C13.0673 10.6058 12.0929 9.12248 11.7426 7.41172Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,7 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 18.5982V15H13V18.5982C13 20.5 12.2383 22 12 22C11.7617 22 11 20.5 11 18.5982Z" fill="black"/>
<path d="M9.5 6C9.17534 5.83333 7.78566 5.2 6.61693 4C5.4482 2.8 6.12239 2 7.13026 2H12V6H9.5Z" fill="black"/>
<path d="M14.5 6C14.8247 5.83333 16.2143 5.2 17.3831 4C18.5518 2.8 17.8776 2 16.8697 2H12V6H14.5Z" fill="black"/>
<path d="M9.42857 6H14.5714L15 10H9L9.42857 6Z" fill="black"/>
<path d="M12 9C8.93114 9 6.32353 10.6927 5.37867 13.0483C4.96746 14.0735 5.89543 15 7 15H17C18.1046 15 19.0325 14.0735 18.6213 13.0483C17.6765 10.6927 15.0689 9 12 9Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 681 B

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22ZM11 14C12.6569 14 14 12.6569 14 11C14 9.34315 12.6569 8 11 8C9.34315 8 8 9.34315 8 11C8 12.6569 9.34315 14 11 14ZM16 11C16 12.0191 15.6951 12.967 15.1716 13.7574L17.2071 15.7929C17.5976 16.1834 17.5976 16.8166 17.2071 17.2071C16.8166 17.5976 16.1834 17.5976 15.7929 17.2071L13.7574 15.1716C12.967 15.6951 12.0191 16 11 16C8.23858 16 6 13.7614 6 11C6 8.23858 8.23858 6 11 6C13.7614 6 16 8.23858 16 11Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 682 B

View File

@@ -1,3 +0,0 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="15" cy="15" r="14" stroke="#E3E8F0" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 170 B

View File

@@ -1,3 +0,0 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M29 15C29 12.4307 28.293 9.91095 26.9563 7.7167C25.6197 5.52246 23.705 3.73836 21.4219 2.55979C19.1389 1.38123 16.5755 0.853662 14.0126 1.03487C11.4497 1.21607 8.98611 2.09906 6.8916 3.58713" stroke="#737D8C" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 387 B

View File

@@ -437,6 +437,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
// These are always installed regardless of the labs flag so that cross-signing features // These are always installed regardless of the labs flag so that cross-signing features
// can toggle on without reloading and also be accessed immediately after login. // can toggle on without reloading and also be accessed immediately after login.
cryptoCallbacks: { ...crossSigningCallbacks }, cryptoCallbacks: { ...crossSigningCallbacks },
enableEncryptedStateEvents: SettingsStore.getValue("feature_msc4362_encrypted_state_events"),
roomNameGenerator: (_: string, state: RoomNameState) => { roomNameGenerator: (_: string, state: RoomNameState) => {
switch (state.type) { switch (state.type) {
case RoomNameType.Generated: case RoomNameType.Generated:

View File

@@ -20,7 +20,7 @@ interface LargeLoaderProps {
export const LargeLoader: React.FC<LargeLoaderProps> = ({ text }) => { export const LargeLoader: React.FC<LargeLoaderProps> = ({ text }) => {
return ( return (
<div className="mx_LargeLoader"> <div className="mx_LargeLoader">
<Spinner w={45} h={45} /> <Spinner size={45} />
<div className="mx_LargeLoader_text">{text}</div> <div className="mx_LargeLoader_text">{text}</div>
</div> </div>
); );

View File

@@ -150,7 +150,7 @@ const Tile: React.FC<ITileProps> = ({
tabIndex={isActive ? 0 : -1} tabIndex={isActive ? 0 : -1}
title={_t("space|joining_space")} title={_t("space|joining_space")}
> >
<Spinner w={24} h={24} /> <Spinner size={24} />
</AccessibleButton> </AccessibleButton>
); );
} else if (joinedRoom || room.join_rule === JoinRule.Knock) { } else if (joinedRoom || room.join_rule === JoinRule.Knock) {

View File

@@ -373,7 +373,7 @@ export default class ForgotPassword extends React.Component<Props, State> {
public renderSetPassword(): JSX.Element { public renderSetPassword(): JSX.Element {
const submitButtonChild = const submitButtonChild =
this.state.phase === Phase.ResettingPassword ? <Spinner w={16} h={16} /> : _t("auth|reset_password_action"); this.state.phase === Phase.ResettingPassword ? <Spinner size={16} /> : _t("auth|reset_password_action");
return ( return (
<> <>

View File

@@ -38,7 +38,7 @@ export const EnterEmail: React.FC<EnterEmailProps> = ({
onLoginClick, onLoginClick,
onSubmitForm, onSubmitForm,
}) => { }) => {
const submitButtonChild = loading ? <Spinner w={16} h={16} /> : _t("auth|forgot_password_send_email"); const submitButtonChild = loading ? <Spinner size={16} /> : _t("auth|forgot_password_send_email");
const emailFieldRef = useRef<Field>(null); const emailFieldRef = useRef<Field>(null);

View File

@@ -460,7 +460,7 @@ export class EmailIdentityAuthEntry extends React.Component<
a: (text: string) => ( a: (text: string) => (
<Fragment> <Fragment>
<AccessibleButton kind="link_inline" onClick={null} disabled> <AccessibleButton kind="link_inline" onClick={null} disabled>
{text} <Spinner w={14} h={14} /> {text} <Spinner size={14} />
</AccessibleButton> </AccessibleButton>
</Fragment> </Fragment>
), ),
@@ -875,7 +875,7 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
{errorSection} {errorSection}
<div className="mx_InteractiveAuthEntryComponents_sso_buttons"> <div className="mx_InteractiveAuthEntryComponents_sso_buttons">
{this.props.busy ? ( {this.props.busy ? (
<Spinner w={24} h={24} /> <Spinner size={24} />
) : ( ) : (
<> <>
{cancelButton} {cancelButton}

View File

@@ -11,7 +11,17 @@ import { type Room } from "matrix-js-sdk/src/matrix";
import { HistoryVisibleBannerViewModel } from "../../../viewmodels/composer/HistoryVisibleBannerViewModel"; import { HistoryVisibleBannerViewModel } from "../../../viewmodels/composer/HistoryVisibleBannerViewModel";
export const HistoryVisibleBanner: React.FC<{ room: Room; threadId?: string | null }> = (props) => { /** Wrapper around {@link HistoryVisibleBannerViewModel} for the creation of an auto-disposed view model. */
export const HistoryVisibleBanner: React.FC<{
/** The room instance associated with this banner view model. */
room: Room;
/**
* If not null, specifies the ID of the thread currently being viewed in the thread timeline side view,
* where the banner view is displayed as a child of the message composer.
*/
threadId: string | null;
}> = (props) => {
const vm = useCreateAutoDisposedViewModel(() => new HistoryVisibleBannerViewModel(props)); const vm = useCreateAutoDisposedViewModel(() => new HistoryVisibleBannerViewModel(props));
return <HistoryVisibleBannerView vm={vm} />; return <HistoryVisibleBannerView vm={vm} />;
}; };

View File

@@ -17,6 +17,7 @@ import {
MarkAsReadIcon, MarkAsReadIcon,
MarkAsUnreadIcon, MarkAsUnreadIcon,
LeaveIcon, LeaveIcon,
UserAddIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons"; } from "@vector-im/compound-design-tokens/assets/web/icons";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
@@ -43,7 +44,6 @@ import { shouldShowComponent } from "../../../customisations/helpers/UIComponent
import { UIComponent } from "../../../settings/UIFeature"; import { UIComponent } from "../../../settings/UIFeature";
import { DeveloperToolsOption } from "./DeveloperToolsOption"; import { DeveloperToolsOption } from "./DeveloperToolsOption";
import { useSettingValue } from "../../../hooks/useSettings"; import { useSettingValue } from "../../../hooks/useSettings";
import { Icon as InviteIcon } from "../../../../res/img/element-icons/room/invite.svg";
export interface RoomGeneralContextMenuProps extends IContextMenuProps { export interface RoomGeneralContextMenuProps extends IContextMenuProps {
room: Room; room: Room;
@@ -190,7 +190,7 @@ export const RoomGeneralContextMenu: React.FC<RoomGeneralContextMenuProps> = ({
onPostInviteClick, onPostInviteClick,
)} )}
label={_t("action|invite")} label={_t("action|invite")}
icon={<InviteIcon />} icon={<UserAddIcon />}
/> />
); );
} }

View File

@@ -15,6 +15,7 @@ import {
LeaveIcon, LeaveIcon,
SearchIcon, SearchIcon,
PreferencesIcon, PreferencesIcon,
UserAddIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons"; } from "@vector-im/compound-design-tokens/assets/web/icons";
import { type IProps as IContextMenuProps } from "../../structures/ContextMenu"; import { type IProps as IContextMenuProps } from "../../structures/ContextMenu";
@@ -40,7 +41,6 @@ import { shouldShowComponent } from "../../../customisations/helpers/UIComponent
import { UIComponent } from "../../../settings/UIFeature"; import { UIComponent } from "../../../settings/UIFeature";
import PosthogTrackers from "../../../PosthogTrackers"; import PosthogTrackers from "../../../PosthogTrackers";
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { Icon as InviteIcon } from "../../../../res/img/element-icons/room/invite.svg";
interface IProps extends IContextMenuProps { interface IProps extends IContextMenuProps {
space?: Room; space?: Room;
@@ -69,7 +69,7 @@ const SpaceContextMenu: React.FC<IProps> = ({ space, hideHeader, onFinished, ...
<IconizedContextMenuOption <IconizedContextMenuOption
data-testid="invite-option" data-testid="invite-option"
className="mx_SpacePanel_contextMenu_inviteButton" className="mx_SpacePanel_contextMenu_inviteButton"
icon={<InviteIcon />} icon={<UserAddIcon />}
label={_t("action|invite")} label={_t("action|invite")}
onClick={onInviteClick} onClick={onInviteClick}
/> />

View File

@@ -40,6 +40,7 @@ interface IProps {
defaultName?: string; defaultName?: string;
parentSpace?: Room; parentSpace?: Room;
defaultEncrypted?: boolean; defaultEncrypted?: boolean;
defaultStateEncrypted?: boolean;
onFinished(proceed?: false): void; onFinished(proceed?: false): void;
onFinished(proceed: true, opts: IOpts): void; onFinished(proceed: true, opts: IOpts): void;
} }
@@ -58,6 +59,11 @@ interface IState {
* Indicates whether end-to-end encryption is enabled for the room. * Indicates whether end-to-end encryption is enabled for the room.
*/ */
isEncrypted: boolean; isEncrypted: boolean;
/**
* Indicates whether end-to-end state encryption is enabled for this room.
* See MSC4362. Available if feature_msc4362_encrypted_state_events is enabled.
*/
isStateEncrypted: boolean;
/** /**
* The room name. * The room name.
*/ */
@@ -117,6 +123,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
this.state = { this.state = {
isPublicKnockRoom: defaultPublic || false, isPublicKnockRoom: defaultPublic || false,
isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(cli), isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(cli),
isStateEncrypted: this.props.defaultStateEncrypted ?? false,
joinRule, joinRule,
name: this.props.defaultName || "", name: this.props.defaultName || "",
topic: "", topic: "",
@@ -141,7 +148,10 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
const { alias } = this.state; const { alias } = this.state;
createOpts.room_alias_name = alias.substring(1, alias.indexOf(":")); createOpts.room_alias_name = alias.substring(1, alias.indexOf(":"));
} else { } else {
const encryptedStateFeature = SettingsStore.getValue("feature_msc4362_encrypted_state_events", null, false);
opts.encryption = this.state.isEncrypted; opts.encryption = this.state.isEncrypted;
opts.stateEncryption = encryptedStateFeature && this.state.isStateEncrypted;
} }
if (this.state.topic) { if (this.state.topic) {
@@ -236,6 +246,10 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
this.setState({ isEncrypted: evt.target.checked }); this.setState({ isEncrypted: evt.target.checked });
}; };
private onStateEncryptedChange: ChangeEventHandler<HTMLInputElement> = (evt): void => {
this.setState({ isStateEncrypted: evt.target.checked });
};
private onAliasChange = (alias: string): void => { private onAliasChange = (alias: string): void => {
this.setState({ alias }); this.setState({ alias });
}; };
@@ -378,6 +392,29 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
); );
} }
let e2eeStateSection: JSX.Element | undefined;
if (
SettingsStore.getValue("feature_msc4362_encrypted_state_events", null, false) &&
this.state.joinRule !== JoinRule.Public
) {
let microcopy: string;
if (!this.state.canChangeEncryption) {
microcopy = _t("create_room|encryption_forced");
} else {
microcopy = _t("create_room|state_encrypted_warning");
}
e2eeStateSection = (
<SettingsToggleInput
name="state-encryption-toggle"
label={_t("create_room|state_encryption_label")}
onChange={this.onStateEncryptedChange}
checked={this.state.isStateEncrypted}
disabled={!this.state.canChangeEncryption}
helpMessage={microcopy}
/>
);
}
let federateLabel = _t("create_room|unfederated_label_default_off"); let federateLabel = _t("create_room|unfederated_label_default_off");
if (SdkConfig.get().default_federate === false) { if (SdkConfig.get().default_federate === false) {
// We only change the label if the default setting is different to avoid jarring text changes to the // We only change the label if the default setting is different to avoid jarring text changes to the
@@ -441,6 +478,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
{visibilitySection} {visibilitySection}
{e2eeSection} {e2eeSection}
{e2eeStateSection}
{aliasField} {aliasField}
{this.advancedSettingsEnabled && ( {this.advancedSettingsEnabled && (
<details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details"> <details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details">

View File

@@ -391,7 +391,7 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
</div> </div>
{isExporting ? ( {isExporting ? (
<div data-testid="export-progress" className="mx_ExportDialog_progress"> <div data-testid="export-progress" className="mx_ExportDialog_progress">
<Spinner w={24} h={24} /> <Spinner size={24} />
<p>{exportProgressText}</p> <p>{exportProgressText}</p>
<DialogButtons <DialogButtons
primaryButton={_t("action|cancel")} primaryButton={_t("action|cancel")}

View File

@@ -52,7 +52,7 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
const entries = c.transactions const entries = c.transactions
.filter((t) => t.status === TransactionStatus.Error || t.didPreviouslyFail) .filter((t) => t.status === TransactionStatus.Error || t.didPreviouslyFail)
.map((t, j) => { .map((t, j) => {
let button = <Spinner w={19} h={19} />; let button = <Spinner size={19} />;
if (t.status === TransactionStatus.Error) { if (t.status === TransactionStatus.Error) {
button = ( button = (
<AccessibleButton kind="link" onClick={() => t.run()}> <AccessibleButton kind="link" onClick={() => t.run()}>

View File

@@ -1279,7 +1279,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
aria-label={_t("action|search")} aria-label={_t("action|search")}
aria-describedby="mx_SpotlightDialog_keyboardPrompt" aria-describedby="mx_SpotlightDialog_keyboardPrompt"
/> />
{(publicRoomsLoading || peopleLoading || profileLoading) && <Spinner w={24} h={24} />} {(publicRoomsLoading || peopleLoading || profileLoading) && <Spinner size={24} />}
</div> </div>
<div <div

View File

@@ -6,13 +6,12 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React from "react"; import React from "react";
import { InlineSpinner as BaseInlineSpinner } from "@vector-im/compound-web";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
interface IProps { interface IProps {
w?: number; size?: number;
h?: number;
children?: React.ReactNode;
} }
export default class InlineSpinner extends React.PureComponent<IProps> { export default class InlineSpinner extends React.PureComponent<IProps> {
@@ -23,15 +22,14 @@ export default class InlineSpinner extends React.PureComponent<IProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
return ( return (
<div className="mx_InlineSpinner"> <span className="mx_InlineSpinner">
<div <BaseInlineSpinner
className="mx_InlineSpinner_icon mx_Spinner_icon" size={this.props.size}
style={{ width: this.props.w, height: this.props.h }}
aria-label={_t("common|loading")} aria-label={_t("common|loading")}
> role="progressbar"
{this.props.children} data-testid="spinner"
</div> />
</div> </span>
); );
} }
} }

View File

@@ -8,11 +8,15 @@ Please see LICENSE files in the repository root for full details.
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";
import { JoinRule } from "matrix-js-sdk/src/matrix"; import { JoinRule } from "matrix-js-sdk/src/matrix";
import { GroupIcon, LockSolidIcon, PublicIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import {
GroupIcon,
LockSolidIcon,
PublicIcon,
AskToJoinIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import Dropdown from "./Dropdown"; import Dropdown from "./Dropdown";
import { type NonEmptyArray } from "../../../@types/common"; import { type NonEmptyArray } from "../../../@types/common";
import { Icon as AskToJoinIcon } from "../../../../res/img/element-icons/ask-to-join.svg";
interface IProps { interface IProps {
value: JoinRule; value: JoinRule;

View File

@@ -87,11 +87,7 @@ const MiniAvatarUploader: React.FC<IProps> = ({
{children} {children}
<div className="mx_MiniAvatarUploader_indicator"> <div className="mx_MiniAvatarUploader_indicator">
{busy ? ( {busy ? <Spinner size={20} /> : <TakePhotoSolidIcon className="mx_MiniAvatarUploader_cameraIcon" />}
<Spinner w={20} h={20} />
) : (
<TakePhotoSolidIcon className="mx_MiniAvatarUploader_cameraIcon" />
)}
</div> </div>
</AccessibleButton> </AccessibleButton>
</React.Fragment> </React.Fragment>

View File

@@ -246,7 +246,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
</p> </p>
); );
} else if (this.state.loading) { } else if (this.state.loading) {
header = <Spinner w={16} h={16} />; header = <Spinner size={16} />;
} }
const { isQuoteExpanded } = this.props; const { isQuoteExpanded } = this.props;

View File

@@ -7,24 +7,23 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React from "react"; import React from "react";
import { InlineSpinner } from "@vector-im/compound-web";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
interface IProps { interface IProps {
w?: number; size?: number;
h?: number;
message?: string; message?: string;
onFinished: any; // XXX: Spinner pretends to be a dialog so it must accept an onFinished, but it never calls it onFinished: any; // XXX: Spinner pretends to be a dialog so it must accept an onFinished, but it never calls it
} }
export default class Spinner extends React.PureComponent<IProps> { export default class Spinner extends React.PureComponent<IProps> {
public static defaultProps: Partial<IProps> = { public static defaultProps: Partial<IProps> = {
w: 32, size: 32,
h: 32,
}; };
public render(): React.ReactNode { public render(): React.ReactNode {
const { w, h, message } = this.props; const { size, message } = this.props;
return ( return (
<div className="mx_Spinner"> <div className="mx_Spinner">
{message && ( {message && (
@@ -32,13 +31,7 @@ export default class Spinner extends React.PureComponent<IProps> {
<div className="mx_Spinner_Msg">{message}</div>&nbsp; <div className="mx_Spinner_Msg">{message}</div>&nbsp;
</React.Fragment> </React.Fragment>
)} )}
<div <InlineSpinner size={size} aria-label={_t("common|loading")} role="progressbar" data-testid="spinner" />
className="mx_Spinner_icon"
style={{ width: w, height: h }}
aria-label={_t("common|loading")}
role="progressbar"
data-testid="spinner"
/>
</div> </div>
); );
} }

View File

@@ -22,7 +22,7 @@ const MapFallback: React.FC<Props> = ({ className, isLoading, children, ...rest
return ( return (
<div className={classNames("mx_MapFallback", className)} {...rest}> <div className={classNames("mx_MapFallback", className)} {...rest}>
<MapFallbackImage className="mx_MapFallback_bg" /> <MapFallbackImage className="mx_MapFallback_bg" />
{isLoading ? <Spinner h={32} w={32} /> : <LocationMarkerIcon className="mx_MapFallback_icon" />} {isLoading ? <Spinner size={32} /> : <LocationMarkerIcon className="mx_MapFallback_icon" />}
{children} {children}
</div> </div>
); );

View File

@@ -43,7 +43,7 @@ export default function DownloadActionButton({ mxEvent, mediaEventHelperGet }: I
if (!canDownload) return null; if (!canDownload) return null;
const spinner = loading ? <Spinner w={18} h={18} /> : undefined; const spinner = loading ? <Spinner size={18} /> : undefined;
const classes = classNames({ const classes = classNames({
mx_MessageActionBar_iconButton: true, mx_MessageActionBar_iconButton: true,
mx_MessageActionBar_downloadButton: true, mx_MessageActionBar_downloadButton: true,

View File

@@ -40,6 +40,9 @@ const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => {
let subtitle: string; let subtitle: string;
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId); const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
const room = cli?.getRoom(roomId); const room = cli?.getRoom(roomId);
const stateEncrypted = content["io.element.msc4362.encrypt_state_events"] && cli.enableEncryptedStateEvents;
if (prevContent.algorithm === MEGOLM_ENCRYPTION_ALGORITHM) { if (prevContent.algorithm === MEGOLM_ENCRYPTION_ALGORITHM) {
subtitle = _t("timeline|m.room.encryption|parameters_changed"); subtitle = _t("timeline|m.room.encryption|parameters_changed");
} else if (dmPartner) { } else if (dmPartner) {
@@ -47,6 +50,8 @@ const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => {
subtitle = _t("timeline|m.room.encryption|enabled_dm", { displayName }); subtitle = _t("timeline|m.room.encryption|enabled_dm", { displayName });
} else if (room && isLocalRoom(room)) { } else if (room && isLocalRoom(room)) {
subtitle = _t("timeline|m.room.encryption|enabled_local"); subtitle = _t("timeline|m.room.encryption|enabled_local");
} else if (stateEncrypted) {
subtitle = _t("timeline|m.room.encryption|state_enabled");
} else { } else {
subtitle = _t("timeline|m.room.encryption|enabled"); subtitle = _t("timeline|m.room.encryption|enabled");
} }
@@ -54,7 +59,7 @@ const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => {
return ( return (
<EventTileBubble <EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon" className="mx_cryptoEvent mx_cryptoEvent_icon"
title={_t("common|encryption_enabled")} title={stateEncrypted ? _t("common|state_encryption_enabled") : _t("common|encryption_enabled")}
subtitle={subtitle} subtitle={subtitle}
timestamp={timestamp} timestamp={timestamp}
/> />

View File

@@ -628,7 +628,7 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
return <Blurhash className="mx_Blurhash" hash={blurhash} width={width} height={height} />; return <Blurhash className="mx_Blurhash" hash={blurhash} width={width} height={height} />;
} }
} }
return <Spinner w={32} h={32} />; return <Spinner size={32} />;
} }
// Overridden by MStickerBody // Overridden by MStickerBody

View File

@@ -365,7 +365,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
</div> </div>
<div data-testid="totalVotes" className="mx_MPollBody_totalVotes"> <div data-testid="totalVotes" className="mx_MPollBody_totalVotes">
{totalText} {totalText}
{isFetchingResponses && <Spinner w={16} h={16} />} {isFetchingResponses && <Spinner size={16} />}
</div> </div>
</fieldset> </fieldset>
); );

View File

@@ -9,7 +9,12 @@ Please see LICENSE files in the repository root for full details.
import { ClientEvent, EventType, type Room, RoomEvent, RoomType } from "matrix-js-sdk/src/matrix"; import { ClientEvent, EventType, type Room, RoomEvent, RoomType } from "matrix-js-sdk/src/matrix";
import React, { type JSX, useContext, useEffect, useState } from "react"; import React, { type JSX, useContext, useEffect, useState } from "react";
import { Tooltip } from "@vector-im/compound-web"; import { Tooltip } from "@vector-im/compound-web";
import { PlusIcon, UserAddSolidIcon, SearchIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import {
PlusIcon,
UserAddSolidIcon,
SearchIcon,
UserAddIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
@@ -52,7 +57,6 @@ import IconizedContextMenu, {
import SpaceContextMenu from "../context_menus/SpaceContextMenu"; import SpaceContextMenu from "../context_menus/SpaceContextMenu";
import InlineSpinner from "../elements/InlineSpinner"; import InlineSpinner from "../elements/InlineSpinner";
import { HomeButtonContextMenu } from "../spaces/SpacePanel"; import { HomeButtonContextMenu } from "../spaces/SpacePanel";
import { Icon as InviteIcon } from "../../../../res/img/element-icons/room/invite.svg";
import { Icon as HashVideoIcon } from "../../../../res/img/element-icons/roomlist/hash-video.svg"; import { Icon as HashVideoIcon } from "../../../../res/img/element-icons/roomlist/hash-video.svg";
const contextMenuBelow = (elementRect: DOMRect): MenuProps => { const contextMenuBelow = (elementRect: DOMRect): MenuProps => {
@@ -181,7 +185,7 @@ const LegacyRoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
inviteOption = ( inviteOption = (
<IconizedContextMenuOption <IconizedContextMenuOption
label={_t("action|invite")} label={_t("action|invite")}
icon={<InviteIcon />} icon={<UserAddIcon />}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();

View File

@@ -675,7 +675,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
return ( return (
<div className={classes} ref={this.ref} role="region" aria-label={_t("a11y|message_composer")}> <div className={classes} ref={this.ref} role="region" aria-label={_t("a11y|message_composer")}>
<HistoryVisibleBanner room={this.props.room} threadId={threadId} /> <HistoryVisibleBanner room={this.props.room} threadId={threadId ?? null} />
<div className="mx_MessageComposer_wrapper"> <div className="mx_MessageComposer_wrapper">
<UserIdentityWarning room={this.props.room} key={this.props.room.roomId} /> <UserIdentityWarning room={this.props.room} key={this.props.room.roomId} />
<ReplyPreview <ReplyPreview

View File

@@ -15,6 +15,7 @@ import {
RoomViewLifecycle, RoomViewLifecycle,
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
import { Button } from "@vector-im/compound-web"; import { Button } from "@vector-im/compound-web";
import { AskToJoinIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
@@ -29,7 +30,6 @@ import RoomAvatar from "../avatars/RoomAvatar";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature"; import { UIFeature } from "../../../settings/UIFeature";
import { ModuleRunner } from "../../../modules/ModuleRunner"; import { ModuleRunner } from "../../../modules/ModuleRunner";
import { Icon as AskToJoinIcon } from "../../../../res/img/element-icons/ask-to-join.svg";
import Field from "../elements/Field"; import Field from "../elements/Field";
import { ModuleApi } from "../../../modules/Api.ts"; import { ModuleApi } from "../../../modules/Api.ts";
@@ -371,7 +371,7 @@ class RoomPreviewBar extends React.Component<IProps, IState> {
if (this.props.previewLoading) { if (this.props.previewLoading) {
footer = ( footer = (
<div> <div>
<Spinner w={20} h={20} /> <Spinner size={20} />
{_t("room|loading_preview")} {_t("room|loading_preview")}
</div> </div>
); );

View File

@@ -103,7 +103,7 @@ const DeviceNameEditor: React.FC<Props & { stopEditing: () => void }> = ({ devic
> >
{_t("action|cancel")} {_t("action|cancel")}
</AccessibleButton> </AccessibleButton>
{isLoading && <Spinner w={16} h={16} />} {isLoading && <Spinner size={16} />}
</div> </div>
</form> </form>
); );

View File

@@ -195,7 +195,7 @@ const DeviceDetails: React.FC<Props> = ({
> >
<span className="mx_DeviceDetails_signOutButtonContent"> <span className="mx_DeviceDetails_signOutButtonContent">
{_t("settings|sessions|sign_out")} {_t("settings|sessions|sign_out")}
{isSigningOut && <Spinner w={16} h={16} />} {isSigningOut && <Spinner size={16} />}
</span> </span>
</AccessibleButton> </AccessibleButton>
)} )}

View File

@@ -195,7 +195,7 @@ const DeviceListItem: React.FC<{
}) => { }) => {
const tileContent = ( const tileContent = (
<> <>
{isSigningOut && <Spinner w={16} h={16} />} {isSigningOut && <Spinner size={16} />}
<DeviceExpandDetailsButton isExpanded={isExpanded} onClick={onDeviceExpandToggle} /> <DeviceExpandDetailsButton isExpanded={isExpanded} onClick={onDeviceExpandToggle} />
</> </>
); );
@@ -325,7 +325,7 @@ export const FilteredDeviceList = ({
onClick={() => onSignOutDevices(selectedDeviceIds)} onClick={() => onSignOutDevices(selectedDeviceIds)}
className="mx_FilteredDeviceList_headerButton" className="mx_FilteredDeviceList_headerButton"
> >
{isSigningOut && <Spinner w={16} h={16} />} {isSigningOut && <Spinner size={16} />}
{_t("action|sign_out")} {_t("action|sign_out")}
</AccessibleButton> </AccessibleButton>
<AccessibleButton <AccessibleButton

View File

@@ -54,6 +54,7 @@ interface IState {
history: HistoryVisibility; history: HistoryVisibility;
hasAliases: boolean; hasAliases: boolean;
encrypted: boolean | null; encrypted: boolean | null;
stateEncrypted: boolean | null;
showAdvancedSection: boolean; showAdvancedSection: boolean;
} }
@@ -79,6 +80,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
), ),
hasAliases: false, // async loaded in componentDidMount hasAliases: false, // async loaded in componentDidMount
encrypted: null, // async loaded in componentDidMount encrypted: null, // async loaded in componentDidMount
stateEncrypted: null, // async loaded in componentDidMount
showAdvancedSection: false, showAdvancedSection: false,
}; };
} }
@@ -89,6 +91,9 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
this.setState({ this.setState({
hasAliases: await this.hasAliases(), hasAliases: await this.hasAliases(),
encrypted: Boolean(await this.context.getCrypto()?.isEncryptionEnabledInRoom(this.props.room.roomId)), encrypted: Boolean(await this.context.getCrypto()?.isEncryptionEnabledInRoom(this.props.room.roomId)),
stateEncrypted: Boolean(
await this.context.getCrypto()?.isStateEncryptionEnabledInRoom(this.props.room.roomId),
),
}); });
} }
@@ -480,6 +485,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const client = this.context; const client = this.context;
const room = this.props.room; const room = this.props.room;
const isEncrypted = this.state.encrypted; const isEncrypted = this.state.encrypted;
const isStateEncrypted = this.state.stateEncrypted;
const isEncryptionLoading = isEncrypted === null; const isEncryptionLoading = isEncrypted === null;
const hasEncryptionPermission = room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, client); const hasEncryptionPermission = room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, client);
const isEncryptionForceDisabled = shouldForceDisableEncryption(client); const isEncryptionForceDisabled = shouldForceDisableEncryption(client);
@@ -533,6 +539,14 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
{isEncryptionForceDisabled && !isEncrypted && ( {isEncryptionForceDisabled && !isEncrypted && (
<Caption>{_t("room_settings|security|encryption_forced")}</Caption> <Caption>{_t("room_settings|security|encryption_forced")}</Caption>
)} )}
{isStateEncrypted && (
<SettingsToggleInput
name="enable-state-encryption"
checked={isStateEncrypted}
label={_t("common|state_encryption_enabled")}
disabled={true}
/>
)}
{encryptionSettings} {encryptionSettings}
</> </>
)} )}

View File

@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import React, { useState } from "react"; import React, { useState } from "react";
import { type Room } from "matrix-js-sdk/src/matrix"; import { type Room } from "matrix-js-sdk/src/matrix";
import { sleep } from "matrix-js-sdk/src/utils"; import { sleep } from "matrix-js-sdk/src/utils";
import { LinkIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { LinkIcon, UserAddIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { copyPlaintext } from "../../../utils/strings"; import { copyPlaintext } from "../../../utils/strings";
@@ -18,7 +18,6 @@ import { showRoomInviteDialog } from "../../../RoomInvite";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature"; import { UIComponent } from "../../../settings/UIFeature";
import { Icon as InviteIcon } from "../../../../res/img/element-icons/room/invite.svg";
import SpacePillButton from "../../structures/SpacePillButton.tsx"; import SpacePillButton from "../../structures/SpacePillButton.tsx";
interface IProps { interface IProps {
@@ -51,7 +50,7 @@ const SpacePublicShare: React.FC<IProps> = ({ space, onFinished }) => {
{space.canInvite(MatrixClientPeg.safeGet().getSafeUserId()) && {space.canInvite(MatrixClientPeg.safeGet().getSafeUserId()) &&
shouldShowComponent(UIComponent.InviteUsers) ? ( shouldShowComponent(UIComponent.InviteUsers) ? (
<SpacePillButton <SpacePillButton
icon={<InviteIcon />} icon={<UserAddIcon />}
title={_t("space|invite")} title={_t("space|invite")}
description={_t("space|invite_description")} description={_t("space|invite_description")}
onClick={() => { onClick={() => {

View File

@@ -21,8 +21,12 @@ import {
Preset, Preset,
RestrictedAllowType, RestrictedAllowType,
Visibility, Visibility,
Direction,
RoomStateEvent,
type RoomState,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { type RoomEncryptionEventContent } from "matrix-js-sdk/src/types";
import Modal, { type IHandle } from "./Modal"; import Modal, { type IHandle } from "./Modal";
import { _t, UserFriendlyError } from "./languageHandler"; import { _t, UserFriendlyError } from "./languageHandler";
@@ -44,6 +48,7 @@ import { doesRoomVersionSupport, PreferredRoomVersions } from "./utils/Preferred
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import { MEGOLM_ENCRYPTION_ALGORITHM } from "./utils/crypto"; import { MEGOLM_ENCRYPTION_ALGORITHM } from "./utils/crypto";
import { ElementCallMemberEventType } from "./call-types"; import { ElementCallMemberEventType } from "./call-types";
import { htmlSerializeFromMdIfNeeded } from "./editor/serialize";
// we define a number of interfaces which take their names from the js-sdk // we define a number of interfaces which take their names from the js-sdk
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@@ -66,6 +71,10 @@ export interface IOpts {
spinner?: boolean; spinner?: boolean;
guestAccess?: boolean; guestAccess?: boolean;
encryption?: boolean; encryption?: boolean;
/**
* Encrypt state events as per MSC4362
*/
stateEncryption?: boolean;
inlineErrors?: boolean; inlineErrors?: boolean;
andView?: boolean; andView?: boolean;
avatar?: File | string; // will upload if given file, else mxcUrl is needed avatar?: File | string; // will upload if given file, else mxcUrl is needed
@@ -113,6 +122,7 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
if (opts.spinner === undefined) opts.spinner = true; if (opts.spinner === undefined) opts.spinner = true;
if (opts.guestAccess === undefined) opts.guestAccess = true; if (opts.guestAccess === undefined) opts.guestAccess = true;
if (opts.encryption === undefined) opts.encryption = false; if (opts.encryption === undefined) opts.encryption = false;
if (opts.stateEncryption === undefined) opts.stateEncryption = false;
if (client.isGuest()) { if (client.isGuest()) {
dis.dispatch({ action: "require_registration" }); dis.dispatch({ action: "require_registration" });
@@ -207,12 +217,16 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
} }
if (opts.encryption) { if (opts.encryption) {
const content: RoomEncryptionEventContent = {
algorithm: MEGOLM_ENCRYPTION_ALGORITHM,
};
if (opts.stateEncryption) {
content["io.element.msc4362.encrypt_state_events"] = true;
}
createOpts.initial_state.push({ createOpts.initial_state.push({
type: "m.room.encryption", type: "m.room.encryption",
state_key: "", state_key: "",
content: { content,
algorithm: MEGOLM_ENCRYPTION_ALGORITHM,
},
}); });
} }
@@ -256,6 +270,9 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
}); });
} }
// If we are not encrypting state, copy name, topic, avatar over to
// createOpts so we pass them in when we call Client.createRoom().
if (!opts.stateEncryption) {
if (opts.name) { if (opts.name) {
createOpts.name = opts.name; createOpts.name = opts.name;
} }
@@ -275,6 +292,7 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
content: { url }, content: { url },
}); });
} }
}
if (opts.historyVisibility) { if (opts.historyVisibility) {
createOpts.initial_state.push({ createOpts.initial_state.push({
@@ -330,6 +348,13 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
if (opts.dmUserId) await Rooms.setDMRoom(client, roomId, opts.dmUserId); if (opts.dmUserId) await Rooms.setDMRoom(client, roomId, opts.dmUserId);
}) })
.then(async () => {
// We need to set up initial state manually if state encryption is enabled, since it needs
// to be encrypted.
if (opts.encryption && opts.stateEncryption) {
await enableStateEventEncryption(client, await room, opts);
}
})
.finally(function () { .finally(function () {
if (modal) modal.close(); if (modal) modal.close();
}) })
@@ -401,6 +426,73 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
); );
} }
async function enableStateEventEncryption(client: MatrixClient, room: Room, opts: IOpts): Promise<void> {
// Don't send our state events until encryption is enabled. If this times
// out after 30 seconds, we throw since we don't want to send the events
// unencrypted.
await waitForRoomEncryption(room, 30000);
// Set room name
if (opts.name) {
await client.setRoomName(room.roomId, opts.name);
}
// Set room topic
if (opts.topic) {
const htmlTopic = htmlSerializeFromMdIfNeeded(opts.topic, { forceHTML: false });
await client.setRoomTopic(room.roomId, opts.topic, htmlTopic);
}
// Set room avatar
if (opts.avatar) {
let url: string;
if (opts.avatar instanceof File) {
({ content_uri: url } = await client.uploadContent(opts.avatar));
} else {
url = opts.avatar;
}
await client.sendStateEvent(room.roomId, EventType.RoomAvatar, { url }, "");
}
}
/**
* Wait until the supplied room has an `m.room.encryption` event, or time out
* after 30 seconds.
*/
export async function waitForRoomEncryption(room: Room, waitTimeMs: number): Promise<void> {
if (room.hasEncryptionStateEvent()) {
return;
}
// Start a 30s timeout and return "timed_out" if we hit it
const { promise: timeoutPromise, resolve: timeoutResolve } = Promise.withResolvers();
const timeout = setTimeout(timeoutResolve, waitTimeMs, "timed_out");
// Listen for a RoomEncryption state update and return
// "received_encryption_state" if we get it
const roomState = room.getLiveTimeline().getState(Direction.Forward)!;
const { promise: stateUpdatePromise, resolve: stateUpdateResolve } = Promise.withResolvers();
const onRoomStateUpdate = (state: RoomState): void => {
if (state.getStateEvents(EventType.RoomEncryption, "")) {
stateUpdateResolve("received_encryption_state");
}
};
roomState.on(RoomStateEvent.Update, onRoomStateUpdate);
// Wait for one of the above to happen
const resolution = await Promise.race([timeoutPromise, stateUpdatePromise]);
// Clear the listener and the timeout
roomState.off(RoomStateEvent.Update, onRoomStateUpdate);
clearTimeout(timeout);
// Fail if we hit the timeout
if (resolution === "timed_out") {
logger.warn("Timed out while waiting for room to enable encryption");
throw new Error("Timed out while waiting for room to enable encryption");
}
}
/* /*
* Ensure that for every user in a room, there is at least one device that we * Ensure that for every user in a room, there is at least one device that we
* can encrypt to. * can encrypt to.

View File

@@ -579,6 +579,7 @@
"someone": "Someone", "someone": "Someone",
"space": "Space", "space": "Space",
"spaces": "Spaces", "spaces": "Spaces",
"state_encryption_enabled": "Experimental state encryption enabled",
"sticker": "Sticker", "sticker": "Sticker",
"stickerpack": "Stickerpack", "stickerpack": "Stickerpack",
"success": "Success", "success": "Success",
@@ -686,6 +687,8 @@
"join_rule_restricted_label": "Everyone in <SpaceName/> will be able to find and join this room.", "join_rule_restricted_label": "Everyone in <SpaceName/> will be able to find and join this room.",
"name_validation_required": "Please enter a name for the room", "name_validation_required": "Please enter a name for the room",
"room_visibility_label": "Room visibility", "room_visibility_label": "Room visibility",
"state_encrypted_warning": "Enables experimental support for encrypting state events, which hides metadata such as room names and topics from the server. This metadata will also be hidden from people joining rooms later, and people whose clients do not support MSC4362.",
"state_encryption_label": "Encrypt state events",
"title_private_room": "Create a private room", "title_private_room": "Create a private room",
"title_public_room": "Create a public room", "title_public_room": "Create a public room",
"title_video_room": "Create a video room", "title_video_room": "Create a video room",
@@ -1522,6 +1525,8 @@
"dynamic_room_predecessors": "Dynamic room predecessors", "dynamic_room_predecessors": "Dynamic room predecessors",
"dynamic_room_predecessors_description": "Enable MSC3946 (to support late-arriving room archives)", "dynamic_room_predecessors_description": "Enable MSC3946 (to support late-arriving room archives)",
"element_call_video_rooms": "Element Call video rooms", "element_call_video_rooms": "Element Call video rooms",
"encrypted_state_events": "Encrypted state events (MSC4362)",
"encrypted_state_events_description": "Enables experimental support for encrypting state events, which hides metadata such as room names and topics from the server. This metadata will also be hidden from people joining rooms later, and people whose clients do not support MSC4362.",
"exclude_insecure_devices": "Exclude insecure devices when sending/receiving messages", "exclude_insecure_devices": "Exclude insecure devices when sending/receiving messages",
"exclude_insecure_devices_description": "When this mode is enabled, encrypted messages will not be shared with unverified devices, and messages from unverified devices will be shown as an error. Note that if you enable this mode, you may be unable to communicate with users who have not verified their devices.", "exclude_insecure_devices_description": "When this mode is enabled, encrypted messages will not be shared with unverified devices, and messages from unverified devices will be shown as an error. Note that if you enable this mode, you may be unable to communicate with users who have not verified their devices.",
"experimental_description": "Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. <a>Learn more</a>.", "experimental_description": "Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. <a>Learn more</a>.",
@@ -3579,6 +3584,7 @@
"enabled_dm": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their profile picture.", "enabled_dm": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their profile picture.",
"enabled_local": "Messages in this chat will be end-to-end encrypted.", "enabled_local": "Messages in this chat will be end-to-end encrypted.",
"parameters_changed": "Some encryption parameters have been changed.", "parameters_changed": "Some encryption parameters have been changed.",
"state_enabled": "Messages and state events in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their profile picture.",
"unsupported": "The encryption used by this room isn't supported." "unsupported": "The encryption used by this room isn't supported."
}, },
"m.room.guest_access": { "m.room.guest_access": {

View File

@@ -229,6 +229,7 @@ export interface Settings {
"feature_new_room_list": IFeature; "feature_new_room_list": IFeature;
"feature_ask_to_join": IFeature; "feature_ask_to_join": IFeature;
"feature_notifications": IFeature; "feature_notifications": IFeature;
"feature_msc4362_encrypted_state_events": IFeature;
// These are in the feature namespace but aren't actually features // These are in the feature namespace but aren't actually features
"feature_hidebold": IBaseSetting<boolean>; "feature_hidebold": IBaseSetting<boolean>;
@@ -788,6 +789,16 @@ export const SETTINGS: Settings = {
supportedLevelsAreOrdered: true, supportedLevelsAreOrdered: true,
default: false, default: false,
}, },
"feature_msc4362_encrypted_state_events": {
isFeature: true,
labsGroup: LabGroup.Encryption,
displayName: _td("labs|encrypted_state_events"),
description: _td("labs|encrypted_state_events_description"),
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED,
supportedLevelsAreOrdered: true,
shouldWarn: true,
default: false,
},
"useCompactLayout": { "useCompactLayout": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
displayName: _td("settings|preferences|compact_modern"), displayName: _td("settings|preferences|compact_modern"),

View File

@@ -16,10 +16,40 @@ import SettingsStore from "../../settings/SettingsStore";
import { SettingLevel } from "../../settings/SettingLevel"; import { SettingLevel } from "../../settings/SettingLevel";
interface Props { interface Props {
/**
* The room instance associated with this banner view model.
*/
room: Room; room: Room;
threadId?: string | null;
/**
* If not null, indicates the ID of the thread currently being viewed in the thread
* timeline side view, where the banner view is displayed as a child of the message
* composer.
*/
threadId: string | null;
} }
/**
* View model for the history visible banner, which prompts users that the current room
* history may be shared with new invitees, if they have not already acknowledged the
* banner.
*
* The view model operates using a simple 2-case algorithm:
*
* 1. When a user opens an encrypted room where `history_visibility` is not set to `joined`,
* and the user hasn't previously dismissed it for this particular room, display a banner.
* If the user dismisses the banner, update the client's local store to record that the
* banner has been dismissed.
* 2. When the user opens an encrypted room where `history_visibility` is set to `joined`, clear
* the dismissal flag if it was previously set. This ensures that if the room's history
* visibility changes from public to private and back to public, the banner will reappear
* when appropriate.
*
* This banner is only shown in the regular timeline view, not the thread timeline view, which is
* done by conditioning on the presence of `threadId` in the viewmodel's {@link Props}.
*
* See https://github.com/element-hq/element-meta/issues/2875 for more information.
*/
export class HistoryVisibleBannerViewModel export class HistoryVisibleBannerViewModel
extends BaseViewModel<HistoryVisibleBannerViewSnapshot, Props> extends BaseViewModel<HistoryVisibleBannerViewSnapshot, Props>
implements HistoryVisibleBannerViewModelInterface implements HistoryVisibleBannerViewModelInterface
@@ -34,6 +64,12 @@ export class HistoryVisibleBannerViewModel
*/ */
private readonly acknowledgedWatcher: string; private readonly acknowledgedWatcher: string;
/**
* Computes the latest banner snapshot given the VM's props.
* @param room - The room the banner will be shown in.
* @param threadId - The thread ID passed in from the parent {@link MessageComposer}.
* @returns The latest snapshot. See {@link HistoryVisibleBannerViewSnapshot}.
*/
private static readonly computeSnapshot = ( private static readonly computeSnapshot = (
room: Room, room: Room,
threadId?: string | null, threadId?: string | null,
@@ -51,6 +87,10 @@ export class HistoryVisibleBannerViewModel
}; };
}; };
/**
* Creates a new view model instance.
* @param props - Properties for this view model. See {@link Props}.
*/
public constructor(props: Props) { public constructor(props: Props) {
super(props, HistoryVisibleBannerViewModel.computeSnapshot(props.room, props.threadId)); super(props, HistoryVisibleBannerViewModel.computeSnapshot(props.room, props.threadId));
@@ -69,6 +109,10 @@ export class HistoryVisibleBannerViewModel
); );
} }
/**
* Recompute and update this VM instance's snapshot. This will update the `acknowledgedHistoryVisibility`
* store entry if necessary.
*/
private setSnapshot(): void { private setSnapshot(): void {
const acknowledged = SettingsStore.getValue("acknowledgedHistoryVisibility", this.props.room.roomId); const acknowledged = SettingsStore.getValue("acknowledgedHistoryVisibility", this.props.room.roomId);
@@ -109,6 +153,9 @@ export class HistoryVisibleBannerViewModel
); );
} }
/**
* Dispose of the viewmodel and its settings listeners.
*/
public dispose(): void { public dispose(): void {
super.dispose(); super.dispose();
SettingsStore.unwatchSetting(this.featureWatcher); SettingsStore.unwatchSetting(this.featureWatcher);

View File

@@ -17,7 +17,7 @@ import {
type IEvent, type IEvent,
type RoomMember, type RoomMember,
type MatrixClient, type MatrixClient,
type RoomState, RoomState,
EventType, EventType,
type IEventRelation, type IEventRelation,
type IUnsigned, type IUnsigned,
@@ -31,6 +31,7 @@ import {
type OidcClientConfig, type OidcClientConfig,
type GroupCall, type GroupCall,
HistoryVisibility, HistoryVisibility,
type ICreateRoomOpts,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types"; import { KnownMembership } from "matrix-js-sdk/src/types";
import { normalize } from "matrix-js-sdk/src/utils"; import { normalize } from "matrix-js-sdk/src/utils";
@@ -85,6 +86,7 @@ export function createTestClient(): MatrixClient {
const eventEmitter = new EventEmitter(); const eventEmitter = new EventEmitter();
let txnId = 1; let txnId = 1;
let createdRoom: Room | undefined;
const client = { const client = {
getHomeserverUrl: jest.fn(), getHomeserverUrl: jest.fn(),
@@ -124,6 +126,7 @@ export function createTestClient(): MatrixClient {
getDeviceVerificationStatus: jest.fn(), getDeviceVerificationStatus: jest.fn(),
resetKeyBackup: jest.fn(), resetKeyBackup: jest.fn(),
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false), isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false),
isStateEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false),
getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]), getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]),
setDeviceIsolationMode: jest.fn(), setDeviceIsolationMode: jest.fn(),
prepareToEncrypt: jest.fn(), prepareToEncrypt: jest.fn(),
@@ -162,7 +165,14 @@ export function createTestClient(): MatrixClient {
}), }),
getPushActionsForEvent: jest.fn(), getPushActionsForEvent: jest.fn(),
getRoom: jest.fn().mockImplementation((roomId) => mkStubRoom(roomId, "My room", client)), getRoom: jest.fn().mockImplementation((roomId) => {
// If the test called `createRoom`, return the mocked room it created.
if (createdRoom) {
return createdRoom;
} else {
return mkStubRoom(roomId, "My room", client);
}
}),
getRooms: jest.fn().mockReturnValue([]), getRooms: jest.fn().mockReturnValue([]),
getVisibleRooms: jest.fn().mockReturnValue([]), getVisibleRooms: jest.fn().mockReturnValue([]),
loginFlows: jest.fn(), loginFlows: jest.fn(),
@@ -201,6 +211,7 @@ export function createTestClient(): MatrixClient {
setAccountData: jest.fn(), setAccountData: jest.fn(),
deleteAccountData: jest.fn(), deleteAccountData: jest.fn(),
setRoomAccountData: jest.fn(), setRoomAccountData: jest.fn(),
setRoomName: jest.fn(),
setRoomTopic: jest.fn(), setRoomTopic: jest.fn(),
setRoomReadMarkers: jest.fn().mockResolvedValue({}), setRoomReadMarkers: jest.fn().mockResolvedValue({}),
sendTyping: jest.fn().mockResolvedValue({}), sendTyping: jest.fn().mockResolvedValue({}),
@@ -213,7 +224,23 @@ export function createTestClient(): MatrixClient {
getRoomHierarchy: jest.fn().mockReturnValue({ getRoomHierarchy: jest.fn().mockReturnValue({
rooms: [], rooms: [],
}), }),
createRoom: jest.fn().mockResolvedValue({ room_id: "!1:example.org" }), createRoom: jest.fn(async (createOpts?: ICreateRoomOpts) => {
const initialState = createOpts?.initial_state?.map((event, i) =>
mkEvent({
...event,
room: "!1:example.org",
user: "@user:example.com",
event: true,
}),
);
createdRoom = mkStubRoom(
"!1:example.org",
"My room",
client,
initialState && mkRoomState("!1:example.org", initialState),
);
return { room_id: "!1:example.org" };
}),
setPowerLevel: jest.fn().mockResolvedValue(undefined), setPowerLevel: jest.fn().mockResolvedValue(undefined),
pushRules: {}, pushRules: {},
decryptEventIfNeeded: () => Promise.resolve(), decryptEventIfNeeded: () => Promise.resolve(),
@@ -616,10 +643,11 @@ export function mkStubRoom(
roomId: string | null | undefined = null, roomId: string | null | undefined = null,
name: string | undefined, name: string | undefined,
client: MatrixClient | undefined, client: MatrixClient | undefined,
state?: RoomState | undefined,
): Room { ): Room {
const stubTimeline = { const stubTimeline = {
getEvents: (): MatrixEvent[] => [], getEvents: (): MatrixEvent[] => [],
getState: (): RoomState | undefined => undefined, getState: (): RoomState | undefined => state,
} as unknown as EventTimeline; } as unknown as EventTimeline;
return { return {
canInvite: jest.fn().mockReturnValue(false), canInvite: jest.fn().mockReturnValue(false),
@@ -701,6 +729,22 @@ export function mkStubRoom(
} as unknown as Room; } as unknown as Room;
} }
export function mkRoomState(
roomId: string = "!1:example.org",
stateEvents: MatrixEvent[] = [],
members: RoomMember[] = [],
): RoomState {
const roomState = new RoomState(roomId);
roomState.setStateEvents(stateEvents);
for (const member of members) {
roomState.members[member.userId] = member;
}
return roomState;
}
export function mkServerConfig( export function mkServerConfig(
hsUrl: string, hsUrl: string,
isUrl: string, isUrl: string,

View File

@@ -95,13 +95,24 @@ exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during sta
<div <div
class="mx_Spinner" class="mx_Spinner"
> >
<div <svg
aria-label="Loading…" aria-label="Loading…"
class="mx_Spinner_icon" class="_icon_11k6c_18"
data-testid="spinner" data-testid="spinner"
fill="currentColor"
height="1em"
role="progressbar" role="progressbar"
style="width: 32px; height: 32px;" style="width: 32px; height: 32px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
fill-rule="evenodd"
/> />
</svg>
</div> </div>
</div> </div>
</div> </div>
@@ -224,13 +235,24 @@ exports[`<MatrixChat /> should render spinner while app is loading 1`] = `
<div <div
class="mx_Spinner" class="mx_Spinner"
> >
<div <svg
aria-label="Loading…" aria-label="Loading…"
class="mx_Spinner_icon" class="_icon_11k6c_18"
data-testid="spinner" data-testid="spinner"
fill="currentColor"
height="1em"
role="progressbar" role="progressbar"
style="width: 32px; height: 32px;" style="width: 32px; height: 32px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
fill-rule="evenodd"
/> />
</svg>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -54,13 +54,24 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
<div <div
class="mx_Spinner" class="mx_Spinner"
> >
<div <svg
aria-label="Loading…" aria-label="Loading…"
class="mx_Spinner_icon" class="_icon_11k6c_18"
data-testid="spinner" data-testid="spinner"
fill="currentColor"
height="1em"
role="progressbar" role="progressbar"
style="width: 45px; height: 45px;" style="width: 45px; height: 45px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
fill-rule="evenodd"
/> />
</svg>
</div> </div>
<div <div
class="mx_LargeLoader_text" class="mx_LargeLoader_text"
@@ -3422,13 +3433,24 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
Loading… Loading…
</div> </div>
   
<div <svg
aria-label="Loading…" aria-label="Loading…"
class="mx_Spinner_icon" class="_icon_11k6c_18"
data-testid="spinner" data-testid="spinner"
fill="currentColor"
height="1em"
role="progressbar" role="progressbar"
style="width: 32px; height: 32px;" style="width: 32px; height: 32px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
fill-rule="evenodd"
/> />
</svg>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -8,13 +8,24 @@ exports[`<LoginSplashView /> Renders a spinner 1`] = `
<div <div
class="mx_Spinner" class="mx_Spinner"
> >
<div <svg
aria-label="Loading…" aria-label="Loading…"
class="mx_Spinner_icon" class="_icon_11k6c_18"
data-testid="spinner" data-testid="spinner"
fill="currentColor"
height="1em"
role="progressbar" role="progressbar"
style="width: 32px; height: 32px;" style="width: 32px; height: 32px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
fill-rule="evenodd"
/> />
</svg>
</div> </div>
<div <div
class="mx_LoginSplashView_splashButtons" class="mx_LoginSplashView_splashButtons"
@@ -46,13 +57,24 @@ exports[`<LoginSplashView /> Renders an error message 1`] = `
<div <div
class="mx_Spinner" class="mx_Spinner"
> >
<div <svg
aria-label="Loading…" aria-label="Loading…"
class="mx_Spinner_icon" class="_icon_11k6c_18"
data-testid="spinner" data-testid="spinner"
fill="currentColor"
height="1em"
role="progressbar" role="progressbar"
style="width: 32px; height: 32px;" style="width: 32px; height: 32px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
fill-rule="evenodd"
/> />
</svg>
</div> </div>
<div <div
class="mx_LoginSplashView_splashButtons" class="mx_LoginSplashView_splashButtons"

View File

@@ -54,7 +54,7 @@ describe("HistoryVisibleBannerViewModel", () => {
}); });
it("should not show the banner in unencrypted rooms", () => { it("should not show the banner in unencrypted rooms", () => {
const vm = new HistoryVisibleBannerViewModel({ room }); const vm = new HistoryVisibleBannerViewModel({ room, threadId: null });
expect(vm.getSnapshot().visible).toBe(false); expect(vm.getSnapshot().visible).toBe(false);
}); });
@@ -76,7 +76,7 @@ describe("HistoryVisibleBannerViewModel", () => {
}), }),
]); ]);
const vm = new HistoryVisibleBannerViewModel({ room }); const vm = new HistoryVisibleBannerViewModel({ room, threadId: null });
expect(vm.getSnapshot().visible).toBe(false); expect(vm.getSnapshot().visible).toBe(false);
}); });
@@ -99,7 +99,7 @@ describe("HistoryVisibleBannerViewModel", () => {
}), }),
]); ]);
const vm = new HistoryVisibleBannerViewModel({ room }); const vm = new HistoryVisibleBannerViewModel({ room, threadId: null });
expect(vm.getSnapshot().visible).toBe(false); expect(vm.getSnapshot().visible).toBe(false);
vm.dispose(); vm.dispose();
}); });
@@ -145,7 +145,7 @@ describe("HistoryVisibleBannerViewModel", () => {
}), }),
]); ]);
const vm = new HistoryVisibleBannerViewModel({ room }); const vm = new HistoryVisibleBannerViewModel({ room, threadId: null });
expect(vm.getSnapshot().visible).toBe(true); expect(vm.getSnapshot().visible).toBe(true);
await vm.onClose(); await vm.onClose();
expect(vm.getSnapshot().visible).toBe(false); expect(vm.getSnapshot().visible).toBe(false);

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React from "react"; import React from "react";
import { fireEvent, render, screen, within } from "jest-matrix-react"; import { act, fireEvent, render, screen, within } from "jest-matrix-react";
import { type Room, JoinRule, MatrixError, Preset, Visibility } from "matrix-js-sdk/src/matrix"; import { type Room, JoinRule, MatrixError, Preset, Visibility } from "matrix-js-sdk/src/matrix";
import CreateRoomDialog from "../../../../../src/components/views/dialogs/CreateRoomDialog"; import CreateRoomDialog from "../../../../../src/components/views/dialogs/CreateRoomDialog";
@@ -247,6 +247,7 @@ describe("<CreateRoomDialog />", () => {
createOpts: {}, createOpts: {},
name: roomName, name: roomName,
encryption: true, encryption: true,
stateEncryption: false,
parentSpace: undefined, parentSpace: undefined,
roomType: undefined, roomType: undefined,
}); });
@@ -260,6 +261,29 @@ describe("<CreateRoomDialog />", () => {
await flushPromises(); await flushPromises();
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
describe("when the state encryption labs flag is on", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === "feature_msc4362_encrypted_state_events",
);
});
it("should turn on state encryption when toggled", async () => {
// Given we have the create room dialog open
const { asFragment } = getComponent();
await flushPromises();
expect(asFragment()).toMatchSnapshot();
// When I click the Encrypt state events toggle
const toggle = screen.getByRole("switch", { name: "Encrypt state events" });
expect(toggle).not.toBeChecked();
act(() => toggle.click());
// Then it changes state
expect(toggle).toBeChecked();
});
});
}); });
describe("for a knock room", () => { describe("for a knock room", () => {
@@ -308,6 +332,7 @@ describe("<CreateRoomDialog />", () => {
}, },
name: roomName, name: roomName,
encryption: true, encryption: true,
stateEncryption: false,
joinRule: JoinRule.Knock, joinRule: JoinRule.Knock,
parentSpace: undefined, parentSpace: undefined,
roomType: undefined, roomType: undefined,
@@ -326,6 +351,7 @@ describe("<CreateRoomDialog />", () => {
}, },
name: roomName, name: roomName,
encryption: true, encryption: true,
stateEncryption: false,
joinRule: JoinRule.Knock, joinRule: JoinRule.Knock,
parentSpace: undefined, parentSpace: undefined,
roomType: undefined, roomType: undefined,

View File

@@ -390,6 +390,273 @@ exports[`<CreateRoomDialog /> for a private room should render not the advanced
</span> </span>
</div> </div>
</div> </div>
<div
class="_inline-field_19upo_32"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_udcm8_10"
>
<input
class="_input_udcm8_24"
id="_r_7n_"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="_r_7n_"
>
Encrypt state events
</label>
<span
class="_message_19upo_85 _help-message_19upo_91"
id="radix-_r_7p_"
>
Enables experimental support for encrypting state events, which hides metadata such as room names and topics from the server. This metadata will also be hidden from people joining rooms later, and people whose clients do not support MSC4362.
</span>
</div>
</div>
</form>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
data-testid="dialog-cancel-button"
type="button"
>
Cancel
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
Create room
</button>
</span>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`<CreateRoomDialog /> for a private room when the state encryption labs flag is on should turn on state encryption when toggled 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_CreateRoomDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
tabindex="-1"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Create a private room
</h1>
</div>
<div
class="mx_Dialog_content"
>
<form
class="_root_19upo_16"
>
<div
class="mx_Field mx_Field_input mx_CreateRoomDialog_name"
>
<input
id="mx_Field_29"
label="Name"
placeholder="Name"
type="text"
value=""
/>
<label
for="mx_Field_29"
>
Name
</label>
</div>
<div
class="mx_Field mx_Field_input mx_CreateRoomDialog_topic"
>
<input
id="mx_Field_30"
label="Topic (optional)"
placeholder="Topic (optional)"
type="text"
value=""
/>
<label
for="mx_Field_30"
>
Topic (optional)
</label>
</div>
<div>
<div
class="mx_Dropdown mx_JoinRuleDropdown mx_Dropdown_disabled"
>
<div
aria-describedby="mx_JoinRuleDropdown_value"
aria-disabled="true"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Room visibility"
aria-owns="mx_JoinRuleDropdown_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput mx_AccessibleButton_disabled"
disabled=""
role="button"
tabindex="0"
>
<div
class="mx_Dropdown_option"
id="mx_JoinRuleDropdown_value"
>
<div
class="mx_JoinRuleDropdown_invite"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 22q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412A1.93 1.93 0 0 1 6 8h1V6q0-2.075 1.463-3.537Q9.926 1 12 1q2.075 0 3.537 1.463Q17 3.925 17 6v2h1q.824 0 1.413.588Q20 9.175 20 10v10q0 .824-.587 1.413A1.93 1.93 0 0 1 18 22zM9 8h6V6q0-1.25-.875-2.125A2.9 2.9 0 0 0 12 3q-1.25 0-2.125.875A2.9 2.9 0 0 0 9 6z"
/>
</svg>
Private room (invite only)
</div>
</div>
<svg
class="mx_Dropdown_arrow"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
<p>
Only people invited will be able to find and join this room. You can change this at any time from room settings.
</p>
</div>
<div
class="_inline-field_19upo_32"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_udcm8_10"
>
<input
checked=""
class="_input_udcm8_24"
id="_r_86_"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="_r_86_"
>
Enable end-to-end encryption
</label>
<span
class="_message_19upo_85 _help-message_19upo_91"
id="radix-_r_88_"
>
You can't disable this later. Bridges & most bots won't work yet.
</span>
</div>
</div>
<div
class="_inline-field_19upo_32"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_udcm8_10"
>
<input
class="_input_udcm8_24"
id="_r_89_"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="_r_89_"
>
Encrypt state events
</label>
<span
class="_message_19upo_85 _help-message_19upo_91"
id="radix-_r_8b_"
>
Enables experimental support for encrypting state events, which hides metadata such as room names and topics from the server. This metadata will also be hidden from people joining rooms later, and people whose clients do not support MSC4362.
</span>
</div>
</div>
</form> </form>
</div> </div>
<div <div

View File

@@ -5,15 +5,28 @@ exports[`InviteProgressBody should match snapshot 1`] = `
<div <div
class="mx_InviteProgressBody" class="mx_InviteProgressBody"
> >
<div <span
class="mx_InlineSpinner" class="mx_InlineSpinner"
> >
<div <svg
aria-label="Loading…" aria-label="Loading…"
class="mx_InlineSpinner_icon mx_Spinner_icon" class="_icon_11k6c_18"
style="width: 32px; height: 32px;" data-testid="spinner"
fill="currentColor"
height="1em"
role="progressbar"
style="width: 20px; height: 20px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
fill-rule="evenodd"
/> />
</div> </svg>
</span>
<h1> <h1>
Preparing invitations... Preparing invitations...
</h1> </h1>

View File

@@ -174,15 +174,28 @@ exports[`<RoomSettingsDialog /> poll history displays poll history when tab clic
<div <div
class="mx_PollHistoryList_loading mx_PollHistoryList_noResultsYet" class="mx_PollHistoryList_loading mx_PollHistoryList_noResultsYet"
> >
<div <span
class="mx_InlineSpinner" class="mx_InlineSpinner"
> >
<div <svg
aria-label="Loading…" aria-label="Loading…"
class="mx_InlineSpinner_icon mx_Spinner_icon" class="_icon_11k6c_18"
style="width: 16px; height: 16px;" data-testid="spinner"
fill="currentColor"
height="1em"
role="progressbar"
style="width: 20px; height: 20px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
fill-rule="evenodd"
/> />
</div> </svg>
</span>
Loading polls Loading polls
</div> </div>
</div> </div>

View File

@@ -168,13 +168,24 @@ exports[`ShareDialog should not render the socials if disabled 1`] = `
<div <div
class="mx_Spinner" class="mx_Spinner"
> >
<div <svg
aria-label="Loading…" aria-label="Loading…"
class="mx_Spinner_icon" class="_icon_11k6c_18"
data-testid="spinner" data-testid="spinner"
fill="currentColor"
height="1em"
role="progressbar" role="progressbar"
style="width: 32px; height: 32px;" style="width: 32px; height: 32px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
fill-rule="evenodd"
/> />
</svg>
</div> </div>
</div> </div>
<span> <span>
@@ -255,13 +266,24 @@ exports[`ShareDialog should render a share dialog for a matrix event 1`] = `
<div <div
class="mx_Spinner" class="mx_Spinner"
> >
<div <svg
aria-label="Loading…" aria-label="Loading…"
class="mx_Spinner_icon" class="_icon_11k6c_18"
data-testid="spinner" data-testid="spinner"
fill="currentColor"
height="1em"
role="progressbar" role="progressbar"
style="width: 32px; height: 32px;" style="width: 32px; height: 32px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
fill-rule="evenodd"
/> />
</svg>
</div> </div>
</div> </div>
<span> <span>
@@ -429,13 +451,24 @@ exports[`ShareDialog should render a share dialog for a room 1`] = `
<div <div
class="mx_Spinner" class="mx_Spinner"
> >
<div <svg
aria-label="Loading…" aria-label="Loading…"
class="mx_Spinner_icon" class="_icon_11k6c_18"
data-testid="spinner" data-testid="spinner"
fill="currentColor"
height="1em"
role="progressbar" role="progressbar"
style="width: 32px; height: 32px;" style="width: 32px; height: 32px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
fill-rule="evenodd"
/> />
</svg>
</div> </div>
</div> </div>
<span> <span>
@@ -602,13 +635,24 @@ exports[`ShareDialog should render a share dialog for a room member 1`] = `
<div <div
class="mx_Spinner" class="mx_Spinner"
> >
<div <svg
aria-label="Loading…" aria-label="Loading…"
class="mx_Spinner_icon" class="_icon_11k6c_18"
data-testid="spinner" data-testid="spinner"
fill="currentColor"
height="1em"
role="progressbar" role="progressbar"
style="width: 32px; height: 32px;" style="width: 32px; height: 32px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
fill-rule="evenodd"
/> />
</svg>
</div> </div>
</div> </div>
<span> <span>
@@ -748,13 +792,24 @@ exports[`ShareDialog should render a share dialog for an URL 1`] = `
<div <div
class="mx_Spinner" class="mx_Spinner"
> >
<div <svg
aria-label="Loading…" aria-label="Loading…"
class="mx_Spinner_icon" class="_icon_11k6c_18"
data-testid="spinner" data-testid="spinner"
fill="currentColor"
height="1em"
role="progressbar" role="progressbar"
style="width: 32px; height: 32px;" style="width: 32px; height: 32px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
fill-rule="evenodd"
/> />
</svg>
</div> </div>
</div> </div>
<span> <span>

Some files were not shown because too many files have changed in this diff Show More