Compare commits
19 Commits
t3chguy/cs
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff3f069122 | ||
|
|
6f0369e623 | ||
|
|
7a69ab8be4 | ||
|
|
4da149e56f | ||
|
|
e696f92bd3 | ||
|
|
48c360f688 | ||
|
|
3ee50c59f8 | ||
|
|
ba2386ae41 | ||
|
|
fab2997107 | ||
|
|
60ef5d880c | ||
|
|
c486299deb | ||
|
|
3fb0f65735 | ||
|
|
154baba303 | ||
|
|
8d918e3b16 | ||
|
|
011b975d3f | ||
|
|
00954542f3 | ||
|
|
1cf9e546fd | ||
|
|
cf25d5e258 | ||
|
|
86a38cd2d2 |
49
CHANGELOG.md
@@ -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.
|
||||||
|
|||||||
22
docs/labs.md
@@ -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.
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
32
packages/shared-components/scripts/storybook-screenshot-update.sh
Executable 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
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 984 KiB After Width: | Height: | Size: 984 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 */
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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:
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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")}
|
||||||
|
|||||||
@@ -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()}>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
<div className="mx_Spinner_Msg">{message}</div>
|
||||||
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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={() => {
|
||||||
|
|||||||
@@ -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,24 +270,28 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.name) {
|
// If we are not encrypting state, copy name, topic, avatar over to
|
||||||
createOpts.name = opts.name;
|
// createOpts so we pass them in when we call Client.createRoom().
|
||||||
}
|
if (!opts.stateEncryption) {
|
||||||
|
if (opts.name) {
|
||||||
if (opts.topic) {
|
createOpts.name = opts.name;
|
||||||
createOpts.topic = opts.topic;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.avatar) {
|
|
||||||
let url = opts.avatar;
|
|
||||||
if (opts.avatar instanceof File) {
|
|
||||||
({ content_uri: url } = await client.uploadContent(opts.avatar));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createOpts.initial_state.push({
|
if (opts.topic) {
|
||||||
type: EventType.RoomAvatar,
|
createOpts.topic = opts.topic;
|
||||||
content: { url },
|
}
|
||||||
});
|
|
||||||
|
if (opts.avatar) {
|
||||||
|
let url = opts.avatar;
|
||||||
|
if (opts.avatar instanceof File) {
|
||||||
|
({ content_uri: url } = await client.uploadContent(opts.avatar));
|
||||||
|
}
|
||||||
|
|
||||||
|
createOpts.initial_state.push({
|
||||||
|
type: EventType.RoomAvatar,
|
||||||
|
content: { url },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.historyVisibility) {
|
if (opts.historyVisibility) {
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
</div>
|
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"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
<h1>
|
<h1>
|
||||||
Preparing invitations...
|
Preparing invitations...
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -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"
|
||||||
</div>
|
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"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
Loading polls
|
Loading polls
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||