Compare commits

..

104 Commits

Author SHA1 Message Date
Will Hunt
a3f1a8c649 Merge branch 'develop' into hs/remove-legacy-setting-toggle 2025-02-25 16:08:01 +00:00
Half-Shot
57e8e51821 update apperance screenshot 2025-02-25 16:07:37 +00:00
Florian Duros
f95218e2b7 New room list: add space menu in room header (#29352)
* feat(new room list): add space menu in view model

* test(new room list): add space menu in view model

* feat(new room list): add space menu in room list header

* chore: update i18n

* test(new room list): add tests for space menu

* test(new room list): update room list tests

* test(e2e): add tests for space menu
2025-02-25 14:06:01 +00:00
Hubert Chathi
62a287219d add test to ensure that dehydrated device gets removed on identity reset (#29354) 2025-02-25 13:46:57 +00:00
RiotRobot
db45a17d43 Reset matrix-js-sdk back to develop branch 2025-02-25 13:46:58 +00:00
RiotRobot
9b1de5634d Merge branch 'master' into develop 2025-02-25 13:46:45 +00:00
RiotRobot
230e26e1ab v1.11.93 2025-02-25 13:27:04 +00:00
Michael Telatynski
cf5ffacff2 Upgrade dependency to matrix-js-sdk@37.0.0 2025-02-25 13:17:51 +00:00
R Midhun Suresh
fe353542cb Room List - Store sorted rooms in skip list (#29345)
* Implement a skip list for storing rooms

This data structure stores rooms in a given sorted order and allows for
very fast insertions and deletions.

* Export function to get last timestamp of room

* Write tests for the skip list

* Implement enough of the new store to get a list of rooms

* Make it possible to swap sorting algorithm

* Fix comment

* Don't attach to window object

We don't want the store to be created if the labs flag is off

* Remove the store class

Probably best to include this PR with the minimal vm implmentation
2025-02-25 13:12:37 +00:00
Michael Telatynski
0b624bf645 Update release.yml permissions 2025-02-25 13:20:03 +00:00
Richard van der Hoff
3cc1ccd029 Dismiss "Key storage out of sync" toast when secrets received (#29348)
* DeviceListener: improve logging

use a LogSpan to tie together logs from the same run, and add some more logs
for various cases

* Regression playwright test

* Remove unused mocking of `getCrossSigningId`

DeviceListener no longer reads this thing

* Clean up unit tests

Remove redundant describe block

* Remove the "out of sync" toast when we are no longer out of sync

Receiving the crypto secrets via secret sharing should make the toast go away.
2025-02-25 12:13:42 +00:00
Michael Telatynski
05b8fff58a Dynamically load Element Web modules in Docker entrypoint (#29346) (#29358)
* Dynamically load Element Web modules in Docker entrypoint



* Iterate



* Drop environment for PR runs



* Iterate



---------



(cherry picked from commit 8ef84349b5)

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-25 11:57:57 +00:00
Florian Duros
43efd911c7 New room list: add dial to search section (#29359)
* feat(new room list): add dial to new room list

* test(new room list): test dial button

* test(new room list): update room list test
2025-02-25 11:03:29 +00:00
Michael Telatynski
2e883b40eb Add docker healthcheck (#29351)
* Add docker healthcheck

and test ELEMENT_WEB_PORT envvar

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>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-25 09:16:22 +00:00
Florian Duros
14e3a77dc2 Minor CSS fixes for the new room list (#29334)
* refactor: use `var(--cpd-border-width-1)` instead of `1px`

* refactor: use header font in header
2025-02-24 18:59:26 +00:00
Florian Duros
efc6149a8b Update @vector-im/compound-design-tokens & @vector-im/compound-web (#29307)
* chore: update `@vector-im/compound-design-tokens` & `@vector-im/compound-web` to last version

* chore: use `error-solid` icon instead of `error`

* chore: update jest snapshot

* fix: `AccessibleButton` lint
2025-02-24 17:08:12 +00:00
Michael Telatynski
8ef84349b5 Dynamically load Element Web modules in Docker entrypoint (#29346)
* Dynamically load Element Web modules in Docker entrypoint

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

* Iterate

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

* Drop environment for PR runs

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

* Iterate

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-24 16:54:57 +00:00
Florian Duros
7d94fa9b03 New room list: add compose menu for spaces in header (#29347)
* feat(new room list): add compose menu in header for spaces

* test(new room list): add tests for space

* test(e2e new room list): update space test

* chore: formatting and reuse type var
2025-02-24 15:51:50 +00:00
ElementRobot
37136ecf46 [create-pull-request] automated change (#29342)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-24 06:21:38 +00:00
ElementRobot
99b9eee86e [create-pull-request] automated change (#29338)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-22 06:14:13 +00:00
David Langley
27c0e97e44 Fix test that doesn't make sense in usePublicRoomDirectory-test.tsx (#29335)
* remove test that doesn't make sense

* Actually let's fix the test rather than remove it

* Add comment, remove consoles.
2025-02-22 00:17:13 +00:00
David Langley
0cbc6f99d0 Set language explicitly (#29332) 2025-02-22 00:16:33 +00:00
Half-Shot
b4c926f85c lint 2025-02-21 13:47:42 +00:00
Half-Shot
a413ae3f43 Update tests 2025-02-21 13:47:40 +00:00
Half-Shot
88e52601c8 Remove unused import. 2025-02-21 13:47:07 +00:00
Half-Shot
51cb4a5cfd Remove unused checkbox setting. 2025-02-21 13:47:07 +00:00
Half-Shot
ffedde2509 Update Apperance settings to use toggle switches. 2025-02-21 13:47:07 +00:00
Half-Shot
03da3b55b5 forgot a comma 2025-02-21 13:47:04 +00:00
Half-Shot
206304b5d7 Reformat other :not sections 2025-02-21 13:46:46 +00:00
Half-Shot
956d936235 fix tests 2025-02-21 13:46:32 +00:00
Half-Shot
ef4d9ea8b7 Add a test for setting an ID server. 2025-02-21 13:46:19 +00:00
Half-Shot
5aca14db74 Update test 2025-02-21 13:45:57 +00:00
David Langley
d8904a6e56 getByRole is slow on large trees, use getByText (#29331) 2025-02-21 13:45:52 +00:00
Half-Shot
8a9eb35bf9 Use EditInPlace for identity server picker. 2025-02-21 13:45:13 +00:00
Will Hunt
22943ee06a Use EditInPlace control for Identity Server picker to improve a11y (#29280)
* Use EditInPlace for identity server picker.

* Exclude picker from default dialog button styles.

* Remove unused import.

* Update test

* Remove unused css

* Update test

* drop only

* Add a test for setting an ID server.

* Add a unit test for SetIdServer

* fix tests

* Reformat mx_Dialog button :not list to use a more readable selector.

* Reformat other :not sections

* forgot a comma

* We're in 2025 now.

* Update copyright + use class methods.
2025-02-21 13:18:14 +00:00
David Langley
2e1798edc4 Add padding to room header icon (#29271)
* Add padding to room header icon

* Add screenshot tests for room header icon

* Add type to visibility import

* Use visibility type

* lint
2025-02-21 11:27:57 +00:00
ElementRobot
3470182410 [create-pull-request] automated change (#29327)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-21 06:19:03 +00:00
ElementRobot
9d8d38eeb8 [create-pull-request] automated change (#29326)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-21 06:15:12 +00:00
Florian Duros
fb57924350 First step to add header to new room list (#29320)
* feat: create new header

* test: add tests to view model

* test: add tests to view

* feat: add header to new room list

* test(e2e): update RoomListView snapshot

* test(e2e): add tests for room list header

* refactor: minor code improvement
2025-02-20 15:40:19 +00:00
David Baker
e8954f08ce Move CSS for ResetIdentityPanel into EncryptionCard (#29311)
* Move CSS for ResetIdentityPanel into EncryptionCard

This allows it to be re-used in other components. It's a *bit* magic
that EncryptionCard applies style to divs within it, although it
somewhat makes sense that it wants them styled a particular way.
The alternative would be to add another component for a div child of
encryption card like EncryptionCardButtons that just makes it flexbox
and centered: I'm not sure which is better.

* Update snapshot

* Update snapshot

* Do it the other way

Because we only want it in the destructive cards, not the other ones.

* Use flex component

* Also use gap prop and update snapshots

* Fix justification

* Snaspshots again

* Set align-items to normal

As center affected the list items too. Also add it to the flex
component because it didn't have it as an option.
2025-02-20 13:20:49 +00:00
ElementRobot
e161f9fb18 [create-pull-request] automated change (#29321)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-20 06:14:23 +00: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
RiotRobot
8c4996b437 v1.11.93-rc.0 2025-02-18 13:04:49 +00:00
RiotRobot
03d27e2808 Upgrade dependency to matrix-js-sdk@37.0.0-rc.0 2025-02-18 13:01:30 +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
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
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
376 changed files with 31622 additions and 5791 deletions

5
.github/CODEOWNERS vendored
View File

@@ -11,10 +11,11 @@
/src/stores/SetupEncryptionStore.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/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
/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
/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
/src/i18n/strings

138
.github/workflows/docker.yaml vendored Normal file
View File

@@ -0,0 +1,138 @@
name: Docker
on:
workflow_dispatch: {}
push:
tags: [v*]
pull_request: {}
schedule:
# This job can take a while, and we have usage limits, so just publish develop only twice a day
- cron: "0 7/12 * * *"
concurrency: ${{ github.workflow }}-${{ github.ref_name }}
permissions: {}
jobs:
buildx:
name: Docker Buildx
runs-on: ubuntu-24.04
environment: ${{ github.event_name != 'pull_request' && 'dockerhub' || '' }}
permissions:
id-token: write # needed for signing the images with GitHub OIDC Token
packages: write # needed for publishing packages to GHCR
env:
TEST_TAG: vectorim/element-web:test
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for docker-package to be able to calculate the version
- name: Install Cosign
uses: sigstore/cosign-installer@c56c2d3e59e4281cc41dea2217323ba5694b171e # v3
if: github.event_name != 'pull_request'
- name: Set up QEMU
uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3
with:
install: true
- name: Login to Docker Hub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
if: github.event_name != 'pull_request'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and load
id: test-build
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
with:
context: .
load: true
- name: Test the image
env:
IMAGEID: ${{ steps.test-build.outputs.imageid }}
run: |
set -x
# Make a fake module to test the image
MODULE_PATH="modules/module_name/index.js"
mkdir -p $(dirname $MODULE_PATH)
echo 'alert("Testing");' > $MODULE_PATH
# Spin up a container of the image
ELEMENT_WEB_PORT=8181
CONTAINER_ID=$(
docker run \
--rm \
-e "ELEMENT_WEB_PORT=$ELEMENT_WEB_PORT" \
-dp "$ELEMENT_WEB_PORT:$ELEMENT_WEB_PORT" \
-v $(pwd)/modules:/tmp/element-web-modules \
"$IMAGEID" \
)
# Run some smoke tests
wget --retry-connrefused --tries=5 -q --wait=3 --spider "http://localhost:$ELEMENT_WEB_PORT/modules/module_name/index.js"
MODULE_0=$(curl "http://localhost:$ELEMENT_WEB_PORT/config.json" | jq -r .modules[0])
test "$MODULE_0" = "/${MODULE_PATH}"
# Check healthcheck
test "$(docker inspect -f {{.State.Running}} $CONTAINER_ID)" == "true"
# Clean up
docker stop "$CONTAINER_ID"
- name: Docker meta
id: meta
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
if: github.event_name != 'pull_request'
with:
images: |
vectorim/element-web
ghcr.io/element-hq/element-web
tags: |
type=ref,event=branch
type=ref,event=tag
flavor: |
latest=${{ contains(github.ref_name, '-rc.') && 'false' || 'auto' }}
- name: Build and push
id: build-and-push
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
if: github.event_name != 'pull_request'
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Sign the images with GitHub OIDC Token
env:
DIGEST: ${{ steps.build-and-push.outputs.digest }}
TAGS: ${{ steps.meta.outputs.tags }}
if: github.event_name != 'pull_request'
run: |
images=""
for tag in ${TAGS}; do
images+="${tag}@${DIGEST} "
done
cosign sign --yes ${images}
- name: Update repo description
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4
if: github.event_name != 'pull_request'
continue-on-error: true
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: vectorim/element-web

View File

@@ -1,79 +0,0 @@
name: Dockerhub
on:
workflow_dispatch: {}
push:
tags: [v*]
schedule:
# This job can take a while, and we have usage limits, so just publish develop only twice a day
- cron: "0 7/12 * * *"
concurrency: ${{ github.workflow }}-${{ github.ref_name }}
permissions: {}
jobs:
buildx:
name: Docker Buildx
runs-on: ubuntu-24.04
environment: dockerhub
permissions:
id-token: write # needed for signing the images with GitHub OIDC Token
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for docker-package to be able to calculate the version
- name: Install Cosign
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3
- name: Set up QEMU
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3
with:
install: true
- name: Login to Docker Hub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
with:
images: |
vectorim/element-web
tags: |
type=ref,event=branch
type=ref,event=tag
flavor: |
latest=${{ contains(github.ref_name, '-rc.') && 'false' || 'auto' }}
- name: Build and push
id: build-and-push
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Sign the images with GitHub OIDC Token
env:
DIGEST: ${{ steps.build-and-push.outputs.digest }}
TAGS: ${{ steps.meta.outputs.tags }}
run: |
images=""
for tag in ${TAGS}; do
images+="${tag}@${DIGEST} "
done
cosign sign --yes ${images}
- name: Update repo description
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4
continue-on-error: true
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: vectorim/element-web

View File

@@ -19,6 +19,7 @@ jobs:
contents: write
issues: write
pull-requests: read
id-token: write
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
@@ -50,7 +51,7 @@ jobs:
permissions:
checks: read
steps:
- name: Wait for dockerhub
- name: Wait for docker build
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
with:
ref: master

View File

@@ -104,7 +104,7 @@ jobs:
- name: Skip SonarCloud in merge queue
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:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success

View File

@@ -1,3 +1,66 @@
Changes in [1.11.93](https://github.com/element-hq/element-web/releases/tag/v1.11.93) (2025-02-25)
==================================================================================================
## ✨ Features
* [backport] Dynamically load Element Web modules in Docker entrypoint ([#29358](https://github.com/element-hq/element-web/pull/29358)). Contributed by @t3chguy.
* ChangeRecoveryKey: error handling ([#29262](https://github.com/element-hq/element-web/pull/29262)). Contributed by @richvdh.
* Dehydration: enable dehydrated device on "Set up recovery" ([#29265](https://github.com/element-hq/element-web/pull/29265)). Contributed by @richvdh.
* Render reason for invite rejection. ([#29257](https://github.com/element-hq/element-web/pull/29257)). Contributed by @Half-Shot.
* New room list: add search section ([#29251](https://github.com/element-hq/element-web/pull/29251)). Contributed by @florianduros.
* New room list: hide favourites and people meta spaces ([#29241](https://github.com/element-hq/element-web/pull/29241)). Contributed by @florianduros.
* New Room List: Create new labs flag ([#29239](https://github.com/element-hq/element-web/pull/29239)). Contributed by @MidhunSureshR.
* Stop URl preview from covering message box ([#29215](https://github.com/element-hq/element-web/pull/29215)). Contributed by @edent.
* Rename "security key" into "recovery key" ([#29217](https://github.com/element-hq/element-web/pull/29217)). Contributed by @florianduros.
* Add new verification section to user profile ([#29200](https://github.com/element-hq/element-web/pull/29200)). Contributed by @MidhunSureshR.
* Initial support for runtime modules ([#29104](https://github.com/element-hq/element-web/pull/29104)). Contributed by @t3chguy.
* Add `Forgot recovery key?` button to encryption tab ([#29202](https://github.com/element-hq/element-web/pull/29202)). Contributed by @florianduros.
* Add KeyIcon to key storage out of sync toast ([#29201](https://github.com/element-hq/element-web/pull/29201)). Contributed by @florianduros.
* Improve rendering of empty topics in the timeline ([#29152](https://github.com/element-hq/element-web/pull/29152)). Contributed by @Half-Shot.
## 🐛 Bug Fixes
* Fix font scaling in member list ([#29285](https://github.com/element-hq/element-web/pull/29285)). Contributed by @florianduros.
* Grow member list search field when resizing the right panel ([#29267](https://github.com/element-hq/element-web/pull/29267)). Contributed by @langleyd.
* Don't reload roomview on offline connectivity check ([#29243](https://github.com/element-hq/element-web/pull/29243)). Contributed by @dbkr.
* Respect user's 12/24 hour preference consistently ([#29237](https://github.com/element-hq/element-web/pull/29237)). Contributed by @t3chguy.
* Restore the accessibility role on call views ([#29225](https://github.com/element-hq/element-web/pull/29225)). Contributed by @robintown.
* Revert `GoToHome` keyboard shortcut to `Ctrl``Shift``H` on macOS ([#28577](https://github.com/element-hq/element-web/pull/28577)). Contributed by @gy-mate.
* Encryption tab: display correct encryption panel when user cancels the reset identity flow ([#29216](https://github.com/element-hq/element-web/pull/29216)). Contributed by @florianduros.
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)
==================================================================================================
## ✨ Features

View File

@@ -1,3 +1,5 @@
# syntax=docker.io/docker/dockerfile:1.7-labs
# Builder
FROM --platform=$BUILDPLATFORM node:22-bullseye AS builder
@@ -8,7 +10,7 @@ ARG JS_SDK_BRANCH="master"
WORKDIR /src
COPY . /src
COPY --exclude=docker . /src
RUN /src/scripts/docker-link-repos.sh
RUN yarn --network-timeout=200000 install
RUN /src/scripts/docker-package.sh
@@ -19,11 +21,15 @@ RUN cp /src/config.sample.json /src/webapp/config.json
# App
FROM nginx:alpine-slim
# Install jq and moreutils for sponge, both used by our entrypoints
RUN apk add jq moreutils
COPY --from=builder /src/webapp /app
# Override default nginx config. Templates in `/etc/nginx/templates` are passed
# through `envsubst` by the nginx docker image entry point.
COPY /docker/nginx-templates/* /etc/nginx/templates/
COPY /docker/docker-entrypoint.d/* /docker-entrypoint.d/
# Tell nginx to put its pidfile elsewhere, so it can run as non-root
RUN sed -i -e 's,/var/run/nginx.pid,/tmp/nginx.pid,' /etc/nginx/nginx.conf
@@ -40,3 +46,5 @@ USER nginx
# HTTP listen port
ENV ELEMENT_WEB_PORT=80
HEALTHCHECK --start-period=5s CMD wget --retry-connrefused --tries=5 -q --wait=3 --spider http://localhost:$ELEMENT_WEB_PORT/config.json

View File

@@ -0,0 +1,34 @@
#!/bin/sh
# Loads modules from `/tmp/element-web-modules` into config.json's `modules` field
set -e
entrypoint_log() {
if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then
echo "$@"
fi
}
# Copy these config files as a base
mkdir /tmp/element-web-config
cp /app/config*.json /tmp/element-web-config/
# If there are modules to be loaded
if [ -d "/tmp/element-web-modules" ]; then
cd /tmp/element-web-modules
for MODULE in *
do
# If the module has a package.json, use its main field as the entrypoint
ENTRYPOINT="index.js"
if [ -f "/tmp/element-web-modules/$MODULE/package.json" ]; then
ENTRYPOINT=$(jq -r '.main' "/tmp/element-web-modules/$MODULE/package.json")
fi
entrypoint_log "Loading module $MODULE with entrypoint $ENTRYPOINT"
# Append the module to the config
jq ".modules += [\"/modules/$MODULE/$ENTRYPOINT\"]" /tmp/element-web-config/config.json | sponge /tmp/element-web-config/config.json
done
fi

View File

@@ -18,8 +18,12 @@ server {
}
# covers config.json and config.hostname.json requests as it is prefix.
location /config {
root /tmp/element-web-config;
add_header Cache-Control "no-cache";
}
location /modules {
alias /tmp/element-web-modules;
}
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;

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.
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`.
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`
@@ -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
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.
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.
4. `url_macos`: Optional. Direct link to download macOS desktop app.
5. `url_win32`: Optional. Direct link to download Windows 32-bit desktop app.
6. `url_win64`: Optional. Direct link to download Windows 64-bit desktop app.
5. `url_win64`: Optional. Direct link to download Windows x86 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.
When `desktop_builds` is not specified at all, the app will assume desktop downloads are available from https://element.io

View File

@@ -66,6 +66,18 @@ on other runtimes may require root privileges. To resolve this, either run the
image as root (`docker run --user 0`) or, better, change the port that nginx
listens on via the `ELEMENT_WEB_PORT` environment variable.
[Element Web Modules](https://github.com/element-hq/element-modules/tree/main/packages/element-web-module-api) can be dynamically loaded
by being made available (e.g. via bind mount) in a directory within `/tmp/element-web-modules/`.
The default entrypoint will be index.js in that directory but can be overridden if a package.json file is found with a `main` directive.
These modules will be presented in a `/modules` subdirectory within the webroot, and automatically added to the config.json `modules` field.
If you wish to use docker in read-only mode,
you should follow the [upstream instructions](https://hub.docker.com/_/nginx#:~:text=Running%20nginx%20in%20read%2Donly%20mode)
but additionally include the following directories:
- /tmp/element-web-config/
- /etc/nginx/conf.d/
The behaviour of the docker image can be customised via the following
environment variables:

View File

@@ -112,3 +112,7 @@ Unreliable in encrypted rooms.
## Knock rooms (`feature_ask_to_join`) [In Development]
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
D1[\Deploy staging.element.io/]
D2[\Check dockerhub/]
D2[\Check docker build/]
D3[\Deploy app.element.io/]
D4[\Check desktop package/]
@@ -213,10 +213,10 @@ switched back to the version of the dependency from the master branch to not lea
# Deploying
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.
- [ ] 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
- [ ] Test staging.element.io

View File

@@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.11.91",
"version": "1.11.93",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -74,7 +74,7 @@
"@types/react-dom": "18.3.5",
"oidc-client-ts": "3.1.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": "npm:wrap-ansi@^7.0.0"
},
@@ -88,11 +88,11 @@
"@matrix-org/emojibase-bindings": "^1.3.4",
"@matrix-org/react-sdk-module-api": "^2.4.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/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^3.0.0",
"@vector-im/compound-web": "^7.6.1",
"@vector-im/compound-design-tokens": "^4.0.0",
"@vector-im/compound-web": "^7.6.4",
"@vector-im/matrix-wysiwyg": "2.38.0",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
@@ -274,7 +274,7 @@
"postcss-preset-env": "^10.0.0",
"postcss-scss": "^4.0.4",
"postcss-simple-vars": "^7.0.1",
"prettier": "3.4.2",
"prettier": "3.5.1",
"process": "^0.11.10",
"raw-loader": "^4.0.2",
"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

View File

@@ -11,6 +11,7 @@ import { registerAccountMas } from "../oidc";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { TestClientServerAPI } from "../csAPI";
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
// 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 registerAccountMas(page, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
await app.settings.openUserSettings("Security & Privacy");
await expect(page.getByText("This session is backing up your keys.")).toBeVisible();
// Wait for the ui to load
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) => {

View File

@@ -58,8 +58,8 @@ test.describe("Backups", () => {
// Create another
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
await currentDialogLocator.getByLabel("Security Key").fill(securityKey);
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
await currentDialogLocator.getByLabel("Recovery Key").fill(securityKey);
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
// Should be successful
@@ -90,8 +90,8 @@ test.describe("Backups", () => {
// Try to create another
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
// But cancel the security key dialog, to simulate not having the secret storage passphrase
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
// But cancel the recovery key dialog, to simulate not having the secret storage passphrase
await currentDialogLocator.getByTestId("dialog-cancel-button").click();
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();
// 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();
// 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.
*/
import { type Locator, type Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { viewRoomSummaryByName } from "../right-panel/utils";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { completeCreateSecretStorageDialog, createBot, logIntoElement } from "./utils.ts";
import { type Client } from "../../pages/client.ts";
import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
const ROOM_NAME = "Test room";
const NAME = "Alice";
function getMemberTileByName(page: Page, name: string): Locator {
return page.locator(`.mx_MemberTileView, [title="${name}"]`);
}
test.use({
displayName: NAME,
synapseConfig: {
@@ -57,36 +50,40 @@ test.describe("Dehydration", () => {
await completeCreateSecretStorageDialog(page);
// Open the settings again
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();
await expectDehydratedDeviceEnabled(app);
// the dehydrated device gets created with the name "Dehydrated
// device". We want to make sure that it is not visible as a normal
// device.
const sessionsTab = await app.settings.openUserSettings("Sessions");
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();
// now check that the user info right-panel shows the dehydrated device
// 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();
await expectDehydratedDeviceEnabled(app);
});
test("Reset recovery key during login re-creates dehydrated device", async ({
@@ -119,6 +116,40 @@ test.describe("Dehydration", () => {
expect(dehydratedDeviceIds.length).toBe(1);
expect(dehydratedDeviceIds[0]).not.toEqual(initialDehydratedDeviceIds[0]);
});
test("'Reset cryptographic identity' removes dehydrated device", async ({ page, homeserver, app, credentials }) => {
await logIntoElement(page, credentials);
// Create a dehydrated device by setting up recovery (see "'Set up
// recovery' creates dehydrated device" test above)
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 expectDehydratedDeviceEnabled(app);
// After recovery is set up, we reset our cryptographic identity, which
// should drop the dehydrated device.
await settingsDialogLocator.getByRole("button", { name: "Reset cryptographic identity" }).click();
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
await expectDehydratedDeviceDisabled(app);
});
});
async function getDehydratedDeviceIds(client: Client): Promise<string[]> {
@@ -134,3 +165,29 @@ 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);
}
/** Wait for our user to not have a dehydrated device */
async function expectDehydratedDeviceDisabled(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(0);
}

View File

@@ -21,6 +21,7 @@ import {
waitForVerificationRequest,
} from "./utils";
import { type Bot } from "../../pages/bot";
import { Toasts } from "../../pages/toasts.ts";
test.describe("Device verification", { tag: "@no-webkit" }, () => {
let aliceBotClient: Bot;
@@ -72,6 +73,51 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, false);
});
// Regression test for https://github.com/element-hq/element-web/issues/29110
test("No toast after verification, even if the secrets take a while to arrive", async ({ page, credentials }) => {
// Before we log in, the bot creates an encrypted room, so that we can test the toast behaviour that only happens
// when we are in an encrypted room.
await aliceBotClient.createRoom({
initial_state: [
{
type: "m.room.encryption",
state_key: "",
content: { algorithm: "m.megolm.v1.aes-sha2" },
},
],
});
// In order to simulate a real environment more accurately, we need to slow down the arrival of the
// `m.secret.send` to-device messages. That's slightly tricky to do directly, so instead we delay the *outgoing*
// `m.secret.request` messages.
await page.route("**/_matrix/client/v3/sendToDevice/m.secret.request/**", async (route) => {
await route.fulfill({ json: {} });
await new Promise((f) => setTimeout(f, 1000));
await route.fetch();
});
await logIntoElement(page, credentials);
// Launch the verification request between alice and the bot
const verificationRequest = await initiateAliceVerificationRequest(page);
// Handle emoji SAS verification
const infoDialog = page.locator(".mx_InfoDialog");
// the bot chooses to do an emoji verification
const verifier = await verificationRequest.evaluateHandle((request) => request.startVerification("m.sas.v1"));
// Handle emoji request and check that emojis are matching
await doTwoWaySasVerification(page, verifier);
await infoDialog.getByRole("button", { name: "They match" }).click();
await infoDialog.getByRole("button", { name: "Got it" }).click();
// There should be no toast (other than the notifications one)
const toasts = new Toasts(page);
await toasts.rejectToast("Notifications");
await toasts.assertNoToasts();
});
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
// A mode 0x02 verification: "self-verifying in which the current device does not yet trust the master key"
await logIntoElement(page, credentials);
@@ -119,7 +165,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
await logIntoElement(page, credentials);
// 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
const dialog = page.locator(".mx_Dialog");
@@ -136,15 +182,15 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
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);
// 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");
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();
await dialog.locator("#mx_securityKey").fill(aliceRecoveryKey.encodedPrivateKey);
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();

View File

@@ -17,6 +17,7 @@ import {
logIntoElement,
logOutOfElement,
verify,
waitForDevices,
} from "./utils";
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
@@ -144,25 +145,8 @@ test.describe("Cryptography", function () {
// bob deletes his second device
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.
async function awaitOneDevice(iterations = 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();
// wait for the logout to propagate.
await waitForDevices(app, bob.credentials.userId, 1);
// close and reopen the room, to get the shield to update.
await app.viewRoomByName("Bob");
@@ -285,11 +269,7 @@ test.describe("Cryptography", function () {
// 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
// his user info.
await app.toggleRoomInfoPanel();
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");
await waitForDevices(app, bob.credentials.userId, 1);
// Our app is blocked from syncing while Bob sends his messages.
await app.client.network.goOffline();

View File

@@ -35,7 +35,7 @@ test.describe("Key storage out of sync toast", () => {
await page.getByRole("button", { name: "Enter recovery 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 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 { Page } from "@playwright/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";
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 ({
app,
page,
bot: bob,
user: aliceCredentials,
toasts,
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
const bobVerificationRequest = await bob.evaluateHandle(
@@ -84,13 +87,17 @@ test.describe("User verification", () => {
});
test("can abort emoji verification when emoji mismatch", async ({
app,
page,
bot: bob,
user: aliceCredentials,
toasts,
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
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

@@ -145,11 +145,13 @@ export async function checkDeviceIsCrossSigned(app: ElementAppPage): Promise<voi
* @param app -` ElementAppPage` wrapper for the playwright `Page`.
* @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 checkBackupKeyIn4S - whether to check that the backup key is stored in 4S
*/
export async function checkDeviceIsConnectedKeyBackup(
app: ElementAppPage,
expectedBackupVersion: string,
checkBackupPrivateKeyInCache: boolean,
checkBackupKeyIn4S: boolean = true,
): Promise<void> {
// Sanity check the given backup version: if it's null, something went wrong earlier in the test.
if (!expectedBackupVersion) {
@@ -192,7 +194,7 @@ export async function checkDeviceIsConnectedKeyBackup(
// The active backup version is as expected
expect(activeBackupVersion).toBe(expectedBackupVersion);
// The backup key is stored in 4S
expect(backupKeyIn4S).toBe(true);
if (checkBackupKeyIn4S) expect(backupKeyIn4S).toBe(true);
if (checkBackupPrivateKeyInCache) {
// The backup key is available locally
@@ -216,13 +218,13 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur
// if a securityKey was given, verify the new device
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()) {
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_primary:not([disabled])", { hasText: "Continue" }).click();
await page.getByRole("button", { name: "Done" }).click();
@@ -249,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 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) {
const settings = await app.settings.openUserSettings("Encryption");
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.getByRole("button", { name: "Continue", disabled: false }).click();
await app.page.getByRole("button", { name: "Done" }).click();
@@ -291,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).
*
* Returns the security key
* Returns the recovery key
*/
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
await app.settings.openUserSettings("Security & Privacy");
@@ -319,9 +321,9 @@ export async function completeCreateSecretStorageDialog(
const currentDialogLocator = page.locator(".mx_Dialog");
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 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();
// copy the recovery key to use it later
const recoveryKey = await page.evaluate(() => navigator.clipboard.readText());
@@ -345,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) {
await page.getByRole("button", { name: "Copy" }).click();
@@ -502,3 +504,31 @@ export async function deleteCachedSecrets(page: Page) {
});
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.
*/
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 { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { type Credentials } from "../../plugins/homeserver";
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
credentials: async ({}, use, testInfo) => {
await use({
username: `user_${testInfo.testId}`,
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
password: "oETo7MPf0o",
});
} as CredentialsWithDisplayName);
},
});

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../../element-web-test";
import type { Page } from "@playwright/test";
test.describe("Header section of the room list", () => {
test.use({
labsFlags: ["feature_new_room_list"],
});
/**
* Get the header section of the room list
* @param page
*/
function getHeaderSection(page: Page) {
return page.getByTestId("room-list-header");
}
test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
await app.closeNotificationToast();
});
test("should render the header section", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListHeader = getHeaderSection(page);
await expect(roomListHeader).toMatchScreenshot("room-list-header.png");
const composeMenu = roomListHeader.getByRole("button", { name: "Add" });
await composeMenu.click();
await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-compose-menu.png");
// New message should open the direct messages dialog
await page.getByRole("menuitem", { name: "New message" }).click();
await expect(page.getByRole("heading", { name: "Direct Messages" })).toBeVisible();
await app.closeDialog();
// New room should open the room creation dialog
await composeMenu.click();
await page.getByRole("menuitem", { name: "New room" }).click();
await expect(page.getByRole("heading", { name: "Create a private room" })).toBeVisible();
await app.closeDialog();
});
test("should render the header section for a space", { tag: "@screenshot" }, async ({ page, app, user }) => {
await app.client.createSpace({ name: "MySpace" });
await page.getByRole("button", { name: "MySpace" }).click();
const roomListHeader = getHeaderSection(page);
await expect(roomListHeader).toMatchScreenshot("room-list-space-header.png");
await expect(roomListHeader.getByRole("heading", { name: "MySpace" })).toBeVisible();
await expect(roomListHeader.getByRole("button", { name: "Add" })).toBeVisible();
const spaceMenu = roomListHeader.getByRole("button", { name: "Open space menu" });
await spaceMenu.click();
await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-space-menu.png");
// It should open the space home
await page.getByRole("menuitem", { name: "Space home" }).click();
await expect(page.getByRole("main").getByRole("heading", { name: "MySpace" })).toBeVisible();
// It should open the invite dialog
await spaceMenu.click();
await page.getByRole("menuitem", { name: "Invite" }).click();
await expect(page.getByRole("heading", { name: "Invite to MySpace" })).toBeVisible();
await app.closeDialog();
// It should open the space preferences
await spaceMenu.click();
await page.getByRole("menuitem", { name: "Preferences" }).click();
await expect(page.getByRole("heading", { name: "Preferences" })).toBeVisible();
await app.closeDialog();
// It should open the space settings
await spaceMenu.click();
await page.getByRole("menuitem", { name: "Space Settings" }).click();
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();
await app.closeDialog();
});
});

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

@@ -57,8 +57,8 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
await util.openThread("ThreadRoot");
// 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
await util.assertUnread(room2, 30 - 7);
// 30 remaining messages are unread - 6 messages are displayed under the thread root
await util.assertUnread(room2, 30 - 6);
});
test("Creating a new thread based on a reply makes the room unread", async ({

View File

@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import { type Page } from "@playwright/test";
import { type Visibility } from "matrix-js-sdk/src/matrix";
import { test, expect } from "../../element-web-test";
import { type ElementAppPage } from "../../pages/ElementAppPage";
@@ -85,6 +86,15 @@ test.describe("Room Header", () => {
await expect(header).toMatchScreenshot("room-header-long-name.png");
},
);
test("should render room header icon correctly", { tag: "@screenshot" }, async ({ page, app, user }) => {
await app.client.createRoom({ name: "Test Room", visibility: "public" as Visibility });
await app.viewRoomByName("Test Room");
const header = page.locator(".mx_RoomHeader");
await expect(header).toMatchScreenshot("room-header-with-icon.png");
});
});
test.describe("with a video room", () => {

View File

@@ -50,8 +50,8 @@ test.describe("Appearance user settings tab", () => {
// Click "Show advanced" link button
await tab.getByRole("button", { name: "Show advanced" }).click();
await tab.locator(".mx_Checkbox", { hasText: "Use bundled emoji font" }).click();
await tab.locator(".mx_Checkbox", { hasText: "Use a system font" }).click();
await tab.getByRole("switch", { name: "Use bundled emoji font" }).click();
await tab.getByRole("switch", { name: "Use a system font" }).click();
// Assert that the font-family value was removed
await expect(page.locator("body")).toHaveCSS("font-family", '""');

View File

@@ -111,21 +111,4 @@ test.describe("Encryption tab", () => {
// The user is prompted to reset their identity
await expect(dialog.getByText("Forgot your recovery key? Youll need to reset your identity.")).toBeVisible();
});
test("should warn before turning off key storage", { tag: "@screenshot" }, async ({ page, app, util }) => {
await verifySession(app, recoveryKey.encodedPrivateKey);
await util.openEncryptionTab();
await page.getByRole("checkbox", { name: "Allow key storage" }).click();
await expect(
page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }),
).toBeVisible();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("delete-key-storage-confirm.png");
await page.getByRole("button", { name: "Delete key storage" }).click();
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked();
});
});

View File

@@ -43,7 +43,7 @@ class Helpers {
*/
async verifyDevice(recoveryKey: GeneratedSecretStorageKey) {
// Select the security phrase
await this.page.getByRole("button", { name: "Verify with Security Key" }).click();
await this.page.getByRole("button", { name: "Verify with Recovery Key" }).click();
await this.enterRecoveryKey(recoveryKey);
await this.page.getByRole("button", { name: "Done" }).click();
}
@@ -91,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
* @param title - The title of the dialog
* @param confirmButtonLabel - The label of the confirm button

View File

@@ -25,18 +25,14 @@ test.describe("Security user settings tab", () => {
},
});
test.beforeEach(async ({ page, user }) => {
test.beforeEach(async ({ page, app, user }) => {
// Dismiss "Notification" toast
await page
.locator(".mx_Toast_toast", { hasText: "Notifications" })
.getByRole("button", { name: "Dismiss" })
.click();
await app.closeNotificationToast();
await page.locator(".mx_Toast_buttons").getByRole("button", { name: "Yes" }).click(); // Allow analytics
});
test.describe("AnalyticsLearnMoreDialog", () => {
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page }) => {
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => {
const tab = await app.settings.openUserSettings("Security");
await tab.getByRole("button", { name: "Learn more" }).click();
await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot(
@@ -45,16 +41,57 @@ test.describe("Security user settings tab", () => {
});
});
test("should contain section to set ID server", async ({ app }) => {
test("should be able to set an ID server", async ({ app, context, user, page }) => {
const tab = await app.settings.openUserSettings("Security");
const setIdServer = tab.locator(".mx_SetIdServer");
await context.route("https://identity.example.org/_matrix/identity/v2", async (route) => {
await route.fulfill({
status: 200,
json: {},
});
});
await context.route("https://identity.example.org/_matrix/identity/v2/account/register", async (route) => {
await route.fulfill({
status: 200,
json: {
token: "AToken",
},
});
});
await context.route("https://identity.example.org/_matrix/identity/v2/account", async (route) => {
await route.fulfill({
status: 200,
json: {
user_id: user.userId,
},
});
});
await context.route("https://identity.example.org/_matrix/identity/v2/terms", async (route) => {
await route.fulfill({
status: 200,
json: {
policies: {},
},
});
});
const setIdServer = tab.locator(".mx_IdentityServerPicker");
await setIdServer.scrollIntoViewIfNeeded();
// Assert that an input area for identity server exists
await expect(setIdServer.getByRole("textbox", { name: "Enter a new identity server" })).toBeVisible();
const textElement = setIdServer.getByRole("textbox", { name: "Enter a new identity server" });
await textElement.click();
await textElement.fill("https://identity.example.org");
await setIdServer.getByRole("button", { name: "Change" }).click();
await expect(setIdServer.getByText("Checking server")).toBeVisible();
// Accept terms
await page.getByTestId("dialog-primary-button").click();
// Check identity has changed.
await expect(setIdServer.getByText("Your identity server has been changed")).toBeVisible();
// Ensure section title is updated.
await expect(tab.getByText(`Identity server (identity.example.org)`, { exact: true })).toBeVisible();
});
test("should enable show integrations as enabled", async ({ app, page }) => {
test("should enable show integrations as enabled", async ({ app, page, user }) => {
const tab = await app.settings.openUserSettings("Security");
const setIntegrationManager = tab.locator(".mx_SetIntegrationManager");

View File

@@ -19,7 +19,6 @@ test.describe("UserView", () => {
const rightPanel = page.locator("#mx_RightPanel");
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", {
mask: [page.locator(".mx_UserInfo_profile_mxid")],
css: `

View File

@@ -202,4 +202,15 @@ export class ElementAppPage {
}
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();
}
}

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: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 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.

After

Width:  |  Height:  |  Size: 5.2 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: 74 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: 45 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

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