Compare commits

...

59 Commits

Author SHA1 Message Date
RiotRobot
bb5bf5a462 v1.12.6 2025-12-03 17:21:30 +00:00
ElementRobot
916c5a0232 Add option to pick call options for voice calls. (#31407) (#31413)
* Add option to pick call options for voice calls.

* hook on the right thing

* Fix wrong call being disabled

* update snaps

* Add tests for menus

* more snaps

* snap snap

(cherry picked from commit a352a3838e)

Co-authored-by: Will Hunt <2072976+Half-Shot@users.noreply.github.com>
2025-12-03 16:29:41 +00:00
RiotRobot
f7e6cb6129 v1.12.5 2025-12-02 15:04:27 +00:00
RiotRobot
9dc9b169ab Upgrade dependency to matrix-js-sdk@39.3.0 2025-12-02 14:46:48 +00:00
RiotRobot
dae90a059f v1.12.5-rc.0 2025-11-25 14:35:44 +00:00
RiotRobot
2f727430e1 Upgrade dependency to matrix-js-sdk@39.3.0-rc.0 2025-11-25 14:12:52 +00:00
ElementRobot
4392aa1ed0 [create-pull-request] automated change (#31315)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-25 06:23:04 +00:00
Florian Duros
a721c5f4ea chore: update element-hq/element-web-module-api to 1.6.0 (#31309) 2025-11-24 18:30:59 +00:00
David Langley
79f1176b92 Fix location sharing flake by hiding MapLibre info link from screenshots (#31290)
* Hide MapLibre info link from screenshots

* Update Reply-to-the-location-on-ThreadView-linux.png
2025-11-24 16:38:02 +00:00
Michael Telatynski
92bb15fbba Update Emojibase to v17 (#31307)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-24 12:08:11 +00:00
Matthew Hodgson
7aa7793640 Return to using Fira Code as the default monospace font (#31302)
* Return to using Fira Code as the default monospace font.

because Inconsolata lacks Box Drawings and Geometric Shapes glyphs.
Fixes https://github.com/element-hq/element-web/issues/31289
Partially reverts https://github.com/matrix-org/matrix-react-sdk/pull/3008

* fix quotes

* fix tests

* fix quotes

* appease prettier

* fix snapshot tests, hopefully

* more snapshots
2025-11-24 11:52:32 +00:00
ElementRobot
f282be05ca [create-pull-request] automated change (#31295)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-24 09:10:02 +00:00
ElementRobot
744922cbcc [create-pull-request] automated change (#31270)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-24 09:07:40 +00:00
David Langley
7183d91930 Improve viewSomethingBehindModal logic (#31301) 2025-11-21 17:04:52 +00:00
byteplow
cdedcc0b5a Adds tooltip for compose menu (#31122)
* Adds tooltip for compose menu button

* fix tests

* prettier

* tweak aria attributes
2025-11-21 16:42:44 +00:00
Richard van der Hoff
b679693702 Documentation for initialisation/login code (#31297)
* Documentation in MatrixChat and Lifecycle

* State transition diagram for `View`
2025-11-21 14:58:03 +00:00
Florian Duros
fbb43d5e61 Fix message edition and reply when multiple rooms at displayed the same moment (#31280)
* feat: implement `ExtrasApi#setRoomIdsForSpace`

* fix: message reply with multiple room views

* fix: message edition when multiple rooms are displayed

* test: check that the view room action is not dispatch when replying

* test: check that the view room action is not dispatch when editing

* refactor: use `ExtraApis#getVisibleRoomBySpaceKey` instead of  `ExtraApis#setRoomIdsForSpace`

* test: update tests to use `getVisibleRoomBySpaceKey`
2025-11-21 14:51:23 +00:00
Florian Duros
a79f6e7aa5 Add option to hide pinned message banner in room view (#31296)
* feat: add `hidePinnedMessageBanner` to room view

* test: add test for `hidePinnedMessageBanner`
2025-11-21 13:45:22 +00:00
David Langley
81c375007e Fix location sharing dialog screenshot flake (#31288)
* FIx location screenshot flake

* const

* Use the map marker to click on the map.
2025-11-21 13:14:42 +00:00
Hubert Chathi
aee24be1b4 Key storage out of sync: reset key backup when needed (#31279)
* add function to pause device listener

* add function to check if key backup key missing both locally and in 4s

* reset backup if backup key missing both locally and in 4s

* fixup! add function to check if key backup key missing both locally and in 4s

* Drop KEY_STORAGE_OUT_OF_SYNC_STORE in favour of checking cross-signing

Check if cross-signing needs resetting, because that seems to be what
KEY_STORAGE_OUT_OF_SYNC_STORE is actually trying to do.

* add a function for resetting key backup and waiting until it's ready

* trigger key storage out of sync toast when missing backup key locally

and fetch it when user enters their recovery key

* reset backup when needed if user forgets recovery key

* rename function as suggested in code review
2025-11-20 20:25:31 +00:00
Richard van der Hoff
1285b73be6 Documentation and symbolic constants for dispatcher actions (#31278)
* Remove unreachable code

`view_last_screen` is never used.

* Remove unused action `user_activity_started`

Nothing listens to this, so it's pointless.

* Symbolic constant for `Action.UserActivity`

* Define symbolic constants for more `Action`s

Constants for some actions that are emitted by `Lifecycle`
2025-11-20 18:18:04 +00:00
Richard van der Hoff
c203f02731 Rename callback on E2eSetup component (#31274)
* Rename callback on E2eSetup component

`BaseDialog.onFinished` is unused when `hasCancel=false`, so this callback is
only used when the user clicks cancel. For clarity, rename it.

* Test for cancellation behaviour
2025-11-20 18:17:51 +00:00
Matthew Hodgson
64130a018b update twemoji to not monochromise emoji with BLACK in their name (#31281) 2025-11-20 11:29:35 +00:00
Matthew Hodgson
e2fc1574bf upgrade to twemoji 17.0.2 and fix #14695 (#31267)
* upgrade to twemoji 17.0.2 and fix #14695

See 356e12591c

* add u3030 (wavy-dash) too
2025-11-19 17:45:12 +00:00
David Langley
de0492b786 Use 22.18 (#31273) 2025-11-19 17:36:00 +00:00
Hubert Chathi
0a46edaaff Remove obsolete checks that the server supports cross-signing (#31275)
We already depend on an API version that includes cross-signing
2025-11-19 17:28:08 +00:00
ElementRobot
dd89cee328 [create-pull-request] automated change (#31271)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-19 06:30:34 +00:00
Michael Telatynski
29ff8a6199 Fix invalid events crashing entire room rather than just their tile (#31256)
* Fix invalid events crashing entire room rather than just their tile

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

* Add test

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-18 17:33:49 +00:00
RiotRobot
184e6e3f29 Merge branch 'master' into develop 2025-11-18 14:41:20 +00:00
RiotRobot
7a01cdae0a v1.12.4 2025-11-18 14:36:41 +00:00
RiotRobot
06656a6472 Upgrade dependency to matrix-js-sdk@39.2.0 2025-11-18 14:29:25 +00:00
R Midhun Suresh
5de9d5d24f Bump version (#31263) 2025-11-18 12:32:49 +00:00
Florian Duros
0eff1caab2 Add options to hide right panel in room view (#31252)
* feat: add options to hide right panel in room view

This option is added for the module API.

* test: add test for hideRightPanel=true of room view

* test: update snapshot ids
2025-11-18 10:05:53 +00:00
R Midhun Suresh
b7acbe65c1 Remove contain property (#31259)
This was telling the browser that the children of this div will never go
out of the bounding rect of the div. This is incorrect because the
expand button is supposed to sit at the edge of the space panel with
half of it outside the bounding rect.
2025-11-18 09:37:09 +00:00
ElementRobot
5736635a65 [create-pull-request] automated change (#31258)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-18 06:22:43 +00:00
Andrew Ferrazzutti
fcd23b48e0 Delayed event management: split endpoints, no auth (#31183)
* Delayed event management: split endpoints, no auth

Use the new js-sdk client methods for calling the dedicated,
unauthenticated endpoints for each of the cancel/restart/send actions
for updating a delayed event.

Note that these methods are compatible with homeservers that support
only the original endpoint where the update action is in the request
body.

* REPLACEME: pull in dependant js-sdk branch

see matrix-org/matrix-js-sdk#5066

* Format with Prettier

* Update matrix-js-sdk
2025-11-17 16:18:25 +00:00
David Baker
250d6571fe Fix pill buttons in dialogs (#31246)
* Fix pill buttons in dialogs

Add the magic class that makes the buttons not be broken by the
dialog button styles.

* Update snapshot
2025-11-17 15:14:34 +00:00
Will Hunt
f3a880f1c3 Support using Element Call for voice calls in DMs (#30817)
* Add voiceOnly options.

* tweaks

* Nearly working demo

* Lots of minor fixes

* Better working version

* remove unused payload

* bits and pieces

* Cleanup based on new hints

* Simple refactor for skipLobby (and remove returnToLobby)

* Tidyup

* Remove unused tests

* Update tests for voice calls

* Add video room support.

* Add a test for video rooms

* tidy

* remove console log line

* lint and tests

* Bunch of fixes

* Fixes

* Use correct title

* make linter happier

* Update tests

* cleanup

* Drop only

* update snaps

* Document

* lint

* Update snapshots

* Remove duplicate test

* add brackets

* fix jest
2025-11-17 11:50:22 +00:00
Michael Telatynski
3d683ec5c6 Fix gen-workflow-mermaid to be compatible with Node 22.18+ (#31250)
* Fix gen-workflow-mermaid to be compatible with Node 22.18+

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-11-17 10:33:45 +00:00
ElementRobot
81f1841aea [create-pull-request] automated change (#31249)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-17 09:15:38 +00:00
ElementRobot
e62125e61f [create-pull-request] automated change (#31247)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-15 06:22:04 +00:00
Michael Telatynski
c675453d72 Deflake ManualDeviceKeyVerificationDialog-test (#31244)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-14 15:07:05 +00:00
Michael Telatynski
ac0a91be9e Improve screen reader accessibility of auth pages (#31236)
* Improve screen reader accessibility of auth pages

Using a combination of auto-focus + aria-live to ensure content is read as the states progress

For https://element-io.atlassian.net/browse/PSB-971

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

* Iterate

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

* Update snapshot

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

* Iterate

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

* Update snapshots

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

* Fix double landmark

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

* Update screenshot

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-14 12:46:15 +00:00
David Baker
333bec33ee Prevent direct imports from shared components (#31238)
Because we use the package now so importing directly causes subtle breakage.
2025-11-14 10:51:34 +00:00
ElementRobot
f400d8db0a [create-pull-request] automated change (#31241)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-14 06:31:19 +00:00
ElementRobot
bb9c9982ef [create-pull-request] automated change (#31240)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-14 06:23:07 +00:00
Michael Telatynski
e2fd873f5e Update storybook to v10 (#31234)
* Update dependency @storybook/test-runner to ^0.24.0

* Storybook migration

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

* Add missing file

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

* Update yarn.lock

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

* Make jest happier

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>
2025-11-13 16:33:51 +00:00
Michael Telatynski
ac255445d1 Remove ts-node (#31228)
* Remove ts-node

Node can handle typescript nowadays

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-11-13 16:33:36 +00:00
Will Hunt
425bc64aa9 Add posthog tracking for key backup toasts (#31195)
* Add tracking for setting up key backups.

* fix lint

* Update @matrix-org/analytics-events to 0.30.0
2025-11-13 13:25:42 +00:00
Michael Telatynski
9c6aa6942c Match engines.node to .node-version (#31233) 2025-11-13 11:43:44 +00:00
renovate[bot]
5d66f9bd1a Update dependency @casualbot/jest-sonar-reporter to v2.4.0 (#28908)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-13 11:33:08 +00:00
renovate[bot]
b894f8d65f Update jest to v30 (major) (#30117)
* Update jest to v30

* Update jest to v30

* Update snapshots

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

* Apply jsdom patch

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

* Fix 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-11-13 10:50:33 +00:00
ElementRobot
6a1f0a7d22 [create-pull-request] automated change (#31231)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-13 10:47:04 +00:00
David Langley
bb582fa8f3 Fix blank sections at the top and bottom of the member list when scrolling (#31198)
* Add memberlist overscan

* Update memberlist.spec.ts
2025-11-12 19:02:58 +00:00
renovate[bot]
11b2ecb041 Update dependency caniuse-lite to v1.0.30001754 (#31219)
* Update dependency caniuse-lite to v1.0.30001754

* 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-11-12 17:34:21 +00:00
David Langley
2ce59df1fe Fix emoji category selection with keyboard (#31162)
* Use firstVisible category for roving tab index

* Adding category keyboard navigation tests

* Reduce repetition in categories definition and add tests

* Remove ternary operators

* Simplify
2025-11-12 16:39:27 +00:00
RiotRobot
842edc6577 v1.12.4-rc.0 2025-11-11 14:45:49 +00:00
ElementRobot
faadcf902e Install shared components during EW install (#31196) (#31208)
* fix: install shared components during EW install

* chore: ignore `@element-hq/web-shared-components` for knip

* chore: remove shared-components operation in layered.sh

EW `yarn install` is also installing shared-componenents dependencies.
`link:` in `package.json` works nearly like `yarn link`.

* Iterate



---------



(cherry picked from commit e883b05206)

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Florian Duros <florianduros@element.io>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-11 14:18:14 +00:00
RiotRobot
191f951303 Upgrade dependency to matrix-js-sdk@39.2.0-rc.0 2025-11-11 12:46:14 +00:00
359 changed files with 4660 additions and 2991 deletions

View File

@@ -167,6 +167,10 @@ module.exports = {
group: ["@vector-im/compound-design-tokens/icons/*"],
message: "Please use @vector-im/compound-design-tokens/assets/web/icons/* instead",
},
{
group: ["**/packages/shared-components/**", "../packages/shared-components/**"],
message: "Please use @element-hq/web-shared-components",
},
],
},
],

View File

@@ -43,7 +43,7 @@ jobs:
working-directory: element-web
run: |
yarn install --frozen-lockfile
yarn ts-node ./scripts/gen-workflow-mermaid.ts ../element-desktop ../element-web ../matrix-js-sdk > docs/automations.md
yarn node ./scripts/gen-workflow-mermaid.ts ../element-desktop ../element-web ../matrix-js-sdk > docs/automations.md
echo "- [Automations](automations.md)" >> docs/SUMMARY.md
- name: Setup mdBook

View File

@@ -2,7 +2,6 @@
/dist
/lib
/node_modules
/packages/
/webapp
/*.log
yarn.lock

View File

@@ -1,3 +1,71 @@
Changes in [1.12.6](https://github.com/element-hq/element-web/releases/tag/v1.12.6) (2025-12-03)
================================================================================================
This release fixes a bug where 1:1 calling was incorrectly not available if no Element Call focus was set.
## 🐛 Bug Fixes
* Add option to pick call options for voice calls. ([#31413](https://github.com/element-hq/element-web/pull/31413)).
Changes in [1.12.5](https://github.com/element-hq/element-web/releases/tag/v1.12.5) (2025-12-02)
================================================================================================
## ✨ Features
* Update Emojibase to v17 ([#31307](https://github.com/element-hq/element-web/pull/31307)). Contributed by @t3chguy.
* Adds tooltip for compose menu ([#31122](https://github.com/element-hq/element-web/pull/31122)). Contributed by @byteplow.
* Add option to hide pinned message banner in room view ([#31296](https://github.com/element-hq/element-web/pull/31296)). Contributed by @florianduros.
* update twemoji to not monochromise emoji with BLACK in their name ([#31281](https://github.com/element-hq/element-web/pull/31281)). Contributed by @ara4n.
* upgrade to twemoji 17.0.2 and fix #14695 ([#31267](https://github.com/element-hq/element-web/pull/31267)). Contributed by @ara4n.
* Add options to hide right panel in room view ([#31252](https://github.com/element-hq/element-web/pull/31252)). Contributed by @florianduros.
* Delayed event management: split endpoints, no auth ([#31183](https://github.com/element-hq/element-web/pull/31183)). Contributed by @AndrewFerr.
* Support using Element Call for voice calls in DMs ([#30817](https://github.com/element-hq/element-web/pull/30817)). Contributed by @Half-Shot.
* Improve screen reader accessibility of auth pages ([#31236](https://github.com/element-hq/element-web/pull/31236)). Contributed by @t3chguy.
* Add posthog tracking for key backup toasts ([#31195](https://github.com/element-hq/element-web/pull/31195)). Contributed by @Half-Shot.
## 🐛 Bug Fixes
* Return to using Fira Code as the default monospace font ([#31302](https://github.com/element-hq/element-web/pull/31302)). Contributed by @ara4n.
* Fix case of home screen being displayed erroneously ([#31301](https://github.com/element-hq/element-web/pull/31301)). Contributed by @langleyd.
* Fix message edition and reply when multiple rooms at displayed the same moment ([#31280](https://github.com/element-hq/element-web/pull/31280)). Contributed by @florianduros.
* Key storage out of sync: reset key backup when needed ([#31279](https://github.com/element-hq/element-web/pull/31279)). Contributed by @uhoreg.
* Fix invalid events crashing entire room rather than just their tile ([#31256](https://github.com/element-hq/element-web/pull/31256)). Contributed by @t3chguy.
* Fix expand button of space panel getting cut off at the edges ([#31259](https://github.com/element-hq/element-web/pull/31259)). Contributed by @MidhunSureshR.
* Fix pill buttons in dialogs ([#31246](https://github.com/element-hq/element-web/pull/31246)). Contributed by @dbkr.
* Fix blank sections at the top and bottom of the member list when scrolling ([#31198](https://github.com/element-hq/element-web/pull/31198)). Contributed by @langleyd.
* Fix emoji category selection with keyboard ([#31162](https://github.com/element-hq/element-web/pull/31162)). Contributed by @langleyd.
Changes in [1.12.4](https://github.com/element-hq/element-web/releases/tag/v1.12.4) (2025-11-18)
================================================================================================
## ✨ Features
* Apply aria-hidden to emoji in SAS verification ([#31204](https://github.com/element-hq/element-web/pull/31204)). Contributed by @t3chguy.
* Add options to hide header and composer of room view for the module api ([#31095](https://github.com/element-hq/element-web/pull/31095)). Contributed by @florianduros.
* Experimental Module API Additions ([#30863](https://github.com/element-hq/element-web/pull/30863)). Contributed by @dbkr.
* Change polls to use fieldset/legend markup ([#31160](https://github.com/element-hq/element-web/pull/31160)). Contributed by @langleyd.
* Use compound Button styles for Jitsi button ([#31159](https://github.com/element-hq/element-web/pull/31159)). Contributed by @Half-Shot.
* Add FocusLock to emoji picker ([#31146](https://github.com/element-hq/element-web/pull/31146)). Contributed by @langleyd.
* Move room name, avatar, and topic to IOpts. ([#30981](https://github.com/element-hq/element-web/pull/30981)). Contributed by @kaylendog.
* Add a devtool for looking at users and their devices ([#30983](https://github.com/element-hq/element-web/pull/30983)). Contributed by @uhoreg.
## 🐛 Bug Fixes
* Fix room list handling of membership changes ([#31197](https://github.com/element-hq/element-web/pull/31197)). Contributed by @t3chguy.
* Fix room list unable to be resized when displayed after a module ([#31186](https://github.com/element-hq/element-web/pull/31186)). Contributed by @florianduros.
* Inhibit keyboard highlights in dialogs when effector is not in focus ([#31181](https://github.com/element-hq/element-web/pull/31181)). Contributed by @t3chguy.
* Strip mentions from forwarded messages ([#30884](https://github.com/element-hq/element-web/pull/30884)). Contributed by @twassman.
* Don't allow pin or edit of messages with a send status ([#31158](https://github.com/element-hq/element-web/pull/31158)). Contributed by @langleyd.
* Hide room header buttons if the room hasn't been created yet. ([#31092](https://github.com/element-hq/element-web/pull/31092)). Contributed by @Half-Shot.
* Fix screen readers not indicating the emoji picker search field is focused. ([#31128](https://github.com/element-hq/element-web/pull/31128)). Contributed by @langleyd.
* Fix emoji picker highlight missing when not active element ([#31148](https://github.com/element-hq/element-web/pull/31148)). Contributed by @t3chguy.
* Add relevant aria attribute for selected emoji in the emoji picker ([#31125](https://github.com/element-hq/element-web/pull/31125)). Contributed by @t3chguy.
* Fix tooltips within context menu portals being unreliable ([#31129](https://github.com/element-hq/element-web/pull/31129)). Contributed by @t3chguy.
* Avoid excessive re-render of room list and member list ([#31131](https://github.com/element-hq/element-web/pull/31131)). Contributed by @florianduros.
* Make emoji picker height responsive. ([#31130](https://github.com/element-hq/element-web/pull/31130)). Contributed by @langleyd.
* Emoji Picker: Focused emoji does not move with the arrow keys ([#30893](https://github.com/element-hq/element-web/pull/30893)). Contributed by @langleyd.
* Fix audio player seek bar position ([#31127](https://github.com/element-hq/element-web/pull/31127)). Contributed by @florianduros.
* Add aria label to emoji picker search ([#31126](https://github.com/element-hq/element-web/pull/31126)). Contributed by @langleyd.
Changes in [1.12.3](https://github.com/element-hq/element-web/releases/tag/v1.12.3) (2025-11-04)
================================================================================================
## 🦖 Deprecations

View File

@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
*/
import * as YAML from "yaml";
import * as fs from "fs";
import * as fs from "node:fs";
export type BuildConfig = {
// Dev note: make everything here optional for user safety. Invalid

View File

@@ -5,11 +5,11 @@ 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 * as fs from "fs";
import * as childProcess from "child_process";
import * as fs from "node:fs";
import * as childProcess from "node:child_process";
import * as semver from "semver";
import { type BuildConfig } from "./BuildConfig";
import { type BuildConfig } from "./BuildConfig.ts";
// This expects to be run from ./scripts/install.ts

View File

@@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { readBuildConfig } from "../BuildConfig";
import { installer } from "../installer";
import { readBuildConfig } from "../BuildConfig.ts";
import { installer } from "../installer.ts";
const buildConf = readBuildConfig();
installer(buildConf);

View File

@@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.12.3",
"version": "1.12.6",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -38,16 +38,16 @@
"clean": "rimraf lib webapp",
"build": "yarn clean && yarn build:genfiles && yarn build:bundle",
"build-stats": "yarn clean && yarn build:genfiles && yarn build:bundle-stats",
"build:res": "ts-node scripts/copy-res.ts",
"build:res": "node scripts/copy-res.ts",
"build:genfiles": "yarn build:res && yarn build:module_system",
"build:modernizr": "modernizr -c .modernizr.json -d src/vector/modernizr.js",
"build:bundle": "webpack --progress --mode production",
"build:bundle-stats": "webpack --progress --mode production --json > webpack-stats.json",
"build:module_system": "ts-node --project ./tsconfig.module_system.json module_system/scripts/install.ts",
"build:module_system": "node module_system/scripts/install.ts",
"dist": "./scripts/package.sh",
"start": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n modules,res \"yarn build:module_system\" \"yarn build:res\" && concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n res,element-js \"yarn start:res\" \"yarn start:js\"",
"start:https": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n res,element-js \"yarn start:res\" \"yarn start:js --server-type https\"",
"start:res": "ts-node scripts/copy-res.ts -w",
"start:res": "node scripts/copy-res.ts -w",
"start:js": "webpack serve --output-path webapp --output-filename=bundles/_dev_/[name].js --output-chunk-filename=bundles/_dev_/[name].js --mode development",
"lint": "yarn lint:types && yarn lint:js && yarn lint:style && yarn lint:workflows",
"lint:js": "eslint --max-warnings 0 src test playwright module_system && prettier --check .",
@@ -75,20 +75,20 @@
"@types/react-dom": "19.2.2",
"oidc-client-ts": "3.3.0",
"jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001751",
"caniuse-lite": "1.0.30001754",
"testcontainers": "^11.0.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@element-hq/element-web-module-api": "1.5.0",
"@element-hq/element-web-module-api": "1.6.0",
"@element-hq/web-shared-components": "link:packages/shared-components",
"@fontsource/inconsolata": "^5",
"@fontsource/fira-code": "^5",
"@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7",
"@matrix-org/analytics-events": "^0.29.2",
"@matrix-org/emojibase-bindings": "^1.3.4",
"@matrix-org/analytics-events": "^0.30.0",
"@matrix-org/emojibase-bindings": "^1.5.0",
"@matrix-org/react-sdk-module-api": "^2.4.0",
"@matrix-org/spec": "^1.7.0",
"@sentry/browser": "^10.0.0",
@@ -109,7 +109,7 @@
"diff-dom": "^5.0.0",
"diff-match-patch": "^1.0.5",
"domutils": "^3.2.2",
"emojibase-regex": "15.3.2",
"emojibase-regex": "^17.0.0",
"escape-html": "^1.0.3",
"file-saver": "^2.0.5",
"filesize": "11.0.13",
@@ -131,8 +131,8 @@
"maplibre-gl": "^5.0.0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^1.10.0",
"matrix-js-sdk": "39.3.0",
"matrix-widget-api": "^1.14.0",
"memoize-one": "^6.0.0",
"mime": "^4.0.4",
"oidc-client-ts": "^3.0.1",
@@ -181,14 +181,14 @@
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@babel/runtime": "^7.12.5",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@casualbot/jest-sonar-reporter": "2.4.0",
"@element-hq/element-call-embedded": "0.16.1",
"@element-hq/element-web-playwright-common": "^2.0.0",
"@peculiar/webcrypto": "^1.4.3",
"@playwright/test": "^1.50.1",
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
"@sentry/webpack-plugin": "^4.0.0",
"@storybook/react-vite": "^9.1.10",
"@storybook/react-vite": "^10.0.7",
"@stylistic/eslint-plugin": "^5.0.0",
"@svgr/webpack": "^8.0.0",
"@testing-library/dom": "^10.4.0",
@@ -204,7 +204,7 @@
"@types/express": "^5.0.0",
"@types/file-saver": "^2.0.3",
"@types/glob-to-regexp": "^0.4.1",
"@types/jest": "29.5.12",
"@types/jest": "30.0.0",
"@types/jitsi-meet": "^2.0.2",
"@types/jsrsasign": "^10.5.4",
"@types/katex": "^0.16.0",
@@ -226,7 +226,7 @@
"@types/ua-parser-js": "^0.7.36",
"@typescript-eslint/eslint-plugin": "^8.19.0",
"@typescript-eslint/parser": "^8.19.0",
"babel-jest": "^29.0.0",
"babel-jest": "^30.0.0",
"babel-loader": "^10.0.0",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"blob-polyfill": "^9.0.0",
@@ -258,10 +258,10 @@
"html-webpack-plugin": "^5.5.3",
"husky": "^9.0.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.6.2",
"jest": "^30.0.0",
"jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^29.7.0",
"jest-mock": "^29.6.2",
"jest-environment-jsdom": "^30.0.0",
"jest-mock": "^30.0.0",
"jest-raw-loader": "^1.0.1",
"jsqr": "^1.4.0",
"knip": "^5.36.2",
@@ -289,14 +289,13 @@
"rimraf": "^6.0.0",
"semver": "^7.5.2",
"source-map-loader": "^5.0.0",
"storybook": "^9.1.10",
"storybook": "^10.0.7",
"stylelint": "^16.23.0",
"stylelint-config-standard": "^39.0.0",
"stylelint-scss": "^6.0.0",
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
"terser-webpack-plugin": "^5.3.9",
"testcontainers": "^11.0.0",
"ts-node": "^10.9.1",
"typescript": "5.8.3",
"util": "^0.12.5",
"web-streams-polyfill": "^4.0.0",
@@ -314,7 +313,7 @@
"relativePaths": true
},
"engines": {
"node": ">=20.0.0"
"node": ">=22.18"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@@ -5,17 +5,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 { waitForPageReady } from "@storybook/test-runner";
import { waitForPageReady, TestRunnerConfig } from "@storybook/test-runner";
import { toMatchImageSnapshot } from "jest-image-snapshot";
const customSnapshotsDir = `${process.cwd()}/playwright/snapshots/`;
const customReceivedDir = `${process.cwd()}/playwright/received/`;
/**
* @type {import('@storybook/test-runner').TestRunnerConfig}
*/
const config = {
setup(page) {
const config: TestRunnerConfig = {
setup() {
expect.extend({ toMatchImageSnapshot });
},
async postVisit(page, context) {

View File

@@ -30,7 +30,7 @@ const config: Config = {
"workers/(.+)Factory": "<rootDir>/__mocks__/workerFactoryMock.js",
},
transformIgnorePatterns: [
"/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error|react-merge-refs)).+$",
"/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error|react-merge-refs|@storybook|storybook)).+$",
],
collectCoverageFrom: [
"<rootDir>/src/**/*.{js,ts,tsx}",

View File

@@ -1,6 +1,6 @@
{
"name": "@element-hq/web-shared-components",
"version": "0.0.0-test.7",
"version": "0.0.0-test.8",
"description": "Shared components for Element",
"author": "New Vector Ltd.",
"repository": {
@@ -57,26 +57,27 @@
"devDependencies": {
"@element-hq/element-web-playwright-common": "^2.0.0",
"@playwright/test": "^1.50.1",
"@storybook/addon-a11y": "^9.1.10",
"@storybook/addon-designs": "^10.0.2",
"@storybook/addon-docs": "^9.1.10",
"@storybook/addon-a11y": "^10.0.7",
"@storybook/addon-designs": "^11.0.1",
"@storybook/addon-docs": "^10.0.7",
"@storybook/icons": "^1.6.0",
"@storybook/react-vite": "^9.1.10",
"@storybook/test-runner": "^0.23.0",
"@storybook/react-vite": "^10.0.7",
"@storybook/test-runner": "^0.24.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.0",
"@types/counterpart": "^0.18.4",
"@types/jest-image-snapshot": "^6.4.0",
"@types/lodash": "^4.17.20",
"@types/react": "^19.2.2",
"concurrently": "^9.2.1",
"eslint": "8",
"eslint-plugin-matrix-org": "^3.0.0",
"eslint-plugin-storybook": "^10.0.0",
"eslint-plugin-storybook": "^10.0.7",
"jest": "^30.2.0",
"jest-image-snapshot": "^6.5.1",
"patch-package": "^8.0.1",
"prettier": "^3.6.2",
"storybook": "^9.1.10",
"storybook": "^10.0.7",
"typescript": "^5.9.3",
"vite": "^7.1.9",
"vite-plugin-dts": "^4.5.4",

View File

@@ -53,7 +53,13 @@ export function Pill({ className, children, label, onClick, ...props }: PropsWit
{label}
</span>
{onClick && (
<IconButton aria-describedby={id} size="16px" onClick={onClick} aria-label={_t("action|delete")}>
<IconButton
aria-describedby={id}
size="16px"
onClick={onClick}
aria-label={_t("action|delete")}
className="mx_Dialog_nonDialogButton"
>
<CloseIcon color="var(--cpd-color-icon-tertiary)" />
</IconButton>
)}

View File

@@ -18,7 +18,7 @@ exports[`Pill renders the pill 1`] = `
<button
aria-describedby="_r_0_"
aria-label="Delete"
class="_icon-button_1pz9o_8"
class="_icon-button_1pz9o_8 mx_Dialog_nonDialogButton"
data-kind="primary"
role="button"
style="--cpd-icon-button-size: 16px;"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
diff --git a/node_modules/jsdom/lib/jsdom/browser/Window.js b/node_modules/jsdom/lib/jsdom/browser/Window.js
index 52d011c..f62f6d6 100644
--- a/node_modules/jsdom/lib/jsdom/browser/Window.js
+++ b/node_modules/jsdom/lib/jsdom/browser/Window.js
@@ -505,10 +505,10 @@ function installOwnProperties(window, options) {
event: makeReplaceablePropertyDescriptor("event", window),
// [LegacyUnforgeable]:
- window: { configurable: false },
- document: { configurable: false },
- location: { configurable: false },
- top: { configurable: false }
+ window: { configurable: true },
+ document: { configurable: true },
+ location: { configurable: true },
+ top: { configurable: true }
});
diff --git a/node_modules/jsdom/lib/jsdom/living/generated/Location.js b/node_modules/jsdom/lib/jsdom/living/generated/Location.js
index fc4d1dd..c855bd5 100644
--- a/node_modules/jsdom/lib/jsdom/living/generated/Location.js
+++ b/node_modules/jsdom/lib/jsdom/living/generated/Location.js
@@ -322,19 +322,19 @@ function getUnforgeables(globalObject) {
}
});
Object.defineProperties(unforgeables, {
- assign: { configurable: false, writable: false },
- replace: { configurable: false, writable: false },
- reload: { configurable: false, writable: false },
- href: { configurable: false },
- toString: { configurable: false, writable: false },
- origin: { configurable: false },
- protocol: { configurable: false },
- host: { configurable: false },
- hostname: { configurable: false },
- port: { configurable: false },
- pathname: { configurable: false },
- search: { configurable: false },
- hash: { configurable: false }
+ assign: { configurable: true, writable: false },
+ replace: { configurable: true, writable: false },
+ reload: { configurable: true, writable: false },
+ href: { configurable: true },
+ toString: { configurable: true, writable: false },
+ origin: { configurable: true },
+ protocol: { configurable: true },
+ host: { configurable: true },
+ hostname: { configurable: true },
+ port: { configurable: true },
+ pathname: { configurable: true },
+ search: { configurable: true },
+ hash: { configurable: true }
});
unforgeablesMap.set(globalObject, unforgeables);
}

View File

@@ -49,7 +49,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
/**
* Take snapshots of mx_EventTile_last on each layout, outputting log for reference/debugging.
* @param detail The snapshot name. Used for outputting logs too.
* @param monospace This changes the font used to render the UI from a default one to Inconsolata. Set to false by default.
* @param monospace This changes the font used to render the UI from a default one to Fira Code. Set to false by default.
*/
const takeSnapshots = async (page: Page, app: ElementAppPage, detail: string, monospace = false) => {
// Check that the audio player is rendered and its button becomes visible
@@ -65,7 +65,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
if (monospace) {
// Assert that the monospace timer is visible
await expect(locator.locator("[role='timer']")).toHaveCSS("font-family", "Inconsolata");
await expect(locator.locator("[role='timer']")).toHaveCSS("font-family", '"Fira Code"');
}
};
@@ -73,7 +73,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
// Enable system font and monospace setting
await app.settings.setValue("useBundledEmojiFont", null, SettingLevel.DEVICE, false);
await app.settings.setValue("useSystemFont", null, SettingLevel.DEVICE, true);
await app.settings.setValue("systemFont", null, SettingLevel.DEVICE, "Inconsolata");
await app.settings.setValue("systemFont", null, SettingLevel.DEVICE, "Fira Code");
}
// Check the status of the seek bar

View File

@@ -49,7 +49,10 @@ test.describe("Encryption state after registration", () => {
"Pa$sW0rD!",
);
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
await page
.getByRole("navigation", { name: "Room list" })
.getByRole("button", { name: "New conversation" })
.click();
await page.getByRole("menuitem", { name: "New room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("test room");
await page.getByRole("button", { name: "Create room" }).click();
@@ -78,7 +81,10 @@ test.describe("Key backup reset from elsewhere", () => {
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailpitClient, testUsername, `${testUsername}@email.com`, testPassword);
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
await page
.getByRole("navigation", { name: "Room list" })
.getByRole("button", { name: "New conversation" })
.click();
await page.getByRole("menuitem", { name: "New room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("test room");
await page.getByRole("button", { name: "Create room" }).click();

View File

@@ -21,7 +21,7 @@ const checkDMRoom = async (page: Page) => {
};
const startDMWithBob = async (page: Page, bob: Bot) => {
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "New conversation" }).click();
await page.getByRole("menuitem", { name: "Start chat" }).click();
await page.getByTestId("invite-dialog-input").fill(bob.credentials.userId);
await page.getByRole("option", { name: bob.credentials.displayName }).click();

View File

@@ -23,7 +23,10 @@ test.describe("Key storage out of sync toast", () => {
await deleteCachedSecrets(page);
// We won't be prompted for crypto setup unless we have an e2e room, so make one
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
await page
.getByRole("navigation", { name: "Room list" })
.getByRole("button", { name: "New conversation" })
.click();
await page.getByRole("menuitem", { name: "New room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
await page.getByRole("button", { name: "Create room" }).click();
@@ -68,7 +71,10 @@ test.describe("'Turn on key storage' toast", () => {
await logIntoElementAndVerify(page, credentials, recoveryKey.encodedPrivateKey);
// We won't be prompted for crypto setup unless we have an e2e room, so make one
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
await page
.getByRole("navigation", { name: "Room list" })
.getByRole("button", { name: "New conversation" })
.click();
await page.getByRole("menuitem", { name: "New room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
await page.getByRole("button", { name: "Create room" }).click();

View File

@@ -438,7 +438,7 @@ export async function sendMessageInCurrentRoom(page: Page, message: string): Pro
* @param isEncrypted - Whether the room should be encrypted
*/
export async function createRoom(page: Page, roomName: string, isEncrypted: boolean): Promise<void> {
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "New conversation" }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
const dialog = page.locator(".mx_Dialog");

View File

@@ -73,7 +73,10 @@ test.describe("Invite dialog", function () {
"should support inviting a user to Direct Messages",
{ tag: "@screenshot" },
async ({ page, app, user, bot }) => {
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
await page
.getByRole("navigation", { name: "Room list" })
.getByRole("button", { name: "New conversation" })
.click();
await page.getByRole("menuitem", { name: "Start chat" }).click();
const other = page.locator(".mx_InviteDialog_other");

View File

@@ -30,7 +30,7 @@ test.describe("Header section of the room list", () => {
const roomListHeader = getHeaderSection(page);
await expect(roomListHeader).toMatchScreenshot("room-list-header.png");
const composeMenu = roomListHeader.getByRole("button", { name: "Add" });
const composeMenu = roomListHeader.getByRole("button", { name: "New conversation" });
await composeMenu.click();
await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-compose-menu.png");
@@ -55,7 +55,7 @@ test.describe("Header section of the room list", () => {
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();
await expect(roomListHeader.getByRole("button", { name: "New conversation" })).toBeVisible();
const spaceMenu = roomListHeader.getByRole("button", { name: "Open space menu" });
await spaceMenu.click();

View File

@@ -315,7 +315,10 @@ test.describe("Room list", () => {
});
test("should be a video room", { tag: "@screenshot" }, async ({ page, app, user }) => {
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
await page
.getByRole("navigation", { name: "Room list" })
.getByRole("button", { name: "New conversation" })
.click();
await page.getByRole("menuitem", { name: "New video room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("video room");
await page.getByRole("button", { name: "Create video room" }).click();

View File

@@ -46,24 +46,21 @@ test.describe("Location sharing", { tag: "@no-firefox" }, () => {
await submitShareLocation(page);
await page.locator(".mx_RoomView_body .mx_EventTile .mx_MLocationBody").click({
position: {
x: 225,
y: 150,
},
});
await page.getByRole("button", { name: "Map marker" }).click();
// Wait for map to load
await expect(page.getByRole("region", { name: "Map" })).toMatchScreenshot(
const dialog = page.getByRole("dialog");
// wait for the dialog to be visible
await expect(dialog).toBeVisible();
// screenshot the map within the dialog
await expect(dialog.getByRole("region", { name: "Map" })).toMatchScreenshot(
"location-pin-drop-message-map.png",
);
// clicking location tile opens maximised map
await expect(page.getByRole("dialog")).toBeVisible();
await app.closeDialog();
await expect(page.locator(".mx_Marker")).toBeVisible();
await expect(page.getByRole("button", { name: "Map marker" })).toBeVisible();
},
);

View File

@@ -129,6 +129,7 @@ test.describe("Login", () => {
await expect(page.getByRole("heading", { name: "Welcome to Element!" })).toBeVisible();
// Start the login process
await expect(axe).toHaveNoViolations();
await page.getByRole("link", { name: "Sign in" }).click();
// first pick the homeserver, as otherwise the user picker won't be visible
@@ -148,8 +149,6 @@ test.describe("Login", () => {
await selectHomeserver(page, homeserver.baseUrl);
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
// Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688
// cy.percySnapshot("Login");
await expect(axe).toHaveNoViolations();
await page.getByRole("textbox", { name: "Username" }).fill(credentials.username);

View File

@@ -80,13 +80,12 @@ test.describe("Memberlist", () => {
await app.scrollListToBottom(memberListContainer);
// Wait for the target member to be visible after scrolling
const targetName = "Member14";
// Member9 is the last in the list as they are lexicographically sorted
const targetName = "Member9";
const targetMember = memberlist.locator(".mx_MemberTileView_name").filter({ hasText: targetName });
await targetMember.waitFor({ state: "visible" });
// Verify Alice is not visible at this point
await expect(memberlist.locator(".mx_MemberTileView_name").filter({ hasText: "Alice" })).toHaveCount(0);
// Alice is not visible and will require scrolling to,
// but is likely in the dom as we have an overscan on the top and bottom of the list.
// Click on a member near the bottom of the list
await expect(targetMember).toBeVisible();
await targetMember.click();

View File

@@ -164,7 +164,7 @@ test.describe("RightPanel", () => {
css: `
/* Use monospace font for consistent mask width */
.mx_UserInfo_profile_mxid {
font-family: Inconsolata !important;
font-family: "Fira Code" !important;
}
`,
});

View File

@@ -45,7 +45,7 @@ test.describe("Create Room", () => {
);
test("should allow us to start a chat and show encryption state", async ({ page, user, app }) => {
await page.getByRole("button", { name: "Add", exact: true }).click();
await page.getByRole("button", { name: "New conversation", exact: true }).click();
await page.getByRole("menuitem", { name: "Start chat" }).click();
await page.getByTestId("invite-dialog-input").fill(user.userId);

View File

@@ -373,7 +373,7 @@ test.describe("Threads", () => {
// Exclude timestamp, read marker, and maplibregl-map from snapshots
const css =
".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .maplibregl-map { visibility: hidden !important; }";
".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .maplibregl-map, .maplibregl-ctrl-attrib { visibility: hidden !important; }";
let locator = page.locator(".mx_RoomView_body");
// User sends message

View File

@@ -24,7 +24,7 @@ test.describe("UserView", () => {
css: `
/* Use monospace font for consistent mask width */
.mx_UserInfo_profile_mxid {
font-family: Inconsolata !important;
font-family: "Fira Code" !important;
}
`,
});

View File

@@ -9,7 +9,7 @@ import type { EventType, Preset } from "matrix-js-sdk/src/matrix";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { test, expect } from "../../element-web-test";
import type { Credentials } from "../../plugins/homeserver";
import type { Bot } from "../../pages/bot";
import { Bot } from "../../pages/bot";
function assertCommonCallParameters(
url: URLSearchParams,
@@ -27,27 +27,28 @@ function assertCommonCallParameters(
expect(hash.get("preload")).toEqual("false");
}
async function sendRTCState(bot: Bot, roomId: string, notification?: "ring" | "notification") {
async function sendRTCState(bot: Bot, roomId: string, notification?: "ring" | "notification", intent?: string) {
const resp = await bot.sendStateEvent(
roomId,
"org.matrix.msc3401.call.member",
{
application: "m.call",
call_id: "",
device_id: "OiDFxsZrjz",
expires: 180000000,
foci_preferred: [
"application": "m.call",
"call_id": "",
"m.call.intent": intent,
"device_id": "OiDFxsZrjz",
"expires": 180000000,
"foci_preferred": [
{
livekit_alias: roomId,
livekit_service_url: "https://example.org",
type: "livekit",
},
],
focus_active: {
"focus_active": {
focus_selection: "oldest_membership",
type: "livekit",
},
scope: "m.room",
"scope": "m.room",
},
`_@${bot.credentials.userId}_OiDFxsZrjz_m.call`,
);
@@ -64,6 +65,7 @@ async function sendRTCState(bot: Bot, roomId: string, notification?: "ring" | "n
event_id: resp.event_id,
rel_type: "org.matrix.msc4075.rtc.notification.parent",
},
"m.call.intent": intent,
"notification_type": notification,
"sender_ts": 1758611895996,
});
@@ -103,15 +105,21 @@ test.describe("Element Call", () => {
});
test.describe("Group Chat", () => {
let charlie: Bot;
test.use({
room: async ({ page, app, user, bot }, use) => {
const roomId = await app.client.createRoom({ name: "TestRoom", invite: [bot.credentials.userId] });
room: async ({ page, app, user, homeserver, bot }, use) => {
charlie = new Bot(page, homeserver, { displayName: "Charlie" });
await charlie.prepareClient();
const roomId = await app.client.createRoom({
name: "TestRoom",
invite: [bot.credentials.userId, charlie.credentials.userId],
});
await use({ roomId });
},
});
test("should be able to start a video call", async ({ page, user, room, app }) => {
await app.viewRoomById(room.roomId);
await expect(page.getByText("Bob joined the room")).toBeVisible();
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
await page.getByRole("button", { name: "Video call" }).click();
await page.getByRole("menuitem", { name: "Element Call" }).click();
@@ -126,9 +134,16 @@ test.describe("Element Call", () => {
expect(hash.get("skipLobby")).toEqual(null);
});
test("should NOT be able to start a voice call", async ({ page, user, room, app }) => {
// Voice calls do not exist in group rooms
await app.viewRoomById(room.roomId);
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
await expect(page.getByRole("button", { name: "Voice call" })).not.toBeVisible();
});
test("should be able to skip lobby by holding down shift", async ({ page, user, bot, room, app }) => {
await app.viewRoomById(room.roomId);
await expect(page.getByText("Bob joined the room")).toBeVisible();
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
await page.getByRole("button", { name: "Video call" }).click();
await page.keyboard.down("Shift");
@@ -147,8 +162,8 @@ test.describe("Element Call", () => {
test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => {
await app.viewRoomById(room.roomId);
// Allow bob to create a call
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
await expect(page.getByText("Bob joined the room")).toBeVisible();
// Fake a start of a call
await sendRTCState(bot, room.roomId);
const button = page.getByTestId("join-call-button");
@@ -156,7 +171,6 @@ test.describe("Element Call", () => {
// And test joining
await button.click();
const frameUrlStr = await page.locator("iframe").getAttribute("src");
console.log(frameUrlStr);
await expect(frameUrlStr).toBeDefined();
const url = new URL(frameUrlStr);
const hash = new URLSearchParams(url.hash.slice(1));
@@ -168,29 +182,29 @@ test.describe("Element Call", () => {
[true, false].forEach((skipLobbyToggle) => {
test(
`should be able to join a call via incoming call toast (skipLobby=${skipLobbyToggle})`,
`should be able to join a call via incoming video call toast (skipLobby=${skipLobbyToggle})`,
{ tag: ["@screenshot"] },
async ({ page, user, bot, room, app }) => {
await app.viewRoomById(room.roomId);
// Allow bob to create a call
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
await expect(page.getByText("Bob joined the room")).toBeVisible();
// Fake a start of a call
await sendRTCState(bot, room.roomId, "notification");
await sendRTCState(bot, room.roomId, "notification", "video");
const toast = page.locator(".mx_Toast_toast");
const button = toast.getByRole("button", { name: "Join" });
if (skipLobbyToggle) {
await toast.getByRole("switch").check();
await expect(toast).toMatchScreenshot("incoming-call-group-video-toast-checked.png");
await expect(toast).toMatchScreenshot(`incoming-call-group-video-toast-checked.png`);
} else {
await toast.getByRole("switch").uncheck();
await expect(toast).toMatchScreenshot("incoming-call-group-video-toast-unchecked.png");
await expect(toast).toMatchScreenshot(`incoming-call-group-video-toast-unchecked.png`);
}
// And test joining
await button.click();
const frameUrlStr = await page.locator("iframe").getAttribute("src");
console.log(frameUrlStr);
await expect(frameUrlStr).toBeDefined();
const url = new URL(frameUrlStr);
const hash = new URLSearchParams(url.hash.slice(1));
@@ -201,6 +215,34 @@ test.describe("Element Call", () => {
},
);
});
test(
`should be able to join a call via incoming voice call toast`,
{ tag: ["@screenshot"] },
async ({ page, user, bot, room, app }) => {
await app.viewRoomById(room.roomId);
// Allow bob to create a call
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
// Fake a start of a call
await sendRTCState(bot, room.roomId, "notification", "audio");
const toast = page.locator(".mx_Toast_toast");
const button = toast.getByRole("button", { name: "Join" });
await expect(toast).toMatchScreenshot(`incoming-call-group-voice-toast.png`);
// And test joining
await button.click();
const frameUrlStr = await page.locator("iframe").getAttribute("src");
await expect(frameUrlStr).toBeDefined();
const url = new URL(frameUrlStr);
const hash = new URLSearchParams(url.hash.slice(1));
assertCommonCallParameters(url.searchParams, hash, user, room);
expect(hash.get("intent")).toEqual("join_existing");
expect(hash.get("skipLobby")).toEqual("true");
},
);
});
test.describe("DMs", () => {
@@ -253,7 +295,6 @@ test.describe("Element Call", () => {
test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => {
await app.viewRoomById(room.roomId);
// Allow bob to create a call
await expect(page.getByText("Bob joined the room")).toBeVisible();
// Fake a start of a call
await sendRTCState(bot, room.roomId);
@@ -262,7 +303,6 @@ test.describe("Element Call", () => {
// And test joining
await button.click();
const frameUrlStr = await page.locator("iframe").getAttribute("src");
console.log(frameUrlStr);
await expect(frameUrlStr).toBeDefined();
const url = new URL(frameUrlStr);
const hash = new URLSearchParams(url.hash.slice(1));
@@ -278,24 +318,31 @@ test.describe("Element Call", () => {
{ tag: ["@screenshot"] },
async ({ page, user, bot, room, app }) => {
await app.viewRoomById(room.roomId);
// Allow bob to create a call
await expect(page.getByText("Bob joined the room")).toBeVisible();
// Fake a start of a call
await sendRTCState(bot, room.roomId, "ring");
await sendRTCState(bot, room.roomId, "ring", "video");
const toast = page.locator(".mx_Toast_toast");
const button = toast.getByRole("button", { name: "Join" });
const button = toast.getByRole("button", { name: "Accept" });
if (skipLobbyToggle) {
await toast.getByRole("switch").check();
await expect(toast).toMatchScreenshot("incoming-call-dm-video-toast-checked.png");
} else {
await toast.getByRole("switch").uncheck();
await expect(toast).toMatchScreenshot("incoming-call-dm-video-toast-unchecked.png");
}
await expect(toast).toMatchScreenshot(
`incoming-call-dm-video-toast-${skipLobbyToggle ? "checked" : "unchecked"}.png`,
{
// Hide UserId
css: `
.mx_IncomingCallToast_AvatarWithDetails span:nth-child(2) {
opacity: 0;
}
`,
},
);
// And test joining
await button.click();
const frameUrlStr = await page.locator("iframe").getAttribute("src");
console.log(frameUrlStr);
await expect(frameUrlStr).toBeDefined();
const url = new URL(frameUrlStr);
const hash = new URLSearchParams(url.hash.slice(1));
@@ -306,6 +353,39 @@ test.describe("Element Call", () => {
},
);
});
test(
`should be able to join a call via incoming voice call toast`,
{ tag: ["@screenshot"] },
async ({ page, user, bot, room, app }) => {
await app.viewRoomById(room.roomId);
await expect(page.getByText("Bob joined the room")).toBeVisible();
// Fake a start of a call
await sendRTCState(bot, room.roomId, "ring", "audio");
const toast = page.locator(".mx_Toast_toast");
const button = toast.getByRole("button", { name: "Accept" });
await expect(toast).toMatchScreenshot(`incoming-call-dm-voice-toast.png`, {
// Hide UserId
css: `
.mx_IncomingCallToast_AvatarWithDetails span:nth-child(2) {
opacity: 0;
}
`,
});
// And test joining
await button.click();
const frameUrlStr = await page.locator("iframe").getAttribute("src");
await expect(frameUrlStr).toBeDefined();
const url = new URL(frameUrlStr);
const hash = new URLSearchParams(url.hash.slice(1));
assertCommonCallParameters(url.searchParams, hash, user, room);
expect(hash.get("intent")).toEqual("join_existing_dm_voice");
expect(hash.get("skipLobby")).toEqual("true");
},
);
});
test.describe("Video Rooms", () => {
@@ -318,7 +398,10 @@ test.describe("Element Call", () => {
},
});
test("should be able to create and join a video room", async ({ page, user }) => {
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
await page
.getByRole("navigation", { name: "Room list" })
.getByRole("button", { name: "New conversation" })
.click();
await page.getByRole("menuitem", { name: "New video room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
await page.getByRole("button", { name: "Create video room" }).click();

View File

@@ -144,7 +144,7 @@ export const expect = baseExpect.extend<Expectations>({
}
/* Use monospace font for timestamp for consistent mask width */
.mx_MessageTimestamp {
font-family: Inconsolata !important;
font-family: "Fira Code" !important;
}
`;

View File

@@ -53,7 +53,10 @@ export class ElementAppPage {
*/
public async openCreateRoomDialog(roomKindname: "New room" | "New video room" = "New room"): Promise<Locator> {
await this.page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
await this.page
.getByRole("navigation", { name: "Room list" })
.getByRole("button", { name: "New conversation" })
.click();
await this.page.getByRole("menuitem", { name: roomKindname }).click();
return this.page.locator(".mx_CreateRoomDialog");
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

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

View File

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

View File

@@ -18,9 +18,6 @@ Please see LICENSE files in the repository root for full details.
flex-direction: column;
max-width: 50%;
position: relative;
/* Contain the amount of layers rendered by constraining what actually needs re-layering via css */
contain: layout paint;
}
.mx_LeftPanel_wrapper,

View File

@@ -25,6 +25,10 @@ Please see LICENSE files in the repository root for full details.
mask-image: url("$(res)/img/element-icons/call/video-call.svg");
}
&.mx_LiveContentSummary_text_voice::before {
mask-image: url("$(res)/img/element-icons/call/voice-call.svg");
}
&.mx_LiveContentSummary_text_active {
color: $accent;

View File

@@ -45,7 +45,7 @@
color: var(--cpd-color-text-secondary);
.mx_KeyPanel_key {
font-family: Inconsolata, monospace;
font-family: "Fira Code", monospace;
/*
* From figma https://www.figma.com/design/qTWRfItpO3RdCjnTKPu4mL/Settings?node-id=375-77471&t=t7lozYrSI1AVZZ3U-4
*/

View File

@@ -15,7 +15,7 @@ $font-family:
"Noto Color Emoji";
$monospace-font-family:
"Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace,
"Fira Code", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace,
"Noto Color Emoji";
/* unified palette */

View File

@@ -15,7 +15,7 @@ $font-family:
"Noto Color Emoji";
$monospace-font-family:
"Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace,
"Fira Code", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace,
"Noto Color Emoji";
/* Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=559%3A120 */

View File

@@ -167,11 +167,11 @@ we don't have an account and should hide them. No account == no guest account ei
<div class="mx_Parent">
<a href="https://element.io" target="_blank" rel="noopener">
<img src="$logoUrl" alt="" class="mx_Logo" />
<img src="$logoUrl" alt="$brand" class="mx_Logo" />
</a>
<h1 class="mx_Header_title">_t("welcome_to_element")</h1>
<!-- XXX: Our translations system isn't smart enough to recognize variables in the HTML, so we manually do it -->
<h4 class="mx_Header_subtitle">_t("powered_by_matrix_with_logo")</h4>
<h2 class="mx_Header_subtitle">_t("powered_by_matrix_with_logo")</h2>
<div class="mx_ButtonGroup">
<div class="mx_ButtonRow">
<a href="#/login" class="mx_ButtonParent mx_ButtonSignIn mx_Button_iconSignIn">

View File

@@ -6,8 +6,8 @@ import parseArgs from "minimist";
import * as chokidar from "chokidar";
import * as fs from "node:fs";
import _ from "lodash";
import { util } from "webpack";
import { Translations } from "matrix-web-i18n";
import webpack from "webpack";
import type { Translations } from "matrix-web-i18n";
const I18N_BASE_PATH = "src/i18n/strings/";
const INCLUDE_LANGS = [...new Set([...fs.readdirSync(I18N_BASE_PATH)])]
@@ -58,7 +58,7 @@ function prepareLangFile(lang: string, dest: string): [filename: string, json: s
const json = JSON.stringify(translations, null, 4);
const jsonBuffer = Buffer.from(json);
const digest = util.createHash("xxhash64").update(jsonBuffer).digest("hex").slice(0, 7);
const digest = webpack.util.createHash("xxhash64").update(jsonBuffer).digest("hex").slice(0, 7);
const filename = `${lang}.${digest}.json`;
return [filename, json];

View File

@@ -5,7 +5,7 @@ import path from "node:path";
import YAML from "yaml";
import parseArgs from "minimist";
import cronstrue from "cronstrue";
import { partition } from "lodash";
import _ from "lodash";
const argv = parseArgs<{
debug: boolean;
@@ -81,7 +81,7 @@ class Graph<T extends Node> {
public removeNode(node: T): Edge<T>[] {
if (!this.nodes.has(node.id)) return [];
this.nodes.delete(node.id);
const [removedEdges, keptEdges] = partition(
const [removedEdges, keptEdges] = _.partition(
this.edges,
([source, destination]) => source === node || destination === node,
);
@@ -384,6 +384,7 @@ class MermaidFlowchartPrinter {
private static INDENT = 4;
private currentIndent = 0;
private text = "";
private readonly markdown: boolean;
public readonly idGenerator = new IdGenerator();
private print(text: string): void {
@@ -400,11 +401,8 @@ class MermaidFlowchartPrinter {
this.currentIndent += delta * MermaidFlowchartPrinter.INDENT;
}
public constructor(
direction: "TD" | "TB" | "BT" | "RL" | "LR",
title?: string,
private readonly markdown = false,
) {
public constructor(direction: "TD" | "TB" | "BT" | "RL" | "LR", title?: string, markdown = false) {
this.markdown = markdown;
if (this.markdown) {
this.print("```mermaid");
}

View File

@@ -88,7 +88,7 @@ export default abstract class BasePlatform {
protected onAction(payload: ActionPayload): void {
switch (payload.action) {
case "on_client_not_viable":
case Action.ClientNotViable:
case Action.OnLoggedOut:
this.setNotificationCount(0);
break;

View File

@@ -1,4 +1,5 @@
/*
Copyright 2025 Element Creations Ltd.
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
@@ -144,6 +145,25 @@ export default class DeviceListener {
this.client = undefined;
}
/**
* Pause the device listener while a function runs.
*
* This can be done if the function makes several changes that would trigger
* multiple events, to suppress warning toasts until the process is
* finished.
*/
public async whilePaused(fn: () => Promise<void>): Promise<void> {
const client = this.client;
try {
this.stop();
await fn();
} finally {
if (client) {
this.start(client);
}
}
}
/**
* Dismiss notifications about our own unverified devices
*
@@ -177,6 +197,67 @@ export default class DeviceListener {
await this.client?.setAccountData(RECOVERY_ACCOUNT_DATA_KEY, { enabled: false });
}
/**
* If a `Kind.KEY_STORAGE_OUT_OF_SYNC` condition from {@link doRecheck}
* requires a reset of cross-signing keys.
*
* We will reset cross-signing keys if both our local cache and 4S don't
* have all cross-signing keys.
*
* In theory, if the set of keys in our cache and in 4S are different, and
* we have a complete set between the two, we could be OK, but that
* should be exceptionally rare, and is more complicated to detect.
*/
public async keyStorageOutOfSyncNeedsCrossSigningReset(forgotRecovery: boolean): Promise<boolean> {
const crypto = this.client?.getCrypto();
if (!crypto) {
return false;
}
const crossSigningStatus = await crypto.getCrossSigningStatus();
const allCrossSigningSecretsCached =
crossSigningStatus.privateKeysCachedLocally.masterKey &&
crossSigningStatus.privateKeysCachedLocally.selfSigningKey &&
crossSigningStatus.privateKeysCachedLocally.userSigningKey;
if (forgotRecovery) {
return !allCrossSigningSecretsCached;
} else {
return !allCrossSigningSecretsCached && !crossSigningStatus.privateKeysInSecretStorage;
}
}
/**
* If a `Kind.KEY_STORAGE_OUT_OF_SYNC` condition from {@link doRecheck}
* requires a reset of key backup.
*
* If the user has their recovery key, we need to reset backup if:
* - the user hasn't disabled backup,
* - we don't have the backup key cached locally, *and*
* - we don't have the backup key stored in 4S.
* (The user should already have a key backup created at this point,
* otherwise `doRecheck` would have triggered a `Kind.TURN_ON_KEY_STORAGE`
* condition.)
*
* If the user has forgotten their recovery key, we need to reset backup if:
* - the user hasn't disabled backup, and
* - we don't have the backup key locally.
*/
public async keyStorageOutOfSyncNeedsBackupReset(forgotRecovery: boolean): Promise<boolean> {
const crypto = this.client?.getCrypto();
if (!crypto) {
return false;
}
const shouldHaveBackup = !(await this.recheckBackupDisabled(this.client!));
const backupKeyCached = (await crypto.getSessionBackupPrivateKey()) !== null;
const backupKeyStored = await this.client!.isKeyBackupKeyStored();
if (forgotRecovery) {
return shouldHaveBackup && !backupKeyCached;
} else {
return shouldHaveBackup && !backupKeyCached && !backupKeyStored;
}
}
private async ensureDeviceIdsAtStartPopulated(): Promise<void> {
if (this.ourDeviceIdsAtStart === null) {
this.ourDeviceIdsAtStart = await this.getDeviceIds();
@@ -318,12 +399,6 @@ export default class DeviceListener {
const cli = this.client;
// cross-signing support was added to Matrix in MSC1756, which landed in spec v1.1
if (!(await cli.isVersionSupported("v1.1"))) {
logSpan.debug("cross-signing not supported");
return;
}
const crypto = cli.getCrypto();
if (!crypto) {
logSpan.debug("crypto not enabled");
@@ -363,7 +438,10 @@ export default class DeviceListener {
// said we are OK with that.
const keyBackupIsOk = keyBackupUploadActive || backupDisabled;
const allSystemsReady = isCurrentDeviceTrusted && allCrossSigningSecretsCached && keyBackupIsOk && recoveryIsOk;
const backupKeyCached = (await crypto.getSessionBackupPrivateKey()) !== null;
const allSystemsReady =
isCurrentDeviceTrusted && allCrossSigningSecretsCached && keyBackupIsOk && recoveryIsOk && backupKeyCached;
await this.reportCryptoSessionStateToAnalytics(cli);
@@ -407,15 +485,22 @@ export default class DeviceListener {
}
} else {
// If we get here, then we are verified, have key backup, and
// 4S, but crypto.isSecretStorageReady returned false, which
// means that 4S doesn't have all the secrets.
logSpan.warn("4S is missing secrets", {
// 4S, but allSystemsReady is false, which means that either
// secretStorageStatus.ready is false (which means that 4S
// doesn't have all the secrets), or we don't have the backup
// key cached locally.
logSpan.warn("4S is missing secrets or backup key not cached", {
crossSigningReady,
secretStorageStatus,
allCrossSigningSecretsCached,
isCurrentDeviceTrusted,
backupKeyCached,
});
showSetupEncryptionToast(SetupKind.KEY_STORAGE_OUT_OF_SYNC_STORE);
// We use the right toast variant based on whether the backup
// key is missing locally. If any of the cross-signing keys are
// missing locally, that is handled by the
// `!allCrossSigningSecretsCached` branch above.
showSetupEncryptionToast(SetupKind.KEY_STORAGE_OUT_OF_SYNC);
}
} else {
logSpan.info("Not yet ready, but shouldShowSetupEncryptionToast==false");

View File

@@ -599,6 +599,9 @@ async function abortLogin(): Promise<void> {
}
/** Attempt to restore the session from localStorage or indexeddb.
*
* If the credentials are found, and the session is successfully restored,
* emits {@link Action.OnLoggedIn}, {@link Action.WillStartClient} and {@link Action.StartedClient}.
*
* @returns true if a session was found; false if no existing session was found.
*
@@ -787,6 +790,8 @@ async function createOidcTokenRefresher(credentials: IMatrixClientCreds): Promis
* optionally clears localstorage, persists new credentials
* to localstorage, starts the new client.
*
* Emits {@link Action.OnLoggedIn}, {@link Action.WillStartClient} and {@link Action.StartedClient}.
*
* @param {IMatrixClientCreds} credentials The credentials to use
* @param {Boolean} clearStorageEnabled True to clear storage before starting the new client
* @param {Boolean} isFreshLogin True if this is a fresh login, false if it is previous session being restored
@@ -1001,7 +1006,7 @@ export function softLogout(): void {
// Ensure that we dispatch a view change **before** stopping the client so
// so that React components unmount first. This avoids React soft crashes
// that can occur when components try to use a null client.
dis.dispatch({ action: "on_client_not_viable" }); // generic version of on_logged_out
dis.dispatch({ action: Action.ClientNotViable }); // generic version of on_logged_out
stopMatrixClient(/*unsetClient=*/ false);
// DO NOT CALL LOGOUT. A soft logout preserves data, logout does not.
@@ -1019,6 +1024,12 @@ export function isLoggingOut(): boolean {
* Starts the matrix client and all other react-sdk services that
* listen for events while a session is logged in.
*
* By the time this method is called, we have successfully logged in if necessary, and the client has been set up with
* the access token.
*
* Emits {@link Acction.WillStartClient} before starting the client, and {@link Action.ClientStarted} when the client has
* been started.
*
* @param client the matrix client to start
* @param startSyncing - `true` to actually start syncing the client.
* @param clientPegOpts - Options to pass through to {@link MatrixClientPeg.start}.
@@ -1034,7 +1045,7 @@ async function startMatrixClient(
// to add listeners for the 'sync' event so otherwise we'd have
// a race condition (and we need to dispatch synchronously for this
// to work).
dis.dispatch({ action: "will_start_client" }, true);
dis.dispatch({ action: Action.WillStartClient }, true);
// reset things first just in case
SdkContextClass.instance.typingStore.reset();
@@ -1080,7 +1091,7 @@ async function startMatrixClient(
// dispatch that we finished starting up to wire up any other bits
// of the matrix client that cannot be set prior to starting up.
dis.dispatch({ action: "client_started" });
dis.dispatch({ action: Action.ClientStarted });
if (isSoftLogout()) {
softLogout();

View File

@@ -15,6 +15,7 @@ import { MatrixClientPeg } from "./MatrixClientPeg";
import dis from "./dispatcher/dispatcher";
import Timer from "./utils/Timer";
import { type ActionPayload } from "./dispatcher/payloads";
import { Action } from "./dispatcher/actions.ts";
// Time in ms after that a user is considered as unavailable/away
const UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
@@ -61,7 +62,7 @@ class Presence {
}
private onAction = (payload: ActionPayload): void => {
if (payload.action === "user_activity") {
if (payload.action === Action.UserActivity) {
this.setState(SetPresence.Online);
this.unavailableTimer?.restart();
}

View File

@@ -939,7 +939,13 @@ for (const evType of ElementCallEventType.names) {
*/
export function hasText(ev: MatrixEvent, client: MatrixClient, showHiddenEvents?: boolean): boolean {
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
return Boolean(handler?.(ev, client, false, showHiddenEvents));
try {
return Boolean(handler?.(ev, client, false, showHiddenEvents));
} catch (e) {
console.error(`Error encountered when trying to render event type=${ev.getType()} id=${ev.getId()}`, e);
// Returning true if we have a handler so we can show an error tile rather than no tile at all
return !!handler;
}
}
/**

View File

@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import dis from "./dispatcher/dispatcher";
import Timer from "./utils/Timer";
import { Action } from "./dispatcher/actions.ts";
// important these are larger than the timeouts of timers
// used with UserActivity.timeWhileActive*,
@@ -190,11 +191,9 @@ export default class UserActivity {
this.lastScreenY = event.screenY;
}
dis.dispatch({ action: "user_activity" });
dis.dispatch({ action: Action.UserActivity });
if (!this.activeNowTimeout.isRunning()) {
this.activeNowTimeout.start();
dis.dispatch({ action: "user_activity_start" });
UserActivity.runTimersUntilTimeout(this.attachedActiveNowTimers, this.activeNowTimeout);
} else {
this.activeNowTimeout.restart();

View File

@@ -6,7 +6,76 @@ 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.
*/
/** constants for MatrixChat.state.view */
/**
* Constants for MatrixChat.state.view.
*
* The `View` is the primary state machine of the application: it has different states for the various setup flows
* that the user may find themselves in. Once we have a functioning client, we can transition to the `LOGGED_IN` state
* which is the "normal" state of the application.
*
* An incomplete state transition diagram follows.
*
* (initial state)
* ┌─────────────────┐ Lock held by other instance ┌─────────────────┐
* │ LOADING │─────────────────────────────►│ CONFIRM_LOCK_ │
* │ │◄─────────────────────────────│ THEFT │
* └─────────────────┘ Lock theft confirmed └─────────────────┘
* Session recovered │ │ │
* ┌──────────────┘ │ └────────────────┐
* │ ┌─────────────┘ │ No previous session
* │ │ Token/OIDC login succeeded │
* │ │ ▼
* │ │ ┌─────────────────┐
* │ │ │ WELCOME │ (from all other states
* │ │ │ │ except LOCK_STOLEN)
* │ │ └─────────────────┘ │
* │ │ "Create Account" │ │ "Sign in" │ Client logged out
* │ │ ┌────────────────────────┘ │ │
* │ │ │ │ ┌────────────────────┘
* │ │ │ │ │
* │ │ ▼ "Create an ▼ ▼ "Forgot
* │ │ ┌─────────────────┐ account" ┌─────────────────┐ password" ┌─────────────────┐
* │ │ │ REGISTER │◄───────────────│ LOGIN │───────────────►│ FORGOT_PASSWORD │
* │ │ │ │───────────────►│ │◄───────────────│ │
* │ │ └─────────────────┘ "Sign in here" └─────────────────┘ Complete / └─────────────────┘
* │ │ │ │ "Sign in instead" ▲
* │ │ └────────────────────────────────┐ │ │
* │ └────────────────────────────────────────┐ │ │ │
* │ ▼ ▼ ▼ │
* │ ┌──────────────────┐ │
* │ │ (postLoginSetup) │ │
* │ └──────────────────┘ │
* │ ┌────────────────────────────────────┘ │ │ │
* │ │ E2EE not enabled ┌─────────────┘ └──────┐ │
* │ │ │ Account has │ Account lacks │
* │ │ │ cross-signing │ cross-signing │
* │ │ │ keys │ keys │
* │ │ Client started and ▼ ▼ │
* │ │ force_verification ┌─────────────────┐ ┌─────────────────┐ │
* │ │ pending │ COMPLETE_ │ │ E2E_SETUP │ │
* │ │ ┌─────────────────►│ SECURITY │ │ │ │
* │ │ │ └─────────────────┘ └─────────────────┘ │ "Forgotten
* │ │ │ ┌───────────────────────┘ │ │ your
* │ │ │ │ ┌───────────────────────────────────────────────┘ │ password?"
* │ │ │ │ │ │
* │ │ │ │ │ (from all other states │
* │ │ │ │ │ except LOCK_STOLEN) │
* │ │ │ │ │ └──────────────┐ │
* ▼ ▼ │ ▼ ▼ Soft logout error ▼ │
* ┌─────────────────┐ ┌─────────────────┐
* │ LOGGED_IN │ Re-authentication succeeded │ SOFT_LOGOUT │
* │ │◄────────────────────────────────────────────────────────│ │
* └─────────────────┘ └─────────────────┘
*
* (from all other states)
* │
* │ Session lock stolen
* ▼
* ┌─────────────────┐
* │ LOCK_STOLEN │
* │ │
* └─────────────────┘
*/
enum Views {
// a special initial state which is only used at startup, while we are
// trying to re-animate a matrix client or register as a guest.

View File

@@ -18,6 +18,7 @@ import { MatrixClientPeg } from "../../MatrixClientPeg";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";
import { type ActionPayload } from "../../dispatcher/payloads";
import { Action } from "../../dispatcher/actions.ts";
interface IProps {
// URL to request embedded page content from
@@ -109,7 +110,7 @@ export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
private onAction = (payload: ActionPayload): void => {
// HACK: Workaround for the context's MatrixClient not being set up at render time.
if (payload.action === "client_started") {
if (payload.action === Action.ClientStarted) {
this.forceUpdate();
}
};

View File

@@ -202,7 +202,10 @@ interface IState {
hideToSRUsers: boolean;
syncError: Error | null;
serverConfig?: ValidatedServerConfig;
/** Has our MatrixClient started? */
ready: boolean;
threepidInvite?: IThreepidInvite;
roomOobData?: object;
pendingInitialSync?: boolean;
@@ -225,7 +228,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private firstSyncPromise: PromiseWithResolvers<void>;
private screenAfterLogin?: IScreen;
/** True if we have successfully completed an OIDC or token login.
*
* XXX it's unclear if this is ever cleared, so what happens if the user logs out and then logs back in?
*/
private tokenLogin?: boolean;
// What to focus on next component update, if anything
private focusNext: FocusNextType;
private subTitleStatus: string;
@@ -386,6 +395,26 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
await Lifecycle.onSessionLockStolen();
}
/**
* Perform actions that are specific to a user that has just logged in (compare {@link onLoggedIn}, which, despite
* its name, is called when an already-logged-in client is restored at session startup).
*
* Called when:
*
* - We successfully completed an OIDC or token login, via {@link initSession}.
* - The {@link Login} or {@link Register} components notify us that we successfully completed a non-OIDC login or
* registration.
*
* In both cases, {@link Action.OnLoggedIn} will already have been emitted, but the call to {@link onLoggedIn} will
* have been suppressed (by either {@link tokenLogin} being set, or the view being set to {@link Views.LOGIN} or
* {@link Views.REGISTER}).
*
* {@link onWillStartClient} and {@link onClientStarted} will already have been called (but not necessarily
* completed).
*
* This method either calls {@link onLiggedIn} directly, or switches to {@link Views.E2E_SETUP} or
* {@link Views.COMPLETE_SECURITY}, which will later call {@link onCompleteSecurityE2eSetupFinished}.
*/
private async postLoginSetup(): Promise<void> {
const cli = MatrixClientPeg.safeGet();
const cryptoEnabled = Boolean(cli.getCrypto());
@@ -427,10 +456,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else {
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
}
} else if (
(await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) &&
!(await shouldSkipSetupEncryption(cli))
) {
} else if (!(await shouldSkipSetupEncryption(cli))) {
// if cross-signing is not yet set up, do so now if possible.
InitialCryptoSetupStore.sharedInstance().startInitialCryptoSetup(
cli,
@@ -606,6 +632,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
);
}
private isLoggedInViewPageDisplayed(): boolean {
return this.loggedInView.current !== null && this.state.page_type !== undefined;
}
private setStateForNewView(state: Partial<IState>): void {
if (state.view === undefined) {
throw new Error("setStateForNewView with no view!");
@@ -815,13 +845,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
break;
}
case "view_last_screen":
// This function does what we want, despite the name. The idea is that it shows
// the last room we were looking at or some reasonable default/guess. We don't
// have to worry about email invites or similar being re-triggered because the
// function will have cleared that state and not execute that path.
this.showScreenAfterLogin();
break;
case "hide_left_panel":
this.setState(
{
@@ -859,13 +882,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.onLoggedIn();
}
break;
case "on_client_not_viable":
case Action.ClientNotViable:
this.onSoftLogout();
break;
case Action.OnLoggedOut:
this.onLoggedOut();
break;
case "will_start_client":
case Action.WillStartClient:
this.setState({ ready: false }, () => {
// if the client is about to start, we are, by definition, not ready.
// Set ready to false now, then it'll be set to true when the sync
@@ -873,7 +896,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.onWillStartClient();
});
break;
case "client_started":
case Action.ClientStarted:
// No need to make this handler async to wait for the result of this
this.onClientStarted().catch((e) => {
logger.error("Exception in onClientStarted", e);
@@ -1078,7 +1101,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.viewWelcome();
return;
}
if (!this.state.currentRoomId && !this.state.currentUserId) {
if (!this.state.currentRoomId && !this.state.currentUserId && !this.isLoggedInViewPageDisplayed()) {
this.viewHome();
}
}
@@ -1379,7 +1403,15 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
/**
* Called when a new logged in session has started
* Called when a new logged in session has started.
*
* Called:
*
* - on {@link Action.OnLoggedIn}, but only when we don't expect a separate call to {@link postLoginSetup}.
* - from {@link postLoginSetup}, when we don't have crypto setup tasks to perform after the login.
*
* It's never actually called if we have crypto setup tasks to perform after login (which we normally do, unless
* crypto is disabled.) XXX: is this a bug or a feature?
*/
private async onLoggedIn(): Promise<void> {
ThemeController.isLogin = false;
@@ -1389,6 +1421,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
await this.onShowPostLoginScreen();
}
/**
* Show the first screen after the application is successfully loaded in a logged-in state.
*
* Called:
*
* - by {@link onLoggedIn}
* - by {@link onCompleteSecurityE2eSetupFinished}
*
* In other words, whenever we think we have completed the login and E2E setup tasks.
*/
private async onShowPostLoginScreen(): Promise<void> {
this.setStateForNewView({ view: Views.LOGGED_IN });
// If a specific screen is set to be shown after login, show that above
@@ -1815,7 +1857,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// if we weren't already coming at this from an existing screen
// and we're logged in, then explicitly default to home.
// if we're not logged in, then the login flow will do the right thing.
if (!this.state.currentRoomId && !this.state.currentUserId) {
if (!this.state.currentRoomId && !this.state.currentUserId && !this.isLoggedInViewPageDisplayed()) {
this.viewHome();
}
} else if (screen === "settings") {
@@ -2053,7 +2095,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
PerformanceMonitor.instance.stop(PerformanceEntryNames.REGISTER);
};
// complete security / e2e setup has finished
/** Called when {@link Views.E2E_SETUP} or {@link Views.COMPLETE_SECURITY} have completed. */
private onCompleteSecurityE2eSetupFinished = async (): Promise<void> => {
const forceVerify = await this.shouldForceVerification();
if (forceVerify) {
@@ -2104,7 +2146,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else if (this.state.view === Views.COMPLETE_SECURITY) {
view = <CompleteSecurity onFinished={this.onCompleteSecurityE2eSetupFinished} />;
} else if (this.state.view === Views.E2E_SETUP) {
view = <E2eSetup onFinished={this.onCompleteSecurityE2eSetupFinished} />;
view = <E2eSetup onCancelled={this.onCompleteSecurityE2eSetupFinished} />;
} else if (this.state.view === Views.LOGGED_IN) {
// `ready` and `view==LOGGED_IN` may be set before `page_type` (because the
// latter is set via the dispatcher). If we don't yet have a `page_type`,

View File

@@ -175,6 +175,16 @@ interface IRoomProps extends RoomViewProps {
* If true, hide the composer
*/
hideComposer?: boolean;
/*
* If true, hide the right panel
*/
hideRightPanel?: boolean;
/**
* If true, hide the pinned messages banner
*/
hidePinnedMessageBanner?: boolean;
}
export { MainSplitContentType };
@@ -1197,7 +1207,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
case Action.EditEvent: {
// Quit early if we're trying to edit events in wrong rendering context
if (payload.timelineRenderingType !== this.state.timelineRenderingType) return;
if (payload.event && payload.event.getRoomId() !== this.state.roomId) {
const roomId: string | undefined = payload.event?.getRoomId();
if (payload.event && roomId !== this.state.roomId) {
// if the room is displayed in a module, we don't want to change the room view
if (roomId && this.roomViewStore.isRoomDisplayedInModule(roomId)) return;
// If the event is in a different room (e.g. because the event to be edited is being displayed
// in the results of an all-rooms search), we need to view that room first.
defaultDispatcher.dispatch<ViewRoomPayload>({
@@ -2459,7 +2475,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
</AuxPanel>
);
const pinnedMessageBanner = (
const pinnedMessageBanner = !this.props.hidePinnedMessageBanner && (
<PinnedMessageBanner room={this.state.room} permalinkCreator={this.permalinkCreator} />
);
@@ -2557,7 +2573,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
);
}
const showRightPanel = !isRoomEncryptionLoading && this.state.room && this.state.showRightPanel;
const showRightPanel =
!this.props.hideRightPanel && !isRoomEncryptionLoading && this.state.room && this.state.showRightPanel;
const rightPanel = showRightPanel ? (
<RightPanel

View File

@@ -13,15 +13,19 @@ import CompleteSecurityBody from "../../views/auth/CompleteSecurityBody";
import { InitialCryptoSetupDialog } from "../../views/dialogs/security/InitialCryptoSetupDialog";
interface IProps {
onFinished: () => void;
/** Callback which is called if the crypto setup failed, and the user clicked the 'cancel' button */
onCancelled: () => void;
}
/**
* An {@link AuthPage} which shows the {@link InitialCryptoSetupDialog}.
*/
export default class E2eSetup extends React.Component<IProps> {
public render(): React.ReactNode {
return (
<AuthPage>
<CompleteSecurityBody>
<InitialCryptoSetupDialog onFinished={this.props.onFinished} />
<InitialCryptoSetupDialog onCancelled={this.props.onCancelled} />
</CompleteSecurityBody>
</AuthPage>
);

View File

@@ -29,16 +29,6 @@ export interface UserInfoVerificationSectionState {
verifySelectedUser: () => Promise<void>;
}
const useHomeserverSupportsCrossSigning = (cli: MatrixClient): boolean => {
return useAsyncMemo<boolean>(
async () => {
return cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
},
[cli],
false,
);
};
const useHasCrossSigningKeys = (cli: MatrixClient, member: User, canVerify: boolean): boolean | undefined => {
return useAsyncMemo(async () => {
if (!canVerify) return undefined;
@@ -56,8 +46,6 @@ export const useUserInfoVerificationViewModel = (
): UserInfoVerificationSectionState => {
const cli = useContext(MatrixClientContext);
const homeserverSupportsCrossSigning = useHomeserverSupportsCrossSigning(cli);
const userTrust = useAsyncMemo<UserVerificationStatus | undefined>(
async () => cli.getCrypto()?.getUserVerificationStatus(member.userId),
[member.userId],
@@ -67,13 +55,7 @@ export const useUserInfoVerificationViewModel = (
const hasUserVerificationStatus = Boolean(userTrust);
const isUserVerified = Boolean(userTrust?.isVerified());
const isMe = member.userId === cli.getUserId();
const canVerify =
hasUserVerificationStatus &&
homeserverSupportsCrossSigning &&
!isUserVerified &&
!isMe &&
devices &&
devices.length > 0;
const canVerify = hasUserVerificationStatus && !isUserVerified && !isMe && devices && devices.length > 0;
const hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify);
const verifySelectedUser = (): Promise<void> => verifyUser(cli, member as User);

View File

@@ -7,6 +7,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import dispatcher from "../../../dispatcher/dispatcher";
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
@@ -19,7 +20,7 @@ import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
import { useEventEmitter, useEventEmitterState, useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import { DefaultTagID } from "../../../stores/room-list/models";
import { useCall, useConnectionState, useParticipantCount } from "../../../hooks/useCall";
import { type ConnectionState } from "../../../models/Call";
import { CallEvent, type ConnectionState } from "../../../models/Call";
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
import DMRoomMap from "../../../utils/DMRoomMap";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
@@ -67,6 +68,10 @@ export interface RoomListItemViewState {
* Whether there are participants in the call.
*/
hasParticipantInCall: boolean;
/**
* Whether the call is a voice or video call.
*/
callType: CallType | undefined;
/**
* Pre-rendered and translated preview for the latest message in the room, or undefined
* if no preview should be shown.
@@ -123,10 +128,10 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
// EC video call or video room
const call = useCall(room.roomId);
const connectionState = useConnectionState(call);
const hasParticipantInCall = useParticipantCount(call) > 0;
const participantCount = useParticipantCount(call);
const callConnectionState = call ? connectionState : null;
const showNotificationDecoration = hasVisibleNotification || hasParticipantInCall;
const showNotificationDecoration = hasVisibleNotification || participantCount > 0;
// Actions
@@ -138,6 +143,9 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
});
}, [room]);
const [callType, setCallType] = useState<CallType>(CallType.Video);
useTypedEventEmitter(call ?? undefined, CallEvent.CallTypeChanged, setCallType);
return {
name,
notificationState,
@@ -148,9 +156,10 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
isBold,
isVideoRoom,
callConnectionState,
hasParticipantInCall,
hasParticipantInCall: participantCount > 0,
messagePreview,
showNotificationDecoration,
callType: call ? callType : undefined,
};
}

View File

@@ -12,6 +12,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
import DeviceListener, { BACKUP_DISABLED_ACCOUNT_DATA_KEY } from "../../../../DeviceListener";
import { useEventEmitterAsyncState } from "../../../../hooks/useEventEmitter";
import { resetKeyBackupAndWait } from "../../../../utils/crypto/resetKeyBackup";
interface KeyStoragePanelState {
/**
@@ -75,63 +76,58 @@ export function useKeyStoragePanelViewModel(): KeyStoragePanelState {
async (enable: boolean) => {
setPendingValue(enable);
try {
// stop the device listener since enabling or (especially) disabling key storage must be
// pause the device listener since enabling or (especially) disabling key storage must be
// done with a sequence of API calls that will put the account in a slightly different
// state each time, so suppress any warning toasts until the process is finished (when
// we'll turn it back on again.)
DeviceListener.sharedInstance().stop();
const crypto = matrixClient.getCrypto();
if (!crypto) {
logger.error("Can't change key backup status: no crypto module available");
return;
}
if (enable) {
const childLogger = logger.getChild("[enable key storage]");
childLogger.info("User requested enabling key storage");
let currentKeyBackup = await crypto.checkKeyBackupAndEnable();
if (currentKeyBackup) {
logger.info(
`Existing key backup is present. version: ${currentKeyBackup.backupInfo.version}`,
currentKeyBackup.trustInfo,
);
// Check if the current key backup can be used. Either of these properties causes the key backup to be used.
if (currentKeyBackup.trustInfo.trusted || currentKeyBackup.trustInfo.matchesDecryptionKey) {
logger.info("Existing key backup can be used");
// state each time, so suppress any warning toasts until the process is finished
await DeviceListener.sharedInstance().whilePaused(async () => {
const crypto = matrixClient.getCrypto();
if (!crypto) {
logger.error("Can't change key backup status: no crypto module available");
return;
}
if (enable) {
const childLogger = logger.getChild("[enable key storage]");
childLogger.info("User requested enabling key storage");
let currentKeyBackup = await crypto.checkKeyBackupAndEnable();
if (currentKeyBackup) {
logger.info(
`Existing key backup is present. version: ${currentKeyBackup.backupInfo.version}`,
currentKeyBackup.trustInfo,
);
// Check if the current key backup can be used. Either of these properties causes the key backup to be used.
if (currentKeyBackup.trustInfo.trusted || currentKeyBackup.trustInfo.matchesDecryptionKey) {
logger.info("Existing key backup can be used");
} else {
logger.warn("Existing key backup cannot be used, creating new backup");
// There aren't any *usable* backups, so we need to create a new one.
currentKeyBackup = null;
}
} else {
logger.warn("Existing key backup cannot be used, creating new backup");
// There aren't any *usable* backups, so we need to create a new one.
currentKeyBackup = null;
logger.info("No existing key backup versions are present, creating new backup");
}
// If there is no usable key backup on the server, create one.
// `resetKeyBackup` will delete any existing backup, so we only do this if there is no usable backup.
if (currentKeyBackup === null) {
await resetKeyBackupAndWait(crypto);
}
// Set the flag so that EX no longer thinks the user wants backup disabled
await matrixClient.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: false });
} else {
logger.info("No existing key backup versions are present, creating new backup");
logger.info("User requested disabling key backup");
// This method will delete the key backup as well as server side recovery keys and other
// server-side crypto data.
await crypto.disableKeyStorage();
// Set a flag to say that the user doesn't want key backup.
// Element X uses this to determine whether to set up automatically,
// so this will stop EX turning it back on spontaneously.
await matrixClient.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: true });
}
// If there is no usable key backup on the server, create one.
// `resetKeyBackup` will delete any existing backup, so we only do this if there is no usable backup.
if (currentKeyBackup === null) {
await crypto.resetKeyBackup();
// resetKeyBackup fires this off in the background without waiting, so we need to do it
// explicitly and wait for it, otherwise it won't be enabled yet when we check again.
await crypto.checkKeyBackupAndEnable();
}
// Set the flag so that EX no longer thinks the user wants backup disabled
await matrixClient.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: false });
} else {
logger.info("User requested disabling key backup");
// This method will delete the key backup as well as server side recovery keys and other
// server-side crypto data.
await crypto.disableKeyStorage();
// Set a flag to say that the user doesn't want key backup.
// Element X uses this to determine whether to set up automatically,
// so this will stop EX turning it back on spontaneously.
await matrixClient.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: true });
}
});
} finally {
setPendingValue(undefined);
DeviceListener.sharedInstance().start(matrixClient);
}
},
[setPendingValue, matrixClient],

View File

@@ -14,5 +14,5 @@ interface Props {
}
export default function AuthBody({ flex, className, children }: PropsWithChildren<Props>): JSX.Element {
return <main className={classNames("mx_AuthBody", className, { mx_AuthBody_flex: flex })}>{children}</main>;
return <div className={classNames("mx_AuthBody", className, { mx_AuthBody_flex: flex })}>{children}</div>;
}

View File

@@ -89,9 +89,14 @@ export default class AuthPage extends React.PureComponent<React.PropsWithChildre
<div className="mx_AuthPage" style={pageStyle}>
<div className={modalClasses} style={modalStyle}>
{modalBlur}
<div className="mx_AuthPage_modalContent" style={modalContentStyle}>
<main
className="mx_AuthPage_modalContent"
style={modalContentStyle}
tabIndex={-1}
aria-live="polite"
>
{this.props.children}
</div>
</main>
</div>
<AuthFooter />
</div>

View File

@@ -26,6 +26,7 @@ export default class Welcome extends React.PureComponent<EmptyObject> {
}
const replaceMap: Record<string, string> = {
"$brand": SdkConfig.get("brand"),
"$riot:ssoUrl": "#/start_sso",
"$riot:casUrl": "#/start_cas",
"$matrixLogo": MATRIX_LOGO_HTML,

View File

@@ -16,23 +16,21 @@ import Spinner from "../../elements/Spinner";
import { InitialCryptoSetupStore, useInitialCryptoSetupStatus } from "../../../../stores/InitialCryptoSetupStore";
interface Props {
onFinished: (success?: boolean) => void;
/** Callback which is called if the crypto setup failed, and the user clicked the 'cancel' button */
onCancelled: () => void;
}
/*
* Walks the user through the process of creating a cross-signing keys.
/**
* Walks the user through the process of creating cross-signing keys.
*
* In most cases, only a spinner is shown, but for more
* complex auth like SSO, the user may need to complete some steps to proceed.
*/
export const InitialCryptoSetupDialog: React.FC<Props> = ({ onFinished }) => {
export const InitialCryptoSetupDialog: React.FC<Props> = ({ onCancelled }) => {
const onRetryClick = useCallback(() => {
InitialCryptoSetupStore.sharedInstance().retry();
}, []);
const onCancelClick = useCallback(() => {
onFinished(false);
}, [onFinished]);
const status = useInitialCryptoSetupStatus(InitialCryptoSetupStore.sharedInstance());
let content;
@@ -44,7 +42,7 @@ export const InitialCryptoSetupDialog: React.FC<Props> = ({ onFinished }) => {
<DialogButtons
primaryButton={_t("action|retry")}
onPrimaryButtonClick={onRetryClick}
onCancel={onCancelClick}
onCancel={onCancelled}
/>
</div>
</div>
@@ -60,7 +58,6 @@ export const InitialCryptoSetupDialog: React.FC<Props> = ({ onFinished }) => {
return (
<BaseDialog
className="mx_CreateCrossSigningDialog"
onFinished={onFinished}
title={_t("encryption|bootstrap_title")}
hasCancel={false}
fixedWidth={false}

View File

@@ -23,7 +23,10 @@ export interface ICategory {
id: CategoryKey;
name: string;
enabled: boolean;
// Whether the category is currently visible
visible: boolean;
// Whether the category is the first visible category
firstVisible: boolean;
ref: RefObject<HTMLButtonElement | null>;
}

View File

@@ -79,71 +79,44 @@ class EmojiPicker extends React.Component<IProps, IState> {
...DATA_BY_CATEGORY,
};
this.categories = [
{
id: "recent",
name: _t("emoji|category_frequently_used"),
enabled: this.recentlyUsed.length > 0,
visible: this.recentlyUsed.length > 0,
ref: React.createRef(),
},
{
id: "people",
name: _t("emoji|category_smileys_people"),
enabled: true,
visible: true,
ref: React.createRef(),
},
{
id: "nature",
name: _t("emoji|category_animals_nature"),
enabled: true,
visible: false,
ref: React.createRef(),
},
{
id: "foods",
name: _t("emoji|category_food_drink"),
enabled: true,
visible: false,
ref: React.createRef(),
},
{
id: "activity",
name: _t("emoji|category_activities"),
enabled: true,
visible: false,
ref: React.createRef(),
},
{
id: "places",
name: _t("emoji|category_travel_places"),
enabled: true,
visible: false,
ref: React.createRef(),
},
{
id: "objects",
name: _t("emoji|category_objects"),
enabled: true,
visible: false,
ref: React.createRef(),
},
{
id: "symbols",
name: _t("emoji|category_symbols"),
enabled: true,
visible: false,
ref: React.createRef(),
},
{
id: "flags",
name: _t("emoji|category_flags"),
enabled: true,
visible: false,
ref: React.createRef(),
},
const hasRecentlyUsed = this.recentlyUsed.length > 0;
const categoryConfig: Array<{
id: CategoryKey;
name: string;
}> = [
{ id: "recent", name: _t("emoji|category_frequently_used") },
{ id: "people", name: _t("emoji|category_smileys_people") },
{ id: "nature", name: _t("emoji|category_animals_nature") },
{ id: "foods", name: _t("emoji|category_food_drink") },
{ id: "activity", name: _t("emoji|category_activities") },
{ id: "places", name: _t("emoji|category_travel_places") },
{ id: "objects", name: _t("emoji|category_objects") },
{ id: "symbols", name: _t("emoji|category_symbols") },
{ id: "flags", name: _t("emoji|category_flags") },
];
this.categories = categoryConfig.map((config) => {
let isEnabled = true;
let isVisible = false;
let firstVisible = false;
if (config.id === "recent") {
isEnabled = hasRecentlyUsed;
isVisible = hasRecentlyUsed;
firstVisible = hasRecentlyUsed;
} else if (config.id === "people") {
isVisible = true;
firstVisible = !hasRecentlyUsed;
}
return {
id: config.id,
name: config.name,
enabled: isEnabled,
visible: isVisible,
firstVisible: firstVisible,
ref: React.createRef(),
};
});
}
private onScroll = (): void => {
@@ -259,6 +232,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
const body = this.scrollRef.current?.containerRef.current;
if (!body) return;
const rect = body.getBoundingClientRect();
let firstVisibleFound = false;
for (const cat of this.categories) {
const elem = body.querySelector(`[data-category-id="${cat.id}"]`);
if (!elem) {
@@ -270,15 +244,24 @@ class EmojiPicker extends React.Component<IProps, IState> {
const y = elemRect.y - rect.y;
const yEnd = elemRect.y + elemRect.height - rect.y;
cat.visible = y < rect.height && yEnd > 0;
if (cat.visible && !firstVisibleFound) {
firstVisibleFound = true;
cat.firstVisible = true;
} else {
cat.firstVisible = false;
}
// We update this here instead of through React to avoid re-render on scroll.
if (!cat.ref.current) continue;
if (cat.visible) {
cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible");
cat.ref.current.setAttribute("aria-selected", "true");
cat.ref.current.setAttribute("tabindex", "0");
} else {
cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
cat.ref.current.setAttribute("aria-selected", "false");
}
if (cat.firstVisible) {
cat.ref.current.setAttribute("tabindex", "0");
} else {
cat.ref.current.setAttribute("tabindex", "-1");
}
}

View File

@@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import classNames from "classnames";
import { findLastIndex } from "lodash";
import { _t } from "../../../languageHandler";
import { type CategoryKey, type ICategory } from "./Category";
@@ -33,14 +32,8 @@ class Header extends React.PureComponent<IProps> {
}
private changeCategoryRelative(delta: number): void {
let current: number;
// As multiple categories may be visible at once, we want to find the one closest to the relative direction
if (delta < 0) {
current = this.props.categories.findIndex((c) => c.visible);
} else {
// XXX: Switch to Array::findLastIndex once we enable ES2023
current = findLastIndex(this.props.categories, (c) => c.visible);
}
// Move to the next/previous category using the first visible as the current.
const current = this.props.categories.findIndex((c) => c.visible);
this.changeCategoryAbsolute(current + delta, delta);
}
@@ -104,7 +97,7 @@ class Header extends React.PureComponent<IProps> {
onClick={() => this.props.onAnchorClick(category.id)}
title={category.name}
role="tab"
tabIndex={category.visible ? 0 : -1} // roving
tabIndex={category.firstVisible ? 0 : -1} // roving
aria-selected={category.visible}
aria-controls={`mx_EmojiPicker_category_${category.id}`}
/>

View File

@@ -10,12 +10,10 @@ import React, { type FC } from "react";
import classNames from "classnames";
import { _t } from "../../../languageHandler";
import { type Call } from "../../../models/Call";
import { useParticipantCount } from "../../../hooks/useCall";
export enum LiveContentType {
Video,
// More coming soon
Voice,
}
interface Props {
@@ -33,6 +31,7 @@ export const LiveContentSummary: FC<Props> = ({ type, text, active, participantC
<span
className={classNames("mx_LiveContentSummary_text", {
mx_LiveContentSummary_text_video: type === LiveContentType.Video,
mx_LiveContentSummary_text_voice: type === LiveContentType.Voice,
mx_LiveContentSummary_text_active: active,
})}
>
@@ -51,16 +50,3 @@ export const LiveContentSummary: FC<Props> = ({ type, text, active, participantC
)}
</span>
);
interface LiveContentSummaryWithCallProps {
call: Call;
}
export const LiveContentSummaryWithCall: FC<LiveContentSummaryWithCallProps> = ({ call }) => (
<LiveContentSummary
type={LiveContentType.Video}
text={_t("common|video")}
active={false}
participantCount={useParticipantCount(call)}
/>
);

View File

@@ -26,6 +26,16 @@ interface IProps {
onClose: () => void;
}
/**
* Height of a single member list item
*/
const MEMBER_LIST_ITEM_HEIGHT = 56;
/**
* Amount to extend the top and bottom of the viewport by.
* From manual testing 15 items seems to be enough to never really see the blank space when scrolling.
*/
const EXTENDED_VIEWPORT_HEIGHT = 15 * MEMBER_LIST_ITEM_HEIGHT;
const MemberListView: React.FC<IProps> = (props: IProps) => {
const vm = useMemberListViewModel(props.roomId);
const { isPresenceEnabled, memberCount } = vm;
@@ -106,6 +116,11 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
isItemFocusable={isItemFocusable}
role="listbox"
aria-label={_t("member_list|list_title")}
fixedItemHeight={MEMBER_LIST_ITEM_HEIGHT}
increaseViewportBy={{
bottom: EXTENDED_VIEWPORT_HEIGHT,
top: EXTENDED_VIEWPORT_HEIGHT,
}}
/>
</Flex>
</BaseCard>

View File

@@ -12,6 +12,8 @@ import NotificationOffIcon from "@vector-im/compound-design-tokens/assets/web/ic
import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid";
import EmailIcon from "@vector-im/compound-design-tokens/assets/web/icons/email-solid";
import { UnreadCounter, Unread } from "@vector-im/compound-web";
import VoiceCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/voice-call-solid";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { Flex } from "@element-hq/web-shared-components";
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
@@ -24,9 +26,9 @@ interface NotificationDecorationProps extends HTMLProps<HTMLDivElement> {
*/
notificationState: RoomNotificationState;
/**
* Whether the room has a video call.
* Whether the room has a voice or video call.
*/
hasVideoCall: boolean;
callType?: CallType;
}
/**
@@ -34,7 +36,7 @@ interface NotificationDecorationProps extends HTMLProps<HTMLDivElement> {
*/
export function NotificationDecoration({
notificationState,
hasVideoCall,
callType,
...props
}: NotificationDecorationProps): JSX.Element | null {
// Listen to the notification state and update the component when it changes
@@ -58,7 +60,7 @@ export function NotificationDecoration({
muted: notificationState.muted,
}));
if (!hasAnyNotificationOrActivity && !muted && !hasVideoCall) return null;
if (!hasAnyNotificationOrActivity && !muted && !callType) return null;
return (
<Flex
@@ -69,7 +71,12 @@ export function NotificationDecoration({
data-testid="notification-decoration"
>
{isUnsentMessage && <ErrorIcon width="20px" height="20px" fill="var(--cpd-color-icon-critical-primary)" />}
{hasVideoCall && <VideoCallIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
{callType === CallType.Video && (
<VideoCallIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />
)}
{callType === CallType.Voice && (
<VoiceCallIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />
)}
{invited && <EmailIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
{isMention && <MentionIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
{(isMention || isNotification) && <UnreadCounter count={count || null} />}

View File

@@ -1,4 +1,5 @@
/*
Copyright (C) 2025 Element Creations Ltd
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
@@ -6,7 +7,7 @@ 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 React, { type JSX, useCallback, useMemo, useState } from "react";
import React, { type JSX, useCallback, useState } from "react";
import { Text, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web";
import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid";
import VoiceCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/voice-call-solid";
@@ -29,7 +30,6 @@ import { _t } from "../../../../languageHandler.tsx";
import { getPlatformCallTypeProps, useRoomCall } from "../../../../hooks/room/useRoomCall.tsx";
import { useRoomThreadNotifications } from "../../../../hooks/room/useRoomThreadNotifications.ts";
import { useGlobalNotificationState } from "../../../../hooks/useGlobalNotificationState.ts";
import SdkConfig from "../../../../SdkConfig.ts";
import { useFeatureEnabled } from "../../../../hooks/useSettings.ts";
import { useEncryptionStatus } from "../../../../hooks/useEncryptionStatus.ts";
import { E2EStatus } from "../../../../utils/ShieldUtils.ts";
@@ -78,16 +78,6 @@ function RoomHeaderButtons({
showVoiceCallButton,
showVideoCallButton,
} = useRoomCall(room);
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
/**
* A special mode where only Element Call is used. In this case we want to
* hide the voice call button
*/
const useElementCallExclusively = useMemo(() => {
return SdkConfig.get("element_call").use_exclusively && groupCallsEnabled;
}, [groupCallsEnabled]);
const threadNotifications = useRoomThreadNotifications(room);
const globalNotificationState = useGlobalNotificationState();
@@ -101,6 +91,11 @@ function RoomHeaderButtons({
[callOptions, videoCallClick],
);
const voiceClick = useCallback(
(ev: React.MouseEvent) => voiceCallClick(ev, callOptions[0]),
[callOptions, voiceCallClick],
);
const toggleCallButton = (
<Tooltip label={isViewingCall ? _t("voip|minimise_call") : _t("voip|maximise_call")}>
<IconButton onClick={toggleCall}>
@@ -126,35 +121,50 @@ function RoomHeaderButtons({
</Tooltip>
);
const callIconWithTooltip = (
const videoCallIconWithTooltip = (
<Tooltip label={videoCallDisabledReason ?? _t("voip|video_call")}>
<VideoCallIcon />
</Tooltip>
);
const [menuOpen, setMenuOpen] = useState(false);
const voiceCallIconWithTooltip = (
<Tooltip label={videoCallDisabledReason ?? _t("voip|voice_call")}>
<VoiceCallIcon />
</Tooltip>
);
const onOpenChange = useCallback(
const [videoMenuOpen, setVideoMenuOpen] = useState(false);
const onVideoOpenChange = useCallback(
(newOpen: boolean) => {
if (!videoCallDisabledReason) setMenuOpen(newOpen);
if (!videoCallDisabledReason) setVideoMenuOpen(newOpen);
},
[videoCallDisabledReason],
);
const [voiceMenuOpen, setVoiceMenuOpen] = useState(false);
const onVoiceOpenChange = useCallback(
(newOpen: boolean) => {
if (!voiceCallDisabledReason) setVoiceMenuOpen(newOpen);
},
[voiceCallDisabledReason],
);
const startVideoCallButton = (
<>
{/* Can be either a menu or just a button depending on the number of call options.*/}
{callOptions.length > 1 ? (
<Menu
open={menuOpen}
onOpenChange={onOpenChange}
open={videoMenuOpen}
onOpenChange={onVideoOpenChange}
title={_t("voip|video_call_using")}
trigger={
<IconButton
disabled={!!videoCallDisabledReason}
aria-label={videoCallDisabledReason ?? _t("voip|video_call")}
>
{callIconWithTooltip}
{videoCallIconWithTooltip}
</IconButton>
}
side="left"
@@ -170,7 +180,7 @@ function RoomHeaderButtons({
children={children}
className="mx_RoomHeader_videoCallOption"
onClick={(ev) => {
setMenuOpen(false);
setVideoMenuOpen(false);
videoCallClick(ev, option);
}}
Icon={VideoCallIcon}
@@ -185,25 +195,61 @@ function RoomHeaderButtons({
aria-label={videoCallDisabledReason ?? _t("voip|video_call")}
onClick={videoClick}
>
{callIconWithTooltip}
{videoCallIconWithTooltip}
</IconButton>
)}
</>
);
let voiceCallButton: JSX.Element | undefined = (
<Tooltip label={voiceCallDisabledReason ?? _t("voip|voice_call")}>
<IconButton
// We need both: isViewingCall and isConnectedToCall
// - in the Lobby we are viewing a call but are not connected to it.
// - in pip view we are connected to the call but not viewing it.
disabled={!!voiceCallDisabledReason || isViewingCall || isConnectedToCall}
aria-label={voiceCallDisabledReason ?? _t("voip|voice_call")}
onClick={(ev) => voiceCallClick(ev, callOptions[0])}
>
<VoiceCallIcon />
</IconButton>
</Tooltip>
const startVoiceCallButton = (
<>
{/* Can be either a menu or just a button depending on the number of call options.*/}
{callOptions.length > 1 ? (
<Menu
open={voiceMenuOpen}
onOpenChange={onVoiceOpenChange}
title={_t("voip|voice_call_using")}
trigger={
<IconButton
disabled={!!voiceCallDisabledReason}
aria-label={voiceCallDisabledReason ?? _t("voip|voice_call")}
>
{voiceCallIconWithTooltip}
</IconButton>
}
side="left"
align="start"
>
{callOptions.map((option) => {
const { label, children } = getPlatformCallTypeProps(option);
return (
<MenuItem
key={option}
label={label}
aria-label={label}
children={children}
className="mx_RoomHeader_videoCallOption"
onClick={(ev) => {
setVoiceMenuOpen(false);
voiceCallClick(ev, option);
}}
Icon={VoiceCallIcon}
onSelect={() => {} /* Dummy handler since we want the click event.*/}
/>
);
})}
</Menu>
) : (
<IconButton
disabled={!!voiceCallDisabledReason}
aria-label={voiceCallDisabledReason ?? _t("voip|voice_call")}
onClick={voiceClick}
>
{voiceCallIconWithTooltip}
</IconButton>
)}
</>
);
const closeLobbyButton = (
<Tooltip label={_t("voip|close_lobby")}>
<IconButton onClick={toggleCall}>
@@ -212,15 +258,19 @@ function RoomHeaderButtons({
</Tooltip>
);
let videoCallButton: JSX.Element | undefined = startVideoCallButton;
let voiceCallButton: JSX.Element | undefined = startVoiceCallButton;
if (isConnectedToCall) {
videoCallButton = toggleCallButton;
voiceCallButton = undefined;
} else if (isViewingCall) {
videoCallButton = closeLobbyButton;
voiceCallButton = undefined;
}
if (!showVideoCallButton) {
videoCallButton = undefined;
}
if (!showVoiceCallButton) {
voiceCallButton = undefined;
}
@@ -258,7 +308,7 @@ function RoomHeaderButtons({
) : (
<>
{!isVideoRoom && videoCallButton}
{!useElementCallExclusively && !isVideoRoom && voiceCallButton}
{!isVideoRoom && voiceCallButton}
</>
)}

View File

@@ -70,10 +70,10 @@ export function RoomListHeaderView(): JSX.Element {
<ComposeMenu vm={vm} />
) : (
<IconButton
aria-label={_t("action|start_chat")}
onClick={(e) => vm.createChatRoom(e.nativeEvent)}
tooltip={_t("action|new_conversation")}
>
<ComposeIcon color="var(--cpd-color-icon-secondary)" />
<ComposeIcon color="var(--cpd-color-icon-secondary)" aria-hidden />
</IconButton>
)}
</div>
@@ -163,8 +163,8 @@ function ComposeMenu({ vm }: ComposeMenuProps): JSX.Element {
side="right"
align="start"
trigger={
<IconButton aria-label={_t("action|add")}>
<ComposeIcon color="var(--cpd-color-icon-secondary)" />
<IconButton tooltip={_t("action|new_conversation")}>
<ComposeIcon color="var(--cpd-color-icon-secondary)" aria-hidden />
</IconButton>
}
>

View File

@@ -132,7 +132,7 @@ export const RoomListItemView = memo(function RoomListItemView({
<NotificationDecoration
notificationState={vm.notificationState}
aria-hidden={true}
hasVideoCall={vm.hasParticipantInCall}
callType={vm.callType}
/>
)}
</>

View File

@@ -1,4 +1,5 @@
/*
* Copyright 2025 Element Creations Ltd.
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
@@ -29,7 +30,8 @@ import { initialiseDehydrationIfEnabled } from "../../../../utils/device/dehydra
import { withSecretStorageKeyCache } from "../../../../SecurityManager";
import { EncryptionCardButtons } from "./EncryptionCardButtons";
import { logErrorAndShowErrorDialog } from "../../../../utils/ErrorUtils.tsx";
import { RECOVERY_ACCOUNT_DATA_KEY } from "../../../../DeviceListener";
import DeviceListener, { RECOVERY_ACCOUNT_DATA_KEY } from "../../../../DeviceListener";
import { resetKeyBackupAndWait } from "../../../../utils/crypto/resetKeyBackup";
/**
* The possible states of the component.
@@ -123,14 +125,27 @@ export function ChangeRecoveryKey({
if (!crypto) return onFinish();
try {
// We need to enable the cache to avoid to prompt the user to enter the new key
// when we will try to access the secret storage during the bootstrap
await withSecretStorageKeyCache(async () => {
await crypto.bootstrapSecretStorage({
setupNewSecretStorage: true,
createSecretStorageKey: async () => recoveryKey,
const deviceListener = DeviceListener.sharedInstance();
// we need to call keyStorageOutOfSyncNeedsBackupReset here because
// deviceListener.whilePaused() sets its client to undefined, so
// keyStorageOutOfSyncNeedsBackupReset won't be able to check
// the backup state.
const needsBackupReset = await deviceListener.keyStorageOutOfSyncNeedsBackupReset(true);
await deviceListener.whilePaused(async () => {
// We need to enable the cache to avoid to prompt the user to enter the new key
// when we will try to access the secret storage during the bootstrap
await withSecretStorageKeyCache(async () => {
await crypto.bootstrapSecretStorage({
setupNewSecretStorage: true,
createSecretStorageKey: async () => recoveryKey,
});
// Reset the key backup if needed
if (needsBackupReset) {
await resetKeyBackupAndWait(crypto);
}
await initialiseDehydrationIfEnabled(matrixClient, { createNewKey: true });
});
await initialiseDehydrationIfEnabled(matrixClient, { createNewKey: true });
});
// Record the fact that the user explicitly enabled recovery.

View File

@@ -121,7 +121,7 @@ export enum Action {
UpdateSystemFont = "update_system_font",
/**
* Changes room based on payload parameters. Should be used with JoinRoomPayload.
* Changes room based on payload parameters. Should be used with ViewRoomPayload.
*/
ViewRoom = "view_room",
@@ -316,16 +316,39 @@ export enum Action {
*/
ShowRoomTopic = "show_room_topic",
/**
* Fired when the client is no longer viable to use: specifically, that we have been "soft-logged out".
*/
ClientNotViable = "client_not_viable",
/**
* Fired when the client was logged out. No additional payload information required.
*/
OnLoggedOut = "on_logged_out",
/**
* Fired when the client was logged in. No additional payload information required.
* Fired when the client was logged in, or has otherwise been set up with authentication data (e.g., by loading the
* access token from local storage). Note that this does not necessarily mean that a login action has happened,
* just that authentication creds have been set up.
*
* No additional payload information required.
*/
OnLoggedIn = "on_logged_in",
/**
* Fired when the client is about to be started, shortly after {@link OnLoggedIn}.
*
* No additional payload information required.
*/
WillStartClient = "will_start_client",
/**
* Fired when the client has started, shortly after {@link WillStartClient}.
*
* No additional payload information required.
*/
ClientStarted = "client_started",
/**
* Overwrites the existing login with fresh session credentials. Use with a OverwriteLoginPayload.
*/
@@ -380,4 +403,10 @@ export enum Action {
* Open the create room dialog
*/
CreateRoom = "view_create_room",
/**
* The `UserActivity` tracker determined that there was some activity from the user (typically a mouse movement
* or keyboard event).
*/
UserActivity = "user_activity",
}

View File

@@ -39,6 +39,7 @@ interface BaseViewRoomPayload extends Pick<ActionPayload, "action"> {
clear_search?: boolean; // Whether to clear the room list search
view_call?: boolean; // Whether to view the call or call lobby for the room
skipLobby?: boolean; // Whether to skip the call lobby when showing the call (only supported for element calls)
voiceOnly?: boolean; // Whether the call is voice only (only supported for element calls)
opts?: JoinRoomPayload["opts"];
deferred_action?: ActionPayload; // Action to fire after MatrixChat handles this ViewRoom action

View File

@@ -142,11 +142,6 @@ export const useRoomCall = (
// If there are multiple options, the user will be prompted to choose.
const callOptions = useMemo((): PlatformCallType[] => {
const options: PlatformCallType[] = [];
if (memberCount <= 2) {
options.push(PlatformCallType.LegacyCall);
} else if (mayEditWidgets || hasJitsiWidget) {
options.push(PlatformCallType.JitsiCall);
}
if (groupCallsEnabled) {
if (hasGroupCall || mayCreateElementCalls) {
options.push(PlatformCallType.ElementCall);
@@ -155,6 +150,11 @@ export const useRoomCall = (
return [PlatformCallType.ElementCall];
}
}
if (memberCount <= 2) {
options.push(PlatformCallType.LegacyCall);
} else if (mayEditWidgets || hasJitsiWidget) {
options.push(PlatformCallType.JitsiCall);
}
if (hasGroupCall && WidgetType.CALL.matches(groupCall.widget.type)) {
// only allow joining the ongoing Element call if there is one.
return [PlatformCallType.ElementCall];
@@ -231,7 +231,7 @@ export const useRoomCall = (
if (widget && promptPinWidget) {
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
} else {
placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined);
placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined, true);
}
},
[promptPinWidget, room, widget],
@@ -244,7 +244,7 @@ export const useRoomCall = (
} else {
// If we have pressed shift then always skip the lobby, otherwise `undefined` will defer
// to the defaults of the call implementation.
placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey || undefined);
placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey || undefined, false);
}
},
[widget, promptPinWidget, room],
@@ -279,7 +279,13 @@ export const useRoomCall = (
const roomDoesNotExist = room instanceof LocalRoom && room.state !== LocalRoomState.CREATED;
// We hide the voice call button if it'd have the same effect as the video call button
let hideVoiceCallButton = isManagedHybridWidgetEnabled(room) || !callOptions.includes(PlatformCallType.LegacyCall);
let hideVoiceCallButton =
isManagedHybridWidgetEnabled(room) ||
// Disable voice calls if Legacy calls are disabled
(!callOptions.includes(PlatformCallType.LegacyCall) &&
// Disable voice calls in ECall if the room is a group (we only present video calls for groups of users)
(!callOptions.includes(PlatformCallType.ElementCall) || memberCount > 2));
let hideVideoCallButton = false;
// We hide both buttons if:
// - they require widgets but widgets are disabled

View File

@@ -4094,9 +4094,11 @@
"user_busy_description": "Die angerufene Person ist momentan beschäftigt.",
"user_is_presenting": "%(sharerName)s präsentiert",
"video_call": "Videoanruf",
"video_call_incoming": "Eingehender Videoanruf",
"video_call_started": "Videoanruf hat begonnen",
"video_call_using": "Videoanruf mit:",
"voice_call": "Sprachanruf",
"voice_call_incoming": "Eingehender Anruf",
"you_are_presenting": "Du präsentierst"
},
"web_default_device_name": "%(appName)s: %(browserName)s auf %(osName)s",

View File

@@ -93,6 +93,7 @@
"maximise": "Maximise",
"mention": "Mention",
"minimise": "Minimise",
"new_conversation": "New conversation",
"new_room": "New room",
"new_video_room": "New video room",
"next": "Next",
@@ -603,6 +604,7 @@
"video": "Video",
"video_room": "Video room",
"view_message": "View message",
"voice": "Voice",
"warning": "Warning"
},
"composer": {
@@ -4096,9 +4098,12 @@
"user_busy_description": "The user you called is busy.",
"user_is_presenting": "%(sharerName)s is presenting",
"video_call": "Video call",
"video_call_incoming": "Incoming video call",
"video_call_started": "Video call started",
"video_call_using": "Video call using:",
"voice_call": "Voice call",
"voice_call_incoming": "Incoming voice call",
"voice_call_using": "Voice call using:",
"you_are_presenting": "You are presenting"
},
"web_default_device_name": "%(appName)s: %(browserName)s on %(osName)s",

View File

@@ -603,6 +603,7 @@
"video": "Video",
"video_room": "Videotuba",
"view_message": "Vaata sõnumit",
"voice": "Hääl",
"warning": "Hoiatus"
},
"composer": {
@@ -805,6 +806,8 @@
},
"developer_mode": "Arendusrežiim",
"developer_tools": "Arendusvahendid",
"device_dehydrated_no": "Virtuaalne seade serveris (dehüdreeritud): ei",
"device_dehydrated_yes": "Virtuaalne seade serveris (dehüdreeritud): jah",
"device_id": "Seadme tunnus: %(deviceId)s",
"device_keys": "Seadme võtmed",
"device_verification_status": {
@@ -4094,9 +4097,11 @@
"user_busy_description": "Kasutaja, kellele sa helistasid, on hõivatud.",
"user_is_presenting": "%(sharerName)s esitab",
"video_call": "Videokõne",
"video_call_incoming": "Saabuv videokõne",
"video_call_started": "Videokõne algas",
"video_call_using": "Videokõne, kus on kasutusel:",
"voice_call": "Häälkõne",
"voice_call_incoming": "Saaduv häälkõne",
"you_are_presenting": "Sina esitad"
},
"web_default_device_name": "%(appName)s: %(browserName)s operatsioonisüsteemis %(osName)s",

View File

@@ -603,6 +603,7 @@
"video": "Vidéo",
"video_room": "Salon vidéo",
"view_message": "Afficher le message",
"voice": "Voix",
"warning": "Attention"
},
"composer": {
@@ -831,6 +832,7 @@
"failed_to_save": "Échec lors de la sauvegarde des paramètres.",
"failed_to_send": "Échec de lenvoi de lévénement !",
"id": "ID : ",
"invalid_device_key_id": "L'ID de la clé de l'appareil est invalide",
"invalid_json": "Ne semble pas être du JSON valide.",
"level": "Rang",
"low_bandwidth_mode": "Mode faible bande passante",
@@ -841,6 +843,7 @@
"notification_state": "Létat des notifications est <strong>%(notificationState)s</strong>",
"notifications_debug": "Débogage des notifications",
"number_of_users": "Nombre dutilisateurs",
"only_joined_members": "Seuls les utilisateurs membres",
"original_event_source": "Évènement source original",
"room_encrypted": "Le salon est <strong>chiffré ✅</strong>",
"room_id": "Identifiant du salon : %(roomId)s",
@@ -887,10 +890,23 @@
"toggle_event": "Afficher/masquer lévènement",
"toolbox": "Boîte à outils",
"use_at_own_risk": "Cette interface ne vérifie pas les types des valeurs. Utilisez la à vos propres risques.",
"user_avatar": "Avatar: %(avatar)s",
"user_displayname": "Nom d'affichage : %(displayname)s",
"user_id": "Identifiant utilisateur : %(userId)s",
"user_no_avatar": "Avatar: <i> Aucun</i>",
"user_no_displayname": "Nom d'affichage : <i>Aucun</i>",
"user_read_up_to": "Lutilisateur a lu jusquà : ",
"user_read_up_to_ignore_synthetic": "Lutilisateur a lu jusquà (ignoreSynthetic) : ",
"user_read_up_to_private": "Lutilisateur a lu jusquà (m.read.private) : ",
"user_read_up_to_private_ignore_synthetic": "Lutilisateur a lu jusquà (m.read.private;ignoreSynthetic) : ",
"user_room_membership": "Adhésion : %(membership)s",
"user_verification_status": {
"identity_changed": "Statut de la vérification : <E2EIcon /> Non vérifié, et identité modifiée",
"unverified": "Statut de la vérification vérification : <E2EIcon /> Non vérifié",
"verified": "Statut de la vérification : <E2EIcon /> Vérifié",
"was_verified": "Statut de la vérifcation vérification : <E2EIcon /> Etait vérifié, mais l'identité a changé."
},
"users": "Utilisateurs",
"value": "Valeur",
"value_colon": "Valeur :",
"value_in_this_room": "Valeur pour ce salon",
@@ -2603,7 +2619,7 @@
"do_not_close_warning": "Ne fermez pas cette fenêtre tant que la réinitialisation n'est pas terminée",
"export_keys": "Exporter les clés",
"import_keys": "Importer les clés",
"other_people_device_description": "Attention : les utilisateurs qui ne se sont pas explicitement vérifiés auprès de vous (par exemple, via des émojis) ne recevront pas vos messages chiffrés. De même, les appareils non vérifiés des utilisateurs vérifiés ne recevront pas vos messages chiffrés.",
"other_people_device_description": "Attention : les utilisateurs qui ne se sont pas explicitement vérifiés auprès de vous (par exemple, via des émojis) ne recevront pas vos messages chiffrés. De même, les appareils non vérifiés des utilisateurs vérifiés ne recevront pas vos messages chiffrés. Cette fonctionalitée requiert le redémarrage de l'application pour prendre effet.",
"other_people_device_label": "Dans les salons chiffrés, envoyez des messages uniquement aux utilisateurs vérifiés",
"other_people_device_title": "Appareils d'autres personnes",
"reset_identity": "Réinitialiser l'identité cryptographique",
@@ -4080,9 +4096,11 @@
"user_busy_description": "Lutilisateur que vous avez appelé est indisponible.",
"user_is_presenting": "%(sharerName)s est à lécran",
"video_call": "Appel vidéo",
"video_call_incoming": "Appel vidéo entrant",
"video_call_started": "Appel vidéo commencé",
"video_call_using": "Appel vidéo utilisant :",
"voice_call": "Appel audio",
"voice_call_incoming": "Appel vocal entrant",
"you_are_presenting": "Vous êtes à lécran"
},
"web_default_device_name": "%(appName)s : %(browserName)s pour %(osName)s",

View File

@@ -93,6 +93,7 @@
"maximise": "Teljes méret",
"mention": "Megemlítés",
"minimise": "Lecsukás",
"new_conversation": "Új beszélgetés",
"new_room": "Új szoba",
"new_video_room": "Új videószoba",
"next": "Következő",
@@ -601,6 +602,7 @@
"video": "Videó",
"video_room": "Videószoba",
"view_message": "Üzenet megjelenítése",
"voice": "Hang",
"warning": "Figyelmeztetés"
},
"composer": {
@@ -803,6 +805,17 @@
},
"developer_mode": "Fejlesztői mód",
"developer_tools": "Fejlesztői eszközök",
"device_dehydrated_no": "Dehidratált: Nem",
"device_dehydrated_yes": "Dehidratált: Igen",
"device_id": "Eszközazonosító:%(deviceId)s",
"device_keys": "Eszközkulcsok",
"device_verification_status": {
"signed_by_owner": "Ellenőrzés állapota: <E2EIcon /> Tulajdonos által aláírt",
"unknown": "Ellenőrzés állapota: Ismeretlen",
"unverified": "Ellenőrzés állapota: <E2EIcon /> A tulajdonos nem írta alá",
"verified": "Ellenőrzés állapota: <E2EIcon /> Keresztaláírással ellenőrizve"
},
"devices": "Kriptográfiai eszközök (%(count)s)",
"edit_setting": "Beállítások szerkesztése",
"edit_values": "Értékek szerkesztése",
"empty_string": "<üres karakterek>",
@@ -818,6 +831,7 @@
"failed_to_save": "A beállítások elmentése sikertelen.",
"failed_to_send": "Az eseményt nem sikerült elküldeni!",
"id": "Azonosító: ",
"invalid_device_key_id": "Érvénytelen eszközkulcs-azonosító",
"invalid_json": "Nem tűnik érvényes JSON szövegnek.",
"level": "Szint",
"low_bandwidth_mode": "Alacsony sávszélességű mód",
@@ -828,6 +842,7 @@
"notification_state": "Értesítés állapota: <strong>%(notificationState)s</strong>",
"notifications_debug": "Értesítések hibakeresése",
"number_of_users": "Felhasználószám",
"only_joined_members": "Csak csatlakozott felhasználók",
"original_event_source": "Eredeti esemény forráskódja",
"room_encrypted": "A szoba <strong>titkosítva van ✅</strong>",
"room_id": "Szoba azon.: %(roomId)s",
@@ -873,10 +888,23 @@
"toggle_event": "esemény be/ki",
"toolbox": "Eszköztár",
"use_at_own_risk": "Ez a felület nem ellenőrzi az érték típusát. Csak saját felelősségére használja.",
"user_avatar": "Profilkép: %(avatar)s",
"user_displayname": "Megjelenített név: %(displayname)s",
"user_id": "Felhasználói azonosító: %(userId)s",
"user_no_avatar": "Profilkép: <i>Nincs</i>",
"user_no_displayname": "Megjelenített név: <i>Nincs</i>",
"user_read_up_to": "A felhasználó eddig olvasta el: ",
"user_read_up_to_ignore_synthetic": "A felhasználó eddig olvasott felfelé (ignoreSynthetic): ",
"user_read_up_to_private": "A felhasználó eddig olvasott felfelé (m.read.private): ",
"user_read_up_to_private_ignore_synthetic": "A felhasználó eddig olvasott felfelé (m.read.private;ignoreSynthetic): ",
"user_room_membership": "Tagság: %(membership)s",
"user_verification_status": {
"identity_changed": "Ellenőrzés állapota: <E2EIcon /> Nincs ellenőrizve, és a személyazonosság megváltozott",
"unverified": "Ellenőrzés állapota: <E2EIcon /> Nem ellenőrzött",
"verified": "Ellenőrzés állapota: <E2EIcon /> Ellenőrzött",
"was_verified": "Ellenőrzési állapot:<E2EIcon /> Ellenőrzés megtörtént, de a személyazonosság megváltozott"
},
"users": "Felhasználók",
"value": "Érték",
"value_colon": "Érték:",
"value_in_this_room": "Érték ebben a szobában",
@@ -989,6 +1017,7 @@
"skip_verification": "Ellenőrzés kihagyása most",
"verify_this_device": "Az eszköz ellenőrzése"
},
"cancelled_verification": "Vagy időtúllépés történt a kérésnél, vagy elutasították a kérést, vagy ellenőrzési történt hiba.",
"cancelling": "Megszakítás…",
"cant_confirm": "Nem tudja megerősíteni?",
"complete_action": "Megértettem",
@@ -996,6 +1025,7 @@
"complete_title": "Ellenőrizve!",
"confirm_identity_description": "A biztonságos üzenetkezelés beállításához ellenőrizze ezt az eszközt.",
"confirm_identity_title": "Erősítse meg, hogy Ön az",
"confirm_the_emojis": "Erősítse meg, hogy a lenti emodzsik megegyeznek a másik eszközön megjelenítettekkel.",
"error_starting_description": "A beszélgetést a másik felhasználóval nem lehetett elindítani.",
"error_starting_title": "Hiba az ellenőrzés indításakor",
"explainer": "Az ezzel felhasználóval váltott biztonságos üzenetek végpontok közti titkosítással védettek, és azt harmadik fél nem tudja elolvasni.",
@@ -1022,15 +1052,21 @@
"wrong_fingerprint": "Nem sikerült ellenőrizni a(z) „%(deviceId)s” eszközt a mellékelt „%(fingerprint)s” ujjlenyomat nem egyezik az eszköz ujjlenyomatával: „%(fprint)s”"
},
"no_support_qr_emoji": "Az ellenőrizni kívánt eszköz nem támogatja sem a QR-kód leolvasását, sem az emodzsis ellenőrzést, amelyeket az %(brand)s támogat. Próbálja meg egy másik klienssel.",
"now_you_can": "Mostantól biztonságosan olvashat vagy küldhet üzeneteket, és bárki, akivel cseveg, szintén megbízhat ebben az eszközben.",
"once_accepted_can_continue": "Az elfogadást követően folytathatja az ellenőrzést.",
"other_party_cancelled": "A másik fél megszakította az ellenőrzést.",
"prompt_encrypted": "Ellenőrizze a szoba összes tagját, hogy meggyőződjön a biztonságáról.",
"prompt_unencrypted": "A titkosított szobákban ellenőrizze az összes tagot, hogy meggyőződjön a biztonságosságáról.",
"qr_or_sas": "%(qrCode)s vagy %(emojiCompare)s",
"qr_prompt": "Ennek az egyedi kódnak a beolvasása",
"qr_reciprocate_check_again_device": "Az ellenőrzés befejezéséhez nézze meg újra a másik eszközén.",
"qr_reciprocate_no": "Nem, nem látok zöld pajzsot",
"qr_reciprocate_same_shield_user": "Majdnem kész! %(displayName)s is ugyanazt a pajzsot mutatja?",
"qr_reciprocate_yes": "Igen, látok egy zöld pajzsot",
"request_toast_accept_user": "Felhasználó ellenőrzése",
"request_toast_decline_counter": "Mellőzés (%(counter)s)",
"request_toast_detail": "%(deviceId)s innen: %(ip)s",
"request_toast_start_verification": "Ellenőrzés indítása",
"sas_caption_self": "Ellenőrizze ezt az eszközt azzal, hogy megerősíti, hogy a következő szám jelenik meg a képernyőjén.",
"sas_caption_user": "Ellenőrizze ezt a felhasználót azzal, hogy megerősíti, hogy a következő szám jelenik meg a képernyőjén.",
"sas_description": "Hasonlítsd össze az egyedi emodzsikat ha valamelyik eszközön nincs kamera",
@@ -1050,12 +1086,18 @@
"unverified_sessions_toast_title": "Ellenőrizetlen bejelentkezései vannak",
"use_another_device": "Másik eszköz használata",
"use_recovery_key": "Helyreállítási kulcs használata",
"verification_dialog_title_choose": "Válassza ki az ellenőrzés módját",
"verification_dialog_title_compare_emojis": "Emodzsik összehasonlítása",
"verification_dialog_title_confirm_green_shield": "Ellenőrizze, hogy a másik eszközén zöld pajzs látható-e.",
"verification_dialog_title_device": "Másik eszköz ellenőrzése",
"verification_dialog_title_failed": "Az ellenőrzés sikertelen",
"verification_dialog_title_start_on_other_device": "Ellenőrzés megkezdése a másik eszközön",
"verification_dialog_title_user": "Ellenőrzési kérés",
"verification_dialog_title_verified": "Eszköz ellenőrizve",
"verification_skip_warning": "Az ellenőrzés nélkül nem fér hozzá az összes üzenetéhez és mások számára megbízhatatlannak fog látszani.",
"verification_success_with_backup": "Ez az eszköz hitelesítve van. A titkosított üzenetekhez hozzáférése van és más felhasználók megbízhatónak látják.",
"verification_success_without_backup": "Az új eszköze ellenőrizve van. Mások megbízhatónak fogják látni.",
"verify_by_completing_one_of": "Az ellenőrzéshez végezze el az alábbiak egyikét:",
"verify_emoji": "Ellenőrzés emodzsival",
"verify_emoji_prompt": "Ellenőrzés egyedi emodzsik összehasonlításával.",
"verify_emoji_prompt_qr": "Ha nem tudod beolvasni az alábbi kódot, ellenőrizd az egyedi emodzsik összehasonlításával.",
@@ -2629,6 +2671,7 @@
"allow_spellcheck": "Helyesírás-ellenőrzés engedélyezése",
"application_language": "Alkalmazás nyelve",
"application_language_reload_hint": "Az alkalmazás újratöltődik egy másik nyelv kiválasztása után",
"avatar_open_menu": "Profilkép menü megnyitása",
"avatar_remove_progress": "Kép eltávolítása...",
"avatar_save_progress": "Kép feltöltése...",
"avatar_upload_error_text": "A fájlformátum nem támogatott, vagy a kép nagyobb, mint %(size)s.",
@@ -2825,6 +2868,10 @@
"rule_suppress_notices": "Botok üzenetei",
"rule_tombstone": "Amikor a szobák fejlesztésre kerülnek",
"show_message_desktop_notification": "Üzenet megjelenítése az asztali értesítésekben",
"sounds_release_announcement": {
"description": "Az értesítési hang és a híváscsengő hangja frissült tisztább, gyorsabb és kevésbé zavaró lett.",
"title": "Frissítettük az értesítési hangokat"
},
"voip": "Hang- és videóhívások"
},
"preferences": {
@@ -2851,6 +2898,7 @@
"room_list_heading": "Szobalista",
"show_avatars_pills": "Profilképek megjelenítése a felhasználók, szobák és események megemlítésénél",
"show_polls_button": "Szavazások gomb megjelenítése",
"startup_window_behaviour_label": "Az indítás és az ablak viselkedése",
"surround_text": "Kijelölt szöveg körülvétele speciális karakterek beírásakor",
"time_heading": "Idő megjelenítése",
"user_timezone": "Időzóna beállítása"
@@ -3020,6 +3068,12 @@
"spaces_explainer": "A terek a szobák és az emberek csoportosításának módjai. A terek mellett, amelyekben tartózkodik, használhat néhány előre beépítettet is.",
"title": "Oldalsáv"
},
"start_automatically": {
"disabled": "Nem",
"enabled": "Igen",
"label": "Az %(brand)s indítása, amikor bejelentkezik a számítógépére.",
"minimised": "Minimalizálva"
},
"tac_only_notifications": "Csak az üzenetszálak központban jelenítsen meg értesítéseket",
"use_12_hour_format": "Az időbélyegek megjelenítése 12 órás formátumban (például du. 2:30)",
"use_command_enter_send_message": "Command + Enter használata az üzenet küldéséhez",
@@ -4014,9 +4068,11 @@
"user_busy_description": "A hívott felhasználó foglalt.",
"user_is_presenting": "%(sharerName)s tartja a bemutatót",
"video_call": "Videóhívás",
"video_call_incoming": "Bejövő videóhívás",
"video_call_started": "A videóhívás elindult",
"video_call_using": "Videóhívás:",
"voice_call": "Hanghívás",
"voice_call_incoming": "Bejövő hanghívás",
"you_are_presenting": "Ön tartja a bemutatót"
},
"web_default_device_name": "%(appName)s: (%(browserName)s itt: %(osName)s)",

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