Compare commits

...

89 Commits

Author SHA1 Message Date
t3chguy
9fc037a00f [create-pull-request] automated change 2025-11-22 06:05:13 +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
renovate[bot]
d85e5fca8d Update dependency vite to v7.1.11 [SECURITY] (#31048)
* Update dependency vite to v7.1.11 [SECURITY]

* Iterate

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 12:32:59 +00:00
renovate[bot]
219a390025 Update dependency @sentry/browser to v10.23.0 (#31223)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-12 12:05:52 +00:00
renovate[bot]
2464178164 Update all non-major dependencies (#31222)
* Update all non-major dependencies

* Make knip happy

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 12:05:10 +00:00
renovate[bot]
f96bfe9e18 Remove dependency @types/serve-static (#31226)
* Update dependency @types/serve-static to v2

* Remove unused @types/serve-static

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 11:59:14 +00:00
renovate[bot]
ce529be5f4 Update dependency testcontainers to v11.8.0 (#31225)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-12 11:42:35 +00:00
renovate[bot]
c2c873520c Update dependency eslint-plugin-storybook to v10.0.6 (#31220)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-12 11:34:55 +00:00
renovate[bot]
36557d7383 Update typescript-eslint monorepo to v8.46.3 (#31221)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-12 11:34:24 +00:00
ElementRobot
03da342a4e [create-pull-request] automated change (#31218)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-12 09:04:07 +00:00
renovate[bot]
caf7451862 Update Node.js to de951cc (#31212)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-11 16:22:54 +00:00
renovate[bot]
5d17207a32 Update nginxinc/nginx-unprivileged:alpine-slim Docker digest to e2b324a (#31211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-11 15:25:57 +00:00
renovate[bot]
0cd108a3b4 Update octokit/graphql-action action to v3 (#31213)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-11 15:25:35 +00:00
renovate[bot]
4a9d065260 Update docker (#31210)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-11 15:25:12 +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
Florian Duros
e883b05206 Install shared components during EW install (#31196)
* 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

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-11 13:35:39 +00:00
RiotRobot
191f951303 Upgrade dependency to matrix-js-sdk@39.2.0-rc.0 2025-11-11 12:46:14 +00:00
ElementRobot
b50fbe2eea [create-pull-request] automated change (#31206)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-11 06:20:27 +00:00
Michael Telatynski
8608268bc7 Apply aria-hidden to emoji in SAS verification (#31204)
* Apply aria-hidden to emoji in SAS verification

So that for screen readers the canonical emoji description is preferred & we avoid the user being read each emoji twice.

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

* Update snapshot

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-10 16:46:30 +00:00
Michael Telatynski
f6e85a38d2 Fix room list handling of membership changes (#31197)
* Fix room list handling of membership changes

Including knock->invite for ask to join

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

* Iterate

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

* lint

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-10 12:40:39 +00:00
ElementRobot
39a5cca737 [create-pull-request] automated change (#31200)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-10 09:07:15 +00:00
Florian Duros
f751f2a55d Fix room list unable to be resized when displayed after a module (#31186)
* fix: recreate resizer when the page type changes

* test: add tests
2025-11-07 13:30:25 +00:00
ElementRobot
f1bb017be7 [create-pull-request] automated change (#31194)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-07 13:17:45 +00:00
Florian Duros
f5e56cc8d5 fix: only scan shared components src and EW src in i18n script (#31188) 2025-11-06 17:32:06 +00:00
Michael Telatynski
3848d570be Inhibit keyboard highlights in dialogs when effector is not in focus (#31181)
* Inhibit keyboard highlight in spotlight dialog when effector is not in focus

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

* Inhibit keyboard highlight in forward dialog when effector is not in focus

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

* Fix search box height bouncing on focus/blur

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-06 16:32:46 +00:00
Richard van der Hoff
eade32a80c Playwright test: withheld sessions for MSC4268 (#31153)
* Playwright test: withheld sessions for MSC4268

Test what happens when a session is withheld as part of MSC4268

* Add comment on `openRoomInfoPanel`
2025-11-06 12:14:14 +00:00
Ben Banfield-Zanin
a0de60a045 Use dedicated bug report subdomain (#31185)
* Update docs for 'bug_report_endpoint_url' to use the dedicated subdomain

* Update {app,develop}.element.io to use the dedicated subdomain for 'bug_report_endpoint_url'
2025-11-06 12:10:24 +00:00
ElementRobot
a88a3575d5 [create-pull-request] automated change (#31184)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-06 06:21:04 +00:00
Florian Duros
abb93545fe fix(e2e): look at the location map before clicking (#31180) 2025-11-05 16:49:41 +00:00
R Midhun Suresh
24aa759544 Add license (#31179) 2025-11-05 16:44:59 +00:00
David Baker
77aa26bcdb Remove duplicate test step (#31166)
As the comment indicated, this apparently was necessary to get stuff
built the first time, but does not appear to be necessary anymore and
looks to be causing occasional flakiness, so let's remove it.
2025-11-05 13:55:19 +00:00
Florian Duros
4d66a85e73 Add options to hide header and composer of room view for the module api (#31095)
* feat: add props to hide header in `RoomView`

* feat: add props to hide composer in `RoomView`

* feat: pass `RoomViewProps` to room view in `renderRoomView`

* refactor: add doc and use existing types

* test: add tests for new room view props
2025-11-05 12:52:10 +00:00
Tol Wassman
52eb8a9979 Strip mentions from forwarded messages (#30884)
* strip mentions from forwarded messages

fixes element-hq/element-web#30883

* call attachMentions() for empty m.mentions in forwarded messages

As there is no EditorModel, attachMentions() currently does nothing

* fix lint and typecheck

* update test "should be navigable using arrow keys"

* update test "forwards pin drop event"

add empty mentions to expected content

* add doc to transformEvent() & elaborate on attachMentions()

* add test "strips mentions from forwarded messages"

* fix lint

* update source of `attachMentions()` import

---------

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2025-11-05 11:28:24 +00:00
David Baker
c4ef57b5f1 Fix the slow test reporter (#31170)
Only runs on develop so we didn't notice
2025-11-05 11:16:48 +00:00
David Baker
d9e3aa52e2 Hopefully fix manual device verification test (#31175)
Looks like it's failing to clear modals between tests
2025-11-05 10:19:21 +00:00
ElementRobot
b8e7c725e2 [create-pull-request] automated change (#31176)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-05 09:17:48 +00:00
ElementRobot
2617a7c3a5 [create-pull-request] automated change (#31177)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-05 09:07:24 +00:00
David Baker
42f8247c2e Experimental Module API Additions (#30863)
* Module API experiments

* Move ResizerNotifier into SDKContext

so we don't have to pass it into RoomView

* Add the MultiRoomViewStore

* Make RoomViewStore able to take a roomId prop

* Different interface to add space panel items

A bit less flexible but probably simpler and will help keep things
actually consistent rather than just allowing modules to stick any
JSX into the space panel (which means they also have to worry about
styling if they *do* want it to be consistent).

* Allow space panel items to be updated

and manage which one is selected, allowing module "spaces" to be
considered spaces

* Remove fetchRoomFn from SpaceNotificationStore

which didn't really seem to have any point as it was only called from
one place

* Switch to using module api via .instance

* Fairly awful workaround

to actually break the dependency nightmare

* Add test for multiroomviewstore

* add test

* Make room names deterministic

So the tests don't fail if you add other tests or run them individually

* Add test for builtinsapi

* Update module api

* RVS is not needed as prop anymore

Since it's passed through context

* Add roomId to prop

* Remove RoomViewStore from state

This is now accessed through class field

* Fix test

* No need to pass RVS from LoggedInView

* Add RoomContextType

* Implement new builtins api

* Add tests

* Fix import

* Fix circular dependency issue

* Fix import

* Add more tests

* Improve comment

* room-id is optional

* Update license

* Add implementation for AccountDataApi

* Add implementation for Room

* Add implementation for ClientApi

* Create ClientApi in Api.ts

* Write tests

* Use nullish coalescing assignment

* Implement openRoom in NavigationApi

* Write tests

* Add implementation for StoresApi

* Write tests

* Fix circular dependency

* Add comments in lieu of type

and fix else block

* Change to class field

---------

Co-authored-by: R Midhun Suresh <hi@midhun.dev>
2025-11-05 07:24:26 +00:00
David Langley
514dd07a28 Fix test:storybook:update (#31173) 2025-11-04 17:27:34 +00:00
RiotRobot
53dc281a24 Reset matrix-js-sdk back to develop branch 2025-11-04 14:13:04 +00:00
RiotRobot
efc5b0260b Merge branch 'master' into develop 2025-11-04 14:12:52 +00:00
RiotRobot
d4c22d43f3 v1.12.3 2025-11-04 14:09:39 +00:00
RiotRobot
926bb56723 Upgrade dependency to matrix-js-sdk@39.1.2 2025-11-04 14:03:39 +00:00
RiotRobot
1dfd92c1eb Upgrade dependency to matrix-js-sdk@39.1.0-rc.1 2025-10-30 09:04:27 +00:00
RiotRobot
68e6b0f845 v1.12.3-rc.0 2025-10-28 15:07:21 +00:00
RiotRobot
d426063a22 Upgrade dependency to matrix-js-sdk@39.1.0-rc.0 2025-10-28 14:05:15 +00:00
403 changed files with 7765 additions and 3950 deletions

View File

@@ -1,3 +1,10 @@
/*
Copyright 2025 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
module.exports = {
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
@@ -160,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

@@ -29,7 +29,7 @@ jobs:
if: github.event_name != 'pull_request'
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
@@ -96,7 +96,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5
if: github.event_name != 'pull_request'
with:
images: |

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

@@ -55,12 +55,6 @@ jobs:
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: "yarn playwright install --with-deps --only-shell"
- name: Build storybook dependencies
# When the first test is ran, it will fail because the dependencies are not yet built.
# This step is to ensure that the dependencies are built before running the tests.
run: "yarn --cwd packages/shared-components test:storybook:ci"
continue-on-error: true
- name: Run Visual tests
run: "yarn --cwd packages/shared-components test:storybook:ci"

View File

@@ -9,7 +9,7 @@ jobs:
name: Move PRs asking for design review to the design board
runs-on: ubuntu-24.04
steps:
- uses: octokit/graphql-action@8ad880e4d437783ea2ab17010324de1075228110 # v2.3.2
- uses: octokit/graphql-action@abaeca7ba4f0325d63b8de7ef943c2418d161b93 # v3.0.0
id: find_team_members
with:
headers: '{"GraphQL-Features": "projects_next_graphql"}'
@@ -52,7 +52,7 @@ jobs:
fi
env:
TEAM: "design"
- uses: octokit/graphql-action@8ad880e4d437783ea2ab17010324de1075228110 # v2.3.2
- uses: octokit/graphql-action@abaeca7ba4f0325d63b8de7ef943c2418d161b93 # v3.0.0
id: add_to_project
if: steps.any_matching_reviewers.outputs.match == 'true'
with:
@@ -76,7 +76,7 @@ jobs:
name: Move PRs asking for design review to the design board
runs-on: ubuntu-24.04
steps:
- uses: octokit/graphql-action@8ad880e4d437783ea2ab17010324de1075228110 # v2.3.2
- uses: octokit/graphql-action@abaeca7ba4f0325d63b8de7ef943c2418d161b93 # v3.0.0
id: find_team_members
with:
headers: '{"GraphQL-Features": "projects_next_graphql"}'
@@ -119,7 +119,7 @@ jobs:
fi
env:
TEAM: "product"
- uses: octokit/graphql-action@8ad880e4d437783ea2ab17010324de1075228110 # v2.3.2
- uses: octokit/graphql-action@abaeca7ba4f0325d63b8de7ef943c2418d161b93 # v3.0.0
id: add_to_project
if: steps.any_matching_reviewers.outputs.match == 'true'
with:

View File

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

View File

@@ -1,3 +1,59 @@
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
* Remove allowVoipWithNoMedia feature flag ([#31087](https://github.com/element-hq/element-web/pull/31087)). Contributed by @Half-Shot.
## ✨ Features
* Change module API to be an instance getter ([#31025](https://github.com/element-hq/element-web/pull/31025)). Contributed by @dbkr.
## 🐛 Bug Fixes
* Show hover elements when keyboard focus is within an event tile ([#31078](https://github.com/element-hq/element-web/pull/31078)). Contributed by @t3chguy.
* Ensure toolbar navigation pattern works in MessageActionBar ([#31080](https://github.com/element-hq/element-web/pull/31080)). Contributed by @t3chguy.
* Ensure sent markers are hidden when showing thread summary. ([#31076](https://github.com/element-hq/element-web/pull/31076)). Contributed by @Half-Shot.
* Fix translation in dev mode ([#31045](https://github.com/element-hq/element-web/pull/31045)). Contributed by @florianduros.
* Fix sort order in space hierarchy ([#30975](https://github.com/element-hq/element-web/pull/30975)). Contributed by @t3chguy.
* New Room list: don't display message preview of thread ([#31043](https://github.com/element-hq/element-web/pull/31043)). Contributed by @florianduros.
* Revert "A11y: move focus to right panel when opened" ([#30999](https://github.com/element-hq/element-web/pull/30999)). Contributed by @florianduros.
* Fix highlights in messages (or search results) breaking links ([#30264](https://github.com/element-hq/element-web/pull/30264)). Contributed by @bojidar-bg.
* Add prepare script ([#31030](https://github.com/element-hq/element-web/pull/31030)). Contributed by @dbkr.
* Fix html exports by adding SDKContext ([#30987](https://github.com/element-hq/element-web/pull/30987)). Contributed by @t3chguy.
Changes in [1.12.2](https://github.com/element-hq/element-web/releases/tag/v1.12.2) (2025-10-21)
================================================================================================
## ✨ Features

View File

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

View File

@@ -407,7 +407,7 @@ The VoIP and Jitsi options are:
If you run your own rageshake server to collect bug reports, the following options may be of interest:
1. `bug_report_endpoint_url`: URL for where to submit rageshake logs to. Rageshakes include feedback submissions and bug reports. When
not present in the config, the app will disable all rageshake functionality. Set to `https://element.io/bugreports/submit` to submit
not present in the config, the app will disable all rageshake functionality. Set to `https://rageshakes.element.io/api/submit` to submit
rageshakes to us, or use your own rageshake server.
2. `uisi_autorageshake_app`: If a user has enabled the "automatically send debug logs on decryption errors" flag, this option will be sent
alongside the rageshake so the rageshake server can filter them by app name. By default, this will be `element-auto-uisi`

View File

@@ -57,7 +57,7 @@ Then you can deploy it to your cluster with something like `kubectl apply -f my-
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api"
],
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"bug_report_endpoint_url": "https://rageshakes.element.io/api/submit",
"defaultCountryCode": "GB",
"show_labs_settings": false,
"features": { },

View File

@@ -17,7 +17,7 @@
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api"
],
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"bug_report_endpoint_url": "https://rageshakes.element.io/api/submit",
"uisi_autorageshake_app": "element-auto-uisi",
"show_labs_settings": false,
"room_directory": {

View File

@@ -17,7 +17,7 @@
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api"
],
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"bug_report_endpoint_url": "https://rageshakes.element.io/api/submit",
"uisi_autorageshake_app": "element-auto-uisi",
"show_labs_settings": true,
"room_directory": {

View File

@@ -5,6 +5,7 @@ export default {
"src/serviceworker/index.ts",
"src/workers/*.worker.ts",
"src/utils/exportUtils/exportJS.js",
"src/vector/localstorage-fix.ts",
"scripts/**",
"playwright/**",
"test/**",
@@ -48,11 +49,13 @@ export default {
// would with a normal library).
"@types/content-type",
"@types/sdp-transform",
// Used in EW but failed because of "link:"
"@element-hq/web-shared-components",
],
ignoreBinaries: [
// Used in scripts & workflows
"jq",
"wait-on",
],
ignoreExportsUsedInFile: true,
} satisfies KnipConfig;

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.2",
"version": "1.12.4",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -29,7 +29,7 @@
"UserFriendlyError"
],
"scripts": {
"i18n": "matrix-gen-i18n src res packages/shared-components && yarn i18n:sort && yarn i18n:lint",
"i18n": "matrix-gen-i18n src res packages/shared-components/src && yarn i18n:sort && yarn i18n:lint",
"i18n:sort": "jq --sort-keys '.' src/i18n/strings/en_EN.json > src/i18n/strings/en_EN.json.tmp && mv src/i18n/strings/en_EN.json.tmp src/i18n/strings/en_EN.json",
"i18n:lint": "matrix-i18n-lint && prettier --log-level=silent --write src/i18n/strings/ --ignore-path /dev/null",
"i18n:diff": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && yarn i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
@@ -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 .",
@@ -65,6 +65,7 @@
"coverage": "yarn test --coverage",
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js",
"install": "yarn --cwd packages/shared-components install --frozen-lockfile",
"postinstall": "patch-package"
},
"resolutions": {
@@ -72,22 +73,21 @@
"@playwright/test": "1.56.1",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"@types/serve-static": "1.15.10",
"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.4.1",
"@element-hq/web-shared-components": "file:packages/shared-components",
"@element-hq/element-web-module-api": "1.5.0",
"@element-hq/web-shared-components": "link:packages/shared-components",
"@fontsource/inconsolata": "^5",
"@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7",
"@matrix-org/analytics-events": "^0.29.2",
"@matrix-org/analytics-events": "^0.30.0",
"@matrix-org/emojibase-bindings": "^1.3.4",
"@matrix-org/react-sdk-module-api": "^2.4.0",
"@matrix-org/spec": "^1.7.0",
@@ -132,14 +132,14 @@
"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-widget-api": "^1.14.0",
"memoize-one": "^6.0.0",
"mime": "^4.0.4",
"oidc-client-ts": "^3.0.1",
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.280.1",
"posthog-js": "1.290.0",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "^19.0.0",
@@ -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

@@ -1,3 +1,10 @@
/*
Copyright 2025 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
module.exports = {
root: true,
plugins: ["matrix-org", "eslint-plugin-react-compiler"],

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}",
@@ -50,7 +50,7 @@ if (env["GITHUB_ACTIONS"] !== undefined) {
// if we're running against the develop branch, also enable the slow test reporter
if (env["GITHUB_REF"] == "refs/heads/develop") {
reporters.push("<rootDir>/test/slowReporter.cjs");
reporters.push("<rootDir>/../../test/slowReporter.cjs");
}
config.reporters = reporters;
}

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": {
@@ -35,7 +35,7 @@
],
"scripts": {
"test": "jest",
"prepare": "patch-package && yarn --cwd ../.. build:res && ts-node scripts/gatherTranslationKeys.ts && vite build",
"prepare": "patch-package && yarn --cwd ../.. build:res && node scripts/gatherTranslationKeys.ts && vite build",
"storybook": "storybook dev -p 6007",
"build-storybook": "storybook build",
"lint": "yarn lint:types && yarn lint:js",
@@ -43,7 +43,7 @@
"lint:types": "tsc --noEmit --jsx react",
"test:storybook": "test-storybook --url http://localhost:6007/",
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" \"yarn storybook --no-open\" \"wait-on tcp:6007 && yarn test-storybook --url http://localhost:6007/ --ci --maxWorkers=2\"",
"test:storybook:update": "playwright-screenshots --entrypoint yarn --with-node-modules && playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot"
"test:storybook:update": "playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot"
},
"dependencies": {
"classnames": "^2.5.1",
@@ -55,27 +55,29 @@
"temporal-polyfill": "^0.3.0"
},
"devDependencies": {
"@storybook/addon-a11y": "^9.1.10",
"@storybook/addon-designs": "^10.0.2",
"@storybook/addon-docs": "^9.1.10",
"@element-hq/element-web-playwright-common": "^2.0.0",
"@playwright/test": "^1.50.1",
"@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",
"ts-node": "^10.9.2",
"storybook": "^10.0.7",
"typescript": "^5.9.3",
"vite": "^7.1.9",
"vite-plugin-dts": "^4.5.4",

View File

@@ -15,7 +15,10 @@ Please see LICENSE files in the repository root for full details.
import * as fs from "fs";
import * as path from "path";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const i18nStringsPath = path.resolve(__dirname, "../../../src/i18n/strings/en_EN.json");
const outPath = path.resolve(__dirname, "../src/i18nKeys.d.ts");
@@ -56,6 +59,9 @@ function main() {
console.log(`Wrote ${keys.length} keys to ${outPath}`);
}
if (require.main === module) {
main();
if (import.meta.url.startsWith("file:")) {
const modulePath = fileURLToPath(import.meta.url);
if (process.argv[1] === modulePath) {
main();
}
}

View File

@@ -6,7 +6,7 @@
*/
import React from "react";
import { type Meta, type StoryObj } from "@storybook/react-vite/*";
import { type Meta, type StoryObj } from "@storybook/react-vite";
import { AvatarWithDetails } from "./AvatarWithDetails";

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;"

View File

@@ -7,7 +7,7 @@
"esModuleInterop": true,
"useDefineForClassFields": true,
"module": "es2022",
"moduleResolution": "node",
"moduleResolution": "bundler",
"target": "es2022",
"noUnusedLocals": true,
"sourceMap": false,
@@ -21,11 +21,5 @@
"rollup/parseAst": ["./node_modules/rollup/dist/parseAst"]
}
},
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"ts-node": {
"files": true,
"moduleTypes": {
"*": "cjs"
}
}
"include": ["./src/**/*.ts", "./src/**/*.tsx"]
}

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,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

@@ -56,4 +56,85 @@ test.describe("History sharing", function () {
});
},
);
test("Messages sent when we believed the room history was unshared should not be visible", async ({
labsFlags,
browser,
page: alicePage,
user: aliceCredentials,
app: aliceElementApp,
homeserver,
}, testInfo) => {
test.setTimeout(60000);
// In this test:
// 1. Alice creates an encrypted room with Bob.
// 2. She sets the history visibility to "shared", but Bob doesn't receive the memo
// 3. Bob sends a message
// 4. Alice invites Charlie
// 5. Charlie can't see the message.
await aliceElementApp.client.bootstrapCrossSigning(aliceCredentials);
await createRoom(alicePage, "TestRoom", true);
// Register a second user, and open it in a second instance of the app
const bobCredentials = await homeserver.registerUser(`user_${testInfo.testId}_bob`, "password", "Bob");
const bobPage = await createNewInstance(browser, bobCredentials, {}, labsFlags);
const bobElementApp = new ElementAppPage(bobPage);
await bobElementApp.client.bootstrapCrossSigning(bobCredentials);
// ... and a third
const charlieCredentials = await homeserver.registerUser(
`user_${testInfo.testId}_charlie`,
"password",
"Charlie",
);
const charliePage = await createNewInstance(browser, charlieCredentials, {}, labsFlags);
const charlieElementApp = new ElementAppPage(charliePage);
await charlieElementApp.client.bootstrapCrossSigning(charlieCredentials);
// Alice invites Bob, and Bob accepts
const roomId = await aliceElementApp.getCurrentRoomIdFromUrl();
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
await bobPage.getByRole("option", { name: "TestRoom" }).click();
await bobPage.getByRole("button", { name: "Accept" }).click();
// Bob sends a message with "shared" visibility
await sendMessageInCurrentRoom(bobPage, "Message1: 'shared' visibility");
await expect(alicePage.getByText("Message1")).toBeVisible();
// Alice sets the history visibility to "joined"
await aliceElementApp.client.sendStateEvent(roomId, "m.room.history_visibility", {
history_visibility: "joined",
});
await expect(
bobPage.getByText(
"Alice made future room history visible to all room members, from the point they joined.",
),
).toBeVisible();
// Bob stops syncing, and sends a message with "joined" visibility.
// (Stopping syncing *before* sending the message means that the active sync will be flushed by sending the
// message, so that Alice's change to the history viz below won't be seen by Bob.)
await bobPage.route(`**/sync*`, (route) => route.fulfill({}));
await sendMessageInCurrentRoom(bobPage, "Message2: 'joined' visibility");
await expect(alicePage.getByText("Message2")).toBeVisible();
// Alice changes the history viz, but Bob doesn't receive the memo
await aliceElementApp.client.sendStateEvent(roomId, "m.room.history_visibility", {
history_visibility: "shared",
});
await sendMessageInCurrentRoom(bobPage, "Message3: 'shared' visibility, but Bob thinks it is still 'joined'");
// Alice now invites Charlie
await aliceElementApp.inviteUserToCurrentRoom(charlieCredentials.userId);
await charliePage.getByRole("option", { name: "TestRoom" }).click();
await charliePage.getByRole("button", { name: "Accept" }).click();
// Message1 should be visible
// Message2 should be invisible
// Message3 should be undecryptable
await expect(charliePage.getByText("Message1")).toBeVisible();
await expect(charliePage.getByText("You don't have access to this message")).toBeVisible();
});
});

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

@@ -30,33 +30,39 @@ test.describe("Location sharing", { tag: "@no-firefox" }, () => {
});
});
test("sends and displays pin drop location message successfully", async ({ page, user, app }) => {
const roomId = await app.client.createRoom({});
await page.goto(`/#/room/${roomId}`);
test(
"sends and displays pin drop location message successfully",
{ tag: "@screenshot" },
async ({ page, user, app }) => {
const roomId = await app.client.createRoom({});
await page.goto(`/#/room/${roomId}`);
const composerOptions = await app.openMessageComposerOptions();
await composerOptions.getByRole("menuitem", { name: "Location", exact: true }).click();
const composerOptions = await app.openMessageComposerOptions();
await composerOptions.getByRole("menuitem", { name: "Location", exact: true }).click();
await selectLocationShareTypeOption(page, "Pin").click();
await selectLocationShareTypeOption(page, "Pin").click();
await page.locator("#mx_LocationPicker_map").click();
await page.locator("#mx_LocationPicker_map").click();
await submitShareLocation(page);
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();
// clicking location tile opens maximised map
await expect(page.getByRole("dialog")).toBeVisible();
const dialog = page.getByRole("dialog");
await app.closeDialog();
// wait for the dialog to be visible
await expect(dialog).toBeVisible();
await expect(page.locator(".mx_Marker")).toBeVisible();
});
// screenshot the map within the dialog
await expect(dialog.getByRole("region", { name: "Map" })).toMatchScreenshot(
"location-pin-drop-message-map.png",
);
await app.closeDialog();
await expect(page.getByRole("button", { name: "Map marker" })).toBeVisible();
},
);
test(
"is prompted for and can consent to live location sharing",

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

@@ -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

@@ -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

@@ -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");
}
@@ -199,6 +202,21 @@ export class ElementAppPage {
return this.page.locator(".mx_RightPanel");
}
/**
* Opens the room info panel if it is not already open.
*
* TODO: fix this so that it works correctly if, say, the member list was open instead of the room info panel.
*
* @returns locator to the right panel
*/
public async openRoomInfoPanel(): Promise<Locator> {
const locator = this.page.getByTestId("right-panel");
if (!(await locator.isVisible())) {
await this.page.getByRole("button", { name: "Room info" }).first().click();
}
return locator;
}
/**
* Opens/closes the memberlist panel
* @returns locator to the memberlist panel
@@ -217,8 +235,8 @@ export class ElementAppPage {
* @param userId - The user to invite to the room.
*/
public async inviteUserToCurrentRoom(userId: string): Promise<void> {
await this.toggleRoomInfoPanel(); // TODO skip this if the room info panel is already open
await this.page.getByTestId("right-panel").getByRole("menuitem", { name: "Invite" }).click();
const rightPanel = await this.openRoomInfoPanel();
await rightPanel.getByRole("menuitem", { name: "Invite" }).click();
const input = this.page.getByRole("dialog").getByTestId("invite-dialog-input");
await input.fill(userId);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 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: 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:0809c1870789e6c24f1344006b60d8bc9f8ca26f4ae4ba0d7957a1caf77e2be3";
const TAG = "main@sha256:ff115ba676f9bdd57b47b32aa862adc6e4a679c766481960fd1f3d3f0d8073e4";
/**
* 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:450b176d61e75b73d1acbbfab1de5ec3ebe08ba0f08ec00da872f7b5aaa23e54";
const TAG = "develop@sha256:f9b271a1d3692f5937d8c928a6b4b1d6051b9395cfa7a9a6008c9e49c95268ad";
/**
* 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

@@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
.mx_SearchBox {
flex: 1 1 0;
min-width: 0;
min-height: 36px; /* to avoid jumping when the X to clear shows/hides */
&.mx_SearchBox_blurred:not(:hover) {
background-color: transparent;

View File

@@ -211,6 +211,10 @@ Please see LICENSE files in the repository root for full details.
}
}
&.mx_SpaceButton_withIcon .mx_SpaceButton_icon {
background-color: $panel-actions;
}
&.mx_SpaceButton_home .mx_SpaceButton_icon::before {
mask-image: url("@vector-im/compound-design-tokens/icons/home-solid.svg");
}

View File

@@ -61,6 +61,11 @@ Please see LICENSE files in the repository root for full details.
/* To match the space around the title */
margin: 0 0 15px 0;
flex-grow: 0;
&:not(:focus-within) + .mx_ForwardList_content {
/* Inhibit the styling if focus is not within the input which handles keyboard accessibility */
--mx_ForwardList_entry_selectedBgColor: transparent;
}
}
.mx_ForwardList_content {
@@ -90,7 +95,7 @@ Please see LICENSE files in the repository root for full details.
&:hover,
&.mx_ForwardList_entry_active {
background-color: $spacePanel-bg-color;
background-color: var(--mx_ForwardList_entry_selectedBgColor, $spacePanel-bg-color);
}
.mx_ForwardList_roomButton {

View File

@@ -150,6 +150,16 @@ Please see LICENSE files in the repository root for full details.
height: unset;
margin-left: $spacing-16;
}
&:not(:focus-within) + #mx_SpotlightDialog_content {
/* Inhibit the styling if focus is not within the input which handles keyboard accessibility */
--mx_SpotlightDialog_option_selectedBgColor: transparent;
/* Hide the enter prompt as in this state pressing enter would not actuate that option */
.mx_SpotlightDialog_enterPrompt {
visibility: hidden;
}
}
}
#mx_SpotlightDialog_content {
@@ -193,6 +203,17 @@ Please see LICENSE files in the repository root for full details.
}
}
.mx_SpotlightDialog_option {
border-radius: 8px;
overflow: hidden;
text-overflow: ellipsis;
&:hover,
&[aria-selected="true"] {
background-color: var(--mx_SpotlightDialog_option_selectedBgColor, $quinary-content);
}
}
.mx_SpotlightDialog_recentlyViewed {
> div {
display: flex;
@@ -202,7 +223,6 @@ Please see LICENSE files in the repository root for full details.
}
.mx_SpotlightDialog_option {
border-radius: 8px;
padding: $spacing-4;
color: $primary-content;
font-size: $font-12px;
@@ -213,8 +233,6 @@ Please see LICENSE files in the repository root for full details.
min-width: 58px;
box-sizing: border-box;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
.mx_DecoratedRoomAvatar {
margin: 0 9px $spacing-4; /* maintain centering */
@@ -223,11 +241,6 @@ Please see LICENSE files in the repository root for full details.
& + .mx_SpotlightDialog_option {
margin-left: $spacing-16;
}
&:hover,
&[aria-selected="true"] {
background-color: $quinary-content;
}
}
}
@@ -236,8 +249,9 @@ Please see LICENSE files in the repository root for full details.
.mx_SpotlightDialog_otherSearches,
.mx_SpotlightDialog_hiddenResults {
.mx_SpotlightDialog_option {
--mx_SpotlightDialog_option_selectedBgColor: $system;
padding: 6px $spacing-4;
border-radius: 8px;
font-size: $font-15px;
line-height: $font-24px;
color: $primary-content;
@@ -245,8 +259,6 @@ Please see LICENSE files in the repository root for full details.
display: flex;
align-items: center;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
.mx_SpotlightDialog_option--endAdornment {
display: inline-flex;
@@ -350,7 +362,7 @@ Please see LICENSE files in the repository root for full details.
}
&:hover::before,
&[aria-selected="true"]::before {
&:focus-visible::before {
background-color: $secondary-content;
}
}
@@ -361,8 +373,6 @@ Please see LICENSE files in the repository root for full details.
&:hover,
&[aria-selected="true"] {
background-color: $system;
.mx_SpotlightDialog_option--menu,
.mx_SpotlightDialog_option--notifications {
display: block;

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

@@ -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

@@ -18,9 +18,4 @@ fi
DIST_VERSION=$("$DIR"/normalize-version.sh "$DIST_VERSION")
yarn --cwd packages/shared-components install
yarn --cwd packages/shared-components link
yarn link @element-hq/web-shared-components
VERSION=$DIST_VERSION yarn build

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

@@ -44,11 +44,3 @@ fi
yarn link matrix-js-sdk
[ -d matrix-analytics-events ] && yarn link @matrix-org/analytics-events
yarn install --frozen-lockfile $@
# Link shared components
pushd packages/shared-components
yarn link
yarn install --frozen-lockfile
popd
yarn link @element-hq/web-shared-components

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

@@ -31,7 +31,7 @@ const notLoggedInMap: Record<Exclude<Views, Views.LOGGED_IN>, ScreenName> = {
[Views.LOCK_STOLEN]: "SessionLockStolen",
};
const loggedInPageTypeMap: Record<PageType, ScreenName> = {
const loggedInPageTypeMap: Record<PageType | string, ScreenName> = {
[PageType.HomePage]: "Home",
[PageType.RoomView]: "Room",
[PageType.UserView]: "User",
@@ -48,10 +48,10 @@ export default class PosthogTrackers {
}
private view: Views = Views.LOADING;
private pageType?: PageType;
private pageType?: PageType | string;
private override?: ScreenName;
public trackPageChange(view: Views, pageType: PageType | undefined, durationMs: number): void {
public trackPageChange(view: Views, pageType: PageType | string | undefined, durationMs: number): void {
this.view = view;
this.pageType = pageType;
if (this.override) return;

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

@@ -68,7 +68,8 @@ import { monitorSyncedPushRules } from "../../utils/pushRules/monitorSyncedPushR
import { type ConfigOptions } from "../../SdkConfig";
import { MatrixClientContextProvider } from "./MatrixClientContextProvider";
import { Landmark, LandmarkNavigation } from "../../accessibility/LandmarkNavigation";
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext.ts";
import { ModuleApi } from "../../modules/Api.ts";
import { SDKContext } from "../../contexts/SDKContext.ts";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
@@ -190,14 +191,30 @@ class LoggedInView extends React.Component<IProps, IState> {
SettingsStore.watchSetting("userTimezone", null, this.onTimezoneUpdate),
];
this.resizer = this.createResizer();
this.resizer.attach();
this.loadResizer();
OwnProfileStore.instance.on(UPDATE_EVENT, this.refreshBackgroundImage);
this.loadResizerPreferences();
this.refreshBackgroundImage();
}
/**
* Load or reload the resizer for the left panel
*/
private loadResizer(): void {
// If the resizer already exists, detach it first
this.resizer?.detach();
this.resizer = this.createResizer();
this.resizer.attach();
this.loadResizerPreferences();
}
public componentDidUpdate(nextProps: Readonly<IProps>, nextState: Readonly<IState>, nextContext: any): void {
if (nextProps.page_type !== this.props.page_type) {
this.loadResizer();
}
}
private onTimezoneUpdate = async (): Promise<void> => {
// TODO: In a future app release, remove support for legacy key.
if (!SettingsStore.getValue("userTimezonePublish")) {
@@ -679,6 +696,10 @@ class LoggedInView extends React.Component<IProps, IState> {
public render(): React.ReactNode {
let pageElement;
const moduleRenderer = this.props.page_type
? ModuleApi.instance.navigation.locationRenderers.get(this.props.page_type)
: undefined;
switch (this.props.page_type) {
case PageTypes.RoomView:
pageElement = (
@@ -690,7 +711,6 @@ class LoggedInView extends React.Component<IProps, IState> {
key={this.props.currentRoomId || "roomview"}
justCreatedOpts={this.props.roomJustCreatedOpts}
forceTimeline={this.props.forceTimeline}
roomViewStore={SdkContextClass.instance.roomViewStore}
/>
);
break;
@@ -706,6 +726,13 @@ class LoggedInView extends React.Component<IProps, IState> {
);
}
break;
default: {
if (moduleRenderer) {
pageElement = moduleRenderer();
} else {
console.warn(`Couldn't render page type "${this.props.page_type}"`);
}
}
}
const wrapperClasses = classNames({
@@ -747,20 +774,22 @@ class LoggedInView extends React.Component<IProps, IState> {
)}
<SpacePanel />
{!useNewRoomList && <BackdropPanel backgroundImage={this.state.backgroundImage} />}
<div
className="mx_LeftPanel_wrapper--user"
ref={this._resizeContainer}
data-collapsed={shouldUseMinimizedUI ? true : undefined}
>
<LeftPanel
pageType={this.props.page_type as PageTypes}
isMinimized={shouldUseMinimizedUI || false}
resizeNotifier={this.context.resizeNotifier}
/>
</div>
{!moduleRenderer && (
<div
className="mx_LeftPanel_wrapper--user"
ref={this._resizeContainer}
data-collapsed={shouldUseMinimizedUI ? true : undefined}
>
<LeftPanel
pageType={this.props.page_type as PageTypes}
isMinimized={shouldUseMinimizedUI || false}
resizeNotifier={this.context.resizeNotifier}
/>
</div>
)}
</div>
</div>
<ResizeHandle passRef={this.resizeHandler} id="lp-resizer" />
{!moduleRenderer && <ResizeHandle passRef={this.resizeHandler} id="lp-resizer" />}
<div className="mx_RoomView_wrapper">{pageElement}</div>
</div>
</div>

View File

@@ -140,6 +140,7 @@ import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/ShareP
import Markdown from "../../Markdown";
import { sanitizeHtmlParams } from "../../Linkify";
import { isOnlyAdmin } from "../../utils/membership";
import { ModuleApi } from "../../modules/Api.ts";
// legacy export
export { default as Views } from "../../Views";
@@ -175,9 +176,11 @@ interface IProps {
interface IState {
// the master view we are showing.
view: Views;
// What the LoggedInView would be showing if visible
// What the LoggedInView would be showing if visible.
// A member of the enum for standard pages or a string for those provided by
// a module.
// eslint-disable-next-line camelcase
page_type?: PageType;
page_type?: PageType | string;
// The ID of the room we're viewing. This is either populated directly
// in the case where we view a room by ID or by RoomView when it resolves
// what ID an alias points at.
@@ -199,7 +202,10 @@ interface IState {
hideToSRUsers: boolean;
syncError: Error | null;
serverConfig?: ValidatedServerConfig;
/** Has our MatrixClient started? */
ready: boolean;
threepidInvite?: IThreepidInvite;
roomOobData?: object;
pendingInitialSync?: boolean;
@@ -222,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;
@@ -383,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());
@@ -424,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,
@@ -603,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!");
@@ -812,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(
{
@@ -856,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
@@ -870,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);
@@ -1075,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();
}
}
@@ -1376,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;
@@ -1386,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
@@ -1812,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") {
@@ -1921,8 +1966,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
userId: userId,
subAction: params?.action,
});
} else {
logger.info(`Ignoring showScreen for '${screen}'`);
} else if (ModuleApi.instance.navigation.locationRenderers.get(screen)) {
this.setState({ page_type: screen });
}
}
@@ -2050,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) {
@@ -2101,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

@@ -44,6 +44,7 @@ import { type CallState, type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { debounce, throttle } from "lodash";
import { CryptoEvent } from "matrix-js-sdk/src/crypto-api";
import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
import { type RoomViewProps } from "@element-hq/element-web-module-api";
import shouldHideEvent from "../../shouldHideEvent";
import { _t } from "../../languageHandler";
@@ -148,7 +149,7 @@ if (DEBUG) {
debuglog = logger.log.bind(console);
}
interface IRoomProps {
interface IRoomProps extends RoomViewProps {
threepidInvite?: IThreepidInvite;
oobData?: IOOBData;
@@ -158,19 +159,37 @@ interface IRoomProps {
// Called with the credentials of a registered user (if they were a ROU that transitioned to PWLU)
onRegistered?(credentials: IMatrixClientCreds): void;
/**
* The RoomViewStore instance for the room to be displayed.
* Only necessary if RoomView should get it's RoomViewStore through the MultiRoomViewStore.
* Omitting this will mean that RoomView renders for the room held in SDKContext.RoomViewStore.
*/
roomViewStore: RoomViewStore;
roomId?: string;
/*
* If true, hide the header
*/
hideHeader?: boolean;
/*
* 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 };
export interface IRoomState {
/**
* The RoomViewStore instance for the room we are displaying
*/
roomViewStore: RoomViewStore;
room?: Room;
roomId?: string;
roomAlias?: string;
@@ -389,6 +408,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private messagePanel: TimelinePanel | null = null;
private roomViewBody = createRef<HTMLDivElement>();
private roomViewStore: RoomViewStore;
public static contextType = SDKContext;
declare public context: React.ContextType<typeof SDKContext>;
@@ -401,9 +422,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
throw new Error("Unable to create RoomView without MatrixClient");
}
if (props.roomId) {
this.roomViewStore = this.context.multiRoomViewStore.getRoomViewStoreForRoom(props.roomId);
} else {
this.roomViewStore = context.roomViewStore;
}
const llMembers = context.client.hasLazyLoadMembersEnabled();
this.state = {
roomViewStore: props.roomViewStore,
roomId: undefined,
roomLoading: true,
peekLoading: false,
@@ -535,7 +561,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
};
private getMainSplitContentType = (room: Room): MainSplitContentType => {
if (this.state.roomViewStore.isViewingCall() || isVideoRoom(room)) {
if (this.roomViewStore.isViewingCall() || isVideoRoom(room)) {
return MainSplitContentType.Call;
}
if (this.context.widgetLayoutStore.hasMaximisedWidget(room)) {
@@ -549,8 +575,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return;
}
const roomLoadError = this.state.roomViewStore.getRoomLoadError() ?? undefined;
if (!initial && !roomLoadError && this.state.roomId !== this.state.roomViewStore.getRoomId()) {
const roomLoadError = this.roomViewStore.getRoomLoadError() ?? undefined;
if (!initial && !roomLoadError && this.state.roomId !== this.roomViewStore.getRoomId()) {
// RoomView explicitly does not support changing what room
// is being viewed: instead it should just be re-mounted when
// switching rooms. Therefore, if the room ID changes, we
@@ -564,7 +590,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// it was, it means we're about to be unmounted.
return;
}
const roomViewStore = this.state.roomViewStore;
const roomViewStore = this.roomViewStore;
const roomId = roomViewStore.getRoomId() ?? null;
const roomAlias = roomViewStore.getRoomAlias() ?? undefined;
const roomLoading = roomViewStore.isRoomLoading();
@@ -611,7 +637,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
newState.showRightPanel = false;
}
const initialEventId = this.state.roomViewStore.getInitialEventId() ?? this.state.initialEventId;
const initialEventId = this.roomViewStore.getInitialEventId() ?? this.state.initialEventId;
if (initialEventId) {
let initialEvent = room?.findEventById(initialEventId);
// The event does not exist in the current sync data
@@ -637,13 +663,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
action: Action.ShowThread,
rootEvent: thread.rootEvent,
initialEvent,
highlighted: this.state.roomViewStore.isInitialEventHighlighted(),
scroll_into_view: this.state.roomViewStore.initialEventScrollIntoView(),
highlighted: this.roomViewStore.isInitialEventHighlighted(),
scroll_into_view: this.roomViewStore.initialEventScrollIntoView(),
});
} else {
newState.initialEventId = initialEventId;
newState.isInitialEventHighlighted = this.state.roomViewStore.isInitialEventHighlighted();
newState.initialEventScrollIntoView = this.state.roomViewStore.initialEventScrollIntoView();
newState.isInitialEventHighlighted = this.roomViewStore.isInitialEventHighlighted();
newState.initialEventScrollIntoView = this.roomViewStore.initialEventScrollIntoView();
}
}
@@ -903,7 +929,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.context.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
}
// Start listening for RoomViewStore updates
this.state.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
this.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
this.context.rightPanelStore.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
@@ -1020,7 +1046,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
window.removeEventListener("beforeunload", this.onPageUnload);
this.state.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
this.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
this.context.rightPanelStore.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
WidgetEchoStore.removeListener(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
@@ -1048,6 +1074,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// clean up if this was a local room
this.context.client?.store.removeRoom(this.state.room.roomId);
}
if (this.props.roomId) this.context.multiRoomViewStore.removeRoomViewStore(this.props.roomId);
}
private onRightPanelStoreUpdate = (): void => {
@@ -1179,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>({
@@ -2070,7 +2104,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (!this.state.room || !this.context?.client) return null;
const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId());
return (
<ScopedRoomContextProvider {...this.state}>
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
<LocalRoomCreateLoader
localRoom={localRoom}
names={names}
@@ -2082,7 +2116,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private renderLocalRoomView(localRoom: LocalRoom): ReactNode {
return (
<ScopedRoomContextProvider {...this.state}>
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
<LocalRoomView
e2eStatus={this.state.e2eStatus}
localRoom={localRoom}
@@ -2098,7 +2132,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode {
return (
<ScopedRoomContextProvider {...this.state}>
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
<WaitingForThirdPartyRoomView
resizeNotifier={this.context.resizeNotifier}
roomView={this.roomView}
@@ -2441,12 +2475,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
</AuxPanel>
);
const pinnedMessageBanner = (
const pinnedMessageBanner = !this.props.hidePinnedMessageBanner && (
<PinnedMessageBanner room={this.state.room} permalinkCreator={this.permalinkCreator} />
);
let messageComposer;
const showComposer =
!this.props.hideComposer &&
!isRoomEncryptionLoading &&
// joined and not showing search results
myMembership === KnownMembership.Join &&
@@ -2538,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
@@ -2640,7 +2676,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
return (
<ScopedRoomContextProvider {...this.state}>
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
<div className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
{showChatEffects && this.roomView.current && (
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
@@ -2657,10 +2693,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
ref={this.roomViewBody}
data-layout={this.state.layout}
>
<RoomHeader
room={this.state.room}
additionalButtons={this.state.viewRoomOpts.buttons}
/>
{!this.props.hideHeader && (
<RoomHeader
room={this.state.room}
additionalButtons={this.state.viewRoomOpts.buttons}
/>
)}
{mainSplitBody}
</div>
</MainSplit>

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

@@ -57,6 +57,7 @@ import {
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { OverflowTileView } from "../rooms/OverflowTileView";
import { attachMentions } from "../../../utils/messages";
const AVATAR_SIZE = 30;
@@ -178,7 +179,18 @@ const Entry: React.FC<IEntryProps<any>> = ({ room, type, content, matrixClient:
);
};
const transformEvent = (event: MatrixEvent): { type: string; content: IContent } => {
/**
* Transform content of a MatrixEvent before forwarding:
* 1. Strip all relations.
* 2. Convert location events into a static pin-drop location share,
* and remove description from self-location shares.
* 3. Pass through attachMentions() to strip mentions (as no EditorModel is present to recalculate from).
*
* @param event - The MatrixEvent to transform.
* @param userId - Current user MXID (passed through to attachMentions()).
* @returns The transformed event type and content.
*/
const transformEvent = (event: MatrixEvent, userId: string): { type: string; content: IContent } => {
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
"m.relates_to": _, // strip relations - in future we will attach a relation pointing at the original event
@@ -213,6 +225,13 @@ const transformEvent = (event: MatrixEvent): { type: string; content: IContent }
};
}
// Mentions can leak information about the context of the original message,
// so pass through attachMentions() to recalculate mentions.
// Currently, this strips all mentions (forces an empty m.mentions),
// as there is no EditorModel to parse pills from.
// Future improvements could actually recalculate mentions based on the message body.
attachMentions(userId, content, null, undefined);
return { type, content };
};
@@ -223,7 +242,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
cli.getProfileInfo(userId).then((info) => setProfileInfo(info));
}, [cli, userId]);
const { type, content } = transformEvent(event);
const { type, content } = transformEvent(event, userId);
// For the message preview we fake the sender as ourselves
const mockEvent = new MatrixEvent({

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

@@ -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

@@ -68,6 +68,8 @@ import { ThreadsActivityCentre } from "./threads-activity-centre/";
import AccessibleButton from "../elements/AccessibleButton";
import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNavigation";
import { KeyboardShortcut } from "../settings/KeyboardShortcut";
import { ModuleApi } from "../../../modules/Api.ts";
import { useModuleSpacePanelItems } from "../../../modules/ExtrasApi.ts";
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement";
const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => {
@@ -290,6 +292,8 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(
const [invites, metaSpaces, actualSpaces, activeSpace] = useSpaces();
const activeSpaces = activeSpace ? [activeSpace] : [];
const moduleSpaceItems = useModuleSpacePanelItems(ModuleApi.instance.extras);
const metaSpacesSection = metaSpaces
.filter((key) => !(key === MetaSpace.VideoRooms && !SettingsStore.getValue("feature_video_rooms")))
.map((key) => {
@@ -341,6 +345,27 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(
</Draggable>
))}
{children}
{moduleSpaceItems.map((item) => (
<li
key={item.spaceKey}
className={classNames("mx_SpaceItem", {
collapsed: isPanelCollapsed,
})}
role="treeitem"
aria-selected={false} // TODO
>
<SpaceButton
{...item}
isNarrow={isPanelCollapsed}
size="32px"
selected={activeSpace === item.spaceKey}
onClick={() => {
SpaceStore.instance.setActiveSpace(item.spaceKey);
item.onSelected?.();
}}
/>
</li>
))}
{shouldShowComponent(UIComponent.CreateSpaces) && (
<CreateSpaceButton isPanelCollapsed={isPanelCollapsed} setPanelCollapsed={setPanelCollapsed} />
)}

View File

@@ -52,6 +52,7 @@ type ButtonProps<T extends keyof HTMLElementTagNameMap> = Omit<
className?: string;
selected?: boolean;
label: string;
icon?: JSX.Element;
contextMenuTooltip?: string;
notificationState?: NotificationState;
isNarrow?: boolean;
@@ -65,6 +66,7 @@ export const SpaceButton = <T extends keyof HTMLElementTagNameMap>({
space,
spaceKey: _spaceKey,
className,
icon,
selected,
label,
contextMenuTooltip,
@@ -84,7 +86,7 @@ export const SpaceButton = <T extends keyof HTMLElementTagNameMap>({
let avatar = (
<div className="mx_SpaceButton_avatarPlaceholder">
<div className="mx_SpaceButton_icon" />
<div className="mx_SpaceButton_icon">{icon}</div>
</div>
);
if (space) {
@@ -143,6 +145,7 @@ export const SpaceButton = <T extends keyof HTMLElementTagNameMap>({
mx_SpaceButton_active: selected,
mx_SpaceButton_hasMenuOpen: menuDisplayed,
mx_SpaceButton_narrow: isNarrow,
mx_SpaceButton_withIcon: Boolean(icon),
})}
aria-label={label}
title={!isNarrow || menuDisplayed ? undefined : label}

View File

@@ -107,7 +107,9 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
if (this.props.sas.emoji) {
const emojiBlocks = this.props.sas.emoji.map((emoji, i) => (
<div className="mx_VerificationShowSas_emojiSas_block" key={i}>
<div className="mx_VerificationShowSas_emojiSas_emoji">{emoji[0]}</div>
<div className="mx_VerificationShowSas_emojiSas_emoji" aria-hidden={true}>
{emoji[0]}
</div>
<div className="mx_VerificationShowSas_emojiSas_label">{tEmoji(emoji, locale)}</div>
</div>
));

View File

@@ -10,6 +10,7 @@ import { createContext } from "react";
import { type IRoomState } from "../components/structures/RoomView";
import { Layout } from "../settings/enums/Layout";
import { type RoomViewStore } from "../stores/RoomViewStore";
export enum TimelineRenderingType {
Room = "Room",
@@ -29,11 +30,12 @@ export enum MainSplitContentType {
Call,
}
const RoomContext = createContext<
IRoomState & {
threadId?: string;
}
>({
export interface RoomContextType extends IRoomState {
threadId?: string;
roomViewStore: RoomViewStore;
}
const RoomContext = createContext<RoomContextType>({
roomLoading: true,
peekLoading: false,
shouldPeek: true,

View File

@@ -25,6 +25,7 @@ import { WidgetPermissionStore } from "../stores/widgets/WidgetPermissionStore";
import { OidcClientStore } from "../stores/oidc/OidcClientStore";
import WidgetStore from "../stores/WidgetStore";
import ResizeNotifier from "../utils/ResizeNotifier";
import { MultiRoomViewStore } from "../stores/MultiRoomViewStore";
// This context is available to components under MatrixChat,
// the context must not be used by components outside a SdkContextClass tree.
@@ -66,6 +67,7 @@ export class SdkContextClass {
protected _UserProfilesStore?: UserProfilesStore;
protected _OidcClientStore?: OidcClientStore;
protected _ResizeNotifier?: ResizeNotifier;
protected _MultiRoomViewStore?: MultiRoomViewStore;
/**
* Automatically construct stores which need to be created eagerly so they can register with
@@ -183,6 +185,13 @@ export class SdkContextClass {
return this._ResizeNotifier;
}
public get multiRoomViewStore(): MultiRoomViewStore {
if (!this._MultiRoomViewStore) {
this._MultiRoomViewStore = new MultiRoomViewStore(defaultDispatcher, this);
}
return this._MultiRoomViewStore;
}
public onLoggedOut(): void {
this._UserProfilesStore = undefined;
}

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

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