Compare commits
79 Commits
hs/user-pr
...
hs/impleme
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35569f35bb | ||
|
|
2f4d845f20 | ||
|
|
cf7d3a4141 | ||
|
|
1e342c6200 | ||
|
|
8025ab47b2 | ||
|
|
a613180927 | ||
|
|
cd7f1a0638 | ||
|
|
8a1fc65beb | ||
|
|
f7d48bb422 | ||
|
|
c7c0e91fdc | ||
|
|
28ca369a10 | ||
|
|
dad1bd6834 | ||
|
|
dba4ca26e8 | ||
|
|
fc06cf1276 | ||
|
|
a73f4f5803 | ||
|
|
d594ce479c | ||
|
|
733007cb28 | ||
|
|
f57660ac14 | ||
|
|
207173db95 | ||
|
|
d70a3a695e | ||
|
|
7ccb9355de | ||
|
|
6b510a535b | ||
|
|
6d05bfc4c5 | ||
|
|
9e7f583acc | ||
|
|
4f702b70aa | ||
|
|
ef3b9eb9e4 | ||
|
|
1e5e4a04ad | ||
|
|
5534c0dbe9 | ||
|
|
1c30bec083 | ||
|
|
1386bc9f5c | ||
|
|
48c3d91383 | ||
|
|
9aa617df1b | ||
|
|
c17d71a90b | ||
|
|
07c253d11f | ||
|
|
cba341f824 | ||
|
|
09fe9281a5 | ||
|
|
80375db934 | ||
|
|
ea4ccda928 | ||
|
|
69d5acb2f3 | ||
|
|
34c2ccebba | ||
|
|
5deb5097b5 | ||
|
|
eeb14d3b7f | ||
|
|
6e88b46f02 | ||
|
|
a50f51257b | ||
|
|
6f71769466 | ||
|
|
08b9f3685d | ||
|
|
4a184e3346 | ||
|
|
de265e1ef6 | ||
|
|
eb086bd795 | ||
|
|
9f15532d12 | ||
|
|
71cf19f4b2 | ||
|
|
1925132a3c | ||
|
|
8fa3d7e4b7 | ||
|
|
1b4a979b6c | ||
|
|
d287ac07a3 | ||
|
|
8903927e0c | ||
|
|
4d48d1b2f2 | ||
|
|
f75d41054f | ||
|
|
701019052c | ||
|
|
cf692e751b | ||
|
|
1a005ad5d2 | ||
|
|
42f7bc1d0d | ||
|
|
b7f89db43c | ||
|
|
98a04e1812 | ||
|
|
42d726a4ff | ||
|
|
b6f5843028 | ||
|
|
81d054bb99 | ||
|
|
a1f56ebbf2 | ||
|
|
a003ebcb35 | ||
|
|
87b4918d34 | ||
|
|
c6f47cfd8e | ||
|
|
a112dfe1db | ||
|
|
4b4cb896eb | ||
|
|
6a1c0502aa | ||
|
|
ea5e525133 | ||
|
|
14d16364db | ||
|
|
aab1fae299 | ||
|
|
f5d6f8f639 | ||
|
|
cc20136170 |
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## Checklist
|
## 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).
|
- [ ] Tests written for new code (and old code if feasible).
|
||||||
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
|
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
|
||||||
- [ ] Linter and other CI checks pass.
|
- [ ] Linter and other CI checks pass.
|
||||||
|
|||||||
2
.github/workflows/docs.yml
vendored
@@ -88,7 +88,7 @@ jobs:
|
|||||||
run: mdbook build
|
run: mdbook build
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
|
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
|
||||||
with:
|
with:
|
||||||
path: ./book
|
path: ./book
|
||||||
|
|
||||||
|
|||||||
49
CHANGELOG.md
@@ -1,3 +1,52 @@
|
|||||||
|
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)
|
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.
|
This release supports the upcoming v12 ("hydra") Matrix room version and is necessary to view and participate in these rooms.
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
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.
|
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
|
## How to contribute
|
||||||
|
|
||||||
The preferred and easiest way to contribute changes to the project is to fork
|
The preferred and easiest way to contribute changes to the project is to fork
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
|
# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
|
||||||
|
|
||||||
# Builder
|
# Builder
|
||||||
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:9e34ba52e1f3c31ed9bd4d0bcf784f5909db17cda61c220e29c8d7a8ebfb402e AS builder
|
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f7f28d1962d93cc096ea6327378d990284757fec281ce48e42436e7b4b167fa2 AS builder
|
||||||
|
|
||||||
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
|
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
|
||||||
ARG USE_CUSTOM_SDKS=false
|
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
|
RUN cp /src/config.sample.json /src/webapp/config.json
|
||||||
|
|
||||||
# App
|
# App
|
||||||
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:ea6c4b8b568824ea94cd1fabd47e1c4e7c0c04744f344a3793f7e9c8ac3a3636
|
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:0d019e980f83728002de7a6d8819d0d4af7179046d3946b8b37749953fbb28e6
|
||||||
|
|
||||||
# Need root user to install packages & manipulate the usr directory
|
# Need root user to install packages & manipulate the usr directory
|
||||||
USER root
|
USER root
|
||||||
|
|||||||
@@ -14,10 +14,9 @@ entrypoint_log() {
|
|||||||
mkdir -p /tmp/element-web-config
|
mkdir -p /tmp/element-web-config
|
||||||
cp /app/config*.json /tmp/element-web-config/
|
cp /app/config*.json /tmp/element-web-config/
|
||||||
|
|
||||||
# If there are modules to be loaded
|
# If the module directory exists AND the module directory has modules in it
|
||||||
if [ -d "/modules" ]; then
|
if [ -d "/modules" ] && [ "$( ls -A '/modules' )" ]; then
|
||||||
cd /modules
|
cd /modules
|
||||||
|
|
||||||
for MODULE in *
|
for MODULE in *
|
||||||
do
|
do
|
||||||
# If the module has a package.json, use its main field as the entrypoint
|
# If the module has a package.json, use its main field as the entrypoint
|
||||||
|
|||||||
@@ -585,6 +585,8 @@ Currently, the following UI feature flags are supported:
|
|||||||
- `UIFeature.BulkUnverifiedSessionsReminder` - Display popup reminders to verify or remove unverified sessions. Defaults
|
- `UIFeature.BulkUnverifiedSessionsReminder` - Display popup reminders to verify or remove unverified sessions. Defaults
|
||||||
to true.
|
to true.
|
||||||
- `UIFeature.locationSharing` - Whether or not location sharing menus will be shown.
|
- `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
|
## Undocumented / developer options
|
||||||
|
|
||||||
|
|||||||
43
docs/e2ee.md
@@ -38,45 +38,20 @@ When `force_disable` is true:
|
|||||||
Note: If the server is configured to forcibly enable encryption for some or all rooms,
|
Note: If the server is configured to forcibly enable encryption for some or all rooms,
|
||||||
this behaviour will be overridden.
|
this behaviour will be overridden.
|
||||||
|
|
||||||
# Secure backup
|
# Setting up recovery
|
||||||
|
|
||||||
By default, Element strongly encourages (but does not require) users to set up
|
By default, Element strongly encourages (but does not require) users to set up
|
||||||
Secure Backup so that cross-signing identity key and message keys can be
|
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.
|
||||||
recovered in case of a disaster where you lose access to all active devices.
|
|
||||||
|
|
||||||
## Requiring secure backup
|
## Removal of old settings
|
||||||
|
|
||||||
To require Secure Backup to be configured before Element can be used, set the
|
Support for the configuration options `secure_backup_required` and `secure_backup_setup_methods`
|
||||||
following on your homeserver's `/.well-known/matrix/client` config:
|
in the `/.well-known/matrix/client` config has been removed.
|
||||||
|
|
||||||
```json
|
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
|
||||||
"io.element.e2ee": {
|
recovery key, the UI only supports auto-generated keys. Using an existing (custom) passphrase
|
||||||
"secure_backup_required": true
|
still works, but is not exposed in the UI when setting up recovery.
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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
|
# Compatibility
|
||||||
|
|
||||||
|
|||||||
11
knip.ts
@@ -2,7 +2,6 @@ import { KnipConfig } from "knip";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
entry: [
|
entry: [
|
||||||
"src/vector/index.ts",
|
|
||||||
"src/serviceworker/index.ts",
|
"src/serviceworker/index.ts",
|
||||||
"src/workers/*.worker.ts",
|
"src/workers/*.worker.ts",
|
||||||
"src/utils/exportUtils/exportJS.js",
|
"src/utils/exportUtils/exportJS.js",
|
||||||
@@ -12,8 +11,6 @@ export default {
|
|||||||
"res/decoder-ring/**",
|
"res/decoder-ring/**",
|
||||||
"res/jitsi_external_api.min.js",
|
"res/jitsi_external_api.min.js",
|
||||||
"docs/**",
|
"docs/**",
|
||||||
// Used by jest
|
|
||||||
"__mocks__/maplibre-gl.js",
|
|
||||||
],
|
],
|
||||||
project: ["**/*.{js,ts,jsx,tsx}"],
|
project: ["**/*.{js,ts,jsx,tsx}"],
|
||||||
ignore: [
|
ignore: [
|
||||||
@@ -42,10 +39,18 @@ export default {
|
|||||||
"util",
|
"util",
|
||||||
// Embedded into webapp
|
// Embedded into webapp
|
||||||
"@element-hq/element-call-embedded",
|
"@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: [
|
ignoreBinaries: [
|
||||||
// Used in scripts & workflows
|
// Used in scripts & workflows
|
||||||
"jq",
|
"jq",
|
||||||
|
"wait-on",
|
||||||
],
|
],
|
||||||
ignoreExportsUsedInFile: true,
|
ignoreExportsUsedInFile: true,
|
||||||
} satisfies KnipConfig;
|
} satisfies KnipConfig;
|
||||||
|
|||||||
18
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "element-web",
|
"name": "element-web",
|
||||||
"version": "1.11.109",
|
"version": "1.11.111",
|
||||||
"description": "Element: the future of secure communication",
|
"description": "Element: the future of secure communication",
|
||||||
"author": "New Vector Ltd.",
|
"author": "New Vector Ltd.",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -75,8 +75,8 @@
|
|||||||
"resolutions": {
|
"resolutions": {
|
||||||
"**/pretty-format/react-is": "19.1.1",
|
"**/pretty-format/react-is": "19.1.1",
|
||||||
"@playwright/test": "1.54.2",
|
"@playwright/test": "1.54.2",
|
||||||
"@types/react": "19.1.10",
|
"@types/react": "19.1.12",
|
||||||
"@types/react-dom": "19.1.7",
|
"@types/react-dom": "19.1.9",
|
||||||
"oidc-client-ts": "3.3.0",
|
"oidc-client-ts": "3.3.0",
|
||||||
"jwt-decode": "4.0.0",
|
"jwt-decode": "4.0.0",
|
||||||
"caniuse-lite": "1.0.30001724",
|
"caniuse-lite": "1.0.30001724",
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
"opus-recorder": "^8.0.3",
|
"opus-recorder": "^8.0.3",
|
||||||
"pako": "^2.0.3",
|
"pako": "^2.0.3",
|
||||||
"png-chunks-extract": "^1.0.0",
|
"png-chunks-extract": "^1.0.0",
|
||||||
"posthog-js": "1.260.1",
|
"posthog-js": "1.261.0",
|
||||||
"qrcode": "1.5.4",
|
"qrcode": "1.5.4",
|
||||||
"re-resizable": "6.11.2",
|
"re-resizable": "6.11.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
"sanitize-html": "2.17.0",
|
"sanitize-html": "2.17.0",
|
||||||
"tar-js": "^0.3.0",
|
"tar-js": "^0.3.0",
|
||||||
"temporal-polyfill": "^0.3.0",
|
"temporal-polyfill": "^0.3.0",
|
||||||
"ua-parser-js": "^1.0.2",
|
"ua-parser-js": "1.0.40",
|
||||||
"uuid": "^11.0.0",
|
"uuid": "^11.0.0",
|
||||||
"what-input": "^5.2.10"
|
"what-input": "^5.2.10"
|
||||||
},
|
},
|
||||||
@@ -184,7 +184,7 @@
|
|||||||
"@babel/preset-typescript": "^7.12.7",
|
"@babel/preset-typescript": "^7.12.7",
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||||
"@element-hq/element-call-embedded": "0.14.1",
|
"@element-hq/element-call-embedded": "0.15.0",
|
||||||
"@element-hq/element-web-playwright-common": "^1.4.6",
|
"@element-hq/element-web-playwright-common": "^1.4.6",
|
||||||
"@peculiar/webcrypto": "^1.4.3",
|
"@peculiar/webcrypto": "^1.4.3",
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
@@ -203,6 +203,7 @@
|
|||||||
"@testing-library/react": "^16.0.0",
|
"@testing-library/react": "^16.0.0",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/commonmark": "^0.27.4",
|
"@types/commonmark": "^0.27.4",
|
||||||
|
"@types/content-type": "^1.1.9",
|
||||||
"@types/counterpart": "^0.18.1",
|
"@types/counterpart": "^0.18.1",
|
||||||
"@types/css-tree": "^2.3.8",
|
"@types/css-tree": "^2.3.8",
|
||||||
"@types/diff-match-patch": "^1.0.32",
|
"@types/diff-match-patch": "^1.0.32",
|
||||||
@@ -221,11 +222,12 @@
|
|||||||
"@types/node-fetch": "^2.6.2",
|
"@types/node-fetch": "^2.6.2",
|
||||||
"@types/pako": "^2.0.0",
|
"@types/pako": "^2.0.0",
|
||||||
"@types/qrcode": "^1.3.5",
|
"@types/qrcode": "^1.3.5",
|
||||||
"@types/react": "19.1.10",
|
"@types/react": "19.1.12",
|
||||||
"@types/react-beautiful-dnd": "^13.0.0",
|
"@types/react-beautiful-dnd": "^13.0.0",
|
||||||
"@types/react-dom": "19.1.7",
|
"@types/react-dom": "19.1.9",
|
||||||
"@types/react-transition-group": "^4.4.0",
|
"@types/react-transition-group": "^4.4.0",
|
||||||
"@types/sanitize-html": "2.16.0",
|
"@types/sanitize-html": "2.16.0",
|
||||||
|
"@types/sdp-transform": "^2.4.10",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@types/tar-js": "^0.3.5",
|
"@types/tar-js": "^0.3.5",
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
|
|||||||
@@ -11,3 +11,42 @@ index 917a7fc..a2710c6 100644
|
|||||||
didOkOrSubmit: boolean;
|
didOkOrSubmit: boolean;
|
||||||
model: M;
|
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;
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
|||||||
@@ -1,34 +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 { 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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -154,8 +154,8 @@ test.describe("Cryptography", function () {
|
|||||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||||
await startDMWithBob(page, bob);
|
await startDMWithBob(page, bob);
|
||||||
// send first message
|
// send first message
|
||||||
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).fill("Hey!");
|
await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!");
|
||||||
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter");
|
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter");
|
||||||
await checkDMRoom(page);
|
await checkDMRoom(page);
|
||||||
const bobRoomId = await bobJoin(page, bob);
|
const bobRoomId = await bobJoin(page, bob);
|
||||||
// We no longer show the grey badge in the composer, check that it is not there.
|
// We no longer show the grey badge in the composer, check that it is not there.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
createSecondBotDevice,
|
createSecondBotDevice,
|
||||||
createSharedRoomWithUser,
|
createSharedRoomWithUser,
|
||||||
enableKeyBackup,
|
enableKeyBackup,
|
||||||
logIntoElement,
|
logIntoElementAndVerify,
|
||||||
logOutOfElement,
|
logOutOfElement,
|
||||||
verify,
|
verify,
|
||||||
waitForDevices,
|
waitForDevices,
|
||||||
@@ -195,7 +195,7 @@ test.describe("Cryptography", function () {
|
|||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
});
|
});
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await logIntoElement(page, aliceCredentials, securityKey);
|
await logIntoElementAndVerify(page, aliceCredentials, securityKey);
|
||||||
|
|
||||||
/* go back to the test room and find Bob's message again */
|
/* go back to the test room and find Bob's message again */
|
||||||
await app.viewRoomById(testRoomId);
|
await app.viewRoomById(testRoomId);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElement } from "./utils";
|
import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElementAndVerify } from "./utils";
|
||||||
import { type Bot } from "../../pages/bot";
|
import { type Bot } from "../../pages/bot";
|
||||||
|
|
||||||
test.describe("Key storage out of sync toast", () => {
|
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);
|
const res = await createBot(page, homeserver, credentials);
|
||||||
recoveryKey = res.recoveryKey;
|
recoveryKey = res.recoveryKey;
|
||||||
|
|
||||||
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
|
await logIntoElementAndVerify(page, credentials, recoveryKey.encodedPrivateKey);
|
||||||
|
|
||||||
await deleteCachedSecrets(page);
|
await deleteCachedSecrets(page);
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ test.describe("'Turn on key storage' toast", () => {
|
|||||||
const recoveryKey = res.recoveryKey;
|
const recoveryKey = res.recoveryKey;
|
||||||
botClient = res.botClient;
|
botClient = res.botClient;
|
||||||
|
|
||||||
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
|
await logIntoElementAndVerify(page, credentials, recoveryKey.encodedPrivateKey);
|
||||||
|
|
||||||
// We won't be prompted for crypto setup unless we have an e2e room, so make one
|
// 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();
|
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();
|
await toast.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
// Then we see the Encryption settings dialog with an option to turn on key storage
|
// Then we see the Encryption settings dialog with an option to turn on key storage
|
||||||
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
|
await expect(page.getByRole("switch", { name: "Allow key storage" })).toBeVisible();
|
||||||
|
|
||||||
// And when we close that
|
// And when we close that
|
||||||
await page.getByRole("button", { name: "Close dialog" }).click();
|
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();
|
await page.getByRole("button", { name: "Go to Settings" }).click();
|
||||||
|
|
||||||
// Then we see Encryption settings again
|
// Then we see Encryption settings again
|
||||||
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
|
await expect(page.getByRole("switch", { name: "Allow key storage" })).toBeVisible();
|
||||||
|
|
||||||
// And when we close that, see the toast, click Dismiss, and Yes, Dismiss
|
// And when we close that, see the toast, click Dismiss, and Yes, Dismiss
|
||||||
await page.getByRole("button", { name: "Close dialog" }).click();
|
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||||
|
|||||||
@@ -206,32 +206,42 @@ export async function checkDeviceIsConnectedKeyBackup(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Fill in the login form in element with the given creds.
|
* 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, securityKey?: string) {
|
export async function logIntoElement(page: Page, credentials: Credentials) {
|
||||||
await page.goto("/#/login");
|
await page.goto("/#/login");
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "Username" }).fill(credentials.userId);
|
await page.getByRole("textbox", { name: "Username" }).fill(credentials.userId);
|
||||||
await page.getByPlaceholder("Password").fill(credentials.password);
|
await page.getByPlaceholder("Password").fill(credentials.password);
|
||||||
await page.getByRole("button", { name: "Sign in" }).click();
|
await page.getByRole("button", { name: "Sign in" }).click();
|
||||||
|
}
|
||||||
|
|
||||||
// if a securityKey was given, verify the new device
|
/**
|
||||||
if (securityKey !== undefined) {
|
* Fill in the login form in Element with the given creds, and then complete the `CompleteSecurity` step, using the
|
||||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click();
|
* 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);
|
||||||
|
|
||||||
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" });
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click();
|
||||||
// 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
|
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" });
|
||||||
// for a recovery key straight away. We click the button if it's there so this works in both cases.
|
// If the user has set a recovery *passphrase*, they'll be prompted for that first and have to click
|
||||||
if (await useSecurityKey.isVisible()) {
|
// through to enter the recovery key which is what we have here. If they haven't, they'll be prompted
|
||||||
await useSecurityKey.click();
|
// for a recovery key straight away. We click the button if it's there so this works in both cases.
|
||||||
}
|
if (await useSecurityKey.isVisible()) {
|
||||||
// Fill in the recovery key
|
await useSecurityKey.click();
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -300,9 +310,9 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
|
|||||||
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
||||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||||
|
|
||||||
const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
|
const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" });
|
||||||
if (!(await keyStorageToggle.isChecked())) {
|
if (!(await keyStorageToggle.isChecked())) {
|
||||||
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
|
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
await encryptionTab.getByRole("button", { name: "Set up recovery" }).click();
|
await encryptionTab.getByRole("button", { name: "Set up recovery" }).click();
|
||||||
@@ -323,11 +333,11 @@ export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
|||||||
export async function disableKeyBackup(app: ElementAppPage): Promise<void> {
|
export async function disableKeyBackup(app: ElementAppPage): Promise<void> {
|
||||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||||
|
|
||||||
const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
|
const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" });
|
||||||
if (await keyStorageToggle.isChecked()) {
|
if (await keyStorageToggle.isChecked()) {
|
||||||
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
|
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).click();
|
||||||
await encryptionTab.getByRole("button", { name: "Delete key storage" }).click();
|
await encryptionTab.getByRole("button", { name: "Delete key storage" }).click();
|
||||||
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).isVisible();
|
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).isVisible();
|
||||||
|
|
||||||
// Wait for the update to account data to stick
|
// Wait for the update to account data to stick
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
|||||||
33
playwright/e2e/devtools/devtools.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
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;
|
||||||
|
}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
38
playwright/e2e/devtools/upgraderoom.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
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");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
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");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -41,7 +41,7 @@ test.describe("Room list", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
|
||||||
const roomListView = getRoomList(page);
|
const roomListView = getRoomList(page);
|
||||||
await expect(roomListView.getByRole("option", { name: "Open room room29" })).toBeVisible();
|
await expect(roomListView.getByRole("option", { name: "Open room room29" })).toBeVisible();
|
||||||
await expect(roomListView).toMatchScreenshot("room-list.png");
|
await expect(roomListView).toMatchScreenshot("room-list.png");
|
||||||
@@ -54,6 +54,7 @@ test.describe("Room list", () => {
|
|||||||
// scrollListToBottom seems to leave the mouse hovered over the list, move it away.
|
// scrollListToBottom seems to leave the mouse hovered over the list, move it away.
|
||||||
await page.getByRole("button", { name: "User menu" }).hover();
|
await page.getByRole("button", { name: "User menu" }).hover();
|
||||||
|
|
||||||
|
await expect(axe).toHaveNoViolations();
|
||||||
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
|
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -193,6 +194,9 @@ test.describe("Room list", () => {
|
|||||||
|
|
||||||
await roomListView.getByRole("option", { name: "Open room room20" }).click();
|
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 page.keyboard.press("Alt+Shift+ArrowDown");
|
await page.keyboard.press("Alt+Shift+ArrowDown");
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "1 notification", level: 1 })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "1 notification", level: 1 })).toBeVisible();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
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
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -57,4 +57,26 @@ test.describe("Location sharing", { tag: "@no-firefox" }, () => {
|
|||||||
|
|
||||||
await expect(page.locator(".mx_Marker")).toBeVisible();
|
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");
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -100,3 +100,51 @@ 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -30,9 +30,8 @@ export class Helpers {
|
|||||||
/**
|
/**
|
||||||
* Get the release announcement with the given name.
|
* Get the release announcement with the given name.
|
||||||
* @param name
|
* @param name
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
private getReleaseAnnouncement(name: string) {
|
public getReleaseAnnouncement(name: string) {
|
||||||
return this.page.getByRole("dialog", { name });
|
return this.page.getByRole("dialog", { name });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,16 +54,6 @@ export class Helpers {
|
|||||||
assertReleaseAnnouncementIsNotVisible(name: string) {
|
assertReleaseAnnouncementIsNotVisible(name: string) {
|
||||||
return expect(this.getReleaseAnnouncement(name)).not.toBeVisible();
|
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 };
|
export { expect };
|
||||||
|
|||||||
@@ -22,25 +22,25 @@ test.describe("Release announcement", () => {
|
|||||||
await app.viewRoomById(roomId);
|
await app.viewRoomById(roomId);
|
||||||
await use({ roomId });
|
await use({ roomId });
|
||||||
},
|
},
|
||||||
|
labsFlags: ["feature_new_room_list"],
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"should display the pinned messages release announcement",
|
"should display the new room list release announcement",
|
||||||
{ tag: "@screenshot" },
|
{ tag: "@screenshot" },
|
||||||
async ({ page, app, room, util }) => {
|
async ({ page, app, room, util }) => {
|
||||||
await app.toggleRoomInfoPanel();
|
const name = "Chats has a new look!";
|
||||||
|
|
||||||
const name = "All new pinned messages";
|
|
||||||
|
|
||||||
// The release announcement should be displayed
|
// The release announcement should be displayed
|
||||||
await util.assertReleaseAnnouncementIsVisible(name);
|
await util.assertReleaseAnnouncementIsVisible(name);
|
||||||
// Hide the release announcement
|
// Hide the release announcement
|
||||||
await util.markReleaseAnnouncementAsRead(name);
|
const dialog = util.getReleaseAnnouncement(name);
|
||||||
|
await dialog.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
await util.assertReleaseAnnouncementIsNotVisible(name);
|
await util.assertReleaseAnnouncementIsNotVisible(name);
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await app.toggleRoomInfoPanel();
|
await expect(page.getByRole("button", { name: "Room options" })).toBeVisible();
|
||||||
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
|
// Check that once the release announcement has been marked as viewed, it does not appear again
|
||||||
await util.assertReleaseAnnouncementIsNotVisible(name);
|
await util.assertReleaseAnnouncementIsNotVisible(name);
|
||||||
},
|
},
|
||||||
|
|||||||
113
playwright/e2e/room/create-room.spec.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -85,7 +85,7 @@ class Helpers {
|
|||||||
* Return the system theme toggle
|
* Return the system theme toggle
|
||||||
*/
|
*/
|
||||||
getMatchSystemThemeCheckbox() {
|
getMatchSystemThemeCheckbox() {
|
||||||
return this.getThemePanel().getByRole("checkbox", { name: "Match system theme" });
|
return this.getThemePanel().getByRole("switch", { name: "Match system theme" });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -219,7 +219,7 @@ class Helpers {
|
|||||||
* Return the compact layout checkbox
|
* Return the compact layout checkbox
|
||||||
*/
|
*/
|
||||||
getCompactLayoutCheckbox() {
|
getCompactLayoutCheckbox() {
|
||||||
return this.getMessageLayoutPanel().getByRole("checkbox", { name: "Show compact text and messages" });
|
return this.getMessageLayoutPanel().getByRole("switch", { name: "Show compact text and messages" });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ test.describe("Encryption tab", () => {
|
|||||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||||
await util.openEncryptionTab();
|
await util.openEncryptionTab();
|
||||||
|
|
||||||
await page.getByRole("checkbox", { name: "Allow key storage" }).click();
|
await page.getByRole("switch", { name: "Allow key storage" }).click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }),
|
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 page.getByRole("button", { name: "Delete key storage" }).click();
|
||||||
|
|
||||||
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked();
|
await expect(page.getByRole("switch", { name: "Allow key storage" })).not.toBeChecked();
|
||||||
|
|
||||||
for (const prom of deleteRequestPromises) {
|
for (const prom of deleteRequestPromises) {
|
||||||
const request = await prom;
|
const request = await prom;
|
||||||
|
|||||||
@@ -104,7 +104,10 @@ class Helpers {
|
|||||||
|
|
||||||
const clipboardContent = await this.app.getClipboard();
|
const clipboardContent = await this.app.getClipboard();
|
||||||
await dialog.getByRole("textbox").fill(clipboardContent);
|
await dialog.getByRole("textbox").fill(clipboardContent);
|
||||||
await dialog.getByRole("button", { name: confirmButtonLabel }).click();
|
const button = dialog.getByRole("button", { name: confirmButtonLabel });
|
||||||
|
await button.click();
|
||||||
|
// Button should disable immediately after clicking.
|
||||||
|
await expect(button).toBeDisabled();
|
||||||
await expect(this.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
|
await expect(this.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
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")],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import { type Locator } from "@playwright/test";
|
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", () => {
|
test.describe("Roles & Permissions room settings tab", () => {
|
||||||
const roomName = "Test room";
|
const roomName = "Test room";
|
||||||
121
playwright/e2e/settings/room-settings/room-security-tab.spec.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* 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();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
42
playwright/e2e/settings/room-settings/room-video-tab.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* 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");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -41,6 +41,18 @@ 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 }) => {
|
test("should be able to set an ID server", async ({ app, context, user, page }) => {
|
||||||
const tab = await app.settings.openUserSettings("Security");
|
const tab = await app.settings.openUserSettings("Security");
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { test, expect } from "../../element-web-test";
|
|||||||
import type { Preset, ICreateRoomOpts } from "matrix-js-sdk/src/matrix";
|
import type { Preset, ICreateRoomOpts } from "matrix-js-sdk/src/matrix";
|
||||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
import { UIFeature } from "../../../src/settings/UIFeature";
|
||||||
|
|
||||||
async function openSpaceCreateMenu(page: Page): Promise<Locator> {
|
async function openSpaceCreateMenu(page: Page): Promise<Locator> {
|
||||||
await page.getByRole("button", { name: "Create a space" }).click();
|
await page.getByRole("button", { name: "Create a space" }).click();
|
||||||
@@ -376,4 +377,68 @@ test.describe("Spaces", () => {
|
|||||||
await app.viewSpaceByName("Root Space");
|
await app.viewSpaceByName("Root Space");
|
||||||
await expect(page.locator(".mx_SpaceRoomView")).toMatchScreenshot("space-room-view.png");
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ async function startDM(app: ElementAppPage, page: Page, name: string): Promise<v
|
|||||||
await result.first().click();
|
await result.first().click();
|
||||||
|
|
||||||
// send first message to start DM
|
// send first message to start DM
|
||||||
const locator = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
const locator = page.getByRole("textbox", { name: "Send a message…" });
|
||||||
await expect(locator).toBeFocused();
|
await expect(locator).toBeFocused();
|
||||||
await locator.fill("Hey!");
|
await locator.fill("Hey!");
|
||||||
await locator.press("Enter");
|
await locator.press("Enter");
|
||||||
@@ -260,7 +260,7 @@ test.describe("Spotlight", () => {
|
|||||||
|
|
||||||
// Send first message to actually start DM
|
// Send first message to actually start DM
|
||||||
await expect(roomHeaderName(page)).toHaveText(bot2.credentials.displayName);
|
await expect(roomHeaderName(page)).toHaveText(bot2.credentials.displayName);
|
||||||
const locator = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
const locator = page.getByRole("textbox", { name: "Send a message…" });
|
||||||
await locator.fill("Hey!");
|
await locator.fill("Hey!");
|
||||||
await locator.press("Enter");
|
await locator.press("Enter");
|
||||||
|
|
||||||
|
|||||||
@@ -908,23 +908,37 @@ test.describe("Timeline", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be able to hide an image", { tag: "@screenshot" }, async ({ page, app, room, context }) => {
|
test(
|
||||||
await app.viewRoomById(room.roomId);
|
"should be able to hide an image",
|
||||||
await sendImage(app.client, room.roomId, NEW_AVATAR);
|
{ tag: "@screenshot" },
|
||||||
await app.timeline.scrollToBottom();
|
async ({ page, app, homeserver, room, context }) => {
|
||||||
const imgTile = page.locator(".mx_MImageBody").first();
|
await app.viewRoomById(room.roomId);
|
||||||
await expect(imgTile).toBeVisible();
|
|
||||||
await imgTile.hover();
|
|
||||||
await page.getByRole("button", { name: "Hide" }).click();
|
|
||||||
|
|
||||||
// Check that the image is now hidden.
|
const bot = new Bot(page, homeserver, {});
|
||||||
await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
|
await bot.prepareClient();
|
||||||
});
|
await app.client.inviteUser(room.roomId, bot.credentials.userId);
|
||||||
|
|
||||||
test("should be able to hide a video", async ({ page, app, room, context }) => {
|
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 }) => {
|
||||||
await app.viewRoomById(room.roomId);
|
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, {
|
const bot = new Bot(page, homeserver, {});
|
||||||
|
await bot.prepareClient();
|
||||||
|
await app.client.inviteUser(room.roomId, bot.credentials.userId);
|
||||||
|
|
||||||
|
const upload = await bot.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" });
|
||||||
|
await bot.sendEvent(room.roomId, null, "m.room.message" as EventType, {
|
||||||
msgtype: "m.video" as MsgType,
|
msgtype: "m.video" as MsgType,
|
||||||
body: "bbb.webm",
|
body: "bbb.webm",
|
||||||
url: upload.content_uri,
|
url: upload.content_uri,
|
||||||
|
|||||||
98
playwright/e2e/widgets/permissions-dialog.spec.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
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",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -51,9 +51,9 @@ export class ElementAppPage {
|
|||||||
/**
|
/**
|
||||||
* Open room creation dialog.
|
* Open room creation dialog.
|
||||||
*/
|
*/
|
||||||
public async openCreateRoomDialog(): Promise<Locator> {
|
public async openCreateRoomDialog(roomKindname: "New room" | "New video room" = "New room"): Promise<Locator> {
|
||||||
await this.page.getByRole("button", { name: "Add room", exact: true }).click();
|
await this.page.getByRole("button", { name: "Add room", exact: true }).click();
|
||||||
await this.page.getByRole("menuitem", { name: "New room", exact: true }).click();
|
await this.page.getByRole("menuitem", { name: roomKindname, exact: true }).click();
|
||||||
return this.page.locator(".mx_CreateRoomDialog");
|
return this.page.locator(".mx_CreateRoomDialog");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class Settings {
|
|||||||
* @param {*} value The new value of the setting, may be null.
|
* @param {*} value The new value of the setting, may be null.
|
||||||
* @return {Promise} Resolves when the setting has been changed.
|
* @return {Promise} Resolves when the setting has been changed.
|
||||||
*/
|
*/
|
||||||
public async setValue(settingName: string, roomId: string, level: SettingLevel, value: any): Promise<void> {
|
public async setValue(settingName: string, roomId: string | null, level: SettingLevel, value: any): Promise<void> {
|
||||||
return this.page.evaluate<
|
return this.page.evaluate<
|
||||||
Promise<void>,
|
Promise<void>,
|
||||||
{
|
{
|
||||||
|
|||||||
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 31 KiB |
@@ -10,7 +10,7 @@ import {
|
|||||||
type StartedPostgreSqlContainer,
|
type StartedPostgreSqlContainer,
|
||||||
} from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
} from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||||
|
|
||||||
const TAG = "main@sha256:430b1f00e74c3f89f078670f676b4333f6bbe5a339962344b3ae84e99e9bcd7f";
|
const TAG = "main@sha256:64b638f2c0ddd7aa0ddcbc39d21cdf3cedab91508b5d7953e2e85c9901ac5b26";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MatrixAuthenticationServiceContainer which freezes the docker digest to
|
* MatrixAuthenticationServiceContainer which freezes the docker digest to
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||||
|
|
||||||
const TAG = "develop@sha256:18e9e77eac01709e9ab4d26cf20c36bf5a1567756bb5a78c00cabf366d65a950";
|
const TAG = "develop@sha256:1784031f7b07de2abffe5b823b59be87a1fb1329a18ae3fc87e66f00f8b79fab";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ 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 {
|
.mx_DevTools_content {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
line-height: $font-17px;
|
line-height: $font-17px;
|
||||||
border-radius: $font-16px;
|
border-radius: $font-16px;
|
||||||
vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
display: inline-flex;
|
display: inline-block;
|
||||||
align-items: center;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -57,6 +56,8 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
margin-inline-start: -0.3em; /* Otherwise the gap is too large */
|
margin-inline-start: -0.3em; /* Otherwise the gap is too large */
|
||||||
margin-inline-end: 0.2em;
|
margin-inline-end: 0.2em;
|
||||||
min-width: $font-16px; /* ensure the avatar is not compressed */
|
min-width: $font-16px; /* ensure the avatar is not compressed */
|
||||||
|
user-select: text;
|
||||||
|
vertical-align: -2.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Pill_text {
|
.mx_Pill_text {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
padding: var(--cpd-space-1x) var(--cpd-space-3x) var(--cpd-space-1x) var(--cpd-space-1x);
|
padding: var(--cpd-space-1x) var(--cpd-space-3x) var(--cpd-space-1x) var(--cpd-space-1x);
|
||||||
font: var(--cpd-font-body-xs-medium);
|
font: var(--cpd-font-body-xs-medium);
|
||||||
background-color: var(--cpd-color-alpha-gray-200);
|
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||||
color: var(--cpd-color-text-secondary);
|
color: var(--cpd-color-text-secondary);
|
||||||
|
|
||||||
border-radius: 99px;
|
border-radius: 99px;
|
||||||
|
|||||||
@@ -32,4 +32,8 @@
|
|||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomListHeaderView_ReleaseAnnouncementAnchor {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h1 {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/@types/global.d.ts
vendored
@@ -71,6 +71,13 @@ type ElectronChannel =
|
|||||||
| "serverSupportedVersions";
|
| "serverSupportedVersions";
|
||||||
|
|
||||||
declare global {
|
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 {
|
interface Window {
|
||||||
mxSendRageshake: (text: string, withLogs?: boolean) => void;
|
mxSendRageshake: (text: string, withLogs?: boolean) => void;
|
||||||
matrixLogger: typeof logger;
|
matrixLogger: typeof logger;
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export enum LegacyCallHandlerEvent {
|
|||||||
CallsChanged = "calls_changed",
|
CallsChanged = "calls_changed",
|
||||||
CallChangeRoom = "call_change_room",
|
CallChangeRoom = "call_change_room",
|
||||||
SilencedCallsChanged = "silenced_calls_changed",
|
SilencedCallsChanged = "silenced_calls_changed",
|
||||||
|
ShownSidebarsChanged = "shown_sidebars_changed",
|
||||||
CallState = "call_state",
|
CallState = "call_state",
|
||||||
ProtocolSupport = "protocol_support",
|
ProtocolSupport = "protocol_support",
|
||||||
}
|
}
|
||||||
@@ -120,6 +121,7 @@ type EventEmitterMap = {
|
|||||||
[LegacyCallHandlerEvent.CallsChanged]: (calls: Map<string, MatrixCall>) => void;
|
[LegacyCallHandlerEvent.CallsChanged]: (calls: Map<string, MatrixCall>) => void;
|
||||||
[LegacyCallHandlerEvent.CallChangeRoom]: (call: MatrixCall) => void;
|
[LegacyCallHandlerEvent.CallChangeRoom]: (call: MatrixCall) => void;
|
||||||
[LegacyCallHandlerEvent.SilencedCallsChanged]: (calls: Set<string>) => void;
|
[LegacyCallHandlerEvent.SilencedCallsChanged]: (calls: Set<string>) => void;
|
||||||
|
[LegacyCallHandlerEvent.ShownSidebarsChanged]: (sidebarsShown: Map<string, boolean>) => void;
|
||||||
[LegacyCallHandlerEvent.CallState]: (mappedRoomId: string | null, status: CallState) => void;
|
[LegacyCallHandlerEvent.CallState]: (mappedRoomId: string | null, status: CallState) => void;
|
||||||
[LegacyCallHandlerEvent.ProtocolSupport]: () => void;
|
[LegacyCallHandlerEvent.ProtocolSupport]: () => void;
|
||||||
};
|
};
|
||||||
@@ -144,6 +146,8 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
|
|||||||
|
|
||||||
private silencedCalls = new Set<string>(); // callIds
|
private silencedCalls = new Set<string>(); // callIds
|
||||||
|
|
||||||
|
private shownSidebars = new Map<string, boolean>(); // callId (call) -> sidebar show
|
||||||
|
|
||||||
private backgroundAudio = new BackgroundAudio();
|
private backgroundAudio = new BackgroundAudio();
|
||||||
private playingSources: Record<string, AudioBufferSourceNode> = {}; // Record them for stopping
|
private playingSources: Record<string, AudioBufferSourceNode> = {}; // Record them for stopping
|
||||||
|
|
||||||
@@ -240,6 +244,15 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
|
|||||||
return false;
|
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> {
|
private async checkProtocols(maxTries: number): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const protocols = await MatrixClientPeg.safeGet().getThirdpartyProtocols();
|
const protocols = await MatrixClientPeg.safeGet().getThirdpartyProtocols();
|
||||||
|
|||||||
@@ -486,13 +486,27 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
|
|||||||
private performCustomEventHandling(ev: MatrixEvent): void {
|
private performCustomEventHandling(ev: MatrixEvent): void {
|
||||||
const cli = MatrixClientPeg.safeGet();
|
const cli = MatrixClientPeg.safeGet();
|
||||||
const room = cli.getRoom(ev.getRoomId());
|
const room = cli.getRoom(ev.getRoomId());
|
||||||
|
const type = ev.getType();
|
||||||
const thisUserHasConnectedDevice =
|
const thisUserHasConnectedDevice =
|
||||||
room && MatrixRTCSession.callMembershipsForRoom(room).some((m) => m.sender === cli.getUserId());
|
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
|
// Check maximum age (<= 15 seconds) of a call notify event that will trigger a ringing notification
|
||||||
if (EventType.CallNotify === ev.getType() && (ev.getAge() ?? 0) < 15000 && !thisUserHasConnectedDevice) {
|
else if (EventType.CallNotify === type && (ev.getAge() ?? 0) < 15000 && !thisUserHasConnectedDevice) {
|
||||||
const content = ev.getContent();
|
const content = ev.getContent();
|
||||||
const roomId = ev.getRoomId();
|
const roomId = ev.getRoomId();
|
||||||
|
|
||||||
if (typeof content.call_id !== "string") {
|
if (typeof content.call_id !== "string") {
|
||||||
logger.warn("Received malformatted CallNotify event. Did not contain 'call_id' of type 'string'");
|
logger.warn("Received malformatted CallNotify event. Did not contain 'call_id' of type 'string'");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
type CacheResult = { roomId: string; viaServers: string[] };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is meant to be a cache of room alias to room ID so that moving between
|
* 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).
|
* rooms happens smoothly (for example using browser back / forward buttons).
|
||||||
@@ -16,12 +18,12 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
* A similar thing could also be achieved via `pushState` with a state object,
|
* 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.
|
* but keeping it separate like this seems easier in case we do want to extend.
|
||||||
*/
|
*/
|
||||||
const aliasToIDMap = new Map<string, string>();
|
const cache = new Map<string, CacheResult>();
|
||||||
|
|
||||||
export function storeRoomAliasInCache(alias: string, id: string): void {
|
export function storeRoomAliasInCache(alias: string, roomId: string, viaServers: string[]): void {
|
||||||
aliasToIDMap.set(alias, id);
|
cache.set(alias, { roomId, viaServers });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCachedRoomIDForAlias(alias: string): string | undefined {
|
export function getCachedRoomIdForAlias(alias: string): CacheResult | undefined {
|
||||||
return aliasToIDMap.get(alias);
|
return cache.get(alias);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { logger as rootLogger } from "matrix-js-sdk/src/logger";
|
|||||||
import Modal from "./Modal";
|
import Modal from "./Modal";
|
||||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
import { _t } from "./languageHandler";
|
import { _t } from "./languageHandler";
|
||||||
import { isSecureBackupRequired } from "./utils/WellKnownUtils";
|
|
||||||
import AccessSecretStorageDialog, {
|
import AccessSecretStorageDialog, {
|
||||||
type KeyParams,
|
type KeyParams,
|
||||||
} from "./components/views/dialogs/security/AccessSecretStorageDialog";
|
} from "./components/views/dialogs/security/AccessSecretStorageDialog";
|
||||||
@@ -232,15 +231,6 @@ async function doAccessSecretStorage(func: () => Promise<void>, opts: AccessSecr
|
|||||||
undefined,
|
undefined,
|
||||||
/* priority = */ false,
|
/* priority = */ false,
|
||||||
/* static = */ true,
|
/* 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;
|
const [confirmed] = await finished;
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
|
|||||||
@@ -25,11 +25,6 @@ import StyledRadioButton from "../../../../components/views/elements/StyledRadio
|
|||||||
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
|
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
|
||||||
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||||
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
|
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
|
||||||
import {
|
|
||||||
getSecureBackupSetupMethods,
|
|
||||||
isSecureBackupRequired,
|
|
||||||
SecureBackupSetupMethod,
|
|
||||||
} from "../../../../utils/WellKnownUtils";
|
|
||||||
import { ModuleRunner } from "../../../../modules/ModuleRunner";
|
import { ModuleRunner } from "../../../../modules/ModuleRunner";
|
||||||
import type Field from "../../../../components/views/elements/Field";
|
import type Field from "../../../../components/views/elements/Field";
|
||||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||||
@@ -39,6 +34,11 @@ import { type IValidationResult } from "../../../../components/views/elements/Va
|
|||||||
import PassphraseConfirmField from "../../../../components/views/auth/PassphraseConfirmField";
|
import PassphraseConfirmField from "../../../../components/views/auth/PassphraseConfirmField";
|
||||||
import { initialiseDehydrationIfEnabled } from "../../../../utils/device/dehydration";
|
import { initialiseDehydrationIfEnabled } from "../../../../utils/device/dehydration";
|
||||||
|
|
||||||
|
enum SecureBackupSetupMethod {
|
||||||
|
Key = "key",
|
||||||
|
Passphrase = "passphrase",
|
||||||
|
}
|
||||||
|
|
||||||
// I made a mistake while converting this and it has to be fixed!
|
// I made a mistake while converting this and it has to be fixed!
|
||||||
enum Phase {
|
enum Phase {
|
||||||
Loading = "loading",
|
Loading = "loading",
|
||||||
@@ -68,7 +68,6 @@ interface IState {
|
|||||||
downloaded: boolean;
|
downloaded: boolean;
|
||||||
setPassphrase: boolean;
|
setPassphrase: boolean;
|
||||||
|
|
||||||
canSkip: boolean;
|
|
||||||
passPhraseKeySelected: string;
|
passPhraseKeySelected: string;
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
}
|
}
|
||||||
@@ -93,16 +92,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
public constructor(props: IProps) {
|
public constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
|
||||||
|
|
||||||
let passPhraseKeySelected: SecureBackupSetupMethod;
|
|
||||||
const setupMethods = getSecureBackupSetupMethods(cli);
|
|
||||||
if (setupMethods.includes(SecureBackupSetupMethod.Key)) {
|
|
||||||
passPhraseKeySelected = SecureBackupSetupMethod.Key;
|
|
||||||
} else {
|
|
||||||
passPhraseKeySelected = SecureBackupSetupMethod.Passphrase;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyFromCustomisations = ModuleRunner.instance.extensions.cryptoSetup.createSecretStorageKey();
|
const keyFromCustomisations = ModuleRunner.instance.extensions.cryptoSetup.createSecretStorageKey();
|
||||||
const phase = keyFromCustomisations ? Phase.Loading : Phase.ChooseKeyPassphrase;
|
const phase = keyFromCustomisations ? Phase.Loading : Phase.ChooseKeyPassphrase;
|
||||||
|
|
||||||
@@ -114,8 +103,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
copied: false,
|
copied: false,
|
||||||
downloaded: false,
|
downloaded: false,
|
||||||
setPassphrase: false,
|
setPassphrase: false,
|
||||||
canSkip: !isSecureBackupRequired(cli),
|
passPhraseKeySelected: SecureBackupSetupMethod.Key,
|
||||||
passPhraseKeySelected,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,11 +379,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderPhaseChooseKeyPassphrase(): JSX.Element {
|
private renderPhaseChooseKeyPassphrase(): JSX.Element {
|
||||||
const setupMethods = getSecureBackupSetupMethods(MatrixClientPeg.safeGet());
|
const optionKey = this.renderOptionKey();
|
||||||
const optionKey = setupMethods.includes(SecureBackupSetupMethod.Key) ? this.renderOptionKey() : null;
|
const optionPassphrase = this.renderOptionPassphrase();
|
||||||
const optionPassphrase = setupMethods.includes(SecureBackupSetupMethod.Passphrase)
|
|
||||||
? this.renderOptionPassphrase()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={this.onChooseKeyPassphraseFormSubmit}>
|
<form onSubmit={this.onChooseKeyPassphraseFormSubmit}>
|
||||||
@@ -410,7 +395,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
primaryButton={_t("action|continue")}
|
primaryButton={_t("action|continue")}
|
||||||
onPrimaryButtonClick={this.onChooseKeyPassphraseFormSubmit}
|
onPrimaryButtonClick={this.onChooseKeyPassphraseFormSubmit}
|
||||||
onCancel={this.onCancelClick}
|
onCancel={this.onCancelClick}
|
||||||
hasCancel={this.state.canSkip}
|
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
@@ -601,7 +585,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t("action|retry")}
|
primaryButton={_t("action|retry")}
|
||||||
onPrimaryButtonClick={this.onLoadRetryClick}
|
onPrimaryButtonClick={this.onLoadRetryClick}
|
||||||
hasCancel={this.state.canSkip}
|
|
||||||
onCancel={this.onCancel}
|
onCancel={this.onCancel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -672,7 +655,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t("action|retry")}
|
primaryButton={_t("action|retry")}
|
||||||
onPrimaryButtonClick={this.bootstrapSecretStorage}
|
onPrimaryButtonClick={this.bootstrapSecretStorage}
|
||||||
hasCancel={this.state.canSkip}
|
|
||||||
onCancel={this.onCancel}
|
onCancel={this.onCancel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ import ThemeController from "../../settings/controllers/ThemeController";
|
|||||||
import { startAnyRegistrationFlow } from "../../Registration";
|
import { startAnyRegistrationFlow } from "../../Registration";
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
|
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
|
||||||
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
|
import { calculateRoomVia, makeRoomPermalink } from "../../utils/permalinks/Permalinks";
|
||||||
import ThemeWatcher, { ThemeWatcherEvent } from "../../settings/watchers/ThemeWatcher";
|
import ThemeWatcher, { ThemeWatcherEvent } from "../../settings/watchers/ThemeWatcher";
|
||||||
import { FontWatcher } from "../../settings/watchers/FontWatcher";
|
import { FontWatcher } from "../../settings/watchers/FontWatcher";
|
||||||
import { storeRoomAliasInCache } from "../../RoomAliasCache";
|
import { storeRoomAliasInCache } from "../../RoomAliasCache";
|
||||||
@@ -238,6 +238,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||||||
private readonly stores: SdkContextClass;
|
private readonly stores: SdkContextClass;
|
||||||
private loadSessionAbortController = new AbortController();
|
private loadSessionAbortController = new AbortController();
|
||||||
|
|
||||||
|
private sessionLoadStarted = false;
|
||||||
|
|
||||||
public constructor(props: IProps) {
|
public constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.stores = SdkContextClass.instance;
|
this.stores = SdkContextClass.instance;
|
||||||
@@ -470,15 +472,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||||||
this.fontWatcher.start();
|
this.fontWatcher.start();
|
||||||
|
|
||||||
initSentry(SdkConfig.get("sentry"));
|
initSentry(SdkConfig.get("sentry"));
|
||||||
|
|
||||||
if (!checkSessionLockFree()) {
|
|
||||||
// another instance holds the lock; confirm its theft before proceeding
|
|
||||||
setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0);
|
|
||||||
} else {
|
|
||||||
this.startInitSession();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("resize", this.onWindowResized);
|
window.addEventListener("resize", this.onWindowResized);
|
||||||
|
|
||||||
|
// Once we start loading the MatrixClient, we can't stop, even if MatrixChat gets unmounted (as it does
|
||||||
|
// in React's Strict Mode). So, start loading the session now, but only if this MatrixChat was not previously
|
||||||
|
// mounted.
|
||||||
|
if (!this.sessionLoadStarted) {
|
||||||
|
this.sessionLoadStarted = true;
|
||||||
|
if (!checkSessionLockFree()) {
|
||||||
|
// another instance holds the lock; confirm its theft before proceeding
|
||||||
|
setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0);
|
||||||
|
} else {
|
||||||
|
this.startInitSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||||
@@ -1019,7 +1026,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||||||
presentedId = theAlias;
|
presentedId = theAlias;
|
||||||
// Store display alias of the presented room in cache to speed future
|
// Store display alias of the presented room in cache to speed future
|
||||||
// navigation.
|
// navigation.
|
||||||
storeRoomAliasInCache(theAlias, room.roomId);
|
storeRoomAliasInCache(theAlias, room.roomId, calculateRoomVia(room));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store this as the ID of the last room accessed. This is so that we can
|
// Store this as the ID of the last room accessed. This is so that we can
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
|
|||||||
secondaryCall={this.state.secondaryCall}
|
secondaryCall={this.state.secondaryCall}
|
||||||
pipMode={pipMode}
|
pipMode={pipMode}
|
||||||
onResize={onResize}
|
onResize={onResize}
|
||||||
|
sidebarShown={false}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner";
|
|||||||
import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext";
|
import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext";
|
||||||
import { DeclineAndBlockInviteDialog } from "../views/dialogs/DeclineAndBlockInviteDialog";
|
import { DeclineAndBlockInviteDialog } from "../views/dialogs/DeclineAndBlockInviteDialog";
|
||||||
import { type FocusMessageSearchPayload } from "../../dispatcher/payloads/FocusMessageSearchPayload.ts";
|
import { type FocusMessageSearchPayload } from "../../dispatcher/payloads/FocusMessageSearchPayload.ts";
|
||||||
|
import { isRoomEncrypted } from "../../hooks/useIsEncrypted";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
||||||
@@ -257,6 +258,7 @@ interface LocalRoomViewProps {
|
|||||||
roomView: RefObject<HTMLElement | null>;
|
roomView: RefObject<HTMLElement | null>;
|
||||||
onFileDrop: (dataTransfer: DataTransfer) => Promise<void>;
|
onFileDrop: (dataTransfer: DataTransfer) => Promise<void>;
|
||||||
mainSplitContentType: MainSplitContentType;
|
mainSplitContentType: MainSplitContentType;
|
||||||
|
e2eStatus?: E2EStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -304,6 +306,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
|
|||||||
} else {
|
} else {
|
||||||
composer = (
|
composer = (
|
||||||
<MessageComposer
|
<MessageComposer
|
||||||
|
e2eStatus={props.e2eStatus}
|
||||||
room={props.localRoom}
|
room={props.localRoom}
|
||||||
resizeNotifier={props.resizeNotifier}
|
resizeNotifier={props.resizeNotifier}
|
||||||
permalinkCreator={props.permalinkCreator}
|
permalinkCreator={props.permalinkCreator}
|
||||||
@@ -1397,10 +1400,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getIsRoomEncrypted(roomId = this.state.roomId): Promise<boolean> {
|
private async getIsRoomEncrypted(roomId = this.state.roomId): Promise<boolean> {
|
||||||
const crypto = this.context.client?.getCrypto();
|
if (!roomId) return false;
|
||||||
if (!crypto || !roomId) return false;
|
|
||||||
|
|
||||||
return await crypto.isEncryptionEnabledInRoom(roomId);
|
const room = this.context.client?.getRoom(roomId);
|
||||||
|
const crypto = this.context.client?.getCrypto();
|
||||||
|
if (!room || !crypto) return false;
|
||||||
|
|
||||||
|
return isRoomEncrypted(room, crypto);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async calculateRecommendedVersion(room: Room): Promise<void> {
|
private async calculateRecommendedVersion(room: Room): Promise<void> {
|
||||||
@@ -2061,6 +2067,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
return (
|
return (
|
||||||
<ScopedRoomContextProvider {...this.state}>
|
<ScopedRoomContextProvider {...this.state}>
|
||||||
<LocalRoomView
|
<LocalRoomView
|
||||||
|
e2eStatus={this.state.e2eStatus}
|
||||||
localRoom={localRoom}
|
localRoom={localRoom}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
permalinkCreator={this.permalinkCreator}
|
permalinkCreator={this.permalinkCreator}
|
||||||
|
|||||||
@@ -184,28 +184,32 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
|
|||||||
|
|
||||||
// Guard against null/undefined events and modified keys which we don't want to handle here but do
|
// Guard against null/undefined events and modified keys which we don't want to handle here but do
|
||||||
// at the settings level shortcuts(E.g. Select next room, etc )
|
// at the settings level shortcuts(E.g. Select next room, etc )
|
||||||
if (e || !isModifiedKeyEvent(e)) {
|
// Guard against null/undefined events and modified keys
|
||||||
if (e.code === Key.ARROW_UP && currentIndex !== undefined) {
|
if (!e || isModifiedKeyEvent(e)) {
|
||||||
scrollToItem(currentIndex - 1, false);
|
onKeyDown?.(e);
|
||||||
handled = true;
|
return;
|
||||||
} else if (e.code === Key.ARROW_DOWN && currentIndex !== undefined) {
|
}
|
||||||
scrollToItem(currentIndex + 1, true);
|
|
||||||
handled = true;
|
if (e.code === Key.ARROW_UP && currentIndex !== undefined) {
|
||||||
} else if (e.code === Key.HOME) {
|
scrollToItem(currentIndex - 1, false);
|
||||||
scrollToIndex(0);
|
handled = true;
|
||||||
handled = true;
|
} else if (e.code === Key.ARROW_DOWN && currentIndex !== undefined) {
|
||||||
} else if (e.code === Key.END) {
|
scrollToItem(currentIndex + 1, true);
|
||||||
scrollToIndex(items.length - 1);
|
handled = true;
|
||||||
handled = true;
|
} else if (e.code === Key.HOME) {
|
||||||
} else if (e.code === Key.PAGE_DOWN && visibleRange && currentIndex !== undefined) {
|
scrollToIndex(0);
|
||||||
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
|
handled = true;
|
||||||
scrollToItem(Math.min(currentIndex + numberDisplayed, items.length - 1), true, `start`);
|
} else if (e.code === Key.END) {
|
||||||
handled = true;
|
scrollToIndex(items.length - 1);
|
||||||
} else if (e.code === Key.PAGE_UP && visibleRange && currentIndex !== undefined) {
|
handled = true;
|
||||||
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
|
} else if (e.code === Key.PAGE_DOWN && visibleRange && currentIndex !== undefined) {
|
||||||
scrollToItem(Math.max(currentIndex - numberDisplayed, 0), false, `start`);
|
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
|
||||||
handled = true;
|
scrollToItem(Math.min(currentIndex + numberDisplayed, items.length - 1), true, `start`);
|
||||||
}
|
handled = true;
|
||||||
|
} else if (e.code === Key.PAGE_UP && visibleRange && currentIndex !== undefined) {
|
||||||
|
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
|
||||||
|
scrollToItem(Math.max(currentIndex - numberDisplayed, 0), false, `start`);
|
||||||
|
handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handled) {
|
if (handled) {
|
||||||
@@ -281,7 +285,10 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
tabIndex={props.tabIndex || undefined} // We don't need to focus the container, so leave it undefined by default
|
// note that either the container of direct children must be focusable to be axe
|
||||||
|
// compliant, so we leave tabIndex as the default so the container can be focused
|
||||||
|
// (virtuoso wraps the children inside another couple of elements so setting it
|
||||||
|
// on those doesn't seem to work, unfortunately)
|
||||||
ref={virtuosoHandleRef}
|
ref={virtuosoHandleRef}
|
||||||
scrollerRef={scrollerRef}
|
scrollerRef={scrollerRef}
|
||||||
onKeyDown={keyDownCallback}
|
onKeyDown={keyDownCallback}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ const WidgetAvatar: React.FC<IProps> = ({ app, className, size = "20px", ...prop
|
|||||||
return (
|
return (
|
||||||
<BaseAvatar
|
<BaseAvatar
|
||||||
{...props}
|
{...props}
|
||||||
|
// Span elements cannot have a label
|
||||||
|
role="img"
|
||||||
name={app.id}
|
name={app.id}
|
||||||
className={classNames("mx_WidgetAvatar", className)}
|
className={classNames("mx_WidgetAvatar", className)}
|
||||||
// MSC2765
|
// MSC2765
|
||||||
|
|||||||
@@ -145,6 +145,9 @@ export default class BaseDialog extends React.Component<IProps> {
|
|||||||
const lockProps: Record<string, any> = {
|
const lockProps: Record<string, any> = {
|
||||||
"onKeyDown": this.onKeyDown,
|
"onKeyDown": this.onKeyDown,
|
||||||
"role": "dialog",
|
"role": "dialog",
|
||||||
|
// Allow the dialog to be keyboard focusable
|
||||||
|
// So the escape key handling works in more cases (say you select the header)
|
||||||
|
"tabIndex": -1,
|
||||||
// This should point to a node describing the dialog.
|
// This should point to a node describing the dialog.
|
||||||
// If we were about to completely follow this recommendation we'd need to
|
// If we were about to completely follow this recommendation we'd need to
|
||||||
// make all the components relying on BaseDialog to be aware of it.
|
// make all the components relying on BaseDialog to be aware of it.
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
|||||||
import { privateShouldBeEncrypted } from "../../../utils/rooms";
|
import { privateShouldBeEncrypted } from "../../../utils/rooms";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import LabelledCheckbox from "../elements/LabelledCheckbox";
|
import LabelledCheckbox from "../elements/LabelledCheckbox";
|
||||||
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
type?: RoomType;
|
type?: RoomType;
|
||||||
@@ -83,6 +84,8 @@ interface IState {
|
|||||||
|
|
||||||
export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
private readonly askToJoinEnabled: boolean;
|
private readonly askToJoinEnabled: boolean;
|
||||||
|
private readonly advancedSettingsEnabled: boolean;
|
||||||
|
private readonly allowCreatingPublicRooms: boolean;
|
||||||
private readonly supportsRestricted: boolean;
|
private readonly supportsRestricted: boolean;
|
||||||
private nameField = createRef<Field>();
|
private nameField = createRef<Field>();
|
||||||
private aliasField = createRef<RoomAliasField>();
|
private aliasField = createRef<RoomAliasField>();
|
||||||
@@ -91,10 +94,14 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.askToJoinEnabled = SettingsStore.getValue("feature_ask_to_join");
|
this.askToJoinEnabled = SettingsStore.getValue("feature_ask_to_join");
|
||||||
|
this.advancedSettingsEnabled = SettingsStore.getValue(UIFeature.AdvancedSettings);
|
||||||
|
this.allowCreatingPublicRooms = SettingsStore.getValue(UIFeature.AllowCreatingPublicRooms);
|
||||||
|
|
||||||
this.supportsRestricted = !!this.props.parentSpace;
|
this.supportsRestricted = !!this.props.parentSpace;
|
||||||
|
const defaultPublic = this.allowCreatingPublicRooms && this.props.defaultPublic;
|
||||||
|
|
||||||
let joinRule = JoinRule.Invite;
|
let joinRule = JoinRule.Invite;
|
||||||
if (this.props.defaultPublic) {
|
if (defaultPublic) {
|
||||||
joinRule = JoinRule.Public;
|
joinRule = JoinRule.Public;
|
||||||
} else if (this.supportsRestricted) {
|
} else if (this.supportsRestricted) {
|
||||||
joinRule = JoinRule.Restricted;
|
joinRule = JoinRule.Restricted;
|
||||||
@@ -102,7 +109,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
const cli = MatrixClientPeg.safeGet();
|
||||||
this.state = {
|
this.state = {
|
||||||
isPublicKnockRoom: this.props.defaultPublic || false,
|
isPublicKnockRoom: defaultPublic || false,
|
||||||
isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(cli),
|
isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(cli),
|
||||||
joinRule,
|
joinRule,
|
||||||
name: this.props.defaultName || "",
|
name: this.props.defaultName || "",
|
||||||
@@ -415,7 +422,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||||||
labelKnock={
|
labelKnock={
|
||||||
this.askToJoinEnabled ? _t("room_settings|security|join_rule_knock") : undefined
|
this.askToJoinEnabled ? _t("room_settings|security|join_rule_knock") : undefined
|
||||||
}
|
}
|
||||||
labelPublic={_t("common|public_room")}
|
labelPublic={this.allowCreatingPublicRooms ? _t("common|public_room") : undefined}
|
||||||
labelRestricted={
|
labelRestricted={
|
||||||
this.supportsRestricted ? _t("create_room|join_rule_restricted") : undefined
|
this.supportsRestricted ? _t("create_room|join_rule_restricted") : undefined
|
||||||
}
|
}
|
||||||
@@ -427,19 +434,21 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||||||
{visibilitySection}
|
{visibilitySection}
|
||||||
{e2eeSection}
|
{e2eeSection}
|
||||||
{aliasField}
|
{aliasField}
|
||||||
<details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details">
|
{this.advancedSettingsEnabled && (
|
||||||
<summary className="mx_CreateRoomDialog_details_summary">
|
<details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details">
|
||||||
{this.state.detailsOpen ? _t("action|hide_advanced") : _t("action|show_advanced")}
|
<summary className="mx_CreateRoomDialog_details_summary">
|
||||||
</summary>
|
{this.state.detailsOpen ? _t("action|hide_advanced") : _t("action|show_advanced")}
|
||||||
<LabelledToggleSwitch
|
</summary>
|
||||||
label={_t("create_room|unfederated", {
|
<LabelledToggleSwitch
|
||||||
serverName: MatrixClientPeg.safeGet().getDomain(),
|
label={_t("create_room|unfederated", {
|
||||||
})}
|
serverName: MatrixClientPeg.safeGet().getDomain(),
|
||||||
onChange={this.onNoFederateChange}
|
})}
|
||||||
value={this.state.noFederate}
|
onChange={this.onNoFederateChange}
|
||||||
/>
|
value={this.state.noFederate}
|
||||||
<p>{federateLabel}</p>
|
/>
|
||||||
</details>
|
<p>{federateLabel}</p>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ const DevtoolsDialog: React.FC<IProps> = ({ roomId, threadRootId, onFinished })
|
|||||||
<BaseTool onBack={onBack}>
|
<BaseTool onBack={onBack}>
|
||||||
{Object.entries(Tools).map(([category, tools]) => (
|
{Object.entries(Tools).map(([category, tools]) => (
|
||||||
<div key={category}>
|
<div key={category}>
|
||||||
<h3>{_t(categoryLabels[category as unknown as Category])}</h3>
|
<h2 className="mx_DevTools_toolHeading">
|
||||||
|
{_t(categoryLabels[category as unknown as Category])}
|
||||||
|
</h2>
|
||||||
{tools.map(([label, tool]) => {
|
{tools.map(([label, tool]) => {
|
||||||
const onClick = (): void => {
|
const onClick = (): void => {
|
||||||
setTool([label, tool]);
|
setTool([label, tool]);
|
||||||
@@ -98,7 +100,7 @@ const DevtoolsDialog: React.FC<IProps> = ({ roomId, threadRootId, onFinished })
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div>
|
<div>
|
||||||
<h3>{_t("common|options")}</h3>
|
<h2 className="mx_DevTools_toolHeading">{_t("common|options")}</h2>
|
||||||
<SettingsFlag name="developerMode" level={SettingLevel.ACCOUNT} />
|
<SettingsFlag name="developerMode" level={SettingLevel.ACCOUNT} />
|
||||||
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
|
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
|
||||||
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
|
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
|
||||||
|
|||||||
@@ -1,55 +1,53 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
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.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { useCallback } from "react";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
|
import { UserTab } from "./UserTab";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onFinished(): void;
|
onFinished(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class IntegrationsDisabledDialog extends React.Component<IProps> {
|
export const IntegrationsDisabledDialog: React.FC<IProps> = ({ onFinished }) => {
|
||||||
private onAcknowledgeClick = (): void => {
|
const onOpenSettingsClick = useCallback(() => {
|
||||||
this.props.onFinished();
|
onFinished();
|
||||||
};
|
dis.dispatch({
|
||||||
|
action: Action.ViewUserSettings,
|
||||||
|
initialTabId: UserTab.Security,
|
||||||
|
});
|
||||||
|
}, [onFinished]);
|
||||||
|
|
||||||
private onOpenSettingsClick = (): void => {
|
return (
|
||||||
this.props.onFinished();
|
<BaseDialog
|
||||||
dis.fire(Action.ViewUserSettings);
|
className="mx_IntegrationsDisabledDialog"
|
||||||
};
|
hasCancel={true}
|
||||||
|
onFinished={onFinished}
|
||||||
public render(): React.ReactNode {
|
title={_t("integrations|disabled_dialog_title")}
|
||||||
return (
|
>
|
||||||
<BaseDialog
|
<div className="mx_IntegrationsDisabledDialog_content">
|
||||||
className="mx_IntegrationsDisabledDialog"
|
<p>
|
||||||
hasCancel={true}
|
{_t("integrations|disabled_dialog_description", {
|
||||||
onFinished={this.props.onFinished}
|
manageIntegrations: _t("integration_manager|manage_title"),
|
||||||
title={_t("integrations|disabled_dialog_title")}
|
})}
|
||||||
>
|
</p>
|
||||||
<div className="mx_IntegrationsDisabledDialog_content">
|
</div>
|
||||||
<p>
|
<DialogButtons
|
||||||
{_t("integrations|disabled_dialog_description", {
|
primaryButton={_t("common|settings")}
|
||||||
manageIntegrations: _t("integration_manager|manage_title"),
|
onPrimaryButtonClick={onOpenSettingsClick}
|
||||||
})}
|
cancelButton={_t("action|ok")}
|
||||||
</p>
|
onCancel={onFinished}
|
||||||
</div>
|
/>
|
||||||
<DialogButtons
|
</BaseDialog>
|
||||||
primaryButton={_t("common|settings")}
|
);
|
||||||
onPrimaryButtonClick={this.onOpenSettingsClick}
|
};
|
||||||
cancelButton={_t("action|ok")}
|
|
||||||
onCancel={this.onAcknowledgeClick}
|
|
||||||
/>
|
|
||||||
</BaseDialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ import { getKeyBindingsManager } from "../../../../KeyBindingsManager";
|
|||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||||
import { PosthogAnalytics } from "../../../../PosthogAnalytics";
|
import { PosthogAnalytics } from "../../../../PosthogAnalytics";
|
||||||
import { getCachedRoomIDForAlias } from "../../../../RoomAliasCache";
|
import { getCachedRoomIdForAlias } from "../../../../RoomAliasCache";
|
||||||
import { showStartChatInviteDialog } from "../../../../RoomInvite";
|
import { showStartChatInviteDialog } from "../../../../RoomInvite";
|
||||||
import { SettingLevel } from "../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../settings/SettingLevel";
|
||||||
import SettingsStore from "../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../settings/SettingsStore";
|
||||||
@@ -912,7 +912,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||||||
if (
|
if (
|
||||||
trimmedQuery.startsWith("#") &&
|
trimmedQuery.startsWith("#") &&
|
||||||
trimmedQuery.includes(":") &&
|
trimmedQuery.includes(":") &&
|
||||||
(!getCachedRoomIDForAlias(trimmedQuery) || !cli.getRoom(getCachedRoomIDForAlias(trimmedQuery)))
|
(!getCachedRoomIdForAlias(trimmedQuery) || !cli.getRoom(getCachedRoomIdForAlias(trimmedQuery)!.roomId))
|
||||||
) {
|
) {
|
||||||
joinRoomSection = (
|
joinRoomSection = (
|
||||||
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_otherSearches" role="group">
|
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_otherSearches" role="group">
|
||||||
|
|||||||
@@ -572,7 +572,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<WidgetAvatar app={this.props.app} size="20px" />
|
<WidgetAvatar app={this.props.app} size="20px" />
|
||||||
<h3>{name}</h3>
|
<h1>{name}</h1>
|
||||||
<span>
|
<span>
|
||||||
{title ? titleSpacer : ""}
|
{title ? titleSpacer : ""}
|
||||||
{title}
|
{title}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ interface IProps {
|
|||||||
width?: number;
|
width?: number;
|
||||||
labelInvite: string;
|
labelInvite: string;
|
||||||
labelKnock?: string;
|
labelKnock?: string;
|
||||||
labelPublic: string;
|
labelPublic?: string;
|
||||||
labelRestricted?: string; // if omitted then this option will be hidden, e.g if unsupported
|
labelRestricted?: string; // if omitted then this option will be hidden, e.g if unsupported
|
||||||
onChange(value: JoinRule): void;
|
onChange(value: JoinRule): void;
|
||||||
}
|
}
|
||||||
@@ -38,11 +38,18 @@ const JoinRuleDropdown: React.FC<IProps> = ({
|
|||||||
<div key={JoinRule.Invite} className="mx_JoinRuleDropdown_invite">
|
<div key={JoinRule.Invite} className="mx_JoinRuleDropdown_invite">
|
||||||
{labelInvite}
|
{labelInvite}
|
||||||
</div>,
|
</div>,
|
||||||
<div key={JoinRule.Public} className="mx_JoinRuleDropdown_public">
|
|
||||||
{labelPublic}
|
|
||||||
</div>,
|
|
||||||
] as NonEmptyArray<ReactElement & { key: string }>;
|
] as NonEmptyArray<ReactElement & { key: string }>;
|
||||||
|
|
||||||
|
if (labelPublic) {
|
||||||
|
options.push(
|
||||||
|
(
|
||||||
|
<div key={JoinRule.Public} className="mx_JoinRuleDropdown_public">
|
||||||
|
{labelPublic}
|
||||||
|
</div>
|
||||||
|
) as ReactElement & { key: string },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (labelKnock) {
|
if (labelKnock) {
|
||||||
options.unshift(
|
options.unshift(
|
||||||
(
|
(
|
||||||
@@ -72,6 +79,7 @@ const JoinRuleDropdown: React.FC<IProps> = ({
|
|||||||
menuWidth={width}
|
menuWidth={width}
|
||||||
value={value}
|
value={value}
|
||||||
label={label}
|
label={label}
|
||||||
|
disabled={options.length === 1}
|
||||||
>
|
>
|
||||||
{options}
|
{options}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ interface Props extends ReplacerOptions {
|
|||||||
const EventContentBody = memo(
|
const EventContentBody = memo(
|
||||||
({ as, mxEvent, stripReply, content, linkify, highlights, includeDir = true, ref, ...options }: Props) => {
|
({ as, mxEvent, stripReply, content, linkify, highlights, includeDir = true, ref, ...options }: Props) => {
|
||||||
const enableBigEmoji = useSettingValue("TextualBody.enableBigEmoji");
|
const enableBigEmoji = useSettingValue("TextualBody.enableBigEmoji");
|
||||||
const [mediaIsVisible] = useMediaVisible(mxEvent?.getId(), mxEvent?.getRoomId());
|
const [mediaIsVisible] = useMediaVisible(mxEvent);
|
||||||
|
|
||||||
const replacer = useReplacer(content, mxEvent, options);
|
const replacer = useReplacer(content, mxEvent, options);
|
||||||
const linkifyOptions = useMemo(
|
const linkifyOptions = useMemo(
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ interface IProps {
|
|||||||
* Quick action button for marking a media event as hidden.
|
* Quick action button for marking a media event as hidden.
|
||||||
*/
|
*/
|
||||||
export const HideActionButton: React.FC<IProps> = ({ mxEvent }) => {
|
export const HideActionButton: React.FC<IProps> = ({ mxEvent }) => {
|
||||||
const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId());
|
const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent);
|
||||||
|
|
||||||
if (!mediaIsVisible) {
|
if (!mediaIsVisible) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -48,4 +48,9 @@ export interface IBodyProps {
|
|||||||
// Set to `true` to disable interactions (e.g. video controls) and to remove controls from the tab order.
|
// Set to `true` to disable interactions (e.g. video controls) and to remove controls from the tab order.
|
||||||
// This may be useful when displaying a preview of the event.
|
// This may be useful when displaying a preview of the event.
|
||||||
inhibitInteraction?: boolean;
|
inhibitInteraction?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional ID for the root element.
|
||||||
|
*/
|
||||||
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -686,7 +686,7 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
// Wrap MImageBody component so we can use a hook here.
|
// Wrap MImageBody component so we can use a hook here.
|
||||||
const MImageBody: React.FC<IBodyProps> = (props) => {
|
const MImageBody: React.FC<IBodyProps> = (props) => {
|
||||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
|
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
|
||||||
return <MImageBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
return <MImageBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class MImageReplyBodyInner extends MImageBodyInner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const MImageReplyBody: React.FC<IBodyProps> = (props) => {
|
const MImageReplyBody: React.FC<IBodyProps> = (props) => {
|
||||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
|
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
|
||||||
return <MImageReplyBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
return <MImageReplyBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class MStickerBodyInner extends MImageBodyInner {
|
|||||||
protected onClick = (ev: React.MouseEvent): void => {
|
protected onClick = (ev: React.MouseEvent): void => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (!this.props.mediaVisible) {
|
if (!this.props.mediaVisible) {
|
||||||
this.props.setMediaVisible?.(true);
|
this.props.setMediaVisible(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ class MStickerBodyInner extends MImageBodyInner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MStickerBody: React.FC<IBodyProps> = (props) => {
|
const MStickerBody: React.FC<IBodyProps> = (props) => {
|
||||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
|
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
|
||||||
return <MStickerBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
return <MStickerBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ class MVideoBodyInner extends React.PureComponent<IProps, IState> {
|
|||||||
|
|
||||||
// Wrap MVideoBody component so we can use a hook here.
|
// Wrap MVideoBody component so we can use a hook here.
|
||||||
const MVideoBody: React.FC<IBodyProps> = (props) => {
|
const MVideoBody: React.FC<IBodyProps> = (props) => {
|
||||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
|
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
|
||||||
return <MVideoBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
return <MVideoBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper
|
|||||||
getRelationsForEvent?: GetRelationsForEvent;
|
getRelationsForEvent?: GetRelationsForEvent;
|
||||||
|
|
||||||
isSeeingThroughMessageHiddenForModeration?: boolean;
|
isSeeingThroughMessageHiddenForModeration?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional ID for the root element.
|
||||||
|
*/
|
||||||
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IOperableEventTile {
|
export interface IOperableEventTile {
|
||||||
@@ -308,6 +313,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
|||||||
getRelationsForEvent: this.props.getRelationsForEvent,
|
getRelationsForEvent: this.props.getRelationsForEvent,
|
||||||
isSeeingThroughMessageHiddenForModeration: this.props.isSeeingThroughMessageHiddenForModeration,
|
isSeeingThroughMessageHiddenForModeration: this.props.isSeeingThroughMessageHiddenForModeration,
|
||||||
inhibitInteraction: this.props.inhibitInteraction,
|
inhibitInteraction: this.props.inhibitInteraction,
|
||||||
|
id: this.props.id,
|
||||||
};
|
};
|
||||||
if (hasCaption) {
|
if (hasCaption) {
|
||||||
return <CaptionBody {...bodyProps} WrappedBodyType={BodyType} />;
|
return <CaptionBody {...bodyProps} WrappedBodyType={BodyType} />;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX } from "react";
|
import React, { type HTMLProps, type JSX } from "react";
|
||||||
import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin-solid";
|
import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin-solid";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
@@ -13,9 +13,9 @@ import { _t } from "../../../languageHandler";
|
|||||||
/**
|
/**
|
||||||
* A badge to indicate that a message is pinned.
|
* A badge to indicate that a message is pinned.
|
||||||
*/
|
*/
|
||||||
export function PinnedMessageBadge(): JSX.Element {
|
export function PinnedMessageBadge(props: Readonly<HTMLProps<HTMLDivElement>>): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="mx_PinnedMessageBadge">
|
<div {...props} className="mx_PinnedMessageBadge">
|
||||||
<PinIcon width="16px" height="16px" />
|
<PinIcon width="16px" height="16px" />
|
||||||
{_t("room|pinned_message_badge")}
|
{_t("room|pinned_message_badge")}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ import { type MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix";
|
|||||||
|
|
||||||
import DisambiguatedProfile from "./DisambiguatedProfile";
|
import DisambiguatedProfile from "./DisambiguatedProfile";
|
||||||
import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile";
|
import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile";
|
||||||
import ModuleApi from "../../../modules/Api";
|
|
||||||
import { CustomComponentsApi } from "../../../modules/customComponentApi";
|
|
||||||
import { MessageProfileComponentProps } from "@element-hq/element-web-module-api";
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
@@ -26,25 +24,17 @@ export default function SenderProfile({ mxEvent, onClick, withTooltip }: IProps)
|
|||||||
userId: mxEvent.getSender(),
|
userId: mxEvent.getSender(),
|
||||||
member: mxEvent.sender,
|
member: mxEvent.sender,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (mxEvent.getContent().msgtype === MsgType.Emote) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
const moduleRenderer = ModuleApi.customComponents.messageProfileRenderer;
|
|
||||||
const renderFn = (moduleProps: MessageProfileComponentProps) => <DisambiguatedProfile
|
|
||||||
fallbackName={moduleProps.mxEvent.sender ?? ""}
|
|
||||||
onClick={moduleProps.onClick}
|
|
||||||
member={moduleProps.member}
|
|
||||||
colored={true}
|
|
||||||
emphasizeDisplayName={true}
|
|
||||||
withTooltip={withTooltip}
|
|
||||||
/>;
|
|
||||||
|
|
||||||
const modProps = {
|
return mxEvent.getContent().msgtype !== MsgType.Emote ? (
|
||||||
onClick,
|
<DisambiguatedProfile
|
||||||
mxEvent: CustomComponentsApi.getModuleMatrixEvent(mxEvent)!,
|
fallbackName={mxEvent.getSender() ?? ""}
|
||||||
member: member || undefined,
|
onClick={onClick}
|
||||||
};
|
member={member}
|
||||||
|
colored={true}
|
||||||
return moduleRenderer ? moduleRenderer(modProps, renderFn) : renderFn(modProps);
|
emphasizeDisplayName={true}
|
||||||
|
withTooltip={withTooltip}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -384,7 +384,12 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||||||
|
|
||||||
if (isEmote) {
|
if (isEmote) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_MEmoteBody mx_EventTile_content" onClick={this.onBodyLinkClick} dir="auto">
|
<div
|
||||||
|
id={this.props.id}
|
||||||
|
className="mx_MEmoteBody mx_EventTile_content"
|
||||||
|
onClick={this.onBodyLinkClick}
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
*
|
*
|
||||||
<span className="mx_MEmoteBody_sender" onClick={this.onEmoteSenderClick}>
|
<span className="mx_MEmoteBody_sender" onClick={this.onEmoteSenderClick}>
|
||||||
{mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()}
|
{mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()}
|
||||||
@@ -397,7 +402,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||||||
}
|
}
|
||||||
if (isNotice) {
|
if (isNotice) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
|
<div id={this.props.id} className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
|
||||||
{body}
|
{body}
|
||||||
{widgets}
|
{widgets}
|
||||||
</div>
|
</div>
|
||||||
@@ -405,14 +410,14 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||||||
}
|
}
|
||||||
if (isCaption) {
|
if (isCaption) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_MTextBody mx_EventTile_caption" onClick={this.onBodyLinkClick}>
|
<div id={this.props.id} className="mx_MTextBody mx_EventTile_caption" onClick={this.onBodyLinkClick}>
|
||||||
{body}
|
{body}
|
||||||
{widgets}
|
{widgets}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
|
<div id={this.props.id} className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
|
||||||
{body}
|
{body}
|
||||||
{widgets}
|
{widgets}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ import RoomName from "../elements/RoomName.tsx";
|
|||||||
import { Flex } from "../../../shared-components/utils/Flex";
|
import { Flex } from "../../../shared-components/utils/Flex";
|
||||||
import { Linkify, topicToHtml } from "../../../HtmlUtils.tsx";
|
import { Linkify, topicToHtml } from "../../../HtmlUtils.tsx";
|
||||||
import { Box } from "../../../shared-components/utils/Box";
|
import { Box } from "../../../shared-components/utils/Box";
|
||||||
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement.tsx";
|
|
||||||
import { useRoomSummaryCardViewModel } from "../../viewmodels/right_panel/RoomSummaryCardViewModel.tsx";
|
import { useRoomSummaryCardViewModel } from "../../viewmodels/right_panel/RoomSummaryCardViewModel.tsx";
|
||||||
import { useRoomTopicViewModel } from "../../viewmodels/right_panel/RoomSummaryCardTopicViewModel.tsx";
|
import { useRoomTopicViewModel } from "../../viewmodels/right_panel/RoomSummaryCardTopicViewModel.tsx";
|
||||||
|
|
||||||
@@ -251,25 +250,15 @@ const RoomSummaryCardView: React.FC<IProps> = ({
|
|||||||
<MenuItem Icon={ThreadsIcon} label={_t("common|threads")} onSelect={vm.onRoomThreadsClick} />
|
<MenuItem Icon={ThreadsIcon} label={_t("common|threads")} onSelect={vm.onRoomThreadsClick} />
|
||||||
{!vm.isVideoRoom && (
|
{!vm.isVideoRoom && (
|
||||||
<>
|
<>
|
||||||
<ReleaseAnnouncement
|
<MenuItem
|
||||||
feature="pinningMessageList"
|
Icon={PinIcon}
|
||||||
header={_t("right_panel|pinned_messages|release_announcement|title")}
|
label={_t("right_panel|pinned_messages_button")}
|
||||||
description={_t("right_panel|pinned_messages|release_announcement|description")}
|
onSelect={vm.onRoomPinsClick}
|
||||||
closeLabel={_t("right_panel|pinned_messages|release_announcement|close")}
|
|
||||||
placement="top"
|
|
||||||
>
|
>
|
||||||
<div>
|
<Text as="span" size="sm">
|
||||||
<MenuItem
|
{vm.pinCount}
|
||||||
Icon={PinIcon}
|
</Text>
|
||||||
label={_t("right_panel|pinned_messages_button")}
|
</MenuItem>
|
||||||
onSelect={vm.onRoomPinsClick}
|
|
||||||
>
|
|
||||||
<Text as="span" size="sm">
|
|
||||||
{vm.pinCount}
|
|
||||||
</Text>
|
|
||||||
</MenuItem>
|
|
||||||
</div>
|
|
||||||
</ReleaseAnnouncement>
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Icon={FilesIcon}
|
Icon={FilesIcon}
|
||||||
label={_t("right_panel|files_button")}
|
label={_t("right_panel|files_button")}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
|||||||
|
|
||||||
const header = (
|
const header = (
|
||||||
<div className="mx_BaseCard_header_title">
|
<div className="mx_BaseCard_header_title">
|
||||||
<Heading size="4" className="mx_BaseCard_header_title_heading">
|
<Heading size="4" className="mx_BaseCard_header_title_heading" as="h1">
|
||||||
{WidgetUtils.getWidgetName(app)}
|
{WidgetUtils.getWidgetName(app)}
|
||||||
</Heading>
|
</Heading>
|
||||||
<ContextMenuButton
|
<ContextMenuButton
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { Flex } from "../../../../shared-components/utils/Flex";
|
|||||||
import PresenceLabel from "../../rooms/PresenceLabel";
|
import PresenceLabel from "../../rooms/PresenceLabel";
|
||||||
import CopyableText from "../../elements/CopyableText";
|
import CopyableText from "../../elements/CopyableText";
|
||||||
import { UserInfoHeaderVerificationView } from "./UserInfoHeaderVerificationView";
|
import { UserInfoHeaderVerificationView } from "./UserInfoHeaderVerificationView";
|
||||||
import ModuleApi from "../../../../modules/Api";
|
|
||||||
|
|
||||||
export interface UserInfoHeaderViewProps {
|
export interface UserInfoHeaderViewProps {
|
||||||
member: Member;
|
member: Member;
|
||||||
@@ -49,27 +48,6 @@ export const UserInfoHeaderView: React.FC<UserInfoHeaderViewProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const moduleRenderer = ModuleApi.customComponents.userInfoRenderer;
|
|
||||||
let usernameSection;
|
|
||||||
if (moduleRenderer && vm.userIdentifier) {
|
|
||||||
usernameSection = moduleRenderer(
|
|
||||||
{
|
|
||||||
userId: vm.userIdentifier,
|
|
||||||
},
|
|
||||||
(props) => <Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
|
|
||||||
<CopyableText getTextToCopy={() => props.userId} border={false}>
|
|
||||||
{props.userId}
|
|
||||||
</CopyableText>
|
|
||||||
</Text>,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
usernameSection = <Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
|
|
||||||
<CopyableText getTextToCopy={() => vm.userIdentifier} border={false}>
|
|
||||||
{vm.userIdentifier}
|
|
||||||
</CopyableText>
|
|
||||||
</Text>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className="mx_UserInfo_avatar">
|
<div className="mx_UserInfo_avatar">
|
||||||
@@ -105,7 +83,11 @@ export const UserInfoHeaderView: React.FC<UserInfoHeaderViewProps> = ({
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{usernameSection}
|
<Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
|
||||||
|
<CopyableText getTextToCopy={() => vm.userIdentifier} border={false}>
|
||||||
|
{vm.userIdentifier}
|
||||||
|
</CopyableText>
|
||||||
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
{!hideVerificationSection && <UserInfoHeaderVerificationView member={member} devices={devices} />}
|
{!hideVerificationSection && <UserInfoHeaderVerificationView member={member} devices={devices} />}
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
type UserVerificationStatus,
|
type UserVerificationStatus,
|
||||||
} from "matrix-js-sdk/src/crypto-api";
|
} from "matrix-js-sdk/src/crypto-api";
|
||||||
import { Tooltip } from "@vector-im/compound-web";
|
import { Tooltip } from "@vector-im/compound-web";
|
||||||
|
import { uniqueId } from "lodash";
|
||||||
|
|
||||||
import ReplyChain from "../elements/ReplyChain";
|
import ReplyChain from "../elements/ReplyChain";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
@@ -918,6 +919,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
public render(): ReactNode {
|
public render(): ReactNode {
|
||||||
const msgtype = this.props.mxEvent.getContent().msgtype;
|
const msgtype = this.props.mxEvent.getContent().msgtype;
|
||||||
const eventType = this.props.mxEvent.getType();
|
const eventType = this.props.mxEvent.getType();
|
||||||
|
const id = uniqueId();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hasRenderer,
|
hasRenderer,
|
||||||
isBubbleMessage,
|
isBubbleMessage,
|
||||||
@@ -1142,7 +1145,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
|
|
||||||
let pinnedMessageBadge: JSX.Element | undefined;
|
let pinnedMessageBadge: JSX.Element | undefined;
|
||||||
if (PinningUtils.isPinned(MatrixClientPeg.safeGet(), this.props.mxEvent)) {
|
if (PinningUtils.isPinned(MatrixClientPeg.safeGet(), this.props.mxEvent)) {
|
||||||
pinnedMessageBadge = <PinnedMessageBadge />;
|
pinnedMessageBadge = <PinnedMessageBadge aria-describedby={id} tabIndex={0} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let reactionsRow: JSX.Element | undefined;
|
let reactionsRow: JSX.Element | undefined;
|
||||||
@@ -1237,7 +1240,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
{avatar}
|
{avatar}
|
||||||
{sender}
|
{sender}
|
||||||
</div>,
|
</div>,
|
||||||
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
<div id={id} className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||||
{this.renderContextMenu()}
|
{this.renderContextMenu()}
|
||||||
{replyChain}
|
{replyChain}
|
||||||
{renderTile(TimelineRenderingType.Thread, {
|
{renderTile(TimelineRenderingType.Thread, {
|
||||||
@@ -1425,7 +1428,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
{sender}
|
{sender}
|
||||||
{ircPadlock}
|
{ircPadlock}
|
||||||
{avatar}
|
{avatar}
|
||||||
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
<div id={id} className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||||
{this.renderContextMenu()}
|
{this.renderContextMenu()}
|
||||||
{groupTimestamp}
|
{groupTimestamp}
|
||||||
{groupPadlock}
|
{groupPadlock}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ interface IProps {
|
|||||||
const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick }) => {
|
const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick }) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const [expanded, toggleExpanded] = useStateToggle();
|
const [expanded, toggleExpanded] = useStateToggle();
|
||||||
const [mediaVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId());
|
const [mediaVisible] = useMediaVisible(mxEvent);
|
||||||
|
|
||||||
const ts = mxEvent.getTs();
|
const ts = mxEvent.getTs();
|
||||||
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(
|
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(
|
||||||
|
|||||||