Compare commits

..

5 Commits

Author SHA1 Message Date
Michael Telatynski
7e670bfe41 Type fixes
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-08-21 16:09:17 +01:00
Michael Telatynski
cb7382f235 Merge branch 'renovate/typescript' of https://github.com/element-hq/element-web into renovate/typescript 2025-08-21 16:04:16 +01:00
Michael Telatynski
f796dce34b Merge branch 'develop' into renovate/typescript 2025-08-21 15:44:28 +01:00
renovate[bot]
523783706f Update dependency typescript to v5.9.2 2025-08-19 12:57:16 +00:00
renovate[bot]
0f530f6c01 Update dependency typescript to v5.9.2 2025-08-10 12:38:05 +00:00
335 changed files with 4972 additions and 7416 deletions

View File

@@ -2,7 +2,6 @@
## Checklist
- [ ] I have read through [review guidelines](../docs/review.md) and [CONTRIBUTING.md](../CONTRIBUTING.md).
- [ ] Tests written for new code (and old code if feasible).
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
- [ ] Linter and other CI checks pass.

View File

@@ -88,7 +88,7 @@ jobs:
run: mdbook build
- name: Upload artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
with:
path: ./book

View File

@@ -1,52 +1,3 @@
Changes in [1.11.111](https://github.com/element-hq/element-web/releases/tag/v1.11.111) (2025-09-10)
====================================================================================================
## ✨ Features
* Do not hide media from your own user by default ([#29797](https://github.com/element-hq/element-web/pull/29797)). Contributed by @Half-Shot.
* Remember whether sidebar is shown for calls when switching rooms ([#30262](https://github.com/element-hq/element-web/pull/30262)). Contributed by @bojidar-bg.
* Open the proper integration settings on integrations disabled error ([#30538](https://github.com/element-hq/element-web/pull/30538)). Contributed by @Half-Shot.
* Show a "progress" dialog while invites are being sent ([#30561](https://github.com/element-hq/element-web/pull/30561)). Contributed by @richvdh.
* Move the room list to the new ListView(backed by react-virtuoso) ([#30515](https://github.com/element-hq/element-web/pull/30515)). Contributed by @langleyd.
## 🐛 Bug Fixes
* [Backport staging] Ensure container starts if it is mounted with an empty /modules directory. ([#30705](https://github.com/element-hq/element-web/pull/30705)). Contributed by @RiotRobot.
* Fix room joining over federation not specifying vias or using aliases ([#30641](https://github.com/element-hq/element-web/pull/30641)). Contributed by @t3chguy.
* Fix stable-suffixed MSC4133 support ([#30649](https://github.com/element-hq/element-web/pull/30649)). Contributed by @dbkr.
* Fix i18n of message when a setting is disabled ([#30646](https://github.com/element-hq/element-web/pull/30646)). Contributed by @dbkr.
* ListView should not handle the arrow keys if there is a modifier applied ([#30633](https://github.com/element-hq/element-web/pull/30633)). Contributed by @langleyd.
* Make BaseDialog's div keyboard focusable and fix test. ([#30631](https://github.com/element-hq/element-web/pull/30631)). Contributed by @langleyd.
* Fix: Allow triple-click text selection to flow around pills ([#30349](https://github.com/element-hq/element-web/pull/30349)). Contributed by @AlirezaMrtz.
* Watch for a 'join' action to know when the call is connected ([#29492](https://github.com/element-hq/element-web/pull/29492)). Contributed by @robintown.
* Fix: add missing tooltip and aria-label to lock icon next to composer ([#30623](https://github.com/element-hq/element-web/pull/30623)). Contributed by @florianduros.
* Don't render context menu when scrolling ([#30613](https://github.com/element-hq/element-web/pull/30613)). Contributed by @langleyd.
Changes in [1.11.110](https://github.com/element-hq/element-web/releases/tag/v1.11.110) (2025-08-27)
====================================================================================================
## ✨ Features
* Hide recovery key when re-entering it while creating or changing it ([#30499](https://github.com/element-hq/element-web/pull/30499)). Contributed by @andybalaam.
* Add `?no_universal_links=true` to OIDC url so EX doesn't try to handle it ([#29439](https://github.com/element-hq/element-web/pull/29439)). Contributed by @t3chguy.
* Show a blue lock for unencrypted rooms and hide the grey shield for encrypted rooms ([#30440](https://github.com/element-hq/element-web/pull/30440)). Contributed by @langleyd.
* Add support for Module API 1.4 ([#30185](https://github.com/element-hq/element-web/pull/30185)). Contributed by @t3chguy.
* MVVM - Introduce some helpers for snapshot management ([#30398](https://github.com/element-hq/element-web/pull/30398)). Contributed by @MidhunSureshR.
## 🐛 Bug Fixes
* A11y: move focus to right panel when opened ([#30553](https://github.com/element-hq/element-web/pull/30553)). Contributed by @florianduros.
* Fix e2e warning icon should be white ([#30539](https://github.com/element-hq/element-web/pull/30539)). Contributed by @florianduros.
* Remove NoOneHere disabled reason. ([#30524](https://github.com/element-hq/element-web/pull/30524)). Contributed by @toger5.
* Fix downloading files with authenticated media API ([#30520](https://github.com/element-hq/element-web/pull/30520)). Contributed by @t3chguy.
* Fix call permissions check confusion around element call ([#30521](https://github.com/element-hq/element-web/pull/30521)). Contributed by @t3chguy.
* Fix line wrap around emoji verification ([#30523](https://github.com/element-hq/element-web/pull/30523)). Contributed by @t3chguy.
* Don't highlight redacted events ([#30519](https://github.com/element-hq/element-web/pull/30519)). Contributed by @t3chguy.
* Fix matrix.to links not being handled in the app ([#30522](https://github.com/element-hq/element-web/pull/30522)). Contributed by @t3chguy.
* Fix issue of new room list taking up the full width ([#30459](https://github.com/element-hq/element-web/pull/30459)). Contributed by @langleyd.
* Fix widget persistence in React development mode ([#30509](https://github.com/element-hq/element-web/pull/30509)). Contributed by @robintown.
* Fix widget initialization in React development mode ([#30463](https://github.com/element-hq/element-web/pull/30463)). Contributed by @robintown.
Changes in [1.11.109](https://github.com/element-hq/element-web/releases/tag/v1.11.109) (2025-08-11)
====================================================================================================
This release supports the upcoming v12 ("hydra") Matrix room version and is necessary to view and participate in these rooms.

View File

@@ -2,11 +2,6 @@
Everyone is welcome to contribute code to Element Web, provided that they are willing to license their contributions to Element under a [Contributor License Agreement](https://cla-assistant.io/element-hq/element-web) (CLA). This ensures that their contribution will be made available under an OSI-approved open-source license, currently licensed under Affero General Public License v3 (AGPLv3) or General Public License v3 (GPLv3) at your choice.
If you're contributing, or thinking about contributing, please come & chat to
us in our development room, [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
This is the best place to ask questions about the code, how to work on the project
or whether a change is likely to be accepted.
## How to contribute
The preferred and easiest way to contribute changes to the project is to fork

View File

@@ -1,7 +1,7 @@
# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
# Builder
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f7f28d1962d93cc096ea6327378d990284757fec281ce48e42436e7b4b167fa2 AS builder
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:9e34ba52e1f3c31ed9bd4d0bcf784f5909db17cda61c220e29c8d7a8ebfb402e AS builder
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
ARG USE_CUSTOM_SDKS=false
@@ -19,7 +19,7 @@ RUN /src/scripts/docker-package.sh
RUN cp /src/config.sample.json /src/webapp/config.json
# App
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:0d019e980f83728002de7a6d8819d0d4af7179046d3946b8b37749953fbb28e6
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:ea6c4b8b568824ea94cd1fabd47e1c4e7c0c04744f344a3793f7e9c8ac3a3636
# Need root user to install packages & manipulate the usr directory
USER root

View File

@@ -27,7 +27,7 @@ Element has several tiers of support for different environments:
- Best effort
- Definition:
- Issues **accepted**, regressions **do not block** the release
- The wider Element Products (including Element Call and the Enterprise Server Suite) do still not officially support these browsers.
- The wider Element Products(including Element Call and the Enterprise Server Suite) do still not officially support these browsers.
- The element web project and its contributors should keep the client functioning and gracefully degrade where other sibling features (E.g. Element Call) may not function.
- Last major release of Firefox ESR and Chrome/Edge Extended Stable
- Community Supported

View File

@@ -14,9 +14,10 @@ entrypoint_log() {
mkdir -p /tmp/element-web-config
cp /app/config*.json /tmp/element-web-config/
# If the module directory exists AND the module directory has modules in it
if [ -d "/modules" ] && [ "$( ls -A '/modules' )" ]; then
# If there are modules to be loaded
if [ -d "/modules" ]; then
cd /modules
for MODULE in *
do
# If the module has a package.json, use its main field as the entrypoint

View File

@@ -585,8 +585,6 @@ Currently, the following UI feature flags are supported:
- `UIFeature.BulkUnverifiedSessionsReminder` - Display popup reminders to verify or remove unverified sessions. Defaults
to true.
- `UIFeature.locationSharing` - Whether or not location sharing menus will be shown.
- `UIFeature.allowCreatingPublicRooms` - Whether or not public rooms can be created.
- `UIFeature.allowCreatingPublicSpaces` - Whether or not public spaces can be created.
## Undocumented / developer options

View File

@@ -38,20 +38,45 @@ When `force_disable` is true:
Note: If the server is configured to forcibly enable encryption for some or all rooms,
this behaviour will be overridden.
# Setting up recovery
# Secure backup
By default, Element strongly encourages (but does not require) users to set up
recovery so that you can access history on your new devices as well as retain access to your message history and cryptographic identity when you lose all of your devices.
Secure Backup so that cross-signing identity key and message keys can be
recovered in case of a disaster where you lose access to all active devices.
## Removal of old settings
## Requiring secure backup
Support for the configuration options `secure_backup_required` and `secure_backup_setup_methods`
in the `/.well-known/matrix/client` config has been removed.
To require Secure Backup to be configured before Element can be used, set the
following on your homeserver's `/.well-known/matrix/client` config:
Setting up recovery is now always recommended to all users by showing a one-off toast and a
permanent red dot on the _Encryption_ tab in the _Settings_ dialog. When creating a new
recovery key, the UI only supports auto-generated keys. Using an existing (custom) passphrase
still works, but is not exposed in the UI when setting up recovery.
```json
{
"io.element.e2ee": {
"secure_backup_required": true
}
}
```
## Preferring setup methods
By default, Element offers users a choice of a random key or user-chosen
passphrase when setting up Secure Backup. If a homeserver admin would like to
only offer one of these, you can signal this via the
`/.well-known/matrix/client` config, for example:
```json
{
"io.element.e2ee": {
"secure_backup_setup_methods": ["passphrase"]
}
}
```
The field `secure_backup_setup_methods` is an array listing the methods the
client should display. Supported values currently include `key` and
`passphrase`. If the `secure_backup_setup_methods` field is not present or
exists but does not contain any supported methods, Element will fallback to the
default value of: `["key", "passphrase"]`.
# Compatibility

11
knip.ts
View File

@@ -2,6 +2,7 @@ import { KnipConfig } from "knip";
export default {
entry: [
"src/vector/index.ts",
"src/serviceworker/index.ts",
"src/workers/*.worker.ts",
"src/utils/exportUtils/exportJS.js",
@@ -11,6 +12,8 @@ export default {
"res/decoder-ring/**",
"res/jitsi_external_api.min.js",
"docs/**",
// Used by jest
"__mocks__/maplibre-gl.js",
],
project: ["**/*.{js,ts,jsx,tsx}"],
ignore: [
@@ -39,18 +42,10 @@ export default {
"util",
// Embedded into webapp
"@element-hq/element-call-embedded",
// Used by matrix-js-sdk, which means we have to include them as a
// dependency so that // we can run `tsc` (since we import the typescript
// source of js-sdk, rather than the transpiled and annotated JS like you
// would with a normal library).
"@types/content-type",
"@types/sdp-transform",
],
ignoreBinaries: [
// Used in scripts & workflows
"jq",
"wait-on",
],
ignoreExportsUsedInFile: true,
} satisfies KnipConfig;

View File

@@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.11.111",
"version": "1.11.109",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -75,8 +75,8 @@
"resolutions": {
"**/pretty-format/react-is": "19.1.1",
"@playwright/test": "1.54.2",
"@types/react": "19.1.12",
"@types/react-dom": "19.1.9",
"@types/react": "19.1.10",
"@types/react-dom": "19.1.7",
"oidc-client-ts": "3.3.0",
"jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001724",
@@ -96,6 +96,7 @@
"@matrix-org/spec": "^1.7.0",
"@sentry/browser": "^10.0.0",
"@types/png-chunks-extract": "^1.0.2",
"@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^6.0.0",
"@vector-im/compound-web": "^8.1.2",
"@vector-im/matrix-wysiwyg": "2.39.0",
@@ -142,7 +143,7 @@
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.261.0",
"posthog-js": "1.260.1",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "^19.0.0",
@@ -152,13 +153,14 @@
"react-focus-lock": "^2.5.1",
"react-string-replace": "^1.1.1",
"react-transition-group": "^4.4.1",
"react-virtuoso": "^4.14.0",
"react-virtualized": "^9.22.5",
"react-virtuoso": "^4.12.6",
"rfc4648": "^1.4.0",
"sanitize-filename": "^1.6.3",
"sanitize-html": "2.17.0",
"tar-js": "^0.3.0",
"temporal-polyfill": "^0.3.0",
"ua-parser-js": "1.0.40",
"ua-parser-js": "^1.0.2",
"uuid": "^11.0.0",
"what-input": "^5.2.10"
},
@@ -184,7 +186,7 @@
"@babel/preset-typescript": "^7.12.7",
"@babel/runtime": "^7.12.5",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@element-hq/element-call-embedded": "0.15.0",
"@element-hq/element-call-embedded": "0.14.1",
"@element-hq/element-web-playwright-common": "^1.4.6",
"@peculiar/webcrypto": "^1.4.3",
"@playwright/test": "^1.50.1",
@@ -203,7 +205,6 @@
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/commonmark": "^0.27.4",
"@types/content-type": "^1.1.9",
"@types/counterpart": "^0.18.1",
"@types/css-tree": "^2.3.8",
"@types/diff-match-patch": "^1.0.32",
@@ -222,12 +223,11 @@
"@types/node-fetch": "^2.6.2",
"@types/pako": "^2.0.0",
"@types/qrcode": "^1.3.5",
"@types/react": "19.1.12",
"@types/react": "19.1.10",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "19.1.9",
"@types/react-dom": "19.1.7",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "2.16.0",
"@types/sdp-transform": "^2.4.10",
"@types/semver": "^7.5.8",
"@types/tar-js": "^0.3.5",
"@types/ua-parser-js": "^0.7.36",
@@ -307,7 +307,7 @@
"terser-webpack-plugin": "^5.3.9",
"testcontainers": "^11.0.0",
"ts-node": "^10.9.1",
"typescript": "5.8.3",
"typescript": "5.9.2",
"util": "^0.12.5",
"vite": "^7.0.1",
"vite-plugin-node-polyfills": "^0.24.0",

View File

@@ -11,42 +11,50 @@ index 917a7fc..a2710c6 100644
didOkOrSubmit: boolean;
model: M;
}>;
diff --git a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js b/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js
index 5d422ed..b823add 100644
--- a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js
+++ b/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js
@@ -124,34 +124,28 @@ var DefaultCryptoSetupExtensions = /*#__PURE__*/function (_CryptoSetupExtension)
(0, _createClass2["default"])(DefaultCryptoSetupExtensions, [{
key: "examineLoginResponse",
value: function examineLoginResponse(response, credentials) {
- console.log("Default empty examineLoginResponse() => void");
}
}, {
key: "persistCredentials",
value: function persistCredentials(credentials) {
- console.log("Default empty persistCredentials() => void");
}
}, {
key: "getSecretStorageKey",
value: function getSecretStorageKey() {
- console.log("Default empty getSecretStorageKey() => null");
return null;
}
}, {
key: "createSecretStorageKey",
value: function createSecretStorageKey() {
- console.log("Default empty createSecretStorageKey() => null");
return null;
}
}, {
key: "catchAccessSecretStorageError",
value: function catchAccessSecretStorageError(e) {
- console.log("Default catchAccessSecretStorageError() => void");
}
}, {
key: "setupEncryptionNeeded",
value: function setupEncryptionNeeded(args) {
- console.log("Default setupEncryptionNeeded() => false");
return false;
}
}, {
diff --git a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.d.ts b/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.d.ts
index cb5f2e5..51daa51 100644
--- a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.d.ts
+++ b/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.d.ts
@@ -66,23 +66,23 @@ export interface SetupEncryptionStoreProjection {
export interface ProvideCryptoSetupExtensions {
examineLoginResponse(response: any, credentials: ExtendedMatrixClientCreds): void;
persistCredentials(credentials: ExtendedMatrixClientCreds): void;
- getSecretStorageKey(): Uint8Array | null;
- createSecretStorageKey(): Uint8Array | null;
+ getSecretStorageKey(): Uint8Array<ArrayBuffer> | null;
+ createSecretStorageKey(): Uint8Array<ArrayBuffer> | null;
catchAccessSecretStorageError(e: Error): void;
setupEncryptionNeeded: (args: CryptoSetupArgs) => boolean;
/** @deprecated This callback is no longer used by matrix-react-sdk */
- getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise<Uint8Array>) | null;
+ getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array<ArrayBuffer>) => void) => Promise<Uint8Array<ArrayBuffer>>) | null;
SHOW_ENCRYPTION_SETUP_UI: boolean;
}
export declare abstract class CryptoSetupExtensionsBase implements ProvideCryptoSetupExtensions {
abstract examineLoginResponse(response: any, credentials: ExtendedMatrixClientCreds): void;
abstract persistCredentials(credentials: ExtendedMatrixClientCreds): void;
- abstract getSecretStorageKey(): Uint8Array | null;
- abstract createSecretStorageKey(): Uint8Array | null;
+ abstract getSecretStorageKey(): Uint8Array<ArrayBuffer> | null;
+ abstract createSecretStorageKey(): Uint8Array<ArrayBuffer> | null;
abstract catchAccessSecretStorageError(e: Error): void;
abstract setupEncryptionNeeded(args: CryptoSetupArgs): boolean;
/** `getDehydrationKeyCallback` is no longer used; we provide an empty impl for type compatibility. */
- getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise<Uint8Array>) | null;
+ getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array<ArrayBuffer>) => void) => Promise<Uint8Array<ArrayBuffer>>) | null;
abstract SHOW_ENCRYPTION_SETUP_UI: boolean;
}
export interface CryptoSetupArgs {
@@ -98,9 +98,9 @@ export declare class DefaultCryptoSetupExtensions extends CryptoSetupExtensionsB
SHOW_ENCRYPTION_SETUP_UI: boolean;
examineLoginResponse(response: any, credentials: ExtendedMatrixClientCreds): void;
persistCredentials(credentials: ExtendedMatrixClientCreds): void;
- getSecretStorageKey(): Uint8Array | null;
- createSecretStorageKey(): Uint8Array | null;
+ getSecretStorageKey(): Uint8Array<ArrayBuffer> | null;
+ createSecretStorageKey(): Uint8Array<ArrayBuffer> | null;
catchAccessSecretStorageError(e: Error): void;
setupEncryptionNeeded(args: CryptoSetupArgs): boolean;
- getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise<Uint8Array>) | null;
+ getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array<ArrayBuffer>) => void) => Promise<Uint8Array<ArrayBuffer>>) | null;
}

View File

@@ -0,0 +1,34 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
test.describe("Create Room", () => {
test.use({ displayName: "Jim" });
test("should allow us to create a public room with name, topic & address set", async ({ page, user, app }) => {
const name = "Test room 1";
const topic = "This room is dedicated to this test and this test only!";
const dialog = await app.openCreateRoomDialog();
// Fill name & topic
await dialog.getByRole("textbox", { name: "Name" }).fill(name);
await dialog.getByRole("textbox", { name: "Topic" }).fill(topic);
// Change room to public
await dialog.getByRole("button", { name: "Room visibility" }).click();
await dialog.getByRole("option", { name: "Public room" }).click();
// Fill room address
await dialog.getByRole("textbox", { name: "Room address" }).fill("test-room-1");
// Submit
await dialog.getByRole("button", { name: "Create room" }).click();
await expect(page).toHaveURL(new RegExp(`/#/room/#test-room-1:${user.homeServer}`));
const header = page.locator(".mx_RoomHeader");
await expect(header).toContainText(name);
});
});

View File

@@ -154,8 +154,8 @@ test.describe("Cryptography", function () {
await app.client.bootstrapCrossSigning(aliceCredentials);
await startDMWithBob(page, bob);
// send first message
await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!");
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter");
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).fill("Hey!");
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter");
await checkDMRoom(page);
const bobRoomId = await bobJoin(page, bob);
// We no longer show the grey badge in the composer, check that it is not there.

View File

@@ -14,7 +14,7 @@ import {
createSecondBotDevice,
createSharedRoomWithUser,
enableKeyBackup,
logIntoElementAndVerify,
logIntoElement,
logOutOfElement,
verify,
waitForDevices,
@@ -195,7 +195,7 @@ test.describe("Cryptography", function () {
window.localStorage.clear();
});
await page.reload();
await logIntoElementAndVerify(page, aliceCredentials, securityKey);
await logIntoElement(page, aliceCredentials, securityKey);
/* go back to the test room and find Bob's message again */
await app.viewRoomById(testRoomId);

View File

@@ -8,7 +8,7 @@
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
import { test, expect } from "../../element-web-test";
import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElementAndVerify } from "./utils";
import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElement } from "./utils";
import { type Bot } from "../../pages/bot";
test.describe("Key storage out of sync toast", () => {
@@ -18,7 +18,7 @@ test.describe("Key storage out of sync toast", () => {
const res = await createBot(page, homeserver, credentials);
recoveryKey = res.recoveryKey;
await logIntoElementAndVerify(page, credentials, recoveryKey.encodedPrivateKey);
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
await deleteCachedSecrets(page);
@@ -65,7 +65,7 @@ test.describe("'Turn on key storage' toast", () => {
const recoveryKey = res.recoveryKey;
botClient = res.botClient;
await logIntoElementAndVerify(page, credentials, recoveryKey.encodedPrivateKey);
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
// We won't be prompted for crypto setup unless we have an e2e room, so make one
await page.getByRole("button", { name: "Add room" }).click();
@@ -126,7 +126,7 @@ test.describe("'Turn on key storage' toast", () => {
await toast.getByRole("button", { name: "Continue" }).click();
// Then we see the Encryption settings dialog with an option to turn on key storage
await expect(page.getByRole("switch", { name: "Allow key storage" })).toBeVisible();
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
// And when we close that
await page.getByRole("button", { name: "Close dialog" }).click();
@@ -153,7 +153,7 @@ test.describe("'Turn on key storage' toast", () => {
await page.getByRole("button", { name: "Go to Settings" }).click();
// Then we see Encryption settings again
await expect(page.getByRole("switch", { name: "Allow key storage" })).toBeVisible();
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
// And when we close that, see the toast, click Dismiss, and Yes, Dismiss
await page.getByRole("button", { name: "Close dialog" }).click();

View File

@@ -206,42 +206,32 @@ export async function checkDeviceIsConnectedKeyBackup(
/**
* Fill in the login form in element with the given creds.
*
* If a `securityKey` is given, verifies the new device using the key.
*/
export async function logIntoElement(page: Page, credentials: Credentials) {
export async function logIntoElement(page: Page, credentials: Credentials, securityKey?: string) {
await page.goto("/#/login");
await page.getByRole("textbox", { name: "Username" }).fill(credentials.userId);
await page.getByPlaceholder("Password").fill(credentials.password);
await page.getByRole("button", { name: "Sign in" }).click();
}
/**
* Fill in the login form in Element with the given creds, and then complete the `CompleteSecurity` step, using the
* given recovery key. (Normally this will verify the new device using the secrets from 4S.)
*
* Afterwards, waits for the application to redirect to the home page.
*/
export async function logIntoElementAndVerify(page: Page, credentials: Credentials, recoveryKey: string) {
await logIntoElement(page, credentials);
// if a securityKey was given, verify the new device
if (securityKey !== undefined) {
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click();
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click();
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" });
// If the user has set a recovery *passphrase*, they'll be prompted for that first and have to click
// through to enter the recovery key which is what we have here. If they haven't, they'll be prompted
// for a recovery key straight away. We click the button if it's there so this works in both cases.
if (await useSecurityKey.isVisible()) {
await useSecurityKey.click();
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" });
// If the user has set a recovery *passphrase*, they'll be prompted for that first and have to click
// through to enter the recovery key which is what we have here. If they haven't, they'll be prompted
// for a recovery key straight away. We click the button if it's there so this works in both cases.
if (await useSecurityKey.isVisible()) {
await useSecurityKey.click();
}
// Fill in the recovery key
await page.locator(".mx_Dialog").getByTitle("Recovery key").fill(securityKey);
await page.getByRole("button", { name: "Continue", disabled: false }).click();
await page.getByRole("button", { name: "Done" }).click();
}
// Fill in the recovery key
await page.locator(".mx_Dialog").getByTitle("Recovery key").fill(recoveryKey);
await page.getByRole("button", { name: "Continue", disabled: false }).click();
await page.getByRole("button", { name: "Done" }).click();
// The application should now redirect to `/#/home`. Wait for that to happen, otherwise if a test immediately does
// a `viewRoomById` or similar, it could race.
await page.waitForURL("/#/home");
}
/**
@@ -310,9 +300,9 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
const encryptionTab = await app.settings.openUserSettings("Encryption");
const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" });
const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
if (!(await keyStorageToggle.isChecked())) {
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).click();
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
}
await encryptionTab.getByRole("button", { name: "Set up recovery" }).click();
@@ -333,11 +323,11 @@ export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
export async function disableKeyBackup(app: ElementAppPage): Promise<void> {
const encryptionTab = await app.settings.openUserSettings("Encryption");
const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" });
const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
if (await keyStorageToggle.isChecked()) {
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).click();
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
await encryptionTab.getByRole("button", { name: "Delete key storage" }).click();
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).isVisible();
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).isVisible();
// Wait for the update to account data to stick
await new Promise((resolve) => setTimeout(resolve, 2000));

View File

@@ -1,33 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
test.describe("Devtools", () => {
test.use({
displayName: "Alice",
});
test("should render the devtools", { tag: "@screenshot" }, async ({ page, homeserver, user, app, axe }) => {
await app.client.createRoom({ name: "Test Room" });
await app.viewRoomByName("Test Room");
const composer = app.getComposer().locator("[contenteditable]");
await composer.fill("/devtools");
await composer.press("Enter");
const dialog = page.locator(".mx_Dialog");
await dialog.getByLabel("Developer mode").check();
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
await expect(axe).toHaveNoViolations();
await expect(dialog).toMatchScreenshot("devtools-dialog.png", {
css: `.mx_CopyableText {
display: none;
}`,
});
});
});

View File

@@ -1,38 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { test, expect } from "../../element-web-test";
test.describe("Room upgrade dialog", () => {
test.use({
displayName: "Alice",
});
test(
"should render the room upgrade dialog",
{ tag: "@screenshot" },
async ({ page, homeserver, user, app, axe }) => {
// Enable developer mode
await app.settings.setValue("developerMode", null, SettingLevel.ACCOUNT, true);
await app.client.createRoom({ name: "Test Room" });
await app.viewRoomByName("Test Room");
const composer = app.getComposer().locator("[contenteditable]");
// Pick a room version that is likely to be supported by all our target homeservers.
await composer.fill("/upgraderoom 5");
await composer.press("Enter");
const dialog = page.locator(".mx_Dialog");
await dialog.getByLabel("Automatically invite members from this room to the new one").check();
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
await expect(axe).toHaveNoViolations();
await expect(dialog).toMatchScreenshot("upgrade-room.png");
},
);
});

View File

@@ -1,28 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
test.describe("Decline and block invite dialog", function () {
test.use({
displayName: "Hanako",
});
test(
"should show decline and block dialog for a room",
{ tag: "@screenshot" },
async ({ page, app, user, bot, axe }) => {
await bot.createRoom({ name: "Test Room", invite: [user.userId] });
await app.viewRoomByName("Test Room");
await page.getByRole("button", { name: "Decline and block" }).click();
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
await expect(axe).toHaveNoViolations();
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("decline-and-block-invite-empty.png");
},
);
});

View File

@@ -68,7 +68,7 @@ test.describe("Room list filters and sort", () => {
So we expect 'Old Room' to show up in the room list.
*/
const roomListView = getRoomList(page);
const oldRoomTile = roomListView.getByRole("option", { name: "Open room Old Room" });
const oldRoomTile = roomListView.getByRole("gridcell", { name: "Open room Old Room" });
await expect(oldRoomTile).toBeVisible();
/*
@@ -139,9 +139,8 @@ test.describe("Room list filters and sort", () => {
// Open the non-favourite room
const roomListView = getRoomList(page);
const tile = roomListView.getByRole("option", { name: "Open room room-non-fav" });
// item may not be in the DOM using scrollListToBottom rather than scrollIntoViewIfNeeded
await app.scrollListToBottom(roomListView);
const tile = roomListView.getByRole("gridcell", { name: "Open room room-non-fav" });
await tile.scrollIntoViewIfNeeded();
await tile.click();
// Enable Favourite filter
@@ -152,7 +151,7 @@ test.describe("Room list filters and sort", () => {
// Ensure the room list is not scrolled
const isScrolledDown = await page
.getByRole("listbox", { name: "Room list", exact: true })
.getByRole("grid", { name: "Room list" })
.evaluate((e) => e.scrollTop !== 0);
expect(isScrolledDown).toStrictEqual(false);
});
@@ -228,37 +227,37 @@ test.describe("Room list filters and sort", () => {
await primaryFilters.getByRole("option", { name: "Unread" }).click();
// only one room should be visible
await expect(roomList.getByRole("option", { name: "unread dm" })).toBeVisible();
await expect(roomList.getByRole("option", { name: "unread room" })).toBeVisible();
await expect.poll(() => roomList.locator("role=option").count()).toBe(4);
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(4);
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
await primaryFilters.getByRole("option", { name: "People" }).click();
await expect(roomList.getByRole("option", { name: "unread dm" })).toBeVisible();
await expect(roomList.getByRole("option", { name: "invited room" })).toBeVisible();
await expect.poll(() => roomList.locator("role=option").count()).toBe(2);
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(2);
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
await expect(roomList.getByRole("option", { name: "unread room" })).toBeVisible();
await expect(roomList.getByRole("option", { name: "favourite room" })).toBeVisible();
await expect(roomList.getByRole("option", { name: "empty room" })).toBeVisible();
await expect(roomList.getByRole("option", { name: "room with mention" })).toBeVisible();
await expect(roomList.getByRole("option", { name: "Low prio room" })).toBeVisible();
await expect.poll(() => roomList.locator("role=option").count()).toBe(5);
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "Low prio room" })).toBeVisible();
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(5);
await getFilterExpandButton(page).click();
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
await expect(roomList.getByRole("option", { name: "favourite room" })).toBeVisible();
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
await primaryFilters.getByRole("option", { name: "Mentions" }).click();
await expect(roomList.getByRole("option", { name: "room with mention" })).toBeVisible();
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
await primaryFilters.getByRole("option", { name: "Invites" }).click();
await expect(roomList.getByRole("option", { name: "invited room" })).toBeVisible();
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
await getFilterCollapseButton(page).click();
await expect(primaryFilters.locator("role=option").first()).toHaveText("Invites");
@@ -269,7 +268,6 @@ test.describe("Room list filters and sort", () => {
{ tag: "@screenshot" },
async ({ page, app, bot }) => {
const roomListView = getRoomList(page);
const primaryFilters = getPrimaryFilters(page);
// Let's configure unread dm room so that we only get notification for mentions and keywords
await app.viewRoomById(unReadDmId);
@@ -278,20 +276,20 @@ test.describe("Room list filters and sort", () => {
await app.settings.closeDialog();
// Let's open a room other than unread room or unread dm
await roomListView.getByRole("option", { name: "Open room favourite room" }).click();
await roomListView.getByRole("gridcell", { name: "Open room favourite room" }).click();
// Let's make the bot send a new message in both rooms
await bot.sendMessage(unReadDmId, "Hello!");
await bot.sendMessage(unReadRoomId, "Hello!");
// Let's activate the unread filter now
await primaryFilters.getByRole("option", { name: "Unread" }).click();
await page.getByRole("option", { name: "Unread" }).click();
// Unread filter should only show unread room and not unread dm!
const unreadDm = roomListView.getByRole("option", { name: "Open room unread room" });
const unreadDm = roomListView.getByRole("gridcell", { name: "Open room unread room" });
await expect(unreadDm).toBeVisible();
await expect(unreadDm).toMatchScreenshot("unread-dm.png");
await expect(roomListView.getByRole("option", { name: "Open room unread dm" })).not.toBeVisible();
await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible();
},
);
@@ -301,7 +299,7 @@ test.describe("Room list filters and sort", () => {
await getRoomOptionsMenu(page).click();
await page.getByRole("menuitemradio", { name: "A-Z" }).click();
await expect(roomListView.getByRole("option").first()).toHaveText(/empty room/);
await expect(roomListView.getByRole("gridcell").first()).toHaveText(/empty room/);
});
test("should move room to the top on message when sorting by activity", async ({ page, bot }) => {
@@ -309,7 +307,7 @@ test.describe("Room list filters and sort", () => {
await bot.sendMessage(unReadDmId, "Hello!");
await expect(roomListView.getByRole("option").first()).toHaveText(/unread dm/);
await expect(roomListView.getByRole("gridcell").first()).toHaveText(/unread dm/);
});
});

View File

@@ -38,7 +38,7 @@ test.describe("Room list panel", () => {
test("should render the room list panel", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomListView(page);
// Wait for the last room to be visible
await expect(roomListView.getByRole("option", { name: "Open room room19" })).toBeVisible();
await expect(roomListView.getByRole("gridcell", { name: "Open room room19" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list-panel.png");
});

View File

@@ -41,38 +41,33 @@ test.describe("Room list", () => {
}
});
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await expect(roomListView.getByRole("option", { name: "Open room room29" })).toBeVisible();
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list.png");
// Put focus on the room list
await roomListView.getByRole("option", { name: "Open room room29" }).click();
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
// Scroll to the end of the room list
await app.scrollListToBottom(roomListView);
// scrollListToBottom seems to leave the mouse hovered over the list, move it away.
await page.getByRole("button", { name: "User menu" }).hover();
await expect(axe).toHaveNoViolations();
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
});
test("should open the room when it is clicked", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.getByRole("option", { name: "Open room room29" }).click();
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
});
test("should open the context menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.getByRole("option", { name: "Open room room29" }).click({ button: "right" });
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click({ button: "right" });
await expect(page.getByRole("menu", { name: "More Options" })).toBeVisible();
});
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomList(page);
const roomItem = roomListView.getByRole("option", { name: "Open room room29" });
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
await roomItem.hover();
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
@@ -102,7 +97,7 @@ test.describe("Room list", () => {
test("should open the notification options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomList(page);
const roomItem = roomListView.getByRole("option", { name: "Open room room29" });
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
await roomItem.hover();
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
@@ -122,10 +117,10 @@ test.describe("Room list", () => {
await expect(roomItem.getByTestId("notification-decoration")).not.toBeVisible();
// Put focus on the room list
await roomListView.getByRole("option", { name: "Open room room28" }).click();
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
// Scroll to the end of the room list
await app.scrollListToBottom(roomListView);
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
// The room decoration should have the muted icon
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
@@ -144,25 +139,25 @@ test.describe("Room list", () => {
test("should scroll to the current room", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
// Put focus on the room list
await roomListView.getByRole("option", { name: "Open room room29" }).click();
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
// Scroll to the end of the room list
await app.scrollListToBottom(roomListView);
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
await expect(roomListView.getByRole("option", { name: "Open room room0" })).toBeVisible();
await roomListView.getByRole("option", { name: "Open room room0" }).click();
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
const filters = page.getByRole("listbox", { name: "Room list filters" });
await filters.getByRole("option", { name: "People" }).click();
await expect(roomListView.getByRole("option", { name: "Open room room0" })).not.toBeVisible();
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();
await filters.getByRole("option", { name: "People" }).click();
await expect(roomListView.getByRole("option", { name: "Open room room0" })).toBeVisible();
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
});
test.describe("Shortcuts", () => {
test("should select the next room", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.getByRole("option", { name: "Open room room29" }).click();
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
await page.keyboard.press("Alt+ArrowDown");
await expect(page.getByRole("heading", { name: "room28", level: 1 })).toBeVisible();
@@ -170,7 +165,7 @@ test.describe("Room list", () => {
test("should select the previous room", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.getByRole("option", { name: "Open room room28" }).click();
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
await page.keyboard.press("Alt+ArrowUp");
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
@@ -178,7 +173,7 @@ test.describe("Room list", () => {
test("should select the last room", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.getByRole("option", { name: "Open room room29" }).click();
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
await page.keyboard.press("Alt+ArrowUp");
await expect(page.getByRole("heading", { name: "room0", level: 1 })).toBeVisible();
@@ -192,10 +187,7 @@ test.describe("Room list", () => {
await bot.joinRoom(roomId);
await bot.sendMessage(roomId, "I am a robot. Beep.");
await roomListView.getByRole("option", { name: "Open room room20" }).click();
// Make sure the room with the unread is visible before we press the keyboard action to select it
await expect(roomListView.getByRole("option", { name: "1 notification" })).toBeVisible();
await roomListView.getByRole("gridcell", { name: "Open room room20" }).click();
await page.keyboard.press("Alt+Shift+ArrowDown");
@@ -207,8 +199,8 @@ test.describe("Room list", () => {
test("should navigate to the room list", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
const room29 = roomListView.getByRole("option", { name: "Open room room29" });
const room28 = roomListView.getByRole("option", { name: "Open room room28" });
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
const room28 = roomListView.getByRole("gridcell", { name: "Open room room28" });
// open the room
await room29.click();
@@ -227,7 +219,7 @@ test.describe("Room list", () => {
test("should navigate to the notification menu", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
const room29 = roomListView.getByRole("option", { name: "Open room room29" });
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
const moreButton = room29.getByRole("button", { name: "More options" });
const notificationButton = room29.getByRole("button", { name: "Notification options" });
@@ -266,7 +258,7 @@ test.describe("Room list", () => {
await page.getByRole("button", { name: "User menu" }).focus();
const roomListView = getRoomList(page);
const publicRoom = roomListView.getByRole("option", { name: "public room" });
const publicRoom = roomListView.getByRole("gridcell", { name: "public room" });
await expect(publicRoom).toBeVisible();
await expect(publicRoom).toMatchScreenshot("room-list-item-public.png");
@@ -276,7 +268,7 @@ test.describe("Room list", () => {
// @ts-ignore Visibility enum is not accessible
await app.client.createRoom({ name: "low priority room", visibility: "public" });
const roomListView = getRoomList(page);
const publicRoom = roomListView.getByRole("option", { name: "low priority room" });
const publicRoom = roomListView.getByRole("gridcell", { name: "low priority room" });
// Make room low priority
await publicRoom.hover();
@@ -301,7 +293,7 @@ test.describe("Room list", () => {
await page.getByRole("button", { name: "Create video room" }).click();
const roomListView = getRoomList(page);
const videoRoom = roomListView.getByRole("option", { name: "video room" });
const videoRoom = roomListView.getByRole("gridcell", { name: "video room" });
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
@@ -320,7 +312,7 @@ test.describe("Room list", () => {
invite: [user.userId],
is_direct: true,
});
const invitedRoom = roomListView.getByRole("option", { name: "invited room" });
const invitedRoom = roomListView.getByRole("gridcell", { name: "invited room" });
await expect(invitedRoom).toBeVisible();
await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png");
});
@@ -335,7 +327,7 @@ test.describe("Room list", () => {
await bot.sendMessage(roomId, "I am a robot. Beep.");
await bot.sendMessage(roomId, "I am a robot. Beep.");
const room = roomListView.getByRole("option", { name: "2 notifications" });
const room = roomListView.getByRole("gridcell", { name: "2 notifications" });
await expect(room).toBeVisible();
await expect(room.getByTestId("notification-decoration")).toHaveText("2");
await expect(room).toMatchScreenshot("room-list-item-notification.png");
@@ -366,7 +358,7 @@ test.describe("Room list", () => {
);
await bot.sendMessage(roomId, "I am a robot. Beep.");
const room = roomListView.getByRole("option", { name: "mention" });
const room = roomListView.getByRole("gridcell", { name: "mention" });
await expect(room).toBeVisible();
await expect(room).toMatchScreenshot("room-list-item-mention.png");
});
@@ -387,7 +379,7 @@ test.describe("Room list", () => {
await bot.joinRoom(roomId);
await bot.sendMessage(roomId, "I am a robot. Beep.");
const room = roomListView.getByRole("option", { name: "activity" });
const room = roomListView.getByRole("gridcell", { name: "activity" });
await expect(room.getByText("I am a robot. Beep.")).toBeVisible();
await expect(room).toMatchScreenshot("room-list-item-message-preview.png");
});
@@ -414,7 +406,7 @@ test.describe("Room list", () => {
await app.viewRoomById(otherRoomId);
await bot.sendMessage(roomId, "I am a robot. Beep.");
const room = roomListView.getByRole("option", { name: "activity" });
const room = roomListView.getByRole("gridcell", { name: "activity" });
await expect(room.getByTestId("notification-decoration")).toBeVisible();
await expect(room).toMatchScreenshot("room-list-item-activity.png");
});
@@ -426,7 +418,7 @@ test.describe("Room list", () => {
await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId);
const room = roomListView.getByRole("option", { name: "mark as unread" });
const room = roomListView.getByRole("gridcell", { name: "mark as unread" });
await room.hover();
await room.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "mark as unread" }).click();
@@ -449,7 +441,7 @@ test.describe("Room list", () => {
await page.getByText("Off").click();
await app.settings.closeDialog();
const room = roomListView.getByRole("option", { name: "silent" });
const room = roomListView.getByRole("gridcell", { name: "silent" });
await expect(room.getByTestId("notification-decoration")).toBeVisible();
await expect(room).toMatchScreenshot("room-list-item-silent.png");
});

View File

@@ -1,5 +1,5 @@
/*
Copyright 2024, 2025 New Vector Ltd.
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
@@ -57,26 +57,4 @@ test.describe("Location sharing", { tag: "@no-firefox" }, () => {
await expect(page.locator(".mx_Marker")).toBeVisible();
});
test(
"is prompted for and can consent to live location sharing",
{ tag: "@screenshot" },
async ({ page, user, app, axe }) => {
await app.viewRoomById(await app.client.createRoom({}));
const composerOptions = await app.openMessageComposerOptions();
await composerOptions.getByRole("menuitem", { name: "Location", exact: true }).click();
const menu = page.locator(".mx_LocationShareMenu");
await menu.getByRole("button", { name: "My live location" }).click();
await menu.getByLabel("Enable live location sharing").check();
axe.disableRules([
"color-contrast", // XXX: Inheriting colour contrast issues from room view.
"region", // XXX: ContextMenu managed=false does not provide a role.
]);
await expect(axe).toHaveNoViolations();
await expect(menu).toMatchScreenshot("location-live-share-dialog.png");
},
);
});

View File

@@ -100,51 +100,3 @@ test.describe("permalinks", () => {
});
});
});
test.describe("triple-click message selection", () => {
test.use({
displayName: "Alice",
});
test("should select entire message line when triple-clicking on message with pills", async ({
page,
app,
user,
bot,
}) => {
await bot.prepareClient();
const roomId = await app.client.createRoom({ name: "Test Room" });
await app.client.inviteUser(roomId, bot.credentials.userId);
await app.viewRoomByName("Test Room");
// Send a message with user and room pills
await app.client.sendMessage(
roomId,
`Testing triple-click message selection. ` +
`User: ${permalinkPrefix}${bot.credentials.userId}, ` +
`Room: ${permalinkPrefix}${roomId}, ` +
`Message: ${permalinkPrefix}${roomId}/$dummy-event, ` +
`and @room mention.`,
);
const timeline = page.locator(".mx_RoomView_timeline");
const messageTile = timeline.locator(".mx_EventTile").last();
// Triple-click on the message body to select its entire content
const messageBody = messageTile.locator(".mx_EventTile_body");
await messageBody.click({ clickCount: 3 });
// Get the expected text content of the message, including pills
const expectedText = await messageBody.innerText();
// Get the currently selected text from the page
const selectedText = await page.evaluate(() => {
const selection = window.getSelection();
return selection ? selection.toString().trim() : "";
});
// Verify that the selected text exactly matches the message content
expect(selectedText).toBe(expectedText);
});
});

View File

@@ -30,8 +30,9 @@ export class Helpers {
/**
* Get the release announcement with the given name.
* @param name
* @private
*/
public getReleaseAnnouncement(name: string) {
private getReleaseAnnouncement(name: string) {
return this.page.getByRole("dialog", { name });
}
@@ -54,6 +55,16 @@ export class Helpers {
assertReleaseAnnouncementIsNotVisible(name: string) {
return expect(this.getReleaseAnnouncement(name)).not.toBeVisible();
}
/**
* Mark the release announcement with the given name as read.
* If the release announcement is not visible, this will throw an error.
* @param name
*/
async markReleaseAnnouncementAsRead(name: string) {
const dialog = this.getReleaseAnnouncement(name);
await dialog.getByRole("button", { name: "Ok" }).click();
}
}
export { expect };

View File

@@ -22,25 +22,25 @@ test.describe("Release announcement", () => {
await app.viewRoomById(roomId);
await use({ roomId });
},
labsFlags: ["feature_new_room_list"],
});
test(
"should display the new room list release announcement",
"should display the pinned messages release announcement",
{ tag: "@screenshot" },
async ({ page, app, room, util }) => {
const name = "Chats has a new look!";
await app.toggleRoomInfoPanel();
const name = "All new pinned messages";
// The release announcement should be displayed
await util.assertReleaseAnnouncementIsVisible(name);
// Hide the release announcement
const dialog = util.getReleaseAnnouncement(name);
await dialog.getByRole("button", { name: "Next" }).click();
await util.markReleaseAnnouncementAsRead(name);
await util.assertReleaseAnnouncementIsNotVisible(name);
await page.reload();
await expect(page.getByRole("button", { name: "Room options" })).toBeVisible();
await app.toggleRoomInfoPanel();
await expect(page.getByRole("menuitem", { name: "Pinned messages" })).toBeVisible();
// Check that once the release announcement has been marked as viewed, it does not appear again
await util.assertReleaseAnnouncementIsNotVisible(name);
},

View File

@@ -1,113 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { UIFeature } from "../../../src/settings/UIFeature";
import { test, expect } from "../../element-web-test";
const name = "Test room";
const topic = "A decently explanatory topic for a test room.";
test.describe("Create Room", () => {
test.use({ displayName: "Jim" });
test(
"should create a public room with name, topic & address set",
{ tag: "@screenshot" },
async ({ page, user, app, axe }) => {
const dialog = await app.openCreateRoomDialog();
// Fill name & topic
await dialog.getByRole("textbox", { name: "Name" }).fill(name);
await dialog.getByRole("textbox", { name: "Topic" }).fill(topic);
// Change room to public
await dialog.getByRole("button", { name: "Room visibility" }).click();
await dialog.getByRole("option", { name: "Public room" }).click();
// Fill room address
await dialog.getByRole("textbox", { name: "Room address" }).fill("test-create-room-standard");
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
await expect(axe).toHaveNoViolations();
// Snapshot it
await expect(dialog).toMatchScreenshot("create-room.png");
// Submit
await dialog.getByRole("button", { name: "Create room" }).click();
await expect(page).toHaveURL(new RegExp(`/#/room/#test-create-room-standard:${user.homeServer}`));
const header = page.locator(".mx_RoomHeader");
await expect(header).toContainText(name);
},
);
test("should allow us to start a chat and show encryption state", async ({ page, user, app }) => {
await page.getByRole("button", { name: "Add", exact: true }).click();
await page.getByText("Start new chat").click();
await page.getByTestId("invite-dialog-input").fill(user.userId);
await page.getByRole("button", { name: "Go" }).click();
await expect(page.getByText("Encryption enabled")).toBeVisible();
await expect(page.getByText("Send your first message to")).toBeVisible();
const composer = page.getByRole("region", { name: "Message composer" });
await expect(composer.getByRole("textbox", { name: "Send a message…" })).toBeVisible();
});
test("should create a video room", { tag: "@screenshot" }, async ({ page, user, app }) => {
await app.settings.setValue("feature_video_rooms", null, SettingLevel.DEVICE, true);
const dialog = await app.openCreateRoomDialog("New video room");
// Fill name & topic
await dialog.getByRole("textbox", { name: "Name" }).fill(name);
await dialog.getByRole("textbox", { name: "Topic" }).fill(topic);
// Change room to public
await dialog.getByRole("button", { name: "Room visibility" }).click();
await dialog.getByRole("option", { name: "Public room" }).click();
// Fill room address
await dialog.getByRole("textbox", { name: "Room address" }).fill("test-create-room-video");
// Snapshot it
await expect(dialog).toMatchScreenshot("create-video-room.png");
// Submit
await dialog.getByRole("button", { name: "Create video room" }).click();
await expect(page).toHaveURL(new RegExp(`/#/room/#test-create-room-video:${user.homeServer}`));
const header = page.locator(".mx_RoomHeader");
await expect(header).toContainText(name);
});
test.describe("Should hide public room option if not allowed", () => {
test.use({
config: {
setting_defaults: {
[UIFeature.AllowCreatingPublicRooms]: false,
},
},
});
test("should disallow creating public rooms", { tag: "@screenshot" }, async ({ page, user, app, axe }) => {
const dialog = await app.openCreateRoomDialog();
// Fill name & topic
await dialog.getByRole("textbox", { name: "Name" }).fill(name);
await dialog.getByRole("textbox", { name: "Topic" }).fill(topic);
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
await expect(axe).toHaveNoViolations();
// Snapshot it
await expect(dialog).toMatchScreenshot("create-room-no-public.png");
// Submit
await dialog.getByRole("button", { name: "Create room" }).click();
await expect(page).toHaveURL(new RegExp(`/#/room/!.+`));
const header = page.locator(".mx_RoomHeader");
await expect(header).toContainText(name);
});
});
});

View File

@@ -85,7 +85,7 @@ class Helpers {
* Return the system theme toggle
*/
getMatchSystemThemeCheckbox() {
return this.getThemePanel().getByRole("switch", { name: "Match system theme" });
return this.getThemePanel().getByRole("checkbox", { name: "Match system theme" });
}
/**
@@ -219,7 +219,7 @@ class Helpers {
* Return the compact layout checkbox
*/
getCompactLayoutCheckbox() {
return this.getMessageLayoutPanel().getByRole("switch", { name: "Show compact text and messages" });
return this.getMessageLayoutPanel().getByRole("checkbox", { name: "Show compact text and messages" });
}
/**

View File

@@ -117,7 +117,7 @@ test.describe("Encryption tab", () => {
await verifySession(app, recoveryKey.encodedPrivateKey);
await util.openEncryptionTab();
await page.getByRole("switch", { name: "Allow key storage" }).click();
await page.getByRole("checkbox", { name: "Allow key storage" }).click();
await expect(
page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }),
@@ -136,7 +136,7 @@ test.describe("Encryption tab", () => {
await page.getByRole("button", { name: "Delete key storage" }).click();
await expect(page.getByRole("switch", { name: "Allow key storage" })).not.toBeChecked();
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked();
for (const prom of deleteRequestPromises) {
const request = await prom;

View File

@@ -104,10 +104,7 @@ class Helpers {
const clipboardContent = await this.app.getClipboard();
await dialog.getByRole("textbox").fill(clipboardContent);
const button = dialog.getByRole("button", { name: confirmButtonLabel });
await button.click();
// Button should disable immediately after clicking.
await expect(button).toBeDisabled();
await dialog.getByRole("button", { name: confirmButtonLabel }).click();
await expect(this.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
}
}

View File

@@ -1,28 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../../element-web-test";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
test.describe("Notifications 2 tab", () => {
test.use({
displayName: "Alice",
});
test("should display notification settings", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
await app.settings.setValue("feature_notification_settings2", null, SettingLevel.DEVICE, true);
await page.setViewportSize({ width: 1024, height: 2000 });
const settings = await app.settings.openUserSettings("Notifications");
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
await expect(axe).toHaveNoViolations();
await expect(settings).toMatchScreenshot("standard-notifications-2-settings.png", {
// Mask the mxid.
mask: [settings.locator("#mx_NotificationSettings2_MentionCheckbox span")],
});
});
});

View File

@@ -1,25 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../../element-web-test";
test.describe("Notifications tab", () => {
test.use({
displayName: "Alice",
});
test("should display notification settings", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
await page.setViewportSize({ width: 1024, height: 1400 });
const settings = await app.settings.openUserSettings("Notifications");
await settings.getByLabel("Enable notifications for this account").check();
await settings.getByLabel("Enable notifications for this device").check();
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
await expect(axe).toHaveNoViolations();
await expect(settings).toMatchScreenshot("standard-notification-settings.png");
});
});

View File

@@ -8,7 +8,7 @@
import { type Locator } from "@playwright/test";
import { test, expect } from "../../../element-web-test";
import { test, expect } from "../../element-web-test";
test.describe("Roles & Permissions room settings tab", () => {
const roomName = "Test room";

View File

@@ -1,121 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { type Locator } from "@playwright/test";
import { test, expect } from "../../../element-web-test";
test.describe("Roles & Permissions room settings tab", () => {
const roomName = "Test room";
test.use({
displayName: "Alice",
});
let settings: Locator;
test.beforeEach(async ({ user, app }) => {
await app.client.createRoom({
name: roomName,
power_level_content_override: {
events: {
// Set the join rules as lower than the history vis to test an edge case.
["m.room.join_rules"]: 80,
["m.room.history_visibility"]: 100,
},
},
});
await app.viewRoomByName(roomName);
settings = await app.settings.openRoomSettings("Security & Privacy");
});
test(
"should be able to toggle on encryption in a room",
{ tag: "@screenshot" },
async ({ page, app, user, axe }) => {
await page.setViewportSize({ width: 1024, height: 1400 });
const encryptedToggle = settings.getByLabel("Encrypted");
await encryptedToggle.click();
// Accept the dialog.
await page.getByRole("button", { name: "Ok " }).click();
await expect(encryptedToggle).toBeChecked();
await expect(encryptedToggle).toBeDisabled();
await settings.getByLabel("Only send messages to verified users.").check();
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
await expect(axe).toHaveNoViolations();
await expect(settings).toMatchScreenshot("room-security-settings.png");
},
);
test(
"should automatically adjust history visibility when a room is changed from public to private",
{ tag: "@screenshot" },
async ({ page, app, user, axe }) => {
await page.setViewportSize({ width: 1024, height: 1400 });
const settingsGroupAccess = page.getByRole("group", { name: "Access" });
const settingsGroupHistory = page.getByRole("group", { name: "Who can read history?" });
await settingsGroupAccess.getByText("Public").click();
await settingsGroupHistory.getByText("Anyone").click();
// Test that we have the warning appear.
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
await expect(axe).toHaveNoViolations();
await expect(settings).toMatchScreenshot("room-security-settings-world-readable.png");
await settingsGroupAccess.getByText("Private (invite only)").click();
// Element should have automatically set the room to "sharing" history visibility
await expect(
settingsGroupHistory.getByText("Members only (since the point in time of selecting this option)"),
).toBeChecked();
},
);
test(
"should disallow changing from public to private if the user cannot alter history",
{ tag: "@screenshot" },
async ({ page, app, user, bot }) => {
await page.setViewportSize({ width: 1024, height: 1400 });
const settingsGroupAccess = page.getByRole("group", { name: "Access" });
const settingsGroupHistory = page.getByRole("group", { name: "Who can read history?" });
await settingsGroupAccess.getByText("Public").click();
await settingsGroupHistory.getByText("Anyone").click();
// De-op ourselves
await app.settings.switchTab("Roles & Permissions");
// Wait for the permissions list to be visible
await expect(settings.getByRole("heading", { name: "Permissions" })).toBeVisible();
const ourComboBox = settings.getByRole("combobox", { name: user.userId });
await ourComboBox.selectOption("Custom level");
const ourPl = settings.getByRole("spinbutton", { name: user.userId });
await ourPl.fill("80");
await ourPl.blur(); // Shows a warning on
// Accept the de-op
await page.getByRole("button", { name: "Continue" }).click();
await settings.getByRole("button", { name: "Apply", disabled: false }).click();
await app.settings.switchTab("Security & Privacy");
await settingsGroupAccess.getByText("Private (invite only)").click();
// Element should have automatically set the room to "sharing" history visibility
const errorDialog = page.getByRole("heading", { name: "Cannot make room private" });
await expect(errorDialog).toBeVisible();
await errorDialog.getByLabel("OK");
await expect(settingsGroupHistory.getByText("Anyone")).toBeChecked();
},
);
});

View File

@@ -1,42 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { type Locator } from "@playwright/test";
import { test, expect } from "../../../element-web-test";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
test.describe("Voice & Video room settings tab", () => {
const roomName = "Test room";
test.use({
displayName: "Alice",
});
let settings: Locator;
test.beforeEach(async ({ user, app, page }) => {
// Execute client actions before setting, as the setting will force a reload.
await app.client.createRoom({ name: roomName });
await app.settings.setValue("feature_group_calls", null, SettingLevel.DEVICE, true);
await app.viewRoomByName(roomName);
settings = await app.settings.openRoomSettings("Voice & Video");
});
test(
"should be able to toggle on Element Call in the room",
{ tag: "@screenshot" },
async ({ page, app, user, axe }) => {
await page.setViewportSize({ width: 1024, height: 1400 });
const callToggle = settings.getByLabel("Enable Element Call as an additional calling option in this room");
await callToggle.check();
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
await expect(axe).toHaveNoViolations();
await expect(settings).toMatchScreenshot("room-video-settings.png");
},
);
});

View File

@@ -41,18 +41,6 @@ test.describe("Security user settings tab", () => {
});
});
test("should render the security tab", { tag: "@screenshot" }, async ({ app, page, user }) => {
await page.setViewportSize({ width: 1024, height: 1400 });
const tab = await app.settings.openUserSettings("Security");
await expect(tab).toMatchScreenshot("security-settings-tab.png", {
mask: [
// Contains IM name.
tab.locator("#mx_SetIntegrationManager_BodyText"),
tab.locator("#mx_SetIntegrationManager_ManagerName"),
],
});
});
test("should be able to set an ID server", async ({ app, context, user, page }) => {
const tab = await app.settings.openUserSettings("Security");

View File

@@ -11,7 +11,6 @@ import { test, expect } from "../../element-web-test";
import type { Preset, ICreateRoomOpts } from "matrix-js-sdk/src/matrix";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { UIFeature } from "../../../src/settings/UIFeature";
async function openSpaceCreateMenu(page: Page): Promise<Locator> {
await page.getByRole("button", { name: "Create a space" }).click();
@@ -377,68 +376,4 @@ test.describe("Spaces", () => {
await app.viewSpaceByName("Root Space");
await expect(page.locator(".mx_SpaceRoomView")).toMatchScreenshot("space-room-view.png");
});
test("should render spaces visibility settings", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
await app.client.createSpace({
name: "My Space",
});
await app.viewSpaceByName("My space");
await page.getByLabel("Settings", { exact: true }).click();
await app.settings.switchTab("Visibility");
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
await expect(axe).toHaveNoViolations();
await expect(page.locator("#mx_tabpanel_SPACE_VISIBILITY_TAB")).toMatchScreenshot(
"space-visibility-settings.png",
);
});
test.describe("Should hide public spaces option if not allowed", () => {
test.use({
config: {
setting_defaults: {
[UIFeature.AllowCreatingPublicSpaces]: false,
},
},
});
test("should disallow creating public rooms", { tag: "@screenshot" }, async ({ page, user, app }) => {
const menu = await openSpaceCreateMenu(page);
await menu
.locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
.setInputFiles("playwright/sample-files/riot.png");
await menu.getByRole("textbox", { name: "Name" }).fill("This is a private space");
await expect(menu.getByRole("textbox", { name: "Address" })).not.toBeVisible();
await menu
.getByRole("textbox", { name: "Description" })
.fill("This is a private space because we can't make public ones");
await menu.getByRole("button", { name: "Create" }).click();
await page.getByRole("button", { name: "Me and my teammates" }).click();
// Create the default General & Random rooms, as well as a custom "Projects" room
await expect(page.getByPlaceholder("General")).toBeVisible();
await expect(page.getByPlaceholder("Random")).toBeVisible();
await page.getByPlaceholder("Support").fill("Projects");
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Skip for now" }).click();
// Assert rooms exist in the room list
const roomList = page.getByRole("tree", { name: "Rooms" });
await expect(roomList.getByRole("treeitem", { name: "General", exact: true })).toBeVisible();
await expect(roomList.getByRole("treeitem", { name: "Random", exact: true })).toBeVisible();
await expect(roomList.getByRole("treeitem", { name: "Projects", exact: true })).toBeVisible();
// Assert rooms exist in the space explorer
await expect(
page.locator(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", { hasText: "General" }),
).toBeVisible();
await expect(
page.locator(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", { hasText: "Random" }),
).toBeVisible();
await expect(
page.locator(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", { hasText: "Projects" }),
).toBeVisible();
});
});
});

View File

@@ -30,7 +30,7 @@ async function startDM(app: ElementAppPage, page: Page, name: string): Promise<v
await result.first().click();
// send first message to start DM
const locator = page.getByRole("textbox", { name: "Send a message…" });
const locator = page.getByRole("textbox", { name: "Send an unencrypted message…" });
await expect(locator).toBeFocused();
await locator.fill("Hey!");
await locator.press("Enter");
@@ -260,7 +260,7 @@ test.describe("Spotlight", () => {
// Send first message to actually start DM
await expect(roomHeaderName(page)).toHaveText(bot2.credentials.displayName);
const locator = page.getByRole("textbox", { name: "Send a message…" });
const locator = page.getByRole("textbox", { name: "Send an unencrypted message…" });
await locator.fill("Hey!");
await locator.press("Enter");

View File

@@ -908,37 +908,23 @@ test.describe("Timeline", () => {
});
});
test(
"should be able to hide an image",
{ tag: "@screenshot" },
async ({ page, app, homeserver, room, context }) => {
await app.viewRoomById(room.roomId);
const bot = new Bot(page, homeserver, {});
await bot.prepareClient();
await app.client.inviteUser(room.roomId, bot.credentials.userId);
await sendImage(bot, room.roomId, NEW_AVATAR);
await app.timeline.scrollToBottom();
const imgTile = page.locator(".mx_MImageBody").first();
await expect(imgTile).toBeVisible();
await imgTile.hover();
await page.getByRole("button", { name: "Hide" }).click();
// Check that the image is now hidden.
await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
},
);
test("should be able to hide a video", async ({ page, app, homeserver, room, context }) => {
test("should be able to hide an image", { tag: "@screenshot" }, async ({ page, app, room, context }) => {
await app.viewRoomById(room.roomId);
await sendImage(app.client, room.roomId, NEW_AVATAR);
await app.timeline.scrollToBottom();
const imgTile = page.locator(".mx_MImageBody").first();
await expect(imgTile).toBeVisible();
await imgTile.hover();
await page.getByRole("button", { name: "Hide" }).click();
const bot = new Bot(page, homeserver, {});
await bot.prepareClient();
await app.client.inviteUser(room.roomId, bot.credentials.userId);
// Check that the image is now hidden.
await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
});
const upload = await bot.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" });
await bot.sendEvent(room.roomId, null, "m.room.message" as EventType, {
test("should be able to hide a video", async ({ page, app, room, context }) => {
await app.viewRoomById(room.roomId);
const upload = await app.client.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" });
await app.client.sendEvent(room.roomId, null, "m.room.message" as EventType, {
msgtype: "m.video" as MsgType,
body: "bbb.webm",
url: upload.content_uri,

View File

@@ -1,98 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
const DEMO_WIDGET_ID = "demo-widget-id";
const DEMO_WIDGET_NAME = "Demo Widget";
const DEMO_WIDGET_TYPE = "demo";
const ROOM_NAME = "Demo";
const DEMO_WIDGET_HTML = `
<html lang="en">
<head>
<title>Demo Widget</title>
<script>
let sendEventCount = 0
window.onmessage = ev => {
if (ev.data.action === 'capabilities') {
window.parent.postMessage(Object.assign({
response: {
capabilities: [
"org.matrix.msc2762.timeline:*",
"org.matrix.msc2762.receive.state_event:m.room.topic",
"org.matrix.msc2762.send.event:net.widget_echo"
]
},
}, ev.data), '*');
}
};
</script>
</head>
</html>
`;
test.describe("Widger permissions dialog", () => {
test.use({
displayName: "Mike",
});
let demoWidgetUrl: string;
test.beforeEach(async ({ webserver }) => {
demoWidgetUrl = webserver.start(DEMO_WIDGET_HTML);
});
test(
"should be updated if user is re-invited into the room with updated state event",
{ tag: "@screenshot" },
async ({ page, app, user, axe }) => {
const roomId = await app.client.createRoom({
name: ROOM_NAME,
});
// setup widget via state event
await app.client.sendStateEvent(
roomId,
"im.vector.modular.widgets",
{
id: DEMO_WIDGET_ID,
creatorUserId: "somebody",
type: DEMO_WIDGET_TYPE,
name: DEMO_WIDGET_NAME,
url: demoWidgetUrl,
},
DEMO_WIDGET_ID,
);
// set initial layout
await app.client.sendStateEvent(
roomId,
"io.element.widgets.layout",
{
widgets: {
[DEMO_WIDGET_ID]: {
container: "top",
index: 1,
width: 100,
height: 0,
},
},
},
"",
);
// open the room
await app.viewRoomByName(ROOM_NAME);
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
await expect(axe).toHaveNoViolations();
await expect(page.locator(".mx_WidgetCapabilitiesPromptDialog")).toMatchScreenshot(
"widget-capabilites-prompt.png",
);
},
);
});

View File

@@ -51,9 +51,9 @@ export class ElementAppPage {
/**
* Open room creation dialog.
*/
public async openCreateRoomDialog(roomKindname: "New room" | "New video room" = "New room"): Promise<Locator> {
public async openCreateRoomDialog(): Promise<Locator> {
await this.page.getByRole("button", { name: "Add room", exact: true }).click();
await this.page.getByRole("menuitem", { name: roomKindname, exact: true }).click();
await this.page.getByRole("menuitem", { name: "New room", exact: true }).click();
return this.page.locator(".mx_CreateRoomDialog");
}

View File

@@ -43,7 +43,7 @@ export class Settings {
* @param {*} value The new value of the setting, may be null.
* @return {Promise} Resolves when the setting has been changed.
*/
public async setValue(settingName: string, roomId: string | null, level: SettingLevel, value: any): Promise<void> {
public async setValue(settingName: string, roomId: string, level: SettingLevel, value: any): Promise<void> {
return this.page.evaluate<
Promise<void>,
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

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

View File

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

View File

@@ -142,7 +142,6 @@
@import "./views/dialogs/_GenericFeatureFeedbackDialog.pcss";
@import "./views/dialogs/_IncomingSasDialog.pcss";
@import "./views/dialogs/_InviteDialog.pcss";
@import "./views/dialogs/_InviteProgressBody.pcss";
@import "./views/dialogs/_JoinRuleDropdown.pcss";
@import "./views/dialogs/_LeaveSpaceDialog.pcss";
@import "./views/dialogs/_LocationViewDialog.pcss";

View File

@@ -24,12 +24,6 @@ Please see LICENSE files in the repository root for full details.
}
}
.mx_DevTools_toolHeading {
color: var(--cpd-color-text-secondary);
font-weight: var(--cpd-font-weight-semibold);
font-size: var(--cpd-font-size-heading-sm);
}
.mx_DevTools_content {
overflow-y: auto;
}

View File

@@ -63,6 +63,17 @@ Please see LICENSE files in the repository root for full details.
height: 25px;
line-height: $font-25px;
}
.mx_InviteDialog_buttonAndSpinner {
.mx_Spinner {
/* Width and height are required to trick the layout engine. */
width: 20px;
height: 20px;
margin-inline-start: 5px;
display: inline-block;
vertical-align: middle;
}
}
}
.mx_InviteDialog_section {
@@ -207,10 +218,6 @@ Please see LICENSE files in the repository root for full details.
flex-direction: column;
flex-grow: 1;
overflow: hidden;
.mx_InviteProgressBody {
margin-top: var(--cpd-space-12x);
}
}
.mx_InviteDialog_transfer {

View File

@@ -1,16 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
.mx_InviteProgressBody {
text-align: center;
font: var(--cpd-font-body-lg-regular);
h1 {
color: var(--cpd-color-text-primary);
font: var(--cpd-font-heading-sm-semibold);
}
}

View File

@@ -11,7 +11,8 @@ Please see LICENSE files in the repository root for full details.
line-height: $font-17px;
border-radius: $font-16px;
vertical-align: text-top;
display: inline-block;
display: inline-flex;
align-items: center;
box-sizing: border-box;
max-width: 100%;
overflow: hidden;
@@ -56,8 +57,6 @@ Please see LICENSE files in the repository root for full details.
margin-inline-start: -0.3em; /* Otherwise the gap is too large */
margin-inline-end: 0.2em;
min-width: $font-16px; /* ensure the avatar is not compressed */
user-select: text;
vertical-align: -2.5px;
}
.mx_Pill_text {

View File

@@ -13,7 +13,7 @@
padding: var(--cpd-space-1x) var(--cpd-space-3x) var(--cpd-space-1x) var(--cpd-space-1x);
font: var(--cpd-font-body-xs-medium);
background-color: var(--cpd-color-bg-subtle-secondary);
background-color: var(--cpd-color-alpha-gray-200);
color: var(--cpd-color-text-secondary);
border-radius: 99px;

View File

@@ -32,8 +32,4 @@
transform: rotate(180deg);
}
}
.mx_RoomListHeaderView_ReleaseAnnouncementAnchor {
display: inline-flex;
}
}

View File

@@ -15,44 +15,40 @@
* |-------------------------------------------------------|
*/
.mx_RoomListItemView {
/* Remove button default style */
background: unset;
border: none;
padding: 0;
text-align: unset;
all: unset;
cursor: pointer;
height: 48px;
width: 100%;
padding-left: var(--cpd-space-3x);
font: var(--cpd-font-body-md-regular);
.mx_RoomListItemView_content {
.mx_RoomListItemView_container {
padding-left: var(--cpd-space-3x);
font: var(--cpd-font-body-md-regular);
height: 100%;
flex: 1;
/* The border is only under the room name and the future hover menu */
border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary);
box-sizing: border-box;
min-width: 0;
padding-right: var(--cpd-space-5x);
.mx_RoomListItemView_text {
.mx_RoomListItemView_content {
height: 100%;
flex: 1;
/* The border is only under the room name and the future hover menu */
border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary);
box-sizing: border-box;
min-width: 0;
}
padding-right: var(--cpd-space-5x);
.mx_RoomListItemView_roomName {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mx_RoomListItemView_text {
min-width: 0;
}
.mx_RoomListItemView_messagePreview {
font: var(--cpd-font-body-sm-regular);
color: var(--cpd-color-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.mx_RoomListItemView_roomName {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mx_RoomListItemView_messagePreview {
font: var(--cpd-font-body-sm-regular);
color: var(--cpd-color-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
@@ -61,7 +57,7 @@
background-color: var(--cpd-color-bg-action-secondary-hovered);
}
.mx_RoomListItemView_menu_open .mx_RoomListItemView_content {
.mx_RoomListItemView_menu_open .mx_RoomListItemView_container .mx_RoomListItemView_content {
/**
* The figma uses 16px padding (--cpd-space-4x) but due to https://github.com/element-hq/compound-web/issues/331
* the icon size of the menu is 18px instead of 20px with a different internal padding

View File

@@ -208,7 +208,7 @@ Please see LICENSE files in the repository root for full details.
margin-right: 12px;
}
h1 {
h3 {
font-size: inherit;
margin: 0;
}

View File

@@ -71,13 +71,6 @@ type ElectronChannel =
| "serverSupportedVersions";
declare global {
// use `number` as the return type in all cases for globalThis.set{Interval,Timeout},
// so we don't accidentally use the methods on NodeJS.Timeout - they only exist in a subset of environments.
// The overload for clear{Interval,Timeout} is resolved as expected.
// We use `ReturnType<typeof setTimeout>` in the code to be agnostic of if this definition gets loaded.
function setInterval(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
function setTimeout(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
interface Window {
mxSendRageshake: (text: string, withLogs?: boolean) => void;
matrixLogger: typeof logger;

View File

@@ -79,12 +79,3 @@ export function isOnlyCtrlOrCmdKeyEvent(ev: React.KeyboardEvent | KeyboardEvent)
return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
}
}
/**
* Checks if the given keyboard event is a modified key event (i.e., if any modifier keys are active).
* @param ev The keyboard event to check
* @returns True if the event is a modified key event, false otherwise
*/
export function isModifiedKeyEvent(ev: React.KeyboardEvent | KeyboardEvent): boolean {
return ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey;
}

View File

@@ -112,7 +112,6 @@ export enum LegacyCallHandlerEvent {
CallsChanged = "calls_changed",
CallChangeRoom = "call_change_room",
SilencedCallsChanged = "silenced_calls_changed",
ShownSidebarsChanged = "shown_sidebars_changed",
CallState = "call_state",
ProtocolSupport = "protocol_support",
}
@@ -121,7 +120,6 @@ type EventEmitterMap = {
[LegacyCallHandlerEvent.CallsChanged]: (calls: Map<string, MatrixCall>) => void;
[LegacyCallHandlerEvent.CallChangeRoom]: (call: MatrixCall) => void;
[LegacyCallHandlerEvent.SilencedCallsChanged]: (calls: Set<string>) => void;
[LegacyCallHandlerEvent.ShownSidebarsChanged]: (sidebarsShown: Map<string, boolean>) => void;
[LegacyCallHandlerEvent.CallState]: (mappedRoomId: string | null, status: CallState) => void;
[LegacyCallHandlerEvent.ProtocolSupport]: () => void;
};
@@ -146,8 +144,6 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
private silencedCalls = new Set<string>(); // callIds
private shownSidebars = new Map<string, boolean>(); // callId (call) -> sidebar show
private backgroundAudio = new BackgroundAudio();
private playingSources: Record<string, AudioBufferSourceNode> = {}; // Record them for stopping
@@ -244,15 +240,6 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
return false;
}
public setCallSidebarShown(callId: string, sidebarShown: boolean): void {
this.shownSidebars.set(callId, sidebarShown);
this.emit(LegacyCallHandlerEvent.ShownSidebarsChanged, this.shownSidebars);
}
public isCallSidebarShown(callId?: string): boolean {
return !!callId && (this.shownSidebars.get(callId) ?? true);
}
private async checkProtocols(maxTries: number): Promise<void> {
try {
const protocols = await MatrixClientPeg.safeGet().getThirdpartyProtocols();

View File

@@ -486,27 +486,13 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
private performCustomEventHandling(ev: MatrixEvent): void {
const cli = MatrixClientPeg.safeGet();
const room = cli.getRoom(ev.getRoomId());
const type = ev.getType();
const thisUserHasConnectedDevice =
room && MatrixRTCSession.callMembershipsForRoom(room).some((m) => m.sender === cli.getUserId());
if (EventType.GroupCallMemberPrefix === type && thisUserHasConnectedDevice) {
const content = ev.getContent();
if (typeof content.call_id !== "string") {
logger.warn(
"Received malformatted GroupCallMemberPrefix event. Did not contain 'call_id' of type 'string'",
);
return;
}
// One of our devices has joined the call, so dismiss it.
ToastStore.sharedInstance().dismissToast(getIncomingCallToastKey(content.call_id, room.roomId));
}
// Check maximum age (<= 15 seconds) of a call notify event that will trigger a ringing notification
else if (EventType.CallNotify === type && (ev.getAge() ?? 0) < 15000 && !thisUserHasConnectedDevice) {
if (EventType.CallNotify === ev.getType() && (ev.getAge() ?? 0) < 15000 && !thisUserHasConnectedDevice) {
const content = ev.getContent();
const roomId = ev.getRoomId();
if (typeof content.call_id !== "string") {
logger.warn("Received malformatted CallNotify event. Did not contain 'call_id' of type 'string'");
return;

View File

@@ -6,8 +6,6 @@ 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.
*/
type CacheResult = { roomId: string; viaServers: string[] };
/**
* This is meant to be a cache of room alias to room ID so that moving between
* rooms happens smoothly (for example using browser back / forward buttons).
@@ -18,12 +16,12 @@ type CacheResult = { roomId: string; viaServers: string[] };
* A similar thing could also be achieved via `pushState` with a state object,
* but keeping it separate like this seems easier in case we do want to extend.
*/
const cache = new Map<string, CacheResult>();
const aliasToIDMap = new Map<string, string>();
export function storeRoomAliasInCache(alias: string, roomId: string, viaServers: string[]): void {
cache.set(alias, { roomId, viaServers });
export function storeRoomAliasInCache(alias: string, id: string): void {
aliasToIDMap.set(alias, id);
}
export function getCachedRoomIdForAlias(alias: string): CacheResult | undefined {
return cache.get(alias);
export function getCachedRoomIDForAlias(alias: string): string | undefined {
return aliasToIDMap.get(alias);
}

View File

@@ -14,6 +14,7 @@ import { logger as rootLogger } from "matrix-js-sdk/src/logger";
import Modal from "./Modal";
import { MatrixClientPeg } from "./MatrixClientPeg";
import { _t } from "./languageHandler";
import { isSecureBackupRequired } from "./utils/WellKnownUtils";
import AccessSecretStorageDialog, {
type KeyParams,
} from "./components/views/dialogs/security/AccessSecretStorageDialog";
@@ -25,7 +26,7 @@ import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDia
// during the same single operation. Use `accessSecretStorage` below to scope a
// single secret storage operation, as it will clear the cached keys once the
// operation ends.
let secretStorageKeys: Record<string, Uint8Array> = {};
let secretStorageKeys: Record<string, Uint8Array<ArrayBuffer>> = {};
let secretStorageKeyInfo: Record<string, SecretStorage.SecretStorageKeyDescription> = {};
let secretStorageBeingAccessed = false;
@@ -50,8 +51,8 @@ export class AccessCancelledError extends Error {
function makeInputToKey(
keyInfo: SecretStorage.SecretStorageKeyDescription,
): (keyParams: KeyParams) => Promise<Uint8Array> {
return async ({ passphrase, recoveryKey }): Promise<Uint8Array> => {
): (keyParams: KeyParams) => Promise<Uint8Array<ArrayBuffer>> {
return async ({ passphrase, recoveryKey }): Promise<Uint8Array<ArrayBuffer>> => {
if (passphrase) {
return deriveRecoveryKeyFromPassphrase(passphrase, keyInfo.passphrase.salt, keyInfo.passphrase.iterations);
} else if (recoveryKey) {
@@ -68,7 +69,7 @@ async function getSecretStorageKey(
keys: Record<string, SecretStorage.SecretStorageKeyDescription>;
},
secretName: string,
): Promise<[string, Uint8Array]> {
): Promise<[string, Uint8Array<ArrayBuffer>]> {
const cli = MatrixClientPeg.safeGet();
const defaultKeyId = await cli.secretStorage.getDefaultKeyId();
@@ -138,7 +139,7 @@ async function getSecretStorageKey(
function cacheSecretStorageKey(
keyId: string,
keyInfo: SecretStorage.SecretStorageKeyDescription,
key: Uint8Array,
key: Uint8Array<ArrayBuffer>,
): void {
if (secretStorageBeingAccessed) {
logger.debug(`Caching 4S key ${keyId}`);
@@ -231,6 +232,15 @@ async function doAccessSecretStorage(func: () => Promise<void>, opts: AccessSecr
undefined,
/* priority = */ false,
/* static = */ true,
/* options = */ {
onBeforeClose: async (reason): Promise<boolean> => {
// If Secure Backup is required, you cannot leave the modal.
if (reason === "backgroundClick") {
return !isSecureBackupRequired(cli);
}
return true;
},
},
);
const [confirmed] = await finished;
if (!confirmed) {

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