Compare commits

...

71 Commits

Author SHA1 Message Date
Robin
569d525c6e Wait for a close action before closing a call
This creates a distinction between the user hanging up and the widget being ready to close, which is useful for allowing Element Call to show error screens when disconnected from the call, for example.
2025-02-20 13:54:10 +07:00
Robin
7799cb2ec5 Avoid destroying calls until they are hidden from the UI
We often want calls to exist even when no more participants are left in the MatrixRTC session. So, we should avoid destroying calls as long as they're being presented in the UI; this means that the user has an intent to either join the call or continue looking at an error screen, and we shouldn't interrupt that interaction.

The RoomViewStore is now what takes care of creating and destroying calls, rather than the CallView. In general it seems kinda impossible to safely create and destroy model objects from React lifecycle hooks, so moving this responsibility to a store seemed appropriate and resolves existing issues with calls in React strict mode.
2025-02-20 13:54:10 +07:00
Michael Telatynski
e47d7aaaff Add Windows 64-bit arm link and remove 32-bit link on compatibility page (#29312)
* Add Windows 64-bit arm link and remove 32-bit link on compatibility page

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

* Update snapshots

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-19 16:17:56 +00:00
Florian Duros
8857c07acb Move view_create_chat & view_create_room to actions.ts (#29319)
* refactor: replace `view_create_chat` by `Action.CreateChat`

* refactor: replace `view_create_room` by `Action.CreateRoom`
2025-02-19 15:11:43 +00:00
Florian Duros
28ed506fe1 refactor: rename RoomListHeader as LegacyRoomListHeader (#29308) 2025-02-19 14:06:01 +00:00
R Midhun Suresh
76b3be6263 Add some basic documentation for MVVM (#29316) 2025-02-19 13:13:28 +00:00
ElementRobot
6c768b8b32 [create-pull-request] automated change (#29313)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-19 10:39:36 +00:00
ElementRobot
809ada17a4 [create-pull-request] automated change (#29314)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-19 06:19:54 +00:00
renovate[bot]
c7762a80f1 Update dependency @sentry/browser to v9 (#29303)
* Update dependency @sentry/browser to v9

* Remove redundant option

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-18 21:11:36 +00:00
renovate[bot]
261923832d Update dependency caniuse-lite to v1.0.30001699 (#29297)
* Update dependency caniuse-lite to v1.0.30001699

* Update tests

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-18 20:29:47 +00:00
renovate[bot]
3daa1bf06a Update all non-major dependencies (#29298)
* Update all non-major dependencies

* Prettier

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-18 20:20:06 +00:00
renovate[bot]
e5c8d7dbf0 Update playwright to v1.50.1 (#29183)
* Update playwright to v1.50.1

* Update snapshots

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-18 18:43:40 +00:00
ElementRobot
441119ca3a [create-pull-request] automated change (#29286)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-18 17:49:15 +00:00
renovate[bot]
acb3e781a4 Update typescript-eslint monorepo to v8.24.0 (#29302)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 16:59:10 +00:00
renovate[bot]
3fb1f6ef4d Update testcontainers-node monorepo to v10.18.0 (#29301)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 16:55:48 +00:00
renovate[bot]
cbfbfad959 Update docker (#29291)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:27:32 +00:00
renovate[bot]
7546bbc1f0 Update dependency @sentry/browser to v8.55.0 (#29299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:07:10 +00:00
renovate[bot]
6a10c86d7a Update dependency @stylistic/eslint-plugin to v3.1.0 (#29300)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:07:06 +00:00
renovate[bot]
6f9e3bfe3e Update dependency @types/node to v18.19.76 (#29296)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:06:06 +00:00
David Baker
61d55462df Honour the backup disable flag from Element X (#29290)
* Honour the backup disable flag from Element X

This unfortunately named and unspecced flag is set by Element X
to denote that the user has chosen to disable key storage and it
should not automatically try to enable it again. This changes Element
web to not prompt to enable recovery if this flag is set.

* Remove unnecessary conditional

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-18 15:06:00 +00:00
renovate[bot]
d391c69e53 Update dependency @formatjs/intl-segmenter to v11.7.9 (#29295)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:02:23 +00:00
renovate[bot]
a6afff9759 Update babel monorepo (#29294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:02:22 +00:00
renovate[bot]
073d8e0b86 Update sigstore/cosign-installer digest to c56c2d3 (#29293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 14:53:35 +00:00
renovate[bot]
ecf5d720b0 Update guibranco/github-status-action-v2 digest to 7ca807c (#29292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 14:52:13 +00:00
David Baker
b7fea97bb6 Factor out duplicated CSS for buttons in encryption settings (#29269)
* Factor out duplicated CSS for buttons in encryption settings

By adding a component to hold the common CSS

* Update snapshot

* Update snapshot

* More snapshots

* Split EncryptionCardButtons out to separate component

* Update imports
2025-02-18 11:01:48 +00:00
Florian Duros
90801eb38b fix(member list): remove hardcoded font size in member list tile (#29285) 2025-02-17 18:06:14 +00:00
Richard van der Hoff
e02da752f0 Fix CODEOWNERS for src/components/views/settings/encryption (#29268)
Also, add `/playwright/e2e/crypto`.
2025-02-17 10:33:24 +00:00
Michael Telatynski
dd2da5c132 Fix up UIA types and remove unused prop (#29255)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-17 09:53:26 +00:00
ElementRobot
52060235e4 [create-pull-request] automated change (#29263)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-17 06:21:42 +00:00
ElementRobot
8d67e88b1d [create-pull-request] automated change (#29276)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-15 06:15:19 +00:00
Richard van der Hoff
a365533367 ChangeRecoveryKey: error handling (#29262)
* CreateSecretStorageDialog: error handling

I'm fed up with setup operations in EW failing silently. Rather than leaving
the user with a mysteriously broken client, let's at least tell them that
something has gone wrong, so that they can report the issue and we can
investigate.

Obviously, showing an unactionable Error dialog is a last resort: ideally, we
should handle the error ourselves, or give the user actionable steps to resolve
the problem. But that takes significant design and engineering.

Just swallowing errors is the worst of all possible options.

* Fix typo in test name

* Improve test coverage
2025-02-14 16:44:34 +00:00
David Langley
6dbc3b489a Grow member list search when resizing the right panel (#29267) 2025-02-14 16:23:30 +00:00
Richard van der Hoff
f822653d65 Dehydration: enable dehydrated device on "Set up recovery" (#29265)
* playwright/dehydration: update check

The old "Security & Privacy" tab is going away, so we need a new way to check
for dehydrated device existence.

* Dehydration: enable dehydrated device on "Set up recovery"

Clicking "Set up recovery" should set up a dehydrated device, if that feature
is enabled.

Fixes #29135

* Empty commit

... to wake up the CLA bot
2025-02-14 15:08:59 +00:00
Richard van der Hoff
09db599fe0 Minor cleanups to initialiseDehydration (#29261)
* dehydration: fix documentation

* initialiseDehydration: improve name

... to make it clearer that it does nothing if dehydration is disabled

* initialiseDehydration: remove dependency on MatrixClientPeg

We're trying to move away from relying on `MatrixClientPeg` everywhere, and
this is a particularly easy win.
2025-02-14 10:59:02 +00:00
Will Hunt
c47ce59478 Render reason for invite rejection. (#29257)
* Render reason for invite rejection.

* Add test

* extra test
2025-02-14 10:58:20 +00:00
ElementRobot
f9a85d37fa [create-pull-request] automated change (#29253)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-14 06:14:47 +00:00
Florian D
2abd5342c2 New room list: add search section (#29251)
* feat(new room list): move `RoomListView` to its own folder and add styling

* feat(new room list): add search section

* test(new room list): add tests for `RoomListSearch`

* test(new room list): add tests for `RoomListView`

* test(e2e): add method to close notification toast to `ElementAppPage`

* test(e2e): add tests for the search section

* test(e2e): add tests for the room list view

* refactor: use Flex component

* fix: loop icon size in search button

* refactor: remove `focus_room_filter` listener
2025-02-13 15:49:09 +00:00
Florian D
85f80b1d0a Replace focus_room_filter dispatch by Action.OpenSpotlight (#29259)
* refactor(room search): replace `focus_room_filter` dispatch by `Action.OpenSpotlight`

* test(LoggedInView): add test to Ctrl+k shortcut
2025-02-13 15:18:41 +00:00
Florian D
4b9382f888 New room list: hide favourites and people meta spaces (#29241)
* feat(new room list)!: hide Favourites and People meta spaces when the new room list is enabled

* test(space store): add testcase for new labs flag

* feat(quick settings): hide pin to sidebar and more options and add extra margin
2025-02-13 10:53:51 +00:00
R Midhun Suresh
03a5ee1c5b New Room List: Create new labs flag (#29239)
* Create new labs flag

* Render empty room list view

* Reload on flag change

* Rename RoomList.tsx to LegacyRoomList.tsx

and rename NewRoomListView.tsx to RoomListView.tsx

* Update labs.md
2025-02-12 12:14:18 +00:00
ElementRobot
902146a829 [create-pull-request] automated change (#29248)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-12 06:20:49 +00:00
ElementRobot
43e918b71e [create-pull-request] automated change (#29247)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-12 06:15:32 +00:00
David Baker
53c97dfa50 Don't reload roomview on offline connectivity check (#29243)
* Don't reload roomview on offline connectivity check

Doesn't look like this was a regression as far as I can see, but
you did have to switch rooms while offline for it to start happening.

There's no use reloading the room until we're online again.

Fixes https://github.com/element-hq/element-web/issues/29072

* Add regression test

* Move it down the file to avoid changing the snapshots
2025-02-11 20:34:32 +00:00
Richard van der Hoff
f7b010a0b3 Clean up unused code in CreateSecretStorageDialog (#29205)
* CreateSecretStorageDialog: remove unused state  `accountPasswordCorrect`

This was never set to anything other than `null`, and never read.

* CreateSecretStorageDialog: remove unused prop `accountPassword`

This was never set, so we may as well remove it.

* CreateSecretStorageDialog: remove unused state `accountPassword`

This is now no longer set to anything other than `""`.

* CreateSecretStorageDialog: remove unused state `canUploadKeysWithPasswordOnly`

This is no longer read, so let's remove the code that populates it.

* CreateSecretStorageDialog: remove unused prop `hasCancel`

This is never set, so may as well remove

* Update src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx
2025-02-11 15:22:01 +00:00
Michael Telatynski
161323b595 Fix permissions 2025-02-11 15:10:59 +00:00
Terence Eden
8bf3ec29b9 Stop URl preview from covering message box (#29215)
* Stop URl preview from covering message box

Fixes #23874 by adding a bit of padding.

1em should be sufficient to prevent the browser's URl preview from covering the entry box.

* test: update timeline screenshots

* test: update test, fewer messages are displayed

---------

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
Co-authored-by: Florian D <florianduros@element.io>
2025-02-11 14:51:36 +00:00
Michael Telatynski
039b95eba0 Ship Docker image also to GHCR.io (#29240)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-11 14:51:19 +00:00
RiotRobot
6b80d3fca0 Reset matrix-js-sdk back to develop branch 2025-02-11 14:35:37 +00:00
RiotRobot
784abbbe14 Merge branch 'master' into develop 2025-02-11 14:35:19 +00:00
RiotRobot
4d55e8f433 v1.11.92 2025-02-11 14:32:06 +00:00
RiotRobot
02990bd275 Upgrade dependency to matrix-js-sdk@36.2.0 2025-02-11 14:25:39 +00:00
Michael Telatynski
6fa8032caa Respect user's 12/24 hour preference consistently (#29237)
* Respect user's 12/24 hour preference consistently

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

* Update test

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-11 14:16:59 +00:00
Richard van der Hoff
67658aef56 Log when we show, and hide, encryption setup toasts (#29235) (#29238)
It's currently hard to debug when someone sees or hides one of these
toasts. Lets's add some logging.
2025-02-11 11:06:03 +00:00
Richard van der Hoff
f2fae82e32 Log when we show, and hide, encryption setup toasts (#29235)
It's currently hard to debug when someone sees or hides one of these
toasts. Lets's add some logging.
2025-02-11 09:44:48 +00:00
Robin
ef69c0ddc7 Restore the accessibility role on call views (#29225)
This was mistakenly removed in a370a5cfa4. You can tell it was unintentional because the 'role' variable was just left unused.
2025-02-11 08:28:29 +00:00
ElementRobot
bc7fe25974 [create-pull-request] automated change (#29236)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-11 06:14:04 +00:00
Florian D
426a2066d9 test(backup mas): use checkDeviceIsConnectedKeyBackup instead at looking at the Security & Privacy tab (#29146)
* test(backup mas): use `checkDeviceIsConnectedKeyBackup` instead at looking at the *Security & Privacy* tab

* doc(backup mas): fix comment
2025-02-10 17:44:56 +00:00
Florian D
047e8e8a9c Rename "security key" into "recovery key" (#29217)
* feat(crypto): rename "security key" into "recovery key" in lang file

* test(crypto): rename "security key" into "recovery key" in test files

* test(e2e crypto): rename "security key" into "recovery key" in test files

* doc(crypto): rename "security key" into "recovery key"
2025-02-10 16:52:39 +00:00
Máté Gyöngyösi
4de9fe60ae Change GoToHome keyboard shortcut to CtrlAlt/ShiftH (#28577)
Co-authored-by: Florian D <florianduros@element.io>
2025-02-10 16:16:34 +00:00
R Midhun Suresh
52b42c0b1c Add new verification section to user profile (#29200)
* Create new verification section

* Remove old code and use new VerificationSection

* Add styling and translation

* Fix tests

* Remove dead code

* Fix broken test

* Remove imports

* Remove console.log

* Update snapshots

* Fix broken tests

* Fix lint

* Make badge expand with content

* Remove unused code
2025-02-10 11:22:58 +00:00
ElementRobot
bb8b4d7991 [create-pull-request] automated change (#29224)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-10 06:19:28 +00:00
ElementRobot
ec994884fb [create-pull-request] automated change (#29218)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-08 06:13:54 +00:00
Florian D
4a8ba810a9 Encryption tab: display correct encryption panel when user cancels the reset identity flow (#29216)
* fix(encryption settings): check encryption state when user cancels the reset identity flow

* test(encryption settings): add test to check encryption state when user cancels the reset identity flow
2025-02-07 13:57:02 +00:00
Michael Telatynski
40a6c69f1a Close stale PRs after 180 days (#29212)
* Close stale PRs after 180 days

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

* Update triage-stale.yml

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-07 13:44:26 +00:00
ElementRobot
eed6b6df12 [create-pull-request] automated change (#29157)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-07 12:49:08 +00:00
Florian D
7b3ce5d9b2 e2e test: by default the bot should not use a passphrase to create the recovery key (#29214)
* test(crypto): by default do not use a passphrase to create the recovery key

* test(crypto): update tests
2025-02-07 09:39:36 +00:00
ElementRobot
8941724020 [Backport staging] Wire up the "Forgot recovery key" button for the "Key storage out of sync" toast (#29190)
* Wire up the "Forgot recovery key" button for the "Key storage out of sync" toast (#29138)

* Wire up the "Forgot recovery key" button for the "Key storage out of sync" toast

* Unused import & fix test

* Test 'forgot' variant

* Fix dependencies

* Add more toast tests

* Unused import

* Test initialState in Encryption Tab

* Let's see if github has any more luck running this test than me

* Working playwright test with screenshot

* year

* Convert playwright test to use the bot client

* Disambiguate

Co-authored-by: Florian Duros <florianduros@element.io>

* Add doc & do other part of rename

* Split out into custom hook

* Fix tests

---------

Co-authored-by: Florian Duros <florianduros@element.io>
(cherry picked from commit 9657d39cd6)

* Update fetchdep.sh to understand merge queues

---------

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-07 00:01:52 +00:00
Michael Telatynski
4a231c6450 Initial support for runtime modules (#29104)
* Initial runtime Modules work

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

* Iterate

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

* Iterate

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

* Iterate

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

* Iterate

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

* Comments

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-06 23:54:18 +00:00
R Midhun Suresh
3c690e685a Add code_style, developer_guide and fix CONTRIBUTING.md (#29210)
* Remove reference to matrix-js-sdk in code_style.md

* Absorb exisiting documentation from matrix-react-sdk

* Crete a separate developer guide

* Remove sign-off from CONTRIBUTING.md

Since sign-off is irrelevant to element-web repo with the introduction
of CLA.

* Link to the new docs in README

* Elaborate on the rule

* Fix lint
2025-02-06 11:02:53 +00:00
RiotRobot
0a8393c9e1 v1.11.92-rc.0 2025-02-04 12:47:45 +00:00
RiotRobot
0fa52e610e Upgrade dependency to matrix-js-sdk@36.2.0-rc.0 2025-02-04 12:31:52 +00:00
281 changed files with 28505 additions and 4070 deletions

5
.github/CODEOWNERS vendored
View File

@@ -11,10 +11,11 @@
/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers /src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers
/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers /test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers
/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers /src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers
/src/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers /src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers /test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
/src/components/views/dialogs/devtools/Crypto.tsx @element-hq/element-crypto-web-reviewers /src/components/views/dialogs/devtools/Crypto.tsx @element-hq/element-crypto-web-reviewers
/playwright/e2e/crypto/ @element-hq/element-crypto-web-reviewers
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
# Ignore translations as those will be updated by GHA for Localazy download # Ignore translations as those will be updated by GHA for Localazy download
/src/i18n/strings /src/i18n/strings

View File

@@ -1,4 +1,4 @@
name: Dockerhub name: Docker
on: on:
workflow_dispatch: {} workflow_dispatch: {}
push: push:
@@ -15,19 +15,20 @@ jobs:
environment: dockerhub environment: dockerhub
permissions: permissions:
id-token: write # needed for signing the images with GitHub OIDC Token id-token: write # needed for signing the images with GitHub OIDC Token
packages: write # needed for publishing packages to GHCR
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 # needed for docker-package to be able to calculate the version fetch-depth: 0 # needed for docker-package to be able to calculate the version
- name: Install Cosign - name: Install Cosign
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3 uses: sigstore/cosign-installer@c56c2d3e59e4281cc41dea2217323ba5694b171e # v3
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3 uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 # v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3 uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3
with: with:
install: true install: true
@@ -37,12 +38,20 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5 uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
with: with:
images: | images: |
vectorim/element-web vectorim/element-web
ghcr.io/element-hq/element-web
tags: | tags: |
type=ref,event=branch type=ref,event=branch
type=ref,event=tag type=ref,event=tag

View File

@@ -50,7 +50,7 @@ jobs:
permissions: permissions:
checks: read checks: read
steps: steps:
- name: Wait for dockerhub - name: Wait for docker build
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
with: with:
ref: master ref: master

View File

@@ -104,7 +104,7 @@ jobs:
- name: Skip SonarCloud in merge queue - name: Skip SonarCloud in merge queue
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true' if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
uses: guibranco/github-status-action-v2@119b3320db3f04d89e91df840844b92d57ce3468 uses: guibranco/github-status-action-v2@7ca807c2ba3401be532d29a876b93262108099fb
with: with:
authToken: ${{ secrets.GITHUB_TOKEN }} authToken: ${{ secrets.GITHUB_TOKEN }}
state: success state: success

View File

@@ -1,21 +0,0 @@
name: Close stale flaky issues
on:
workflow_dispatch: {}
schedule:
- cron: "30 1 * * *"
permissions: {}
jobs:
close:
runs-on: ubuntu-24.04
permissions:
actions: write
issues: write
steps:
- uses: actions/stale@v9
with:
only-labels: "Z-Flaky-Test"
days-before-stale: 14
days-before-close: 0
close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved."
exempt-issue-labels: "Z-Flaky-Test-Disabled"
operations-per-run: 100

27
.github/workflows/triage-stale.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Close stale issues & PRs
on:
workflow_dispatch: {}
schedule:
- cron: "30 1 * * *"
permissions: {}
jobs:
close:
runs-on: ubuntu-24.04
permissions:
actions: write
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
operations-per-run: 100
# Flaky test issue closing
only-issue-labels: "Z-Flaky-Test"
days-before-issue-stale: 14
days-before-issue-close: 0
close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved."
exempt-issue-labels: "Z-Flaky-Test-Disabled"
# Stale PR closing
days-before-pr-stale: 180
days-before-pr-close: 0
close-pr-message: "This PR has been automatically closed because it has been stale for 180 days. If you wish to continue working on this PR, please ping a maintainer to reopen it."

View File

@@ -1,3 +1,36 @@
Changes in [1.11.92](https://github.com/element-hq/element-web/releases/tag/v1.11.92) (2025-02-11)
==================================================================================================
## ✨ Features
* [Backport staging] Log when we show, and hide, encryption setup toasts ([#29238](https://github.com/element-hq/element-web/pull/29238)). Contributed by @richvdh.
* Make profile header section match the designs ([#29163](https://github.com/element-hq/element-web/pull/29163)). Contributed by @MidhunSureshR.
* Always show back button in the right panel ([#29128](https://github.com/element-hq/element-web/pull/29128)). Contributed by @MidhunSureshR.
* Schedule dehydration on reload if the dehydration key is already cached locally ([#29021](https://github.com/element-hq/element-web/pull/29021)). Contributed by @uhoreg.
* update to twemoji 15.1.0 ([#29115](https://github.com/element-hq/element-web/pull/29115)). Contributed by @ara4n.
* Update matrix-widget-api ([#29112](https://github.com/element-hq/element-web/pull/29112)). Contributed by @toger5.
* Allow navigating through the memberlist using up/down keys ([#28949](https://github.com/element-hq/element-web/pull/28949)). Contributed by @MidhunSureshR.
* Style room header icons and facepile for toggled state ([#28968](https://github.com/element-hq/element-web/pull/28968)). Contributed by @MidhunSureshR.
* Move threads header below base card header ([#28969](https://github.com/element-hq/element-web/pull/28969)). Contributed by @MidhunSureshR.
* Add `Advanced` section to the user settings encryption tab ([#28804](https://github.com/element-hq/element-web/pull/28804)). Contributed by @florianduros.
* Fix outstanding UX issues with replies/mentions/keyword notifs ([#28270](https://github.com/element-hq/element-web/pull/28270)). Contributed by @taffyko.
* Distinguish room state and timeline events when dealing with widgets ([#28681](https://github.com/element-hq/element-web/pull/28681)). Contributed by @robintown.
* Switch OIDC primarily to new `/auth_metadata` API ([#29019](https://github.com/element-hq/element-web/pull/29019)). Contributed by @t3chguy.
* More memberlist changes ([#29069](https://github.com/element-hq/element-web/pull/29069)). Contributed by @MidhunSureshR.
## 🐛 Bug Fixes
* [Backport staging] Wire up the "Forgot recovery key" button for the "Key storage out of sync" toast ([#29190](https://github.com/element-hq/element-web/pull/29190)). Contributed by @RiotRobot.
* Encryption tab: hide `Advanced` section when the key storage is out of sync ([#29129](https://github.com/element-hq/element-web/pull/29129)). Contributed by @florianduros.
* Fix share button in discovery settings being disabled incorrectly ([#29151](https://github.com/element-hq/element-web/pull/29151)). Contributed by @t3chguy.
* Ensure switching rooms does not wrongly focus timeline search ([#29153](https://github.com/element-hq/element-web/pull/29153)). Contributed by @t3chguy.
* Stop showing a dialog prompting the user to enter an old recovery key ([#29143](https://github.com/element-hq/element-web/pull/29143)). Contributed by @richvdh.
* Make themed widgets reflect the effective theme ([#28342](https://github.com/element-hq/element-web/pull/28342)). Contributed by @robintown.
* support non-VS16 emoji ligatures in TwemojiMozilla ([#29100](https://github.com/element-hq/element-web/pull/29100)). Contributed by @ara4n.
* e2e test: Verify session with the encryption tab instead of the security \& privacy tab ([#29090](https://github.com/element-hq/element-web/pull/29090)). Contributed by @florianduros.
* Work around cloudflare R2 / aws client incompatability ([#29086](https://github.com/element-hq/element-web/pull/29086)). Contributed by @dbkr.
* Fix identity server settings visibility ([#29083](https://github.com/element-hq/element-web/pull/29083)). Contributed by @dbkr.
Changes in [1.11.91](https://github.com/element-hq/element-web/releases/tag/v1.11.91) (2025-01-28) Changes in [1.11.91](https://github.com/element-hq/element-web/releases/tag/v1.11.91) (2025-01-28)
================================================================================================== ==================================================================================================
## ✨ Features ## ✨ Features

View File

@@ -189,89 +189,6 @@ give away to contributors - if you feel that Matrix-branded apparel is missing
from your life, please mail us your shipping address to matrix at matrix.org from your life, please mail us your shipping address to matrix at matrix.org
and we'll try to fix it :) and we'll try to fix it :)
## Sign off
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've
adopted the same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/html/latest/process/submitting-patches.html), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
http://developercertificate.org/). This is a simple declaration that you wrote
the contribution or otherwise have the right to contribute it to Matrix:
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
If you agree to this for your contribution, then all that's needed is to
include the line in your commit or pull request comment:
```
Signed-off-by: Your Name <your@email.example.org>
```
We accept contributions under a legally identifiable name, such as your name on
government documentation or common-law names (names claimed by legitimate usage
or repute). Unfortunately, we cannot accept anonymous contributions at this
time.
Git allows you to add this signoff automatically when using the `-s` flag to
`git commit`, which uses the name and email set in your `user.name` and
`user.email` git configs.
If you forgot to sign off your commits before making your pull request and are
on Git 2.17+ you can mass signoff using rebase:
```
git rebase --signoff origin/develop
```
## Private sign off
If you would like to provide your legal name privately to the Matrix.org
Foundation (instead of in a public commit or comment), you can do so by emailing
your legal name and a link to the pull request to dco@matrix.org. It helps to
include "sign off" or similar in the subject line. You will then be instructed
further.
Once private sign off is complete, doing so for future contributions will not
be required.
# Review expectations # Review expectations
See https://github.com/element-hq/element-meta/wiki/Review-process See https://github.com/element-hq/element-meta/wiki/Review-process

120
README.md
View File

@@ -182,123 +182,11 @@ Dockerfile.
# Development # Development
Before attempting to develop on Element you **must** read the [developer guide Please read through the following:
for `matrix-react-sdk`](https://github.com/matrix-org/matrix-react-sdk#developer-guide), which
also defines the design, architecture and style for Element too.
Read the [Choosing an issue](docs/choosing-an-issue.md) page for some guidance 1. [Developer guide](./developer_guide.md)
about where to start. Before starting work on a feature, it's best to ensure 2. [Code style](./code_style.md)
your plan aligns well with our vision for Element. Please chat with the team in 3. [Contribution guide](./CONTRIBUTING.md)
[#element-dev:matrix.org](https://matrix.to/#/#element-dev:matrix.org) before
you start so we can ensure it's something we'd be willing to merge.
You should also familiarise yourself with the ["Here be Dragons" guide
](https://docs.google.com/document/d/12jYzvkidrp1h7liEuLIe6BMdU0NUjndUYI971O06ooM)
to the tame & not-so-tame dragons (gotchas) which exist in the codebase.
The idea of Element is to be a relatively lightweight "skin" of customisations on
top of the underlying `matrix-react-sdk`. `matrix-react-sdk` provides both the
higher and lower level React components useful for building Matrix communication
apps using React.
Please note that Element is intended to run correctly without access to the public
internet. So please don't depend on resources (JS libs, CSS, images, fonts)
hosted by external CDNs or servers but instead please package all dependencies
into Element itself.
# Setting up a dev environment
Much of the functionality in Element is actually in the `matrix-js-sdk` module.
It is possible to set these up in a way that makes it easy to track the `develop` branches
in git and to make local changes without having to manually rebuild each time.
First clone and build `matrix-js-sdk`:
```bash
git clone https://github.com/matrix-org/matrix-js-sdk.git
pushd matrix-js-sdk
yarn link
yarn install
popd
```
Clone the repo and switch to the `element-web` directory:
```bash
git clone https://github.com/element-hq/element-web.git
cd element-web
```
Configure the app by copying `config.sample.json` to `config.json` and
modifying it. See the [configuration docs](docs/config.md) for details.
Finally, build and start Element itself:
```bash
yarn link matrix-js-sdk
yarn install
yarn start
```
Wait a few seconds for the initial build to finish; you should see something like:
```
[element-js] <s> [webpack.Progress] 100%
[element-js]
[element-js] 「wdm」: 1840 modules
[element-js] 「wdm」: Compiled successfully.
```
Remember, the command will not terminate since it runs the web server
and rebuilds source files when they change. This development server also
disables caching, so do NOT use it in production.
Open <http://127.0.0.1:8080/> in your browser to see your newly built Element.
**Note**: The build script uses inotify by default on Linux to monitor directories
for changes. If the inotify limits are too low your build will fail silently or with
`Error: EMFILE: too many open files`. To avoid these issues, we recommend a watch limit
of at least `128M` and instance limit around `512`.
You may be interested in issues [#15750](https://github.com/element-hq/element-web/issues/15750) and
[#15774](https://github.com/element-hq/element-web/issues/15774) for further details.
To set a new inotify watch and instance limit, execute:
```
sudo sysctl fs.inotify.max_user_watches=131072
sudo sysctl fs.inotify.max_user_instances=512
sudo sysctl -p
```
If you wish, you can make the new limits permanent, by executing:
```
echo fs.inotify.max_user_watches=131072 | sudo tee -a /etc/sysctl.conf
echo fs.inotify.max_user_instances=512 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
```
---
When you make changes to `matrix-js-sdk` they should be automatically picked up by webpack and built.
If any of these steps error with, `file table overflow`, you are probably on a mac
which has a very low limit on max open files. Run `ulimit -Sn 1024` and try again.
You'll need to do this in each new terminal you open before building Element.
## Running the tests
There are a number of application-level tests in the `tests` directory; these
are designed to run with Jest and JSDOM. To run them
```
yarn test
```
### End-to-End tests
See [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/#end-to-end-tests) for how to run the end-to-end tests.
# Translations # Translations

View File

@@ -5,15 +5,6 @@ adjacent to. As of writing, these are:
- element-desktop - element-desktop
- element-web - element-web
- matrix-js-sdk
Other projects might extend this code style for increased strictness. For example, matrix-events-sdk
has stricter code organization to reduce the maintenance burden. These projects will declare their code
style within their own repos.
Note that some requirements will be layer-specific. Where the requirements don't make sense for the
project, they are used to the best of their ability, used in spirit, or ignored if not applicable,
in that order.
## Guiding principles ## Guiding principles
@@ -234,17 +225,19 @@ Unless otherwise specified, the following applies to all code:
Inheriting all the rules of TypeScript, the following additionally apply: Inheriting all the rules of TypeScript, the following additionally apply:
1. Types for lifecycle functions are not required (render, componentDidMount, and so on). 1. Component source files are named with upper camel case (e.g. views/rooms/EventTile.js)
2. Class components must always have a `Props` interface declared immediately above them. It can be 2. They are organised in a typically two-level hierarchy - first whether the component is a view or a structure, and then a broad functional grouping (e.g. 'rooms' here)
3. Types for lifecycle functions are not required (render, componentDidMount, and so on).
4. Class components must always have a `Props` interface declared immediately above them. It can be
empty if the component accepts no props. empty if the component accepts no props.
3. Class components should have an `State` interface declared immediately above them, but after `Props`. 5. Class components should have an `State` interface declared immediately above them, but after `Props`.
4. Props and State should not be exported. Use `React.ComponentProps<typeof ComponentNameHere>` 6. Props and State should not be exported. Use `React.ComponentProps<typeof ComponentNameHere>`
instead. instead.
5. One component per file, except when a component is a utility component specifically for the "primary" 7. One component per file, except when a component is a utility component specifically for the "primary"
component. The utility component should not be exported. component. The utility component should not be exported.
6. Exported constants, enums, interfaces, functions, etc must be separate from files containing components 8. Exported constants, enums, interfaces, functions, etc must be separate from files containing components
or stores. or stores.
7. Stores should use a singleton pattern with a static instance property: 9. Stores should use a singleton pattern with a static instance property:
```typescript ```typescript
class FooStore { class FooStore {
@@ -261,44 +254,41 @@ Inheriting all the rules of TypeScript, the following additionally apply:
} }
``` ```
8. Stores must support using an alternative MatrixClient and dispatcher instance. 10. Stores must support using an alternative MatrixClient and dispatcher instance.
9. Utilities which require JSX must be split out from utilities which do not. This is to prevent import 11. Utilities which require JSX must be split out from utilities which do not. This is to prevent import
cycles during runtime where components accidentally include more of the app than they intended. cycles during runtime where components accidentally include more of the app than they intended.
10. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities 12. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities
if at all possible. if at all possible.
11. A component should only use CSS class names in line with the component name. 13. A component should only use CSS class names in line with the component name.
1. When knowingly using a class name from another component, document it with a [comment](#comments). 1. When knowingly using a class name from another component, document it with a [comment](#comments).
12. Curly braces within JSX should be padded with a space, however properties on those components should not. 14. Curly braces within JSX should be padded with a space, however properties on those components should not.
See above code example. See above code example.
13. Functions used as properties should either be defined on the class or stored in a variable. They should not 15. Functions used as properties should either be defined on the class or stored in a variable. They should not
be inline unless mocking/short-circuiting the value. be inline unless mocking/short-circuiting the value.
14. Prefer hooks (functional components) over class components. Be consistent with the existing area if unsure 16. Prefer hooks (functional components) over class components. Be consistent with the existing area if unsure
which should be used. which should be used.
1. Unless the component is considered a "structure", in which case use classes. 1. Unless the component is considered a "structure", in which case use classes.
15. Write more views than structures. Structures are chunks of functionality like MatrixChat while views are 17. Write more views than structures. Structures are chunks of functionality like MatrixChat while views are
isolated components. isolated components.
16. Components should serve a single, or near-single, purpose. 18. Components should serve a single, or near-single, purpose.
17. Prefer to derive information from component properties rather than establish state. 19. Prefer to derive information from component properties rather than establish state.
18. Do not use `React.Component::forceUpdate`. 20. Do not use `React.Component::forceUpdate`.
## Stylesheets (\*.pcss = PostCSS + Plugins) ## Stylesheets (\*.pcss = PostCSS + Plugins)
Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, but actually it is not. Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, but actually it is not.
1. Class names must be prefixed with "mx\_". 1. The view's CSS file MUST have the same name as the component (e.g. `view/rooms/_MessageTile.css` for `MessageTile.tsx` component).
2. Class names must denote the component which defines them, followed by any context. 2. Per-view CSS is optional - it could choose to inherit all its styling from the context of the rest of the app, although this is unusual.
The context is not further specified here in terms of meaning or syntax. 3. Class names must be prefixed with "mx\_".
Use whatever is appropriate for your implementation use case. 4. Class names must strictly denote the component which defines them.
Some examples: For example: `mx_MyFoo` for `MyFoo` component.
1. `mx_MyFoo` 5. Class names for DOM elements within a view which aren't components are named by appending a lower camel case identifier to the view's class name - e.g. .mx_MyFoo_randomDiv is how you'd name the class of an arbitrary div within the MyFoo view.
2. `mx_MyFoo_avatar` 6. Use the `$font` variables instead of manual values.
3. `mx_MyFoo_avatarUser` 7. Keep indentation/nesting to a minimum. Maximum suggested nesting is 5 layers.
4. `mx_MyFoo_avatar--user` 8. Use the whole class name instead of shortcuts:
3. Use the `$font` variables instead of manual values.
4. Keep indentation/nesting to a minimum. Maximum suggested nesting is 5 layers.
5. Use the whole class name instead of shortcuts:
```scss ```scss
.mx_MyFoo { .mx_MyFoo {
@@ -309,7 +299,7 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
} }
``` ```
6. Break multiple selectors over multiple lines this way: 9. Break multiple selectors over multiple lines this way:
```scss ```scss
.mx_MyFoo, .mx_MyFoo,
@@ -319,9 +309,9 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
} }
``` ```
7. Non-shared variables should use $lowerCamelCase. Shared variables use $dashed-naming. 10. Non-shared variables should use $lowerCamelCase. Shared variables use $dashed-naming.
8. Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be 11. Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be
[documented](#comments) for what the values mean: [documented](#comments) for what the values mean:
```scss ```scss
.mx_MyFoo { .mx_MyFoo {
@@ -331,7 +321,9 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
} }
``` ```
9. Avoid the use of `!important`. If `!important` is necessary, add a [comment](#comments) explaining why. 12. Avoid the use of `!important`. If `!important` is necessary, add a [comment](#comments) explaining why.
13. The CSS for a component can override the rules for child components. For instance, .mxRoomList .mx_RoomTile {} would be the selector to override styles of RoomTiles when viewed in the context of a RoomList view. Overrides must be scoped to the View's CSS class - i.e. don't just define .mx_RoomTile {} in RoomList.css - only RoomTile.css is allowed to define its own CSS. Instead, say .mx_RoomList .mx_RoomTile {} to scope the override only to the context of RoomList views. N.B. overrides should be relatively rare as in general CSS inheritance should be enough.
14. Components should render only within the bounding box of their outermost DOM element. Page-absolute positioning and negative CSS margins and similar are generally not cool and stop the component from being reused easily in different places.
## Tests ## Tests

126
developer_guide.md Normal file
View File

@@ -0,0 +1,126 @@
# Developer Guide
## Development
Read the [Choosing an issue](docs/choosing-an-issue.md) page for some guidance
about where to start. Before starting work on a feature, it's best to ensure
your plan aligns well with our vision for Element. Please chat with the team in
[#element-dev:matrix.org](https://matrix.to/#/#element-dev:matrix.org) before
you start so we can ensure it's something we'd be willing to merge.
You should also familiarise yourself with the ["Here be Dragons" guide
](https://docs.google.com/document/d/12jYzvkidrp1h7liEuLIe6BMdU0NUjndUYI971O06ooM)
to the tame & not-so-tame dragons (gotchas) which exist in the codebase.
Please note that Element is intended to run correctly without access to the public
internet. So please don't depend on resources (JS libs, CSS, images, fonts)
hosted by external CDNs or servers but instead please package all dependencies
into Element itself.
## Setting up a dev environment
Much of the functionality in Element is actually in the `matrix-js-sdk` module.
It is possible to set these up in a way that makes it easy to track the `develop` branches
in git and to make local changes without having to manually rebuild each time.
First clone and build `matrix-js-sdk`:
```bash
git clone https://github.com/matrix-org/matrix-js-sdk.git
pushd matrix-js-sdk
yarn link
yarn install
popd
```
Clone the repo and switch to the `element-web` directory:
```bash
git clone https://github.com/element-hq/element-web.git
cd element-web
```
Configure the app by copying `config.sample.json` to `config.json` and
modifying it. See the [configuration docs](docs/config.md) for details.
Finally, build and start Element itself:
```bash
yarn link matrix-js-sdk
yarn install
yarn start
```
Wait a few seconds for the initial build to finish; you should see something like:
```
[element-js] <s> [webpack.Progress] 100%
[element-js]
[element-js] 「wdm」: 1840 modules
[element-js] 「wdm」: Compiled successfully.
```
Remember, the command will not terminate since it runs the web server
and rebuilds source files when they change. This development server also
disables caching, so do NOT use it in production.
Open <http://127.0.0.1:8080/> in your browser to see your newly built Element.
**Note**: The build script uses inotify by default on Linux to monitor directories
for changes. If the inotify limits are too low your build will fail silently or with
`Error: EMFILE: too many open files`. To avoid these issues, we recommend a watch limit
of at least `128M` and instance limit around `512`.
You may be interested in issues [#15750](https://github.com/element-hq/element-web/issues/15750) and
[#15774](https://github.com/element-hq/element-web/issues/15774) for further details.
To set a new inotify watch and instance limit, execute:
```
sudo sysctl fs.inotify.max_user_watches=131072
sudo sysctl fs.inotify.max_user_instances=512
sudo sysctl -p
```
If you wish, you can make the new limits permanent, by executing:
```
echo fs.inotify.max_user_watches=131072 | sudo tee -a /etc/sysctl.conf
echo fs.inotify.max_user_instances=512 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
```
---
When you make changes to `matrix-js-sdk` they should be automatically picked up by webpack and built.
If any of these steps error with, `file table overflow`, you are probably on a mac
which has a very low limit on max open files. Run `ulimit -Sn 1024` and try again.
You'll need to do this in each new terminal you open before building Element.
## Running the tests
There are a number of application-level tests in the `tests` directory; these
are designed to run with Jest and JSDOM. To run them
```
yarn test
```
### End-to-End tests
See [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/#end-to-end-tests) for how to run the end-to-end tests.
## General github guidelines
1. **Pull requests must only be filed against the `develop` branch.**
2. Try to keep your pull requests concise. Split them up if necessary.
3. Ensure that you provide a description that explains the fix/feature and its intent.
## Adding new code
New code should be committed as follows:
- All new components: https://github.com/element-hq/element-web/tree/develop/src/components
- CSS: https://github.com/element-hq/element-web/tree/develop/res/css
- Theme specific CSS & resources: https://github.com/element-hq/element-web/tree/develop/res/themes

67
docs/MVVM.md Normal file
View File

@@ -0,0 +1,67 @@
# MVVM
General description of the pattern can be found [here](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel). But the gist of it is that you divide your code into three sections:
1. Model: This is where the business logic and data resides.
2. View Model: This code exists to provide the logic necessary for the UI. It directly uses the Model code.
3. View: This is the UI code itself and depends on the view model.
If you do MVVM right, your view should be dumb i.e it gets data from the view model and merely displays it.
### Practical guidelines for MVVM in element-web
#### Model
This is anywhere your data or business logic comes from. If your view model is accessing something simple exposed from `matrix-js-sdk`, then the sdk is your model. If you're using something more high level in element-web to get your data/logic (eg: `MemberListStore`), then that becomes your model.
#### View Model
1. View model is always a custom react hook named like `useFooViewModel()`.
2. The return type of your view model (known as view state) must be defined as a typescript interface:
```ts
inteface FooViewState {
somethingUseful: string;
somethingElse: BarType;
update: () => Promise<void>
...
}
```
3. Any react state that your UI needs must be in the view model.
#### View
1. Views are simple react components (eg: `FooView`).
2. Views usually start by calling the view model hook, eg:
```tsx
const FooView: React.FC<IProps> = (props: IProps) => {
const vm = useFooViewModel();
....
return(
<div>
{vm.somethingUseful}
</div>
);
}
```
3. Views are also allowed to accept the view model as a prop, eg:
```tsx
const FooView: React.FC<IProps> = ({ vm }: IProps) => {
....
return(
<div>
{vm.somethingUseful}
</div>
);
}
```
4. Multiple views can share the same view model if necessary.
### Benefits
1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
### Example
We started experimenting with MVVM in the redesigned memberlist, you can see the code [here](https://github.com/vector-im/element-web/blob/develop/src/components/views/rooms/MemberList/MemberListView.tsx).

View File

@@ -155,7 +155,7 @@ complete re-branding/private labeling, a more personalised experience can be ach
3. `show_once`: Optional. If true then the notice will only be shown once per device. 3. `show_once`: Optional. If true then the notice will only be shown once per device.
18. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`. 18. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`.
19. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`. 19. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`.
20. `force_verification`: If true, users must verify new logins (eg. with another device / their security key) 20. `force_verification`: If true, users must verify new logins (eg. with another device / their recovery key)
### `desktop_builds` and `mobile_builds` ### `desktop_builds` and `mobile_builds`
@@ -163,14 +163,14 @@ These two options describe the various availability for the application. When th
such as trying to get the user to use an Android app or the desktop app for encrypted search, the config options will be looked such as trying to get the user to use an Android app or the desktop app for encrypted search, the config options will be looked
at to see if the link should be to somewhere else. at to see if the link should be to somewhere else.
Starting with `desktop_builds`, the following subproperties are available: Starting with `desktop_builds`, the following sub-properties are available:
1. `available`: Required. When `true`, the desktop app can be downloaded from somewhere. 1. `available`: Required. When `true`, the desktop app can be downloaded from somewhere.
2. `logo`: Required. A URL to a logo (SVG), intended to be shown at 24x24 pixels. 2. `logo`: Required. A URL to a logo (SVG), intended to be shown at 24x24 pixels.
3. `url`: Required. The download URL for the app. This is used as a hyperlink. 3. `url`: Required. The download URL for the app. This is used as a hyperlink.
4. `url_macos`: Optional. Direct link to download macOS desktop app. 4. `url_macos`: Optional. Direct link to download macOS desktop app.
5. `url_win32`: Optional. Direct link to download Windows 32-bit desktop app. 5. `url_win64`: Optional. Direct link to download Windows x86 64-bit desktop app.
6. `url_win64`: Optional. Direct link to download Windows 64-bit desktop app. 6. `url_win64arm`: Optional. Direct link to download Windows ARM 64-bit desktop app.
7. `url_linux`: Optional. Direct link to download Linux desktop app. 7. `url_linux`: Optional. Direct link to download Linux desktop app.
When `desktop_builds` is not specified at all, the app will assume desktop downloads are available from https://element.io When `desktop_builds` is not specified at all, the app will assume desktop downloads are available from https://element.io
@@ -592,3 +592,4 @@ The following are undocumented or intended for developer use only.
2. `sync_timeline_limit` 2. `sync_timeline_limit`
3. `dangerously_allow_unsafe_and_insecure_passwords` 3. `dangerously_allow_unsafe_and_insecure_passwords`
4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled. 4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled.
5. `modules`: An optional list of modules to load. This is used for testing and development purposes only.

View File

@@ -112,3 +112,7 @@ Unreliable in encrypted rooms.
## Knock rooms (`feature_ask_to_join`) [In Development] ## Knock rooms (`feature_ask_to_join`) [In Development]
Enables knock feature for rooms. This allows users to ask to join a room. Enables knock feature for rooms. This allows users to ask to join a room.
## New room list (`feature_new_room_list`) [In Development]
Enable the new room list that is currently in development.

View File

@@ -128,7 +128,7 @@ flowchart TD
subgraph Deploying subgraph Deploying
D1[\Deploy staging.element.io/] D1[\Deploy staging.element.io/]
D2[\Check dockerhub/] D2[\Check docker build/]
D3[\Deploy app.element.io/] D3[\Deploy app.element.io/]
D4[\Check desktop package/] D4[\Check desktop package/]
@@ -213,10 +213,10 @@ switched back to the version of the dependency from the master branch to not lea
# Deploying # Deploying
We ship the SDKs to npm, this happens as part of the release process. We ship the SDKs to npm, this happens as part of the release process.
We ship Element Web to dockerhub, `*.element.io`, and packages.element.io. We ship Element Web to dockerhub, ghcr.io, `*.element.io`, and packages.element.io.
We ship Element Desktop to packages.element.io. We ship Element Desktop to packages.element.io.
- [ ] Check that element-web has shipped to dockerhub - [ ] Check that element-web has shipped to dockerhub & ghcr.io
- [ ] Check that the staging [deployment](https://github.com/element-hq/element-web/actions/workflows/deploy.yml) has completed successfully - [ ] Check that the staging [deployment](https://github.com/element-hq/element-web/actions/workflows/deploy.yml) has completed successfully
- [ ] Test staging.element.io - [ ] Test staging.element.io

View File

@@ -38,6 +38,8 @@ const config: Config = {
"^!!raw-loader!.*": "jest-raw-loader", "^!!raw-loader!.*": "jest-raw-loader",
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js", "recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock", "^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
// Requires ESM which is incompatible with our current Jest setup
"^@element-hq/element-web-module-api$": "<rootDir>/__mocks__/empty.js",
}, },
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"], transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
collectCoverageFrom: [ collectCoverageFrom: [

View File

@@ -74,12 +74,13 @@
"@types/react-dom": "18.3.5", "@types/react-dom": "18.3.5",
"oidc-client-ts": "3.1.0", "oidc-client-ts": "3.1.0",
"jwt-decode": "4.0.0", "jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001697", "caniuse-lite": "1.0.30001699",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0" "wrap-ansi": "npm:wrap-ansi@^7.0.0"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@element-hq/element-web-module-api": "^0.1.1",
"@fontsource/inconsolata": "^5", "@fontsource/inconsolata": "^5",
"@fontsource/inter": "^5", "@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7", "@formatjs/intl-segmenter": "^11.5.7",
@@ -87,7 +88,7 @@
"@matrix-org/emojibase-bindings": "^1.3.4", "@matrix-org/emojibase-bindings": "^1.3.4",
"@matrix-org/react-sdk-module-api": "^2.4.0", "@matrix-org/react-sdk-module-api": "^2.4.0",
"@matrix-org/spec": "^1.7.0", "@matrix-org/spec": "^1.7.0",
"@sentry/browser": "^8.0.0", "@sentry/browser": "^9.0.0",
"@types/png-chunks-extract": "^1.0.2", "@types/png-chunks-extract": "^1.0.2",
"@types/react-virtualized": "^9.21.30", "@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^3.0.0", "@vector-im/compound-design-tokens": "^3.0.0",
@@ -273,7 +274,7 @@
"postcss-preset-env": "^10.0.0", "postcss-preset-env": "^10.0.0",
"postcss-scss": "^4.0.4", "postcss-scss": "^4.0.4",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"prettier": "3.4.2", "prettier": "3.5.1",
"process": "^0.11.10", "process": "^0.11.10",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"rimraf": "^6.0.0", "rimraf": "^6.0.0",

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/playwright:v1.49.1-noble FROM mcr.microsoft.com/playwright:v1.50.1-noble
WORKDIR /work WORKDIR /work

View File

@@ -11,6 +11,7 @@ import { registerAccountMas } from "../oidc";
import { isDendrite } from "../../plugins/homeserver/dendrite"; import { isDendrite } from "../../plugins/homeserver/dendrite";
import { TestClientServerAPI } from "../csAPI"; import { TestClientServerAPI } from "../csAPI";
import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts"; import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts";
import { checkDeviceIsConnectedKeyBackup } from "./utils";
// These tests register an account with MAS because then we go through the "normal" registration flow // These tests register an account with MAS because then we go through the "normal" registration flow
// and crypto gets set up. Using the 'user' fixture create a user and synthesizes an existing login, // and crypto gets set up. Using the 'user' fixture create a user and synthesizes an existing login,
@@ -24,8 +25,11 @@ test.describe("Encryption state after registration", () => {
await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!"); await registerAccountMas(page, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
await app.settings.openUserSettings("Security & Privacy"); // Wait for the ui to load
await expect(page.getByText("This session is backing up your keys.")).toBeVisible(); await expect(page.locator(".mx_MatrixChat")).toBeVisible();
// Recovery is not set up yet
await checkDeviceIsConnectedKeyBackup(app, "1", true, false);
}); });
test("user is prompted to set up recovery", async ({ page, mailpitClient, app }, testInfo) => { test("user is prompted to set up recovery", async ({ page, mailpitClient, app }, testInfo) => {

View File

@@ -58,8 +58,8 @@ test.describe("Backups", () => {
// Create another // Create another
await securityTab.getByRole("button", { name: "Set up", exact: true }).click(); await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible(); await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
await currentDialogLocator.getByLabel("Security Key").fill(securityKey); await currentDialogLocator.getByLabel("Recovery Key").fill(securityKey);
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click(); await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
// Should be successful // Should be successful
@@ -90,8 +90,8 @@ test.describe("Backups", () => {
// Try to create another // Try to create another
await securityTab.getByRole("button", { name: "Set up", exact: true }).click(); await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible(); await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
// But cancel the security key dialog, to simulate not having the secret storage passphrase // But cancel the recovery key dialog, to simulate not having the secret storage passphrase
await currentDialogLocator.getByTestId("dialog-cancel-button").click(); await currentDialogLocator.getByTestId("dialog-cancel-button").click();
await expect(currentDialogLocator.getByRole("heading", { name: "Starting backup…" })).toBeVisible(); await expect(currentDialogLocator.getByRole("heading", { name: "Starting backup…" })).toBeVisible();

View File

@@ -186,7 +186,7 @@ test.describe("Cryptography", function () {
await page.getByRole("button", { name: "Clear cross-signing keys" }).click(); await page.getByRole("button", { name: "Clear cross-signing keys" }).click();
// Enter the 4S key // Enter the 4S key
await page.getByPlaceholder("Security Key").fill(secretStorageKey); await page.getByPlaceholder("Recovery Key").fill(secretStorageKey);
await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Continue" }).click();
// Enter the password // Enter the password

View File

@@ -6,21 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { type Locator, type Page } from "@playwright/test";
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
import { viewRoomSummaryByName } from "../right-panel/utils";
import { isDendrite } from "../../plugins/homeserver/dendrite"; import { isDendrite } from "../../plugins/homeserver/dendrite";
import { completeCreateSecretStorageDialog, createBot, logIntoElement } from "./utils.ts"; import { completeCreateSecretStorageDialog, createBot, logIntoElement } from "./utils.ts";
import { type Client } from "../../pages/client.ts"; import { type Client } from "../../pages/client.ts";
import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
const ROOM_NAME = "Test room";
const NAME = "Alice"; const NAME = "Alice";
function getMemberTileByName(page: Page, name: string): Locator {
return page.locator(`.mx_MemberTileView, [title="${name}"]`);
}
test.use({ test.use({
displayName: NAME, displayName: NAME,
synapseConfig: { synapseConfig: {
@@ -57,36 +50,40 @@ test.describe("Dehydration", () => {
await completeCreateSecretStorageDialog(page); await completeCreateSecretStorageDialog(page);
// Open the settings again await expectDehydratedDeviceEnabled(app);
await app.settings.openUserSettings("Security & Privacy");
// The Security tab should indicate that there is a dehydrated device present
await expect(securityTab.getByText("Offline device enabled")).toBeVisible();
await app.settings.closeDialog();
// the dehydrated device gets created with the name "Dehydrated // the dehydrated device gets created with the name "Dehydrated
// device". We want to make sure that it is not visible as a normal // device". We want to make sure that it is not visible as a normal
// device. // device.
const sessionsTab = await app.settings.openUserSettings("Sessions"); const sessionsTab = await app.settings.openUserSettings("Sessions");
await expect(sessionsTab.getByText("Dehydrated device")).not.toBeVisible(); await expect(sessionsTab.getByText("Dehydrated device")).not.toBeVisible();
});
test("'Set up recovery' creates dehydrated device", async ({ app, credentials, page }) => {
await logIntoElement(page, credentials);
const settingsDialogLocator = await app.settings.openUserSettings("Encryption");
await settingsDialogLocator.getByRole("button", { name: "Set up recovery" }).click();
// First it displays an informative panel about the recovery key
await expect(settingsDialogLocator.getByRole("heading", { name: "Set up recovery" })).toBeVisible();
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
// Next, it displays the new recovery key. We click on the copy button.
await expect(settingsDialogLocator.getByText("Save your recovery key somewhere safe")).toBeVisible();
await settingsDialogLocator.getByRole("button", { name: "Copy" }).click();
const recoveryKey = await app.getClipboard();
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
await expect(
settingsDialogLocator.getByText("Enter your recovery key to confirm", { exact: true }),
).toBeVisible();
await settingsDialogLocator.getByRole("textbox").fill(recoveryKey);
await settingsDialogLocator.getByRole("button", { name: "Finish set up" }).click();
await app.settings.closeDialog(); await app.settings.closeDialog();
// now check that the user info right-panel shows the dehydrated device await expectDehydratedDeviceEnabled(app);
// as a feature rather than as a normal device
await app.client.createRoom({ name: ROOM_NAME });
await viewRoomSummaryByName(page, app, ROOM_NAME);
await page.locator(".mx_RightPanel").getByRole("menuitem", { name: "People" }).click();
await expect(page.locator(".mx_MemberListView")).toBeVisible();
await getMemberTileByName(page, NAME).click();
await page.locator(".mx_UserInfo_devices .mx_UserInfo_expand").click();
await expect(page.locator(".mx_UserInfo_devices").getByText("Offline device enabled")).toBeVisible();
await expect(page.locator(".mx_UserInfo_devices").getByText("Dehydrated device")).not.toBeVisible();
}); });
test("Reset recovery key during login re-creates dehydrated device", async ({ test("Reset recovery key during login re-creates dehydrated device", async ({
@@ -134,3 +131,16 @@ async function getDehydratedDeviceIds(client: Client): Promise<string[]> {
); );
}); });
} }
/** Wait for our user to have a dehydrated device */
async function expectDehydratedDeviceEnabled(app: ElementAppPage): Promise<void> {
// It might be nice to do this via the UI, but currently this info is not exposed via the UI.
//
// Note we might have to wait for the device list to be refreshed, so we wrap in `expect.poll`.
await expect
.poll(async () => {
const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client);
return dehydratedDeviceIds.length;
})
.toEqual(1);
}

View File

@@ -29,7 +29,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
let expectedBackupVersion: string; let expectedBackupVersion: string;
test.beforeEach(async ({ page, homeserver, credentials }) => { test.beforeEach(async ({ page, homeserver, credentials }) => {
const res = await createBot(page, homeserver, credentials); const res = await createBot(page, homeserver, credentials, true);
aliceBotClient = res.botClient; aliceBotClient = res.botClient;
expectedBackupVersion = res.expectedBackupVersion; expectedBackupVersion = res.expectedBackupVersion;
}); });
@@ -119,7 +119,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
await logIntoElement(page, credentials); await logIntoElement(page, credentials);
// Select the security phrase // Select the security phrase
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key or Phrase" }).click(); await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key or Phrase" }).click();
// Fill the passphrase // Fill the passphrase
const dialog = page.locator(".mx_Dialog"); const dialog = page.locator(".mx_Dialog");
@@ -136,15 +136,15 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true); await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
}); });
test("Verify device with Security Key during login", async ({ page, app, credentials, homeserver }) => { test("Verify device with Recovery Key during login", async ({ page, app, credentials, homeserver }) => {
await logIntoElement(page, credentials); await logIntoElement(page, credentials);
// Select the security phrase // Select the security phrase
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key or Phrase" }).click(); await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key or Phrase" }).click();
// Fill the security key // Fill the recovery key
const dialog = page.locator(".mx_Dialog"); const dialog = page.locator(".mx_Dialog");
await dialog.getByRole("button", { name: "use your Security Key" }).click(); await dialog.getByRole("button", { name: "use your Recovery Key" }).click();
const aliceRecoveryKey = await aliceBotClient.getRecoveryKey(); const aliceRecoveryKey = await aliceBotClient.getRecoveryKey();
await dialog.locator("#mx_securityKey").fill(aliceRecoveryKey.encodedPrivateKey); await dialog.locator("#mx_securityKey").fill(aliceRecoveryKey.encodedPrivateKey);
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click(); await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();

View File

@@ -17,6 +17,7 @@ import {
logIntoElement, logIntoElement,
logOutOfElement, logOutOfElement,
verify, verify,
waitForDevices,
} from "./utils"; } from "./utils";
import { bootstrapCrossSigningForClient } from "../../pages/client.ts"; import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
import { type ElementAppPage } from "../../pages/ElementAppPage.ts"; import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
@@ -144,25 +145,8 @@ test.describe("Cryptography", function () {
// bob deletes his second device // bob deletes his second device
await bobSecondDevice.evaluate((cli) => cli.logout(true)); await bobSecondDevice.evaluate((cli) => cli.logout(true));
// wait for the logout to propagate. Workaround for https://github.com/vector-im/element-web/issues/26263 by repeatedly closing and reopening Bob's user info. // wait for the logout to propagate.
async function awaitOneDevice(iterations = 1) { await waitForDevices(app, bob.credentials.userId, 1);
const rightPanel = page.locator(".mx_RightPanel");
await rightPanel.getByTestId("base-card-back-button").click();
await rightPanel.getByText("Bob").click();
const sessionCountText = await rightPanel
.locator(".mx_UserInfo_devices")
.getByText(" session", { exact: false })
.textContent();
// cf https://github.com/vector-im/element-web/issues/26279: Element-R uses the wrong text here
if (sessionCountText != "1 session" && sessionCountText != "1 verified session") {
if (iterations >= 10) {
throw new Error(`Bob still has ${sessionCountText} after 10 iterations`);
}
await awaitOneDevice(iterations + 1);
}
}
await awaitOneDevice();
// close and reopen the room, to get the shield to update. // close and reopen the room, to get the shield to update.
await app.viewRoomByName("Bob"); await app.viewRoomByName("Bob");
@@ -285,11 +269,7 @@ test.describe("Cryptography", function () {
// Workaround for https://github.com/element-hq/element-web/issues/28640: // Workaround for https://github.com/element-hq/element-web/issues/28640:
// make sure that Alice has seen Bob's identity before she goes offline. We do this by opening // make sure that Alice has seen Bob's identity before she goes offline. We do this by opening
// his user info. // his user info.
await app.toggleRoomInfoPanel(); await waitForDevices(app, bob.credentials.userId, 1);
const rightPanel = page.locator(".mx_RightPanel");
await rightPanel.getByRole("menuitem", { name: "People" }).click();
await rightPanel.getByRole("button", { name: bob.credentials!.userId }).click();
await expect(rightPanel.locator(".mx_UserInfo_devices")).toContainText("1 session");
// Our app is blocked from syncing while Bob sends his messages. // Our app is blocked from syncing while Bob sends his messages.
await app.client.network.goOffline(); await app.client.network.goOffline();

View File

@@ -34,9 +34,8 @@ test.describe("Key storage out of sync toast", () => {
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png"); await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png");
await page.getByRole("button", { name: "Enter recovery key" }).click(); await page.getByRole("button", { name: "Enter recovery key" }).click();
await page.locator(".mx_Dialog").getByRole("button", { name: "use your Security Key" }).click();
await page.getByRole("textbox", { name: "Security key" }).fill(recoveryKey.encodedPrivateKey); await page.getByRole("textbox", { name: "Recovery Key" }).fill(recoveryKey.encodedPrivateKey);
await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Continue" }).click();
await expect(page.getByRole("button", { name: "Enter recovery key" })).not.toBeVisible(); await expect(page.getByRole("button", { name: "Enter recovery key" })).not.toBeVisible();

View File

@@ -8,9 +8,8 @@ Please see LICENSE files in the repository root for full details.
import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix"; import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix";
import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
import { doTwoWaySasVerification, awaitVerifier } from "./utils"; import { doTwoWaySasVerification, awaitVerifier, waitForDevices } from "./utils";
import { type Client } from "../../pages/client"; import { type Client } from "../../pages/client";
test.describe("User verification", () => { test.describe("User verification", () => {
@@ -33,13 +32,17 @@ test.describe("User verification", () => {
}); });
test("can receive a verification request when there is no existing DM", async ({ test("can receive a verification request when there is no existing DM", async ({
app,
page, page,
bot: bob, bot: bob,
user: aliceCredentials, user: aliceCredentials,
toasts, toasts,
room: { roomId: dmRoomId }, room: { roomId: dmRoomId },
}) => { }) => {
await waitForDeviceKeys(page); await waitForDevices(app, bob.credentials.userId, 1);
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
const avatar = page.getByRole("button", { name: "Avatar" });
await avatar.click();
// once Alice has joined, Bob starts the verification // once Alice has joined, Bob starts the verification
const bobVerificationRequest = await bob.evaluateHandle( const bobVerificationRequest = await bob.evaluateHandle(
@@ -84,13 +87,17 @@ test.describe("User verification", () => {
}); });
test("can abort emoji verification when emoji mismatch", async ({ test("can abort emoji verification when emoji mismatch", async ({
app,
page, page,
bot: bob, bot: bob,
user: aliceCredentials, user: aliceCredentials,
toasts, toasts,
room: { roomId: dmRoomId }, room: { roomId: dmRoomId },
}) => { }) => {
await waitForDeviceKeys(page); await waitForDevices(app, bob.credentials.userId, 1);
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
const avatar = page.getByRole("button", { name: "Avatar" });
await avatar.click();
// once Alice has joined, Bob starts the verification // once Alice has joined, Bob starts the verification
const bobVerificationRequest = await bob.evaluateHandle( const bobVerificationRequest = await bob.evaluateHandle(
@@ -154,15 +161,3 @@ async function createDMRoom(client: Client, userId: string): Promise<string> {
], ],
}); });
} }
/**
* Wait until we get the other user's device keys.
* In newer rust-crypto versions, the verification request will be ignored if we
* don't have the sender's device keys.
*/
async function waitForDeviceKeys(page: Page): Promise<void> {
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
const avatar = await page.getByRole("button", { name: "Avatar" });
await avatar.click();
await expect(page.getByText("1 session")).toBeVisible();
}

View File

@@ -28,11 +28,13 @@ import { Bot } from "../../pages/bot";
* @param page - the playwright `page` fixture * @param page - the playwright `page` fixture
* @param homeserver - the homeserver to use * @param homeserver - the homeserver to use
* @param credentials - the credentials to use for the bot client * @param credentials - the credentials to use for the bot client
* @param usePassphrase - whether to use a passphrase when creating the recovery key
*/ */
export async function createBot( export async function createBot(
page: Page, page: Page,
homeserver: HomeserverInstance, homeserver: HomeserverInstance,
credentials: Credentials, credentials: Credentials,
usePassphrase = false,
): Promise<{ botClient: Bot; recoveryKey: GeneratedSecretStorageKey; expectedBackupVersion: string }> { ): Promise<{ botClient: Bot; recoveryKey: GeneratedSecretStorageKey; expectedBackupVersion: string }> {
// Visit the login page of the app, to load the matrix sdk // Visit the login page of the app, to load the matrix sdk
await page.goto("/#/login"); await page.goto("/#/login");
@@ -44,6 +46,7 @@ export async function createBot(
const botClient = new Bot(page, homeserver, { const botClient = new Bot(page, homeserver, {
bootstrapCrossSigning: true, bootstrapCrossSigning: true,
bootstrapSecretStorage: true, bootstrapSecretStorage: true,
usePassphrase,
}); });
botClient.setCredentials(credentials); botClient.setCredentials(credentials);
// Backup is prepared in the background. Poll until it is ready. // Backup is prepared in the background. Poll until it is ready.
@@ -142,11 +145,13 @@ export async function checkDeviceIsCrossSigned(app: ElementAppPage): Promise<voi
* @param app -` ElementAppPage` wrapper for the playwright `Page`. * @param app -` ElementAppPage` wrapper for the playwright `Page`.
* @param expectedBackupVersion - the version of the backup we expect to be connected to. * @param expectedBackupVersion - the version of the backup we expect to be connected to.
* @param checkBackupPrivateKeyInCache - whether to check that the backup decryption key is cached locally * @param checkBackupPrivateKeyInCache - whether to check that the backup decryption key is cached locally
* @param checkBackupKeyIn4S - whether to check that the backup key is stored in 4S
*/ */
export async function checkDeviceIsConnectedKeyBackup( export async function checkDeviceIsConnectedKeyBackup(
app: ElementAppPage, app: ElementAppPage,
expectedBackupVersion: string, expectedBackupVersion: string,
checkBackupPrivateKeyInCache: boolean, checkBackupPrivateKeyInCache: boolean,
checkBackupKeyIn4S: boolean = true,
): Promise<void> { ): Promise<void> {
// Sanity check the given backup version: if it's null, something went wrong earlier in the test. // Sanity check the given backup version: if it's null, something went wrong earlier in the test.
if (!expectedBackupVersion) { if (!expectedBackupVersion) {
@@ -189,7 +194,7 @@ export async function checkDeviceIsConnectedKeyBackup(
// The active backup version is as expected // The active backup version is as expected
expect(activeBackupVersion).toBe(expectedBackupVersion); expect(activeBackupVersion).toBe(expectedBackupVersion);
// The backup key is stored in 4S // The backup key is stored in 4S
expect(backupKeyIn4S).toBe(true); if (checkBackupKeyIn4S) expect(backupKeyIn4S).toBe(true);
if (checkBackupPrivateKeyInCache) { if (checkBackupPrivateKeyInCache) {
// The backup key is available locally // The backup key is available locally
@@ -213,13 +218,13 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur
// if a securityKey was given, verify the new device // if a securityKey was given, verify the new device
if (securityKey !== undefined) { if (securityKey !== undefined) {
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key" }).click(); await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click();
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Security Key" }); const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" });
if (await useSecurityKey.isVisible()) { if (await useSecurityKey.isVisible()) {
await useSecurityKey.click(); await useSecurityKey.click();
} }
// Fill in the security key // Fill in the recovery key
await page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey); await page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
await page.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click(); await page.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
await page.getByRole("button", { name: "Done" }).click(); await page.getByRole("button", { name: "Done" }).click();
@@ -246,15 +251,15 @@ export async function logOutOfElement(page: Page, discardKeys: boolean = false)
} }
/** /**
* Open the encryption settings, and verify the current session using the security key. * Open the encryption settings, and verify the current session using the recovery key.
* *
* @param app - `ElementAppPage` wrapper for the playwright `Page`. * @param app - `ElementAppPage` wrapper for the playwright `Page`.
* @param securityKey - The security key (i.e., 4S key), set up during a previous session. * @param securityKey - The recovery key (i.e., 4S key), set up during a previous session.
*/ */
export async function verifySession(app: ElementAppPage, securityKey: string) { export async function verifySession(app: ElementAppPage, securityKey: string) {
const settings = await app.settings.openUserSettings("Encryption"); const settings = await app.settings.openUserSettings("Encryption");
await settings.getByRole("button", { name: "Verify this device" }).click(); await settings.getByRole("button", { name: "Verify this device" }).click();
await app.page.getByRole("button", { name: "Verify with Security Key" }).click(); await app.page.getByRole("button", { name: "Verify with Recovery Key" }).click();
await app.page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey); await app.page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
await app.page.getByRole("button", { name: "Continue", disabled: false }).click(); await app.page.getByRole("button", { name: "Continue", disabled: false }).click();
await app.page.getByRole("button", { name: "Done" }).click(); await app.page.getByRole("button", { name: "Done" }).click();
@@ -288,7 +293,7 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
* *
* Assumes that the current device has been cross-signed (which means that we skip a step where we set it up). * Assumes that the current device has been cross-signed (which means that we skip a step where we set it up).
* *
* Returns the security key * Returns the recovery key
*/ */
export async function enableKeyBackup(app: ElementAppPage): Promise<string> { export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
await app.settings.openUserSettings("Security & Privacy"); await app.settings.openUserSettings("Security & Privacy");
@@ -316,9 +321,9 @@ export async function completeCreateSecretStorageDialog(
const currentDialogLocator = page.locator(".mx_Dialog"); const currentDialogLocator = page.locator(".mx_Dialog");
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible(); await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
// "Generate a Security Key" is selected by default // "Generate a Recovery Key" is selected by default
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click(); await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible(); await expect(currentDialogLocator.getByRole("heading", { name: "Save your Recovery Key" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click(); await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
// copy the recovery key to use it later // copy the recovery key to use it later
const recoveryKey = await page.evaluate(() => navigator.clipboard.readText()); const recoveryKey = await page.evaluate(() => navigator.clipboard.readText());
@@ -342,7 +347,7 @@ export async function completeCreateSecretStorageDialog(
} }
/** /**
* Click on copy and continue buttons to dismiss the security key dialog * Click on copy and continue buttons to dismiss the recovery key dialog
*/ */
export async function copyAndContinue(page: Page) { export async function copyAndContinue(page: Page) {
await page.getByRole("button", { name: "Copy" }).click(); await page.getByRole("button", { name: "Copy" }).click();
@@ -499,3 +504,31 @@ export async function deleteCachedSecrets(page: Page) {
}); });
await page.reload(); await page.reload();
} }
/**
* Wait until the given user has a given number of devices.
* This function will check the device keys ten times and if
* the expected number of devices were not found by then, an
* error is thrown.
*/
export async function waitForDevices(
app: ElementAppPage,
userId: string,
expectedNumberOfDevices: number,
): Promise<void> {
const result = await app.client.evaluate(
async (cli, { userId, expectedNumberOfDevices }) => {
for (let i = 0; i < 10; ++i) {
const userDeviceMap = await cli.getCrypto()?.getUserDeviceInfo([userId], true);
const deviceMap = userDeviceMap?.get(userId);
if (deviceMap.size === expectedNumberOfDevices) return true;
await new Promise((r) => setTimeout(r, 500));
}
return false;
},
{ userId, expectedNumberOfDevices },
);
if (!result) {
throw new Error(`User ${userId} did not have ${expectedNumberOfDevices} devices within ten iterations!`);
}
}

View File

@@ -6,22 +6,21 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { expect, test as base } from "../../element-web-test"; import { type CredentialsWithDisplayName, expect, test as base } from "../../element-web-test";
import { selectHomeserver } from "../utils"; import { selectHomeserver } from "../utils";
import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts"; import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts";
import { isDendrite } from "../../plugins/homeserver/dendrite"; import { isDendrite } from "../../plugins/homeserver/dendrite";
import { type Credentials } from "../../plugins/homeserver";
const email = "user@nowhere.dummy"; const email = "user@nowhere.dummy";
const test = base.extend<{ credentials: Pick<Credentials, "username" | "password"> }>({ const test = base.extend({
// eslint-disable-next-line no-empty-pattern // eslint-disable-next-line no-empty-pattern
credentials: async ({}, use, testInfo) => { credentials: async ({}, use, testInfo) => {
await use({ await use({
username: `user_${testInfo.testId}`, username: `user_${testInfo.testId}`,
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen. // this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
password: "oETo7MPf0o", password: "oETo7MPf0o",
}); } as CredentialsWithDisplayName);
}, },
}); });

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { type Page } from "@playwright/test";
import { test, expect } from "../../../element-web-test";
test.describe("Search section of the room list", () => {
test.use({
labsFlags: ["feature_new_room_list"],
});
/**
* Get the search section of the room list
* @param page
*/
function getSearchSection(page: Page) {
return page.getByRole("search");
}
test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
await app.closeNotificationToast();
});
test("should render the search section", { tag: "@screenshot" }, async ({ page, app, user }) => {
const searchSection = getSearchSection(page);
// exact=false to ignore the shortcut which is related to the OS
await expect(searchSection.getByRole("button", { name: "Search", exact: false })).toBeVisible();
await expect(searchSection).toMatchScreenshot("search-section.png");
});
test("should open the spotlight when the search button is clicked", async ({ page, app, user }) => {
const searchSection = getSearchSection(page);
await searchSection.getByRole("button", { name: "Search", exact: false }).click();
// The spotlight should be displayed
await expect(page.getByRole("dialog", { name: "Search Dialog" })).toBeVisible();
});
test("should open the room directory when the search button is clicked", async ({ page, app, user }) => {
const searchSection = getSearchSection(page);
await searchSection.getByRole("button", { name: "Explore rooms" }).click();
const dialog = page.getByRole("dialog", { name: "Search Dialog" });
// The room directory should be displayed
await expect(dialog).toBeVisible();
// The public room filter should be displayed
await expect(dialog.getByText("Public rooms")).toBeVisible();
});
});

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { type Page } from "@playwright/test";
import { test, expect } from "../../../element-web-test";
test.describe("Search section of the room list", () => {
test.use({
labsFlags: ["feature_new_room_list"],
});
/**
* Get the room list view
* @param page
*/
function getRoomListView(page: Page) {
return page.getByTestId("room-list-view");
}
test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
await app.closeNotificationToast();
});
test("should render the room list view", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomListView(page);
await expect(roomListView).toMatchScreenshot("room-list-view.png");
});
});

View File

@@ -0,0 +1,35 @@
/*
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("Module loading", () => {
test.use({
displayName: "Manny",
});
test.describe("Example Module", () => {
test.use({
config: {
modules: ["/modules/example-module.js"],
},
page: async ({ page }, use) => {
await page.route("/modules/example-module.js", async (route) => {
await route.fulfill({ path: "playwright/sample-files/example-module.js" });
});
await use(page);
},
});
test("should show alert", async ({ page }) => {
const dialogPromise = page.waitForEvent("dialog");
await page.goto("/");
const dialog = await dialogPromise;
expect(dialog.message()).toBe("Testing module loading successful!");
});
});
});

View File

@@ -57,8 +57,8 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
await util.openThread("ThreadRoot"); await util.openThread("ThreadRoot");
// Then the thread root is marked as read in the main timeline, // Then the thread root is marked as read in the main timeline,
// 30 remaining messages are unread - 7 messages are displayed under the thread root // 30 remaining messages are unread - 6 messages are displayed under the thread root
await util.assertUnread(room2, 30 - 7); await util.assertUnread(room2, 30 - 6);
}); });
test("Creating a new thread based on a reply makes the room unread", async ({ test("Creating a new thread based on a reply makes the room unread", async ({

View File

@@ -67,7 +67,7 @@ test.describe("Encryption tab", () => {
"should prompt to enter the recovery key when the secrets are not cached locally", "should prompt to enter the recovery key when the secrets are not cached locally",
{ tag: "@screenshot" }, { tag: "@screenshot" },
async ({ page, app, util }) => { async ({ page, app, util }) => {
await verifySession(app, "new passphrase"); await verifySession(app, recoveryKey.encodedPrivateKey);
// We need to delete the cached secrets // We need to delete the cached secrets
await deleteCachedSecrets(page); await deleteCachedSecrets(page);
@@ -99,7 +99,7 @@ test.describe("Encryption tab", () => {
app, app,
util, util,
}) => { }) => {
await verifySession(app, "new passphrase"); await verifySession(app, recoveryKey.encodedPrivateKey);
// We need to delete the cached secrets // We need to delete the cached secrets
await deleteCachedSecrets(page); await deleteCachedSecrets(page);

View File

@@ -43,7 +43,7 @@ class Helpers {
*/ */
async verifyDevice(recoveryKey: GeneratedSecretStorageKey) { async verifyDevice(recoveryKey: GeneratedSecretStorageKey) {
// Select the security phrase // Select the security phrase
await this.page.getByRole("button", { name: "Verify with Security Key or Phrase" }).click(); await this.page.getByRole("button", { name: "Verify with Recovery Key" }).click();
await this.enterRecoveryKey(recoveryKey); await this.enterRecoveryKey(recoveryKey);
await this.page.getByRole("button", { name: "Done" }).click(); await this.page.getByRole("button", { name: "Done" }).click();
} }
@@ -53,9 +53,6 @@ class Helpers {
* @param recoveryKey * @param recoveryKey
*/ */
async enterRecoveryKey(recoveryKey: GeneratedSecretStorageKey) { async enterRecoveryKey(recoveryKey: GeneratedSecretStorageKey) {
// Select to use recovery key
await this.page.getByRole("button", { name: "use your Security Key" }).click();
// Fill the recovery key // Fill the recovery key
const dialog = this.page.locator(".mx_Dialog"); const dialog = this.page.locator(".mx_Dialog");
await dialog.getByRole("textbox").fill(recoveryKey.encodedPrivateKey); await dialog.getByRole("textbox").fill(recoveryKey.encodedPrivateKey);
@@ -94,7 +91,7 @@ class Helpers {
} }
/** /**
* Get the security key from the clipboard and fill in the input field * Get the recovery key from the clipboard and fill in the input field
* Then click on the finish button * Then click on the finish button
* @param title - The title of the dialog * @param title - The title of the dialog
* @param confirmButtonLabel - The label of the confirm button * @param confirmButtonLabel - The label of the confirm button

View File

@@ -7,22 +7,25 @@
import { test, expect } from "."; import { test, expect } from ".";
import { checkDeviceIsConnectedKeyBackup, createBot, verifySession } from "../../crypto/utils"; import { checkDeviceIsConnectedKeyBackup, createBot, verifySession } from "../../crypto/utils";
import type { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
test.describe("Recovery section in Encryption tab", () => { test.describe("Recovery section in Encryption tab", () => {
test.use({ test.use({
displayName: "Alice", displayName: "Alice",
}); });
let recoveryKey: GeneratedSecretStorageKey;
test.beforeEach(async ({ page, homeserver, credentials }) => { test.beforeEach(async ({ page, homeserver, credentials }) => {
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key // The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
await createBot(page, homeserver, credentials); const res = await createBot(page, homeserver, credentials);
recoveryKey = res.recoveryKey;
}); });
test( test(
"should change the recovery key", "should change the recovery key",
{ tag: ["@screenshot", "@no-webkit"] }, { tag: ["@screenshot", "@no-webkit"] },
async ({ page, app, homeserver, credentials, util, context }) => { async ({ page, app, homeserver, credentials, util, context }) => {
await verifySession(app, "new passphrase"); await verifySession(app, recoveryKey.encodedPrivateKey);
const dialog = await util.openEncryptionTab(); const dialog = await util.openEncryptionTab();
// The user can only change the recovery key // The user can only change the recovery key
@@ -49,7 +52,7 @@ test.describe("Recovery section in Encryption tab", () => {
); );
test("should setup the recovery key", { tag: ["@screenshot", "@no-webkit"] }, async ({ page, app, util }) => { test("should setup the recovery key", { tag: ["@screenshot", "@no-webkit"] }, async ({ page, app, util }) => {
await verifySession(app, "new passphrase"); await verifySession(app, recoveryKey.encodedPrivateKey);
await util.removeSecretStorageDefaultKeyId(); await util.removeSecretStorageDefaultKeyId();
// The key backup is deleted and the user needs to set it up // The key backup is deleted and the user needs to set it up

View File

@@ -25,13 +25,9 @@ test.describe("Security user settings tab", () => {
}, },
}); });
test.beforeEach(async ({ page, user }) => { test.beforeEach(async ({ page, app, user }) => {
// Dismiss "Notification" toast // Dismiss "Notification" toast
await page await app.closeNotificationToast();
.locator(".mx_Toast_toast", { hasText: "Notifications" })
.getByRole("button", { name: "Dismiss" })
.click();
await page.locator(".mx_Toast_buttons").getByRole("button", { name: "Yes" }).click(); // Allow analytics await page.locator(".mx_Toast_buttons").getByRole("button", { name: "Yes" }).click(); // Allow analytics
}); });

View File

@@ -19,7 +19,6 @@ test.describe("UserView", () => {
const rightPanel = page.locator("#mx_RightPanel"); const rightPanel = page.locator("#mx_RightPanel");
await expect(rightPanel.getByRole("heading", { name: bot.credentials.displayName, exact: true })).toBeVisible(); await expect(rightPanel.getByRole("heading", { name: bot.credentials.displayName, exact: true })).toBeVisible();
await expect(rightPanel.getByText("1 session")).toBeVisible();
await expect(rightPanel).toMatchScreenshot("user-info.png", { await expect(rightPanel).toMatchScreenshot("user-info.png", {
mask: [page.locator(".mx_UserInfo_profile_mxid")], mask: [page.locator(".mx_UserInfo_profile_mxid")],
css: ` css: `

View File

@@ -202,4 +202,15 @@ export class ElementAppPage {
} }
return this.page.locator(`id=${labelledById ?? describedById}`); return this.page.locator(`id=${labelledById ?? describedById}`);
} }
/**
* Close the notification toast
*/
public closeNotificationToast(): Promise<void> {
// Dismiss "Notification" toast
return this.page
.locator(".mx_Toast_toast", { hasText: "Notifications" })
.getByRole("button", { name: "Dismiss" })
.click();
}
} }

View File

@@ -41,6 +41,10 @@ export interface CreateBotOpts {
* Whether to bootstrap the secret storage * Whether to bootstrap the secret storage
*/ */
bootstrapSecretStorage?: boolean; bootstrapSecretStorage?: boolean;
/**
* Whether to use a passphrase when creating the recovery key
*/
usePassphrase?: boolean;
} }
const defaultCreateBotOptions = { const defaultCreateBotOptions = {
@@ -48,6 +52,7 @@ const defaultCreateBotOptions = {
autoAcceptInvites: true, autoAcceptInvites: true,
startClient: true, startClient: true,
bootstrapCrossSigning: true, bootstrapCrossSigning: true,
usePassphrase: false,
} satisfies CreateBotOpts; } satisfies CreateBotOpts;
type ExtendedMatrixClient = MatrixClient & { __playwright_recovery_key: GeneratedSecretStorageKey }; type ExtendedMatrixClient = MatrixClient & { __playwright_recovery_key: GeneratedSecretStorageKey };
@@ -206,8 +211,8 @@ export class Bot extends Client {
} }
if (this.opts.bootstrapSecretStorage) { if (this.opts.bootstrapSecretStorage) {
await clientHandle.evaluate(async (cli) => { await clientHandle.evaluate(async (cli, usePassphrase) => {
const passphrase = "new passphrase"; const passphrase = usePassphrase ? "new passphrase" : undefined;
const recoveryKey = await cli.getCrypto().createRecoveryKeyFromPassphrase(passphrase); const recoveryKey = await cli.getCrypto().createRecoveryKeyFromPassphrase(passphrase);
Object.assign(cli, { __playwright_recovery_key: recoveryKey }); Object.assign(cli, { __playwright_recovery_key: recoveryKey });
@@ -216,7 +221,7 @@ export class Bot extends Client {
setupNewKeyBackup: true, setupNewKeyBackup: true,
createSecretStorageKey: () => Promise.resolve(recoveryKey), createSecretStorageKey: () => Promise.resolve(recoveryKey),
}); });
}); }, this.opts.usePassphrase);
} }
return clientHandle; return clientHandle;

View File

@@ -0,0 +1,16 @@
/*
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.
*/
export default class ExampleModule {
static moduleApiVersion = "^0.1.0";
constructor(api) {
this.api = api;
}
async load() {
alert("Testing module loading successful!");
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 KiB

After

Width:  |  Height:  |  Size: 985 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 62 KiB

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