Compare commits

...

168 Commits

Author SHA1 Message Date
R Midhun Suresh
2198c40fb6 Add clarifying comment 2025-12-04 14:45:59 +05:30
R Midhun Suresh
40e5dd6fca Merge branch 'develop' into midhun/shared-components/disposing-hook 2025-12-04 14:03:03 +05:30
R Midhun Suresh
9d0a8e2ba1 Remove eslint comments 2025-12-04 14:02:07 +05:30
R Midhun Suresh
f95f63648b whatever -> whichever 2025-12-04 14:01:37 +05:30
R Midhun Suresh
42a824b9b4 Use proper license 2025-12-04 13:52:28 +05:30
David Baker
6a57f69cd9 line in the wrong place 2025-12-03 20:51:37 +00:00
David Baker
e1fb8da2e4 Hopefully fail on no permission
or similar errors
2025-12-03 20:41:52 +00:00
David Baker
7320e3702c Update topic workflow for correct topic format 2025-12-03 20:35:08 +00:00
David Baker
386db8f385 Fix the topic update workflow (#31416)
* Fix the topic update workflow

 * Update room IDs with the new ones after upgrades
 * Make room name variable more descriptive
 * Fail if the topic doesn't match

* Return when failing
2025-12-03 19:37:48 +00:00
David Langley
5a9656350e Add Z-Skip-Sonar check (#31409)
* Try fetch PR from the workflow run to check the label and skip sonar

* Use existing pattern to post success for the sonarqube check

* Trigger Build
2025-12-03 18:38:58 +00:00
RiotRobot
046fb335c0 Merge branch 'master' into develop 2025-12-03 17:25:41 +00:00
RiotRobot
bb5bf5a462 v1.12.6 2025-12-03 17:21:30 +00:00
ElementRobot
916c5a0232 Add option to pick call options for voice calls. (#31407) (#31413)
* Add option to pick call options for voice calls.

* hook on the right thing

* Fix wrong call being disabled

* update snaps

* Add tests for menus

* more snaps

* snap snap

(cherry picked from commit a352a3838e)

Co-authored-by: Will Hunt <2072976+Half-Shot@users.noreply.github.com>
2025-12-03 16:29:41 +00:00
David Baker
81b3ec9df2 Make ESLint's TS Root dir relative to .eslintrc.js (#31411)
* Make ESLint's TS Root dir relative to .eslintrc.js

As per https://github.com/typescript-eslint/typescript-eslint/issues/251 seems
like this is the answer for having vscode not getting confused about
multiple projects in a monorepo with different tsconfigs.

* Add it here too for good measure
2025-12-03 16:11:15 +00:00
Will Hunt
a352a3838e Add option to pick call options for voice calls. (#31407)
* Add option to pick call options for voice calls.

* hook on the right thing

* Fix wrong call being disabled

* update snaps

* Add tests for menus

* more snaps

* snap snap
2025-12-03 15:21:15 +00:00
David Baker
61168f0531 Make shared components a regular dependency (#31402)
As other packages need the types too. Also bump version
2025-12-03 13:25:48 +00:00
R Midhun Suresh
3c6f3f7814 Implement new renderNotificationDecoration from module API (#31389)
* Upgrade module api package

* Add a wrapper component

So that we can render the decoration component with just the room.

* Implement module API method

* Add more tests
2025-12-03 11:11:47 +00:00
David Baker
3e2ee7c829 Bump shared components version 2025-12-03 11:19:26 +00:00
David Baker
ac399e8afd Add module API as a ependency of shared components (#31393)
As it refers to the i18n API for types
2025-12-03 10:58:44 +00:00
ElementRobot
4987d6c573 [create-pull-request] automated change (#31397)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-12-03 06:31:32 +00:00
ElementRobot
c883ceeb4b [create-pull-request] automated change (#31396)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-12-03 06:23:32 +00:00
Michael Telatynski
421a69aede Replace more icons with compound (#31381)
* Replace element-icons/chat-bubbles.svg with compound

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

* Replace element-icons/chat-bubbles.svg with compound

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

* Replace external-link.svg with compound

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

* Replace element-icons/hide.svg with compound

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

* Replace element-icons/explore.svg with compound

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

* Replace element-icons/hash-*.svg with compound

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

* Replace element-icons/share.svg with compound

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

* Replace element-icons/group-members.svg with compound

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

* Replace element-icons/mask-as-*.svg with compound

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

* Replace element-icons/low-priority.svg with compound

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

* Replace element-icons/plus-circle.svg with compound

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

* Replace element-icons/roles.svg with compound

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

* Update jest snapshots

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

* Update screenshots

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

* Update screenshots

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-12-02 15:58:46 +00:00
RiotRobot
16fbb27983 Reset matrix-js-sdk back to develop branch 2025-12-02 15:09:42 +00:00
RiotRobot
319034ab7a Merge branch 'master' into develop 2025-12-02 15:08:46 +00:00
RiotRobot
f7e6cb6129 v1.12.5 2025-12-02 15:04:27 +00:00
RiotRobot
9dc9b169ab Upgrade dependency to matrix-js-sdk@39.3.0 2025-12-02 14:46:48 +00:00
David Baker
df5b56a2ca Bump version 2025-12-02 14:09:21 +00:00
David Baker
5370f25870 Introduce i18nContext (#31347)
* Introduce i18nContext

 * Adds a context that holds the module i1n API
 * Switches shared components to use that instead of importing it directly
 * Adds the context to MatrixChat and BaseDalog so it should be available most places in EW

This is a relatively small PR but does change the way the shared components do i18n so
just doing this one by itself (it stands by itself anyway).

This will allow shared components to use i18n when used in modules.

* Add the file

* Fix import lint

* Name the translate function _t

Then it should continue to get picked up by the script

This seems a bit flaky and ew but I'm not sure I want to get into
changing this in this PR.

* Put humanize back to calling something called _t too

* Missed one

* Add i18n context wrapper to stories

* Unused import

* Fix imports

* wrap richitem

* Wrap other richitem & richlist

* One day I will get my head around this syntax

* Fix import spacing

* Add wrapper to test

* unused import

* Hack around dependency cycle

* Make a moduleapi instance for tests

* Add i18n wrapper to jest-matrix-react

* Simple test for i18napi

* Import type

* Move i18n context wrapper to storybook template

* Unused imports & fix pill story

* Move i18n to its own provider

* Add i18ncontext wrapper to jest tests

* imports

* Bump module api to 1.7.0

* tsdoc
2025-12-02 13:47:14 +00:00
Richard van der Hoff
04cf53e7aa Fix bug which caused app not to load correctly when force_verification is enabled (#31265)
* MatrixChat: add a load of logging for view transitions

This stuff was essentially impossible to follow and debug. I think a load of
logging will help.

* Add more comments on `state.view`

* Add a new state between LOADING/SOFT_LOGOUT and LOGGED_IN

... so that we can transition into COMPLETE_SECURITY without going via
LOGGED_IN.

* Remove redundant check for `force_verification`

This check was previously necessary to keep the tests working, because:

 * onLoggedIn would call `onShowPostLoginScreen`,
 * which (without the check) would call `showScreenAfterLogin`
 * which would queue up an action `Action.ViewHomePage`
 * Then we would receive an already-queued `ClientStarted` action, which would
   transition us (correctly) to the `COMPLETE_SECURITY` view
 * Then we would receive the `ViewHomePage` action, taking us back to `LOGGED_IN`.

I don't think the check was necessary in practice, because in practice there
would be enough delay between the OnLoggedIn and ClientStarted actions that the
race didn't happen.

The *problem* with the check was that it meant that, whenever
`force_verification` was enabled, we would get stuck in the LOADING state.

The check is now unnecessary, because `onLoggedIn` no longer calls
`onShowPostLoginScreen`.

* `onShowPostLoginScreen` need no longer be `async`

* Regression test for https://github.com/element-hq/element-web/issues/31203
2025-12-02 11:14:00 +00:00
Richard van der Hoff
57fd3c46bb Deflake MatrixChat-test (#31383)
Add a workaround for the fact that MatrixChat attempts to use React state for
the state of a state machine: a small `sleep` to let the state settle.

As a result, it turns out we may not see the "Syncing..." state, and in general
`waitForSyncAndLoad` doesn't seem to be doing anything useful.
2025-12-02 10:42:15 +00:00
Florian Duros
6228edcd67 Room list: display the menu option on the room list item when clicked/opened (#31380)
* fix: display the menu option on the room list item when clicked/opened

Fix #31366
`onBlur` was called when the room is opened, the focus is moved to the
composr. The hover state was removed.
The hover state is diplayed when `ìsFocused=true` so the keyboard
navigation is not impacted

* test: update room list e2e tests

* test: update marked as unread state
2025-12-02 10:24:11 +00:00
renovate[bot]
4a934b105b Update dependency express to v5.2.0 [SECURITY] (#31385)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-02 09:59:14 +00:00
renovate[bot]
99178bce86 Update vector-im (#31306)
* Update vector-im

* Update Jest snapshots

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 playwright styling overrides

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

* Hold back Compound Design Tokens as threads icons unexpectedly grew a dot in the top corner where we conditionally draw a dot

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>

* Iterate

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

* Update snapshots

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

* Iterate

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

* Update Compound Design Tokens to 6.4.1

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

* Update snapshots

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

* Iterate

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

* Revert snapshots

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

* Iterate

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

* Tweak bubble

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

* Update snapshots

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

* Update storybook snapshots

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

* Update jest snapshots

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

* Update jest snapshots

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-02 09:57:28 +00:00
Michael Telatynski
2e87f7cb90 Clean up unused icons and deduplicate (#31382)
* Remove unused icons

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

* Consolidate brand icons

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

* Remove unused icons

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-02 09:04:21 +00:00
ElementRobot
1acef76d2d [create-pull-request] automated change (#31386)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-12-02 06:23:06 +00:00
renovate[bot]
10d459d209 Update playwright to v1.57.0 (#31332)
* Update playwright to v1.57.0

* Update screenshots

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

* Resolve playwright conflict from storybook test runner

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-12-01 16:23:21 +00:00
Michael Telatynski
45ab536737 Fix handling of SVGs (#31359)
* Fix handling of SVGs

1. Ensure we always include thumbnails for them
2. Show `m.file` handler if we cannot render the SVG
3. When opening ImageView use svg thumbnail if the SVG cannot be rendered

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

* Fix UploadConfirmDialog choking under React devmode

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

* Fix test

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

* Add tests

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-01 16:00:09 +00:00
Andy Balaam
afa186cdf4 Keep the room creation spinner open until the new room is received from server (#31311)
In the upcoming work for encrypted state, we need to keep the spinner
until the m.room.encryption event is received. This change keeps the
spinner just a little longer than before: until the room is received
from the server.

I'm submitting this change partly because I think it makes sense to
delay until we can confirm the room exists and partly to separate this
change from other changes that will be involved in encrypted state. That
way if this causes problems we can discuss or revert it separately.
2025-12-01 15:10:02 +00:00
Michael Telatynski
44cbd260dc Fix word wrapping in expanded left panel buttons (#31377)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-01 13:19:05 +00:00
Michael Telatynski
7ca4c8bd7f Replace more icons with compound (#31378)
* Replace element-icons/room/composer/attach.svg with compound

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

* Replace element-icons/room/*/emoji.svg with compound

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

* Replace element-icons/room/message-bar/edit.svg with compound

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

* Replace element-icons/room/composer/poll.svg with compound

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

* Replace element-icons/room/composer/plain_text.svg with compound

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

* Remove unused star.svg

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

* Replace element-icons/upload.svg with compound

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

* Replace element-icons/settings/preference.svg with compound

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

* Replace element-icons/settings/flask.svg with compound

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

* Replace element-icons/settings/appearance.svg with compound

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

* Update snapshots

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

* Iterate

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

* Update screenshots

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-01 13:16:08 +00:00
Skye Elliot
f00b643774 <Banner/>: Hide Dismiss button if onClose handler is not provided. (#31362)
* feat: Hide `Dismiss` button if `onClose` handler is not provided.

* tests: Update snapshots for `packages/shared-components`.
2025-12-01 11:44:12 +00:00
Michael Telatynski
f46869e114 Replace batch of legacy icons with compound design tokens (#31360)
* Replace element-icons/call/dialpad.svg with compound

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

* Replace element-icons/call/hangup.svg with compound

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

* Replace element-icons/call/video-call.svg with compound

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

* Replace element-icons/call/voice-call.svg with compound

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

* Replace element-icons/cloud-off.svg with compound

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

* Replace element-icons/eye.svg with compound

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

* Remove debug

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

* Update snapshots

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-01 10:38:20 +00:00
Will Hunt
0c293bbbd0 Implement Playwright tests to ensure calls persist across room switches (#31354)
* Add a fake ecall page

* Start to setup a test to check PiP works

* Complete test file

* cleanup

* lint

* use test fail

* lint again

* remove fake

* Fix flake

* better comment
2025-12-01 09:21:32 +00:00
ElementRobot
0577e245da [create-pull-request] automated change (#31375)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-12-01 06:33:11 +00:00
ElementRobot
5620962e04 [create-pull-request] automated change (#31368)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-29 06:21:33 +00:00
Michael Telatynski
60c2482819 Fix aspect ratio on error view background (#31361)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-28 15:44:26 +00:00
dependabot[bot]
ebec5435e1 Bump node-forge from 1.3.1 to 1.3.2 (#31357)
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.3.1 to 1.3.2.
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.3.1...v1.3.2)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-version: 1.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-28 09:46:44 +00:00
ElementRobot
3112a35907 [create-pull-request] automated change (#31356)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-28 06:31:15 +00:00
ElementRobot
fe1505de59 [create-pull-request] automated change (#31355)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-28 06:23:43 +00:00
Richard van der Hoff
1c684489da MSC4380: Invite blocking (#31268)
* Initial implementation of MSC4380

* fix lint

* Update InviteRulesAccountSetting-test

* add some docs

* `block_all` -> `default_action`

* Add a unit test for BlockInvitesConfigController
2025-11-27 18:42:58 +00:00
Michael Telatynski
5869c519ed Tweak rendering of icons for accessibility (#31346)
* Tweak rendering of icons in dropdowns

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

* Tweak rendering of icons in composer format bar

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

* Tweak rendering of icons in jump to bottom button

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

* Tweak rendering of icons in quick settings button

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

* Tweak rendering of icons in left panel search

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

* Delint

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

* Update snapshots

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

* Fix margin

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

* Update test

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

* Update snapshots

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

* Tweak rendering of icons in security user settings tab

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

* Tweak rendering of icons in space hierarchy

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

* Update screenshots

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

* Delint

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

* Iterate

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

* Simplify

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

* Tidy

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

* Add test

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

* Update snapshots

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-27 16:03:19 +00:00
Michael Telatynski
771696e0f0 Fix mdbook for web-docs.element.dev (#31350)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-27 13:22:37 +00:00
Michael Telatynski
54cbbb6ff0 Remove some unused CSS & icons (#31344)
* Remove some unused CSS & icons

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

* Fix type

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

* Remove another unused class

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-27 08:54:55 +00:00
ElementRobot
69abef1f1c [create-pull-request] automated change (#31348)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-27 06:22:39 +00:00
Skye Elliot
fd152c9c7e Implement a shared Banner component. (#31266)
* feat: Create composer `Banner` shared component.

* fix: Yarn resolution issues corrupting package store.

* chore: Revert "fix: Yarn resolution issues corrupting package store."

This reverts commit 2c13354203.

* fix: Revert lockfile changes.

* chore: Resolve linting errors.

* chore: Update playwright screenshots.
2025-11-26 15:13:55 +00:00
ElementRobot
16e71ffd58 [create-pull-request] automated change (#31343)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-26 12:10:47 +00:00
renovate[bot]
3a020c942a Update peter-evans/repository-dispatch digest to 28959ce (#31342)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-26 11:33:59 +00:00
renovate[bot]
803c40c03c Update peter-evans/create-pull-request digest to 84ae59a (#31341)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-26 11:32:49 +00:00
Richard van der Hoff
eeb2c0f690 Fix failure to request persistent storage perms (#31299)
* Move call to `tryPersistStorage` to `OnLoggedIn` handler

I think this needs to happen whether or not we are in the middle of a login
flow.

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

* Inline `MatrixChat.onLoggedIn`

It's now a one-liner, and its semantics are very confusing.

* Factor out `MatrixChat.onLoggedIn`

Now that we've got rid of the confusing `onLoggedIn` method, we can factor out
a method which *actually* handles `OnLoggedIn` actions.
2025-11-26 11:29:58 +00:00
David Langley
32cce82790 add back yarn (#31284) 2025-11-26 09:32:26 +00:00
Richard van der Hoff
5f07fbbc1b Allow the Login screen to use the dark theme (#31293)
* Allow the Login screen to use the dark theme

There is a whole bunch of code dedicated to attempting to force a "light" theme
onto the login view, even if the deployment is configured to prioritise the
dark theme.

Apparently this was done because, in the old days, the
login view looked rubbish with the dark theme, but now it looks fine.

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

* Fix up background colour of auth screen in dark theme

In dark theme, the background of the login/register/welcome box needs to be
translucent black rather than translucent white.

To make this work, I've moved the style from inline to CSS, and used a PostCSS
variable to define the colour.

* Fix up Welcome page colours in dark theme

Make the text and image white, not black, in short.

* Fix incorrect merge

that will teach me to use the Github UI to address merge conflicts.
2025-11-26 07:54:40 +00:00
ElementRobot
8753a9136f [create-pull-request] automated change (#31339)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-26 06:30:37 +00:00
renovate[bot]
b33b1f3d01 Update dependency @storybook/icons to v2 (#31337)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 23:09:17 +00:00
renovate[bot]
08a1525937 Update react monorepo (#31318)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 19:43:37 +00:00
Richard van der Hoff
135db543e1 Increase border-radius on auth pages (#31333)
* Increase border-radius on auth pages

Bring this into line with some of the other radii in the app

* Update snapshots

* Update playwright snapshots
2025-11-25 18:58:49 +00:00
renovate[bot]
b788e772d4 Update browserslist (#31328)
* Update browserslist

* Update browserslist

* 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-25 18:52:27 +00:00
renovate[bot]
e8cf24abce Update dependency eslint-plugin-jest to v29.2.0 (#31331)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 18:45:33 +00:00
renovate[bot]
4a62e87205 Update all non-major dependencies (#31326)
* 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-25 18:24:10 +00:00
renovate[bot]
e7eeb98c9c Update dependency @types/lodash to v4.17.21 (#31327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 16:50:06 +00:00
renovate[bot]
c4f22cc3b3 Update storybook to v10.0.8 (#31319)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-25 16:49:56 +00:00
renovate[bot]
c45d8c599c Update dependency @stylistic/eslint-plugin to v5.6.1 (#31330)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 16:48:01 +00:00
renovate[bot]
75fa94084d Update dependency webpack-bundle-analyzer to v5 (#31323)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-25 16:45:36 +00:00
renovate[bot]
661b026cd9 Update dependency js-xxhash to v5 (#31322)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 16:37:52 +00:00
renovate[bot]
30bd70dacf Update dependency @sentry/browser to v10.26.0 (#31329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 16:37:34 +00:00
renovate[bot]
20dbcd17fa Update typescript-eslint monorepo to v8.47.0 (#31320)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 16:23:10 +00:00
renovate[bot]
716da7784a Update actions/checkout action to v6 (#31321)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 16:09:59 +00:00
renovate[bot]
ad9150df2d Update nginxinc/nginx-unprivileged:alpine-slim Docker digest to 8e23ab3 (#31324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 15:42:33 +00:00
renovate[bot]
5929269e31 Update Node.js to b36a1ea (#31325)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 15:41:40 +00:00
renovate[bot]
0d7d2fb0a4 Update matrix-org (#30038)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 14:54:16 +00:00
Robin
29a54405d8 Fix calls sometimes not knowing that they're presented (#31313)
Because RoomViewStore used two slightly different conditions, the Call.presented flag could get out of sync with the viewingCall flag. But these should effectively be the same thing.

This was causing some subtle bugs if you would join a call, switch to another room, and then click back into the call room via the room list. The call would be visible but not know that it's presented, causing:

1. The hangup sound to get cut off at the end of the call
2. The widget to disappear immediately without offering a 'reconnect' button if you lose connectivity
2025-11-25 14:49:11 +00:00
RiotRobot
dae90a059f v1.12.5-rc.0 2025-11-25 14:35:44 +00:00
RiotRobot
2f727430e1 Upgrade dependency to matrix-js-sdk@39.3.0-rc.0 2025-11-25 14:12:52 +00:00
ElementRobot
4392aa1ed0 [create-pull-request] automated change (#31315)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-11-25 06:23:04 +00:00
Florian Duros
a721c5f4ea chore: update element-hq/element-web-module-api to 1.6.0 (#31309) 2025-11-24 18:30:59 +00:00
David Langley
79f1176b92 Fix location sharing flake by hiding MapLibre info link from screenshots (#31290)
* Hide MapLibre info link from screenshots

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

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

* fix quotes

* fix tests

* fix quotes

* appease prettier

* fix snapshot tests, hopefully

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

* fix tests

* prettier

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

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

* fix: message reply with multiple room views

* fix: message edition when multiple rooms are displayed

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

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

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

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

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

* const

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

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

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

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

* Drop KEY_STORAGE_OUT_OF_SYNC_STORE in favour of checking cross-signing

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

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

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

and fetch it when user enters their recovery key

* reset backup when needed if user forgets recovery key

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

`view_last_screen` is never used.

* Remove unused action `user_activity_started`

Nothing listens to this, so it's pointless.

* Symbolic constant for `Action.UserActivity`

* Define symbolic constants for more `Action`s

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

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

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

See 356e12591c

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

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

* Add test

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

---------

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

This option is added for the module API.

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

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

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

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

* REPLACEME: pull in dependant js-sdk branch

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

* Format with Prettier

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

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

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

* tweaks

* Nearly working demo

* Lots of minor fixes

* Better working version

* remove unused payload

* bits and pieces

* Cleanup based on new hints

* Simple refactor for skipLobby (and remove returnToLobby)

* Tidyup

* Remove unused tests

* Update tests for voice calls

* Add video room support.

* Add a test for video rooms

* tidy

* remove console log line

* lint and tests

* Bunch of fixes

* Fixes

* Use correct title

* make linter happier

* Update tests

* cleanup

* Drop only

* update snaps

* Document

* lint

* Update snapshots

* Remove duplicate test

* add brackets

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

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

* Iterate

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

---------

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

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

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

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

* Iterate

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

* Update snapshot

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

* Iterate

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

* Update snapshots

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

* Fix double landmark

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

* Update screenshot

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

---------

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

* Storybook migration

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

* Add missing file

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

* Update yarn.lock

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

* Make jest happier

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-13 16:33:51 +00:00
Michael Telatynski
ac255445d1 Remove ts-node (#31228)
* Remove ts-node

Node can handle typescript nowadays

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

* Iterate

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-13 16:33:36 +00:00
Will Hunt
425bc64aa9 Add posthog tracking for key backup toasts (#31195)
* Add tracking for setting up key backups.

* fix lint

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

* Update jest to v30

* Update snapshots

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

* Apply jsdom patch

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

* Fix tests

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

---------

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

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

* Update tests

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-12 17:34:21 +00:00
David Langley
2ce59df1fe Fix emoji category selection with keyboard (#31162)
* Use firstVisible category for roving tab index

* Adding category keyboard navigation tests

* Reduce repetition in categories definition and add tests

* Remove ternary operators

* Simplify
2025-11-12 16:39:27 +00:00
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
722 changed files with 10775 additions and 6354 deletions

View File

@@ -1,8 +1,16 @@
/*
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"],
parserOptions: {
project: ["./tsconfig.json"],
tsconfigRootDir: __dirname,
},
env: {
browser: true,
@@ -160,6 +168,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",
},
],
},
],

3
.github/labels.yml vendored
View File

@@ -279,3 +279,6 @@
- name: "Z-Flaky-Test-Disabled"
description: "The flaking test has been disabled"
color: "ededed"
- name: "Z-Skip-Sonar"
description: "Skip SonarQube analysis for this PR"
color: "ededed"

View File

@@ -42,7 +42,7 @@ jobs:
run:
shell: bash
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:

View File

@@ -14,7 +14,7 @@ jobs:
R2_URL: ${{ vars.CF_R2_S3_API }}
VERSION: ${{ github.ref_name }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Download package
run: |

View File

@@ -26,7 +26,7 @@ jobs:
R2_URL: ${{ vars.CF_R2_S3_API }}
R2_PUBLIC_URL: "https://element-web-develop.element.io"
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:

View File

@@ -34,7 +34,7 @@ jobs:
env:
SITE: ${{ inputs.site || 'staging.element.io' }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Load GPG key
run: |

View File

@@ -20,7 +20,7 @@ jobs:
env:
TEST_TAG: vectorim/element-web:test
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
with:
fetch-depth: 0 # needed for docker-package to be able to calculate the version
@@ -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: |
@@ -141,7 +141,7 @@ jobs:
repository: vectorim/element-web
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
if: github.event_name != 'pull_request'
with:
repository: element-hq/element-web-pro

View File

@@ -17,18 +17,18 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Fetch element-desktop
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
with:
repository: element-hq/element-desktop
path: element-desktop
- name: Fetch element-web
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
with:
path: element-web
- name: Fetch matrix-js-sdk
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
with:
repository: matrix-org/matrix-js-sdk
path: matrix-js-sdk
@@ -43,13 +43,13 @@ 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
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2
with:
mdbook-version: "0.4.10"
mdbook-version: "0.5.1"
- name: Install mdbook extensions
run: cargo install mdbook-combiner mdbook-mermaid

View File

@@ -50,7 +50,7 @@ jobs:
runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
with:
repository: element-hq/element-web
@@ -122,7 +122,7 @@ jobs:
- runAllTests: false
project: Pinecone
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
with:
persist-credentials: false
repository: element-hq/element-web
@@ -194,7 +194,7 @@ jobs:
if: always()
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
if: inputs.skip != true
with:
persist-credentials: false

View File

@@ -10,7 +10,7 @@ jobs:
permissions:
pull-requests: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Update synapse image
run: |
@@ -32,7 +32,7 @@ jobs:
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/playwright-image-updates

View File

@@ -41,7 +41,7 @@ jobs:
REPOS: matrix-js-sdk element-web element-desktop
steps:
- name: Checkout Element Desktop
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
if: inputs.element-desktop
with:
repository: element-hq/element-desktop
@@ -51,7 +51,7 @@ jobs:
fetch-tags: true
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
- name: Checkout Element Web
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
if: inputs.element-web
with:
repository: element-hq/element-web
@@ -61,7 +61,7 @@ jobs:
fetch-tags: true
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
- name: Checkout Matrix JS SDK
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
if: inputs.matrix-js-sdk
with:
repository: matrix-org/matrix-js-sdk

View File

@@ -13,7 +13,7 @@ jobs:
steps:
- name: 🧮 Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: 🔧 Set up node environment
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6

View File

@@ -21,7 +21,7 @@ jobs:
issues: read
pull-requests: read
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
with:
persist-credentials: false
repository: element-hq/element-web
@@ -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

@@ -22,7 +22,7 @@ jobs:
name: "Typescript Syntax Check"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
@@ -63,7 +63,7 @@ jobs:
name: "Rethemendex Check"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- run: ./res/css/rethemendex.sh
@@ -73,7 +73,7 @@ jobs:
name: "ESLint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
@@ -97,7 +97,7 @@ jobs:
name: "Style Lint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
@@ -115,7 +115,7 @@ jobs:
name: "Workflow Lint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
@@ -133,7 +133,7 @@ jobs:
name: "Analyse Dead Code"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:

View File

@@ -39,7 +39,7 @@ jobs:
runner: [1, 2]
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
with:
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
@@ -103,7 +103,7 @@ jobs:
run: exit 1
- name: Skip SonarCloud in merge queue
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true' || contains(github.event.pull_request.labels.*.name, 'Z-Skip-Sonar')
uses: guibranco/github-status-action-v2@5530c593759f489bba08272e96986ffc571c1ea1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
@@ -118,7 +118,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
with:
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}

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

@@ -9,7 +9,7 @@ jobs:
update:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
@@ -23,7 +23,7 @@ jobs:
run: "yarn update:jitsi"
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/jitsi-update

View File

@@ -26,13 +26,13 @@ jobs:
env:
HS_URL: ${{ secrets.BETABOT_HS_URL }}
LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }}
PUBLIC_ROOM_ID: "!IemiTbwVankHTFiEoh:matrix.org"
ANNOUNCEMENT_ROOM_ID: "!bijaLdadorKgNGtHdA:matrix.org"
PUBLIC_DISCUSSION_ROOM_ID: "!xUW4PpAe1CmThA3r2wI8IrgwwsK006-zqWdJCljpd10"
ANNOUNCEMENT_ROOM_ID: "!ars5ndgI6IIYZXECiJ-u8YljHNzShJn3nHdB-3rYI2M"
TOKEN: ${{ secrets.BETABOT_ACCESS_TOKEN }}
RELEASE_STATUS: "Release status: ${{ inputs.expected_status }} expected ${{ inputs.expected_date }}"
with:
script: |
const { HS_URL, TOKEN, RELEASE_STATUS, LOBBY_ROOM_ID, PUBLIC_ROOM_ID, ANNOUNCEMENT_ROOM_ID } = process.env;
const { HS_URL, TOKEN, RELEASE_STATUS, LOBBY_ROOM_ID, PUBLIC_DISCUSSION_ROOM_ID, ANNOUNCEMENT_ROOM_ID } = process.env;
const repo = context.repo;
const { data } = await github.rest.repos.getLatestRelease({
@@ -71,18 +71,23 @@ jobs:
const data = await res.json();
console.log(roomId, "got event", data);
if (!regex.test(data.topic)) {
core.setFailed("Topic format is incorrect for room " + roomId);
return;
}
const topic = data.topic.replace(regex, releaseTopic);
if (topic === data.topic) {
console.log(roomId, "nothing to do");
return;
}
if (data["org.matrix.msc3765.topic"]) {
data["org.matrix.msc3765.topic"].forEach(d => {
data["org.matrix.msc3765.topic"]?.["m.text"].forEach(d => {
d.body = d.body.replace(regex, releaseTopic);
});
}
if (data["m.topic"]) {
data["m.topic"].forEach(d => {
data["m.topic"]?.["m.text"].forEach(d => {
d.body = d.body.replace(regex, releaseTopic);
});
}
@@ -97,12 +102,18 @@ jobs:
});
if (res.ok) {
console.log(roomId, "topic updated:", topic);
const resJson = res.json();
if (resJson.errcode) {
core.setFailed(`Error updating ${roomId}: ${resJson.error}`);
} else {
console.log(roomId, "topic updated:", topic);
}
} else {
console.log(roomId, await res.text());
const errText = await res.text();
core.setFailed(`Error updating ${roomId}: ${errText}`);
}
}
await updateReleaseInTopic(LOBBY_ROOM_ID);
await updateReleaseInTopic(PUBLIC_ROOM_ID);
await updateReleaseInTopic(PUBLIC_DISCUSSION_ROOM_ID);
await updateReleaseInTopic(ANNOUNCEMENT_ROOM_ID);

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
# syntax=docker.io/docker/dockerfile:1.19-labs@sha256:dce1c693ef318bca08c964ba3122ae6248e45a1b96d65c4563c8dc6fe80349a2
# syntax=docker.io/docker/dockerfile:1.20-labs@sha256:dbcde2ebc4abc8bb5c3c499b9c9a6876842bf5da243951cd2697f921a7aeb6a9
# Builder
FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:c102f42d665c164b4e5e5549813b1547ac8a9f1d343c7d17ddac106905a1c30b AS builder
FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:b36a1eab6bdeb43cf4808370d18b6706452e810e3563b1ce669d2965af3c0464 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:8e23ab31c214ee1d7f832d63b2ee768d5cbc270a94a2cba0752fed93ad83e345
# Need root user to install packages & manipulate the usr directory
USER root

View File

@@ -4,7 +4,6 @@
title = "Element Web & Desktop"
authors = ["New Vector Ltd.", "The Matrix.org Foundation C.I.C."]
language = "en"
multilingual = false
# The directory that documentation files are stored in
src = "docs"

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/**",
@@ -41,6 +42,8 @@ export default {
"util",
// Embedded into webapp
"@element-hq/element-call-embedded",
// Transitive dep of jest
"jsdom",
// Used by matrix-js-sdk, which means we have to include them as a
// dependency so that // we can run `tsc` (since we import the typescript
@@ -48,11 +51,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.3",
"version": "1.12.6",
"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,36 +65,35 @@
"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": {
"**/pretty-format/react-is": "19.2.0",
"@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",
"@types/react": "19.2.6",
"@types/react-dom": "19.2.3",
"oidc-client-ts": "3.4.1",
"jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001751",
"caniuse-lite": "1.0.30001756",
"testcontainers": "^11.0.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@element-hq/element-web-module-api": "1.5.0",
"@element-hq/web-shared-components": "file:packages/shared-components",
"@fontsource/inconsolata": "^5",
"@element-hq/element-web-module-api": "1.8.0",
"@element-hq/web-shared-components": "link:packages/shared-components",
"@fontsource/fira-code": "^5",
"@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7",
"@matrix-org/analytics-events": "^0.29.2",
"@matrix-org/emojibase-bindings": "^1.3.4",
"@matrix-org/analytics-events": "^0.30.0",
"@matrix-org/emojibase-bindings": "^1.5.0",
"@matrix-org/react-sdk-module-api": "^2.4.0",
"@matrix-org/spec": "^1.7.0",
"@sentry/browser": "^10.0.0",
"@types/png-chunks-extract": "^1.0.2",
"@vector-im/compound-design-tokens": "^6.0.0",
"@vector-im/compound-web": "^8.1.2",
"@vector-im/compound-design-tokens": "6.4.1",
"@vector-im/compound-web": "^8.3.1",
"@vector-im/matrix-wysiwyg": "2.40.0",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
@@ -109,7 +108,7 @@
"diff-dom": "^5.0.0",
"diff-match-patch": "^1.0.5",
"domutils": "^3.2.2",
"emojibase-regex": "15.3.2",
"emojibase-regex": "^17.0.0",
"escape-html": "^1.0.3",
"file-saver": "^2.0.5",
"filesize": "11.0.13",
@@ -119,7 +118,7 @@
"html-entities": "^2.0.0",
"html-react-parser": "^5.2.2",
"is-ip": "^3.1.0",
"js-xxhash": "^4.0.0",
"js-xxhash": "^5.0.0",
"jsrsasign": "^11.0.0",
"jszip": "^3.7.0",
"katex": "^0.16.0",
@@ -132,14 +131,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.297.2",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "^19.0.0",
@@ -181,14 +180,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",
"@playwright/test": "1.57.0",
"@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 +203,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",
@@ -215,9 +214,9 @@
"@types/node-fetch": "^2.6.2",
"@types/pako": "^2.0.0",
"@types/qrcode": "^1.3.5",
"@types/react": "19.2.2",
"@types/react": "19.2.6",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "19.2.2",
"@types/react-dom": "19.2.3",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "2.16.0",
"@types/sdp-transform": "^2.4.10",
@@ -226,7 +225,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 +257,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,19 +288,18 @@
"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",
"webpack": "^5.89.0",
"webpack-bundle-analyzer": "^4.8.0",
"webpack-bundle-analyzer": "^5.0.0",
"webpack-cli": "^6.0.0",
"webpack-dev-server": "^5.0.0",
"webpack-retry-chunk-load-plugin": "^3.1.1",
@@ -314,7 +312,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"],
@@ -9,6 +16,7 @@ module.exports = {
],
parserOptions: {
project: ["./tsconfig.json"],
tsconfigRootDir: __dirname,
},
env: {
browser: true,

View File

@@ -6,6 +6,7 @@ import React, { useLayoutEffect } from "react";
import { setLanguage } from "../src/utils/i18n";
import { TooltipProvider } from "@vector-im/compound-web";
import { StoryContext } from "storybook/internal/csf";
import { I18nApi, I18nContext } from "../src";
export const globalTypes = {
theme: {
@@ -70,9 +71,17 @@ const withTooltipProvider: Decorator = (Story) => {
);
};
const withI18nProvider: Decorator = (Story) => {
return (
<I18nContext.Provider value={new I18nApi()}>
<Story />
</I18nContext.Provider>
);
};
const preview: Preview = {
tags: ["autodocs"],
decorators: [withThemeProvider, withTooltipProvider],
decorators: [withThemeProvider, withTooltipProvider, withI18nProvider],
parameters: {
options: {
storySort: {

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.11",
"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,9 +43,14 @@
"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 /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot"
"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"
},
"resolutions": {
"playwright": "1.57.0"
},
"dependencies": {
"@element-hq/element-web-module-api": "^1.8.0",
"@vector-im/compound-design-tokens": "^6.3.0",
"classnames": "^2.5.1",
"counterpart": "^0.18.6",
"lodash": "^4.17.21",
@@ -56,28 +61,28 @@
},
"devDependencies": {
"@element-hq/element-web-playwright-common": "^2.0.0",
"@playwright/test": "^1.50.1",
"@storybook/addon-a11y": "^9.1.10",
"@storybook/addon-designs": "^10.0.2",
"@storybook/addon-docs": "^9.1.10",
"@storybook/icons": "^1.6.0",
"@storybook/react-vite": "^9.1.10",
"@storybook/test-runner": "^0.23.0",
"@playwright/test": "1.57.0",
"@storybook/addon-a11y": "^10.0.7",
"@storybook/addon-designs": "^11.0.1",
"@storybook/addon-docs": "^10.0.7",
"@storybook/icons": "^2.0.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",
@@ -88,7 +93,6 @@
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"peerDependencies": {
"@vector-im/compound-design-tokens": "^6.0.0",
"@vector-im/compound-web": "^8.2.5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

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

@@ -14,6 +14,8 @@ import { fireEvent } from "@testing-library/dom";
import * as stories from "./AudioPlayerView.stories.tsx";
import { AudioPlayerView, type AudioPlayerViewActions, type AudioPlayerViewSnapshot } from "./AudioPlayerView";
import { MockViewModel } from "../../viewmodel/MockViewModel.ts";
import { I18nContext } from "../../utils/i18nContext.ts";
import { I18nApi } from "../../index.ts";
const { Default, NoMediaName, NoSize, HasError } = composeStories(stories);
@@ -64,7 +66,9 @@ describe("AudioPlayerView", () => {
error: false,
});
render(<AudioPlayerView vm={vm} />);
render(<AudioPlayerView vm={vm} />, {
wrapper: ({ children }) => <I18nContext.Provider value={new I18nApi()}>{children}</I18nContext.Provider>,
});
await user.click(screen.getByRole("button", { name: "Play" }));
expect(togglePlay).toHaveBeenCalled();

View File

@@ -14,7 +14,7 @@ import { Flex } from "../../utils/Flex";
import styles from "./AudioPlayerView.module.css";
import { PlayPauseButton } from "../PlayPauseButton";
import { type PlaybackState } from "../playback";
import { _t } from "../../utils/i18n";
import { useI18n } from "../../utils/i18nContext";
import { formatBytes } from "../../utils/FormattingUtils";
import { Clock } from "../Clock";
import { SeekBar } from "../SeekBar";
@@ -90,6 +90,8 @@ interface AudioPlayerViewProps {
* ```
*/
export function AudioPlayerView({ vm }: Readonly<AudioPlayerViewProps>): JSX.Element {
const { translate: _t } = useI18n();
const {
playbackState,
mediaName = _t("timeline|m.audio|unnamed_audio"),

View File

@@ -23,7 +23,7 @@ exports[`AudioPlayerView renders the audio player in default state 1`] = `
tabindex="-1"
>
<div
class="_indicator-icon_zr2a0_17"
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
@@ -114,7 +114,7 @@ exports[`AudioPlayerView renders the audio player in error state 1`] = `
tabindex="-1"
>
<div
class="_indicator-icon_zr2a0_17"
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
@@ -210,7 +210,7 @@ exports[`AudioPlayerView renders the audio player without media name 1`] = `
tabindex="-1"
>
<div
class="_indicator-icon_zr2a0_17"
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
@@ -301,7 +301,7 @@ exports[`AudioPlayerView renders the audio player without size 1`] = `
tabindex="-1"
>
<div
class="_indicator-icon_zr2a0_17"
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<svg

View File

@@ -11,7 +11,7 @@ import Play from "@vector-im/compound-design-tokens/assets/web/icons/play-solid"
import Pause from "@vector-im/compound-design-tokens/assets/web/icons/pause-solid";
import styles from "./PlayPauseButton.module.css";
import { _t } from "../../utils/i18n";
import { useI18n } from "../../utils/i18nContext";
export interface PlayPauseButtonProps extends HTMLAttributes<HTMLButtonElement> {
/**
@@ -46,6 +46,8 @@ export function PlayPauseButton({
togglePlay,
...rest
}: Readonly<PlayPauseButtonProps>): JSX.Element {
const { translate: _t } = useI18n();
const label = playing ? _t("action|pause") : _t("action|play");
return (

View File

@@ -13,7 +13,7 @@ exports[`PlayPauseButton renders the button in default state 1`] = `
tabindex="0"
>
<div
class="_indicator-icon_zr2a0_17"
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
@@ -45,7 +45,7 @@ exports[`PlayPauseButton renders the button in playing state 1`] = `
tabindex="0"
>
<div
class="_indicator-icon_zr2a0_17"
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<svg

View File

@@ -10,7 +10,7 @@ import { throttle } from "lodash";
import classNames from "classnames";
import style from "./SeekBar.module.css";
import { _t } from "../../utils/i18n";
import { useI18n } from "../../utils/i18nContext";
export interface SeekBarProps extends React.InputHTMLAttributes<HTMLInputElement> {
/**
@@ -33,6 +33,8 @@ interface ISeekCSS extends CSSProperties {
* ```
*/
export function SeekBar({ value = 0, className, ...rest }: Readonly<SeekBarProps>): JSX.Element {
const { translate: _t } = useI18n();
const [newValue, setNewValue] = useState(value);
// Throttle the value setting to avoid excessive re-renders
const setThrottledValue = useMemo(() => throttle(setNewValue, 10), []);

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

@@ -0,0 +1,93 @@
/*
* Copyright (c) 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.
*/
:root {
--cpd-color-gradient-critical-linear: linear-gradient(
180deg,
var(--cpd-color-alpha-red-500) 0%,
var(--cpd-color-alpha-red-400) 20%,
var(--cpd-color-alpha-red-300) 40%,
var(--cpd-color-alpha-red-200) 60%,
var(--cpd-color-alpha-red-100) 80%,
var(--cpd-color-transparent) 100%
);
}
.banner {
container-type: inline-size;
container-name: banner;
display: flex;
align-items: center;
justify-content: start;
gap: var(--cpd-space-3x);
padding: var(--cpd-space-4x);
border-top: 1px solid var(--cpd-color-gray-400);
white-space: nowrap;
}
.banner[data-type="success"] {
background: var(--cpd-color-gradient-subtle-linear);
border-color: var(--cpd-color-green-900);
}
.banner[data-type="critical"] {
background: var(--cpd-color-gradient-critical-linear);
border-color: var(--cpd-color-border-critical-primary);
}
.banner[data-type="info"] {
background: var(--cpd-color-gradient-info-linear);
border-color: var(--cpd-color-blue-900);
}
.banner[data-type="info"] :is(svg) {
color: var(--cpd-color-blue-900);
}
.banner[data-type="success"] :is(.content, svg) {
color: var(--cpd-color-green-900);
}
.banner[data-type="critical"] :is(.content, svg) {
color: var(--cpd-color-red-900);
}
.banner p {
margin: 0;
}
.icon {
/* lock icon dimensions */
min-width: 32px;
min-height: 32px;
max-width: 32px;
max-height: 32px;
margin: 4px;
/* centre svg icons, as they are not full width */
flex: 0;
display: flex;
align-items: center;
justify-content: center;
}
.icon img {
border-radius: 50%;
}
.actions {
margin-left: auto;
flex: 0;
display: flex;
flex-direction: row;
gap: var(--cpd-space-1x);
align-self: center;
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (c) 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.
*/
import React from "react";
import { fn } from "storybook/test";
import { type Meta, type StoryObj } from "@storybook/react-vite";
import { Button } from "@vector-im/compound-web";
import { Banner } from "./Banner";
import { _t } from "../../utils/i18n";
const meta = {
title: "room/Banner",
component: Banner,
tags: ["autodocs"],
args: {
children: <p>Hello! This is a status banner.</p>,
onClose: fn(),
},
} satisfies Meta<typeof Banner>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Info: Story = {
args: {
type: "info",
},
};
export const Success: Story = {
args: {
type: "success",
},
};
export const Critical: Story = {
args: {
type: "critical",
},
};
export const WithAction: Story = {
args: {
children: (
<p>
{_t(
"encryption|pinned_identity_changed",
{ displayName: "Alice", userId: "@alice:example.org" },
{
a: (sub) => <a href="https://example.org">{sub}</a>,
b: (sub) => <b>{sub}</b>,
},
)}
</p>
),
actions: <Button kind="primary">{_t("encryption|withdraw_verification_action")}</Button>,
},
};
export const WithAvatarImage: Story = {
args: {
avatar: <img alt="Example" src="https://picsum.photos/32/32" />,
},
};
export const WithoutClose: Story = {
args: {
onClose: undefined,
},
};

View File

@@ -0,0 +1,41 @@
/*
* Copyright (c) 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.
*/
import React from "react";
import { render } from "jest-matrix-react";
import { composeStories } from "@storybook/react-vite";
import * as stories from "./Banner.stories.tsx";
const { Default, Info, Success, WithAction, WithAvatarImage, Critical } = composeStories(stories);
describe("AvatarWithDetails", () => {
it("renders a default banner", () => {
const { container } = render(<Default />);
expect(container).toMatchSnapshot();
});
it("renders a info banner", () => {
const { container } = render(<Info />);
expect(container).toMatchSnapshot();
});
it("renders a success banner", () => {
const { container } = render(<Success />);
expect(container).toMatchSnapshot();
});
it("renders a critical banner", () => {
const { container } = render(<Critical />);
expect(container).toMatchSnapshot();
});
it("renders a banner with an action", () => {
const { container } = render(<WithAction />);
expect(container).toMatchSnapshot();
});
it("renders a banner with an avatar iamge", () => {
const { container } = render(<WithAvatarImage />);
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,93 @@
/*
* Copyright (c) 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.
*/
import classNames from "classnames";
import React, {
type MouseEventHandler,
type ReactElement,
type ReactNode,
type PropsWithChildren,
useMemo,
} from "react";
import { Button } from "@vector-im/compound-web";
import CheckCircleIcon from "@vector-im/compound-design-tokens/assets/web/icons/check-circle";
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid";
import InfoIcon from "@vector-im/compound-design-tokens/assets/web/icons/info";
import styles from "./Banner.module.css";
import { _t } from "../../utils/i18n";
interface BannerProps {
/**
* The type of the status banner.
*/
type?: "success" | "info" | "critical";
/**
* The banner avatar.
*/
avatar?: React.ReactNode;
className?: string;
/**
* Actions presented to the user in the right-hand side of the banner alongside the dismiss button.
*/
actions?: ReactNode;
/**
* Called when the user presses the "dismiss" button.
*/
onClose?: MouseEventHandler<HTMLButtonElement>;
}
/**
* A banner component used for displaying user-facing information above the message composer.
*
* @example
* ```tsx
* <Banner onClose={onCloseHandler} />
* ```
*/
export function Banner({
type,
children,
avatar,
className,
actions,
onClose,
...props
}: PropsWithChildren<BannerProps>): ReactElement {
const classes = classNames(styles.banner, className);
const icon = useMemo(() => {
switch (type) {
case "critical":
return <ErrorIcon fontSize={24} {...props} />;
case "info":
return <InfoIcon fontSize={24} {...props} />;
case "success":
return <CheckCircleIcon fontSize={24} {...props} />;
default:
return <InfoIcon fontSize={24} {...props} />;
}
}, [type, props]);
return (
<div {...props} className={classes} data-type={type}>
<div className={styles.icon}>{avatar ?? icon}</div>
<span className={styles.content}>{children}</span>
<div className={styles.actions}>
{actions}
{onClose && (
<Button kind="secondary" size="sm" onClick={onClose}>
{_t("action|dismiss")}
</Button>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,290 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`AvatarWithDetails renders a banner with an action 1`] = `
<div>
<div
class="banner"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
/>
<path
clip-rule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
fill-rule="evenodd"
/>
</svg>
</div>
<span
class="content"
>
<p>
encryption|pinned_identity_changed
</p>
</span>
<div
class="actions"
>
<button
class="_button_187yx_8"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
encryption|withdraw_verification_action
</button>
<button
class="_button_187yx_8"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
Dismiss
</button>
</div>
</div>
</div>
`;
exports[`AvatarWithDetails renders a banner with an avatar iamge 1`] = `
<div>
<div
class="banner"
>
<div
class="icon"
>
<img
alt="Example"
src="https://picsum.photos/32/32"
/>
</div>
<span
class="content"
>
<p>
Hello! This is a status banner.
</p>
</span>
<div
class="actions"
>
<button
class="_button_187yx_8"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
Dismiss
</button>
</div>
</div>
</div>
`;
exports[`AvatarWithDetails renders a critical banner 1`] = `
<div>
<div
class="banner"
data-type="critical"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<span
class="content"
>
<p>
Hello! This is a status banner.
</p>
</span>
<div
class="actions"
>
<button
class="_button_187yx_8"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
Dismiss
</button>
</div>
</div>
</div>
`;
exports[`AvatarWithDetails renders a default banner 1`] = `
<div>
<div
class="banner"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
/>
<path
clip-rule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
fill-rule="evenodd"
/>
</svg>
</div>
<span
class="content"
>
<p>
Hello! This is a status banner.
</p>
</span>
<div
class="actions"
>
<button
class="_button_187yx_8"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
Dismiss
</button>
</div>
</div>
</div>
`;
exports[`AvatarWithDetails renders a info banner 1`] = `
<div>
<div
class="banner"
data-type="info"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
/>
<path
clip-rule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
fill-rule="evenodd"
/>
</svg>
</div>
<span
class="content"
>
<p>
Hello! This is a status banner.
</p>
</span>
<div
class="actions"
>
<button
class="_button_187yx_8"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
Dismiss
</button>
</div>
</div>
</div>
`;
exports[`AvatarWithDetails renders a success banner 1`] = `
<div>
<div
class="banner"
data-type="success"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m10.6 13.8-2.15-2.15a.95.95 0 0 0-.7-.275.95.95 0 0 0-.7.275.95.95 0 0 0-.275.7q0 .425.275.7L9.9 15.9q.3.3.7.3t.7-.3l5.65-5.65a.95.95 0 0 0 .275-.7.95.95 0 0 0-.275-.7.95.95 0 0 0-.7-.275.95.95 0 0 0-.7.275zM12 22a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20"
/>
</svg>
</div>
<span
class="content"
>
<p>
Hello! This is a status banner.
</p>
</span>
<div
class="actions"
>
<button
class="_button_187yx_8"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
Dismiss
</button>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,8 @@
/*
* Copyright (c) 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.
*/
export * from "./Banner";

View File

@@ -11,6 +11,7 @@ export * from "./audio/Clock";
export * from "./audio/PlayPauseButton";
export * from "./audio/SeekBar";
export * from "./avatar/AvatarWithDetails";
export * from "./composer/Banner";
export * from "./event-tiles/TextualEventView";
export * from "./message-body/MediaBody";
export * from "./pill-input/Pill";
@@ -22,10 +23,12 @@ export * from "./utils/Flex";
// Utils
export * from "./utils/i18n";
export * from "./utils/i18nContext";
export * from "./utils/humanize";
export * from "./utils/DateUtils";
export * from "./utils/numbers";
export * from "./utils/FormattingUtils";
export * from "./utils/I18nApi";
// MVVM
export * from "./viewmodel";

View File

@@ -12,7 +12,7 @@ import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"
import { Flex } from "../../utils/Flex";
import styles from "./Pill.module.css";
import { _t } from "../../utils/i18n";
import { useI18n } from "../../utils/i18nContext";
export interface PillProps extends Omit<HTMLAttributes<HTMLDivElement>, "onClick"> {
/**
@@ -39,6 +39,7 @@ export interface PillProps extends Omit<HTMLAttributes<HTMLDivElement>, "onClick
*/
export function Pill({ className, children, label, onClick, ...props }: PropsWithChildren<PillProps>): JSX.Element {
const id = useId();
const { translate: _t } = useI18n();
return (
<Flex
@@ -53,7 +54,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,14 +18,14 @@ 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;"
tabindex="0"
>
<div
class="_indicator-icon_zr2a0_17"
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<svg

View File

@@ -8,8 +8,8 @@
import React from "react";
import { fn } from "storybook/test";
import { RichItem } from "./RichItem";
import type { Meta, StoryFn } from "@storybook/react-vite";
import { RichItem } from "./RichItem";
const currentTimestamp = new Date("2025-03-09T12:00:00Z").getTime();

View File

@@ -9,8 +9,8 @@ import React, { type HTMLAttributes, type JSX, memo } from "react";
import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
import styles from "./RichItem.module.css";
import { humanizeTime } from "../../utils/humanize";
import { Flex } from "../../utils/Flex";
import { useI18n } from "../../utils/i18nContext";
export interface RichItemProps extends HTMLAttributes<HTMLLIElement> {
/**
@@ -63,6 +63,8 @@ export const RichItem = memo(function RichItem({
selected,
...props
}: RichItemProps): JSX.Element {
const i18n = useI18n();
return (
<li
className={styles.richItem}
@@ -77,7 +79,7 @@ export const RichItem = memo(function RichItem({
<span className={styles.description}>{description}</span>
{timestamp && (
<span role="timer" className={styles.timestamp}>
{humanizeTime(timestamp)}
{i18n.humanizeTime(timestamp)}
</span>
)}
</li>

View File

@@ -16,16 +16,24 @@ import React, { type ReactElement } from "react";
import { render, type RenderOptions } from "@testing-library/react";
import { TooltipProvider } from "@vector-im/compound-web";
import { I18nApi, I18nContext } from "../..";
const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"]) => {
return ({ children }: { children: React.ReactNode }) => {
if (Wrapper) {
return (
<Wrapper>
<TooltipProvider>{children}</TooltipProvider>
</Wrapper>
<I18nContext.Provider value={new I18nApi()}>
<Wrapper>
<TooltipProvider>{children}</TooltipProvider>
</Wrapper>
</I18nContext.Provider>
);
} else {
return <TooltipProvider>{children}</TooltipProvider>;
return (
<I18nContext.Provider value={new I18nApi()}>
<TooltipProvider>{children}</TooltipProvider>
</I18nContext.Provider>
);
}
};
};

View File

@@ -0,0 +1,22 @@
/*
* 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.
*/
import { type TranslationKey } from "../i18nKeys";
import { I18nApi } from "./I18nApi";
describe("I18nApi", () => {
it("can register a translation and use it", () => {
const i18n = new I18nApi();
i18n.register({
"hello.world": {
en: "Hello, World!",
},
});
expect(i18n.translate("hello.world" as TranslationKey)).toBe("Hello, World!");
});
});

View File

@@ -6,16 +6,17 @@ Please see LICENSE files in the repository root for full details.
*/
import { type I18nApi as II18nApi, type Variables, type Translations } from "@element-hq/element-web-module-api";
import { registerTranslations } from "@element-hq/web-shared-components";
import { _t, getCurrentLanguage, type TranslationKey } from "../languageHandler.tsx";
import { humanizeTime } from "./humanize";
import { _t, getLocale, registerTranslations } from "./i18n";
import { type TranslationKey } from "../i18nKeys";
export class I18nApi implements II18nApi {
/**
* Read the current language of the user in IETF Language Tag format
*/
public get language(): string {
return getCurrentLanguage();
return getLocale();
}
/**
@@ -44,4 +45,8 @@ export class I18nApi implements II18nApi {
public translate(key: TranslationKey, variables?: Variables): string {
return _t(key, variables);
}
public humanizeTime(timeMillis: number): string {
return humanizeTime(timeMillis, this);
}
}

View File

@@ -6,7 +6,9 @@ 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 { _t } from "./i18n";
import { type I18nApi } from "@element-hq/element-web-module-api";
import { _t as _tFromModule } from "./i18n";
// These are the constants we use for when to break the text
const MILLISECONDS_RECENT = 15000;
@@ -21,13 +23,15 @@ const HOURS_1_DAY = 26;
* @param {number} timeMillis The time in millis to compare against.
* @returns {string} The humanized time.
*/
export function humanizeTime(timeMillis: number): string {
export function humanizeTime(timeMillis: number, i18nApi?: I18nApi): string {
const now = Date.now();
let msAgo = now - timeMillis;
const minutes = Math.abs(Math.ceil(msAgo / 60000));
const hours = Math.ceil(minutes / 60);
const days = Math.ceil(hours / 24);
const _t = i18nApi?.translate ?? _tFromModule;
if (msAgo >= 0) {
// Past
if (msAgo <= MILLISECONDS_RECENT) return _t("time|few_seconds_ago");

View File

@@ -0,0 +1,27 @@
/*
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.
*/
import { createContext, useContext } from "react";
import { type I18nApi } from "@element-hq/element-web-module-api";
export const I18nContext = createContext<I18nApi | null>(null);
I18nContext.displayName = "I18nContext";
/**
* A hook to get the i18n API from the context. Will throw if no i18n context is found.
* @throws If no i18n context is found
* @returns The i18n API from the context
*/
export function useI18n(): I18nApi {
const i18n = useContext(I18nContext);
if (!i18n) {
throw new Error("useI18n must be used within an I18nContext.Provider");
}
return i18n;
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2025 New Vector Ltd.
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.
*/

View File

@@ -23,7 +23,7 @@ type VmCreator<B extends BaseViewModel<unknown, unknown>> = () => B;
export function useCreateAutoDisposedViewModel<B extends BaseViewModel<unknown, unknown>>(vmCreator: VmCreator<B>): B {
/**
* The view-model instance may be replaced by a different instance in some scenarios.
* We want to be sure that whatever react component called this hook gets re-rendered
* We want to be sure that whichever react component called this hook gets re-rendered
* when this happens, hence the state.
*/
const [viewModel, setViewModel] = useState<B>(vmCreator);
@@ -54,6 +54,12 @@ export function useCreateAutoDisposedViewModel<B extends BaseViewModel<unknown,
toDispose.dispose();
};
/**
* We explicitly provide an empty dependency array as we don't expect the viewModel/viewCreator to
* change.
* Or to put it in another way, the only reason to use this hook is to create/dispose the view-model
* and that is something that should only happen at the start/end of the lifecycle of this component.
*/
// eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

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

View File

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

View File

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

View File

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

@@ -299,9 +299,7 @@ test.describe("Room list", () => {
const publicRoom = roomListView.getByRole("option", { name: "low priority room" });
// Make room low priority
await publicRoom.hover();
const roomItemMenu = publicRoom.getByRole("button", { name: "More Options" });
await roomItemMenu.click();
await publicRoom.click({ button: "right" });
await page.getByRole("menuitemcheckbox", { name: "Low priority" }).click();
// Should have low priority decoration
@@ -309,13 +307,16 @@ test.describe("Room list", () => {
"This is a low priority room",
);
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
// focus the header to avoid to have hover decoration
await page.getByTestId("room-list-header").click();
await expect(publicRoom).toMatchScreenshot("room-list-item-low-priority.png");
});
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();
@@ -447,12 +448,11 @@ test.describe("Room list", () => {
await bot.joinRoom(roomId);
const room = roomListView.getByRole("option", { name: "mark as unread" });
await room.hover();
await room.getByRole("button", { name: "More Options" }).click();
await room.click({ button: "right" });
await page.getByRole("menuitem", { name: "mark as unread" }).click();
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
// focus the header to avoid to have hover decoration
await page.getByTestId("room-list-header").click();
await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png");
});

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

@@ -0,0 +1,30 @@
/*
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.
*/
import { expect, test } from "../../element-web-test";
import { logIntoElement } from "../crypto/utils.ts";
test.describe(`With force_verification: true`, () => {
test.use({
config: {
force_verification: true,
},
});
test("Can reload after login", async ({ page, credentials }) => {
// The page should reload fine when going to the base client URL
// Regression test for https://github.com/element-hq/element-web/issues/31203
await logIntoElement(page, credentials);
// We should auto-upload the E2EE keys, and show a welcome page
await expect(page.getByRole("heading", { name: `Welcome ${credentials.displayName}` })).toBeVisible();
await page.goto("/");
await expect(page.getByRole("heading", { name: `Welcome ${credentials.displayName}` })).toBeVisible();
});
});

View File

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

View File

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

View File

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

View File

@@ -47,12 +47,11 @@ test.describe("Mark as Unread", () => {
await page.goto("/#/room/" + dummyRoomId);
const roomTile = page.getByLabel(TEST_ROOM_NAME);
await roomTile.focus();
await roomTile.getByRole("button", { name: "More Options" }).click();
await roomTile.click({ button: "right" });
await page.getByRole("menuitem", { name: "Mark as unread" }).click();
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
// focus another room to make the notification decoration appear (room options are display on hover)
await page.getByRole("option", { name: "Open room Room of no consequence" }).click();
await expect(roomTile.getByTestId("notification-decoration")).toBeVisible();
});

View File

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

View File

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

View File

@@ -1,15 +1,25 @@
/*
Copyright 2025 New Vector Ltd.
Copyright (C) 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.
*/
import { readFile } from "node:fs/promises";
import { type Page } from "playwright-core";
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";
// Load a copy of our fake Element Call app, and the latest widget API.
// The fake call app does *just* enough to convince Element Web that a call is ongoing
// and functions like PiP work. It does not actually do anything though, to limit the
// surface we test.
const widgetApi = readFile("node_modules/matrix-widget-api/dist/api.min.js", "utf-8");
const fakeCallClient = readFile("playwright/sample-files/fake-element-call.html", "utf-8");
function assertCommonCallParameters(
url: URLSearchParams,
@@ -27,27 +37,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 +75,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,
});
@@ -87,11 +99,13 @@ test.describe("Element Call", () => {
});
test.beforeEach(async ({ page, user, app }) => {
// Mock a widget page. It doesn't need to actually be Element Call.
await page.route("/widget.html", async (route) => {
// Mock a widget page. We use a fake version of Element Call here.
// We should match on things after .html as these widgets get a ton of extra params.
await page.route(/\/widget.html.+/, async (route) => {
await route.fulfill({
status: 200,
body: "<p> Hello world </p>",
// Do enough to
body: (await fakeCallClient).replace("widgetCodeHere", await widgetApi),
});
});
await app.settings.setValue(
@@ -103,15 +117,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 +146,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 +174,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 +183,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 +194,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 +227,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 +307,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 +315,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 +330,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 +365,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 +410,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();
@@ -336,4 +431,147 @@ test.describe("Element Call", () => {
expect(hash.get("returnToLobby")).toEqual("true");
});
});
test.describe("Switching rooms", () => {
let charlie: Bot;
test.use({
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 app.client.createRoom({
name: "OtherRoom",
});
await use({ roomId });
},
});
async function openAndJoinCall(page: Page, existing = false) {
if (existing) {
await page.getByTestId("join-call-button").click();
} else {
await page.getByRole("button", { name: "Video call" }).click();
await page.getByRole("menuitem", { name: "Element Call" }).click();
}
const iframe = page.locator("iframe");
await expect(iframe).toBeVisible();
const frameUrlStr = await page.locator("iframe").getAttribute("src");
const callFrame = page.frame({ url: frameUrlStr });
await callFrame.getByRole("button", { name: "Join Call" }).click();
await expect(callFrame.getByText("In call", { exact: true })).toBeVisible();
// Wait for Element Web to pickup the RTC session and update the room list entry.
await expect(await page.getByTestId("notification-decoration")).toBeVisible();
}
test("should be able to switch rooms and have the call persist", async ({ page, user, room, app }) => {
await app.viewRoomById(room.roomId);
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
await openAndJoinCall(page);
await app.viewRoomByName("OtherRoom");
// We should have a PiP container here.
await expect(page.locator(".mx_AppTile_persistedWrapper")).toBeVisible();
});
test("should be able to start a call, close it via PiP, and start again in the same room", async ({
page,
user,
room,
app,
}) => {
await app.viewRoomById(room.roomId);
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
await openAndJoinCall(page);
await app.viewRoomByName("OtherRoom");
const pipContainer = page.locator(".mx_WidgetPip");
// We should have a PiP container here.
await expect(pipContainer).toBeVisible();
// Leave the call.
const overlay = page.locator(".mx_WidgetPip_overlay");
await overlay.hover({ timeout: 2000 }); // Show the call footer.
await overlay.getByRole("button", { name: "Leave", exact: true }).click();
// PiP container goes.
await expect(pipContainer).not.toBeVisible();
// Wait for call to stop.
await expect(await page.getByTestId("notification-decoration")).not.toBeVisible();
await app.viewRoomById(room.roomId);
await expect(await page.getByTestId("join-call-button")).not.toBeVisible();
// Join the call again.
await openAndJoinCall(page);
});
test("should be able to start a call, close it via PiP, and start again in a different room", async ({
page,
user,
room,
app,
}) => {
await app.viewRoomById(room.roomId);
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
await openAndJoinCall(page);
await app.viewRoomByName("OtherRoom");
const pipContainer = page.locator(".mx_WidgetPip");
// We should have a PiP container here.
await expect(pipContainer).toBeVisible();
// Leave the call.
const overlay = page.locator(".mx_WidgetPip_overlay");
await overlay.hover({ timeout: 2000 }); // Show the call footer.
await overlay.getByRole("button", { name: "Leave", exact: true }).click();
// PiP container goes.
await expect(pipContainer).not.toBeVisible();
// Wait for call to stop.
await expect(await page.getByTestId("notification-decoration")).not.toBeVisible();
await expect(await page.getByTestId("join-call-button")).not.toBeVisible();
// Join the call again, but from the other room.
await openAndJoinCall(page);
});
// For https://github.com/element-hq/element-web/issues/30838
test.fail(
"should be able to join a call, leave via PiP, and rejoin the call",
async ({ page, user, room, app, bot }) => {
await app.viewRoomById(room.roomId);
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
await sendRTCState(bot, room.roomId);
await openAndJoinCall(page, true);
await app.viewRoomByName("OtherRoom");
const pipContainer = page.locator(".mx_WidgetPip");
// We should have a PiP container here.
await expect(pipContainer).toBeVisible();
// Leave the call.
const overlay = page.locator(".mx_WidgetPip_overlay");
await overlay.hover({ timeout: 2000 }); // Show the call footer.
await overlay.getByRole("button", { name: "Leave", exact: true }).click();
// PiP container goes.
await expect(pipContainer).not.toBeVisible();
// Rejoin the call
await app.viewRoomById(room.roomId);
await openAndJoinCall(page, true);
},
);
});
});

View File

@@ -133,6 +133,7 @@ export const expect = baseExpect.extend<Expectations>({
}
.mx_BaseAvatar {
background-color: var(--cpd-color-fuchsia-1200) !important;
border-color: var(--cpd-color-fuchsia-1200) !important;
color: white !important;
}
.mx_ReplyChain {
@@ -144,7 +145,7 @@ export const expect = baseExpect.extend<Expectations>({
}
/* Use monospace font for timestamp for consistent mask width */
.mx_MessageTimestamp {
font-family: Inconsolata !important;
font-family: "Fira Code" !important;
}
`;

View File

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

View File

@@ -0,0 +1,87 @@
<!doctype html>
<style>
body {
background: rgb(139, 192, 253);
}
</style>
<!-- element-call.spec.ts will insert the widget API in this block -->
<script>
widgetCodeHere;
</script>
<div>
<p>Fake Element Call</p>
<p>State: <span id="state">Loading</span></p>
<button id="join-button">Join Call</button>
<button id="close-button">Close</button>
</div>
<!-- Minimal fake implementation of Element Call. Just enough for testing persistent widgets.-->
<script>
const content = {
"application": "m.call",
"call_id": "",
"device_id": "gycSobuY0z",
"expires": 14400000,
"foci_preferred": [
{
livekit_alias: "any-alias",
livekit_service_url: "https://example.org",
type: "livekit",
},
],
"focus_active": {
focus_selection: "oldest_membership",
type: "livekit",
},
"m.call.intent": "video",
"scope": "m.room",
};
const stateIndicator = document.querySelector("#state");
const { WidgetApi, WidgetApiToWidgetAction, MatrixCapabilities } = mxwidgets();
const widgetId = new URLSearchParams(window.location.search).get("widgetId");
const params = new URLSearchParams(window.location.hash.slice(1));
const userId = params.get("userId");
const deviceId = params.get("deviceId");
const roomId = params.get("roomId");
const api = new WidgetApi(widgetId, "*");
const stateKey = `_${userId}_${deviceId}_m.call`;
async function hangup() {
await api.sendStateEvent("org.matrix.msc3401.call.member", stateKey, {}, roomId);
await api.setAlwaysOnScreen(false);
await api.transport.send("io.element.close", {});
stateIndicator.innerHTML = "Ended";
}
document.querySelector("#join-button").onclick = async () => {
await api.setAlwaysOnScreen(true);
await api.transport.send("io.element.join", {});
await api.sendStateEvent("org.matrix.msc3401.call.member", stateKey, content, roomId);
stateIndicator.innerHTML = "In call";
};
document.querySelector("#close-button").onclick = () => {
hangup();
};
api.requestCapability(MatrixCapabilities.AlwaysOnScreen);
api.requestCapability(`org.matrix.msc2762.timeline:${roomId}`);
api.requestCapabilityToSendState("org.matrix.msc3401.call.member", stateKey);
api.on("ready", (ev) => {
stateIndicator.innerHTML = "Ready";
// Pretend to join a call.
});
api.on("action:im.vector.hangup", async () => {
await hangup();
});
// Start the messaging
api.start();
// If waitForIframeLoad is false, tell the client that we're good to go
api.sendContentLoaded();
</script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

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