Compare commits

..

51 Commits

Author SHA1 Message Date
RiotRobot
1fdd313ae9 v1.11.97 2025-04-08 12:43:24 +00:00
RiotRobot
18cd641cf6 Upgrade dependency to matrix-js-sdk@37.3.0 2025-04-08 12:39:32 +00:00
RiotRobot
286231aa37 v1.11.97-rc.0 2025-04-01 12:41:59 +00:00
RiotRobot
3f20df5e08 Upgrade dependency to matrix-js-sdk@37.3.0-rc.0 2025-04-01 12:30:05 +00:00
R Midhun Suresh
d5e070b300 Increase overscan count (#29392) 2025-04-01 09:21:15 +00:00
Florian Duros
d8ecb6362a New room list: reduce padding between avatar and room list border (#29634)
* feat(room list): reduce padding between avatar and room list border

* test(e2e): update screenshots
2025-03-31 20:33:07 +00:00
Michael Telatynski
bcc4ecf0cb Ensure clicks on spoilers do not get handled by the hidden content (#29618)
* Ensure clicks on spoilers do not get handled by the hidden content

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-03-31 13:37:25 +00:00
Florian Duros
24d9a174d7 fix(room list): add cursor pointer on room list item (#29627) 2025-03-31 13:15:56 +00:00
Michael Telatynski
7970b968c2 Prepare for React 19 upgrade (#29612)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-31 11:02:27 +00:00
Michael Telatynski
59e591c462 Fix missing ambiguous url tooltips on Element Desktop (#29619)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-31 11:02:13 +00:00
Florian Duros
804cb62698 test: fix BST/UTC time in preferences (#29628) 2025-03-31 10:54:51 +00:00
Michael Telatynski
8bb4d44532 Bundle Element Call with Element Web packages (#29309)
* Embed Element Call into Element Web packages

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

* Iterate

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

* Iterate

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

* Pass rageshakeSubmitUrl & posthogApiHost to EC widget

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

* Iterate

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

* Improve coverage

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

* Iterate

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

* Iterate

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

* Improve coverage

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

* Update snapshots

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

* Use @vector-im/element-call-embedded

* Only pass posthog params to EC if Analytics is enabled

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

* Fix test mock

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

* Update EC params to match https://github.com/element-hq/element-call/pull/3089

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

* Update to latest element-call package

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

* yarn.lock

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

* Iterate

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

* Update to element-call-embedded@ v0.9.0-rc.1

* Gate Sentry params behind analytics consent

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

* Update to element-call-embedded v0.9.0-rc.4

* Update Element Call embedded to 0.9.0 release

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Hugh Nimmo-Smith <hughns@element.io>
2025-03-28 20:34:32 +00:00
Michael Telatynski
209ab59978 Replace onHeightChanged with ResizeObserver (#29602)
* Replace onHeightChanged with ResizeObserver

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-03-28 10:36:10 +00:00
Florian Duros
6ae11dab52 New room list: fix spacing and padding (#29607)
* fix(menu): reduce gap between button and button size

* fix(notification decoration): increase gap between icons

* fix(room list item): different right padding depending on the menu, notification decoration and the regular case

* test: update snapshots

* test(e2e): update snapshots
2025-03-28 10:21:31 +00:00
Michael Telatynski
fac982811c Update usages of refs for React 19 compatibility (#29536)
* Update usages of refs for React 19 compatibility

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

* Iterate

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

* Iterate

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

* Simplify

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-28 10:07:41 +00:00
Will Hunt
d7730f417b Hide an event notification if it is redacted (#29605)
* Hide notifications from events that have been redacted.

* lint

* add a void

* Remove ?.
2025-03-28 08:46:43 +00:00
ElementRobot
829b588dbf [create-pull-request] automated change (#29610)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-03-28 06:28:09 +00:00
ElementRobot
9a7cc7eb34 [create-pull-request] automated change (#29611)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-03-28 06:21:36 +00:00
Andrew Ferrazzutti
e537da4251 Docker: Use nginx-unprivileged as base image (#29353)
Instead of manually tweaking directory ownership & pidfile config to
enable running as non-root, use the official first-party base image for
achieving non-root.

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-27 20:43:13 +00:00
David Baker
094a7071e2 Make fetchdep check out matching branch name (#29601)
* Make fetchdep check out matching branch name

on a push to a branch.

* Remove buildkite support entirely
2025-03-27 13:54:05 +00:00
Florian Duros
a5673f603f e2e test: use encryption tab in enableKeyBackup instead of security & settings tab (#29234)
* test(e2e crypto): use encryption tab in `enableKeyBackup` instead of security & settings tab

* test(e2e crypto): verify device before trying to enable key backup

* doc: improve `enableKeyBackup` documentation
2025-03-27 11:35:54 +00:00
dependabot[bot]
0c210b9b3a Bump axios from 1.8.1 to 1.8.4 (#29590)
Bumps [axios](https://github.com/axios/axios) from 1.8.1 to 1.8.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.8.1...v1.8.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-27 10:46:04 +00:00
Michael Telatynski
05df321f34 Import JSX explicitly for React 19 compatibility (#29535)
* Import JSX explicitly for React 19 compatibility

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

* Fixup

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

* Fixup

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-27 10:43:58 +00:00
ElementRobot
8116dc5f60 [create-pull-request] automated change (#29597)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-03-27 10:20:28 +00:00
Florian Duros
d090499329 test(e2e room list): fix flaky test of activity decoration (#29600) 2025-03-27 09:23:06 +00:00
Florian Duros
6784d071a6 test(e2e dehydrated device): use Encryption tab instead of Security & Privacy tab (#29593) 2025-03-26 22:53:16 +00:00
Michael Telatynski
3f47487472 Switch away from nesting React trees and mangling the DOM (#29586)
* Switch away from nesting React trees and mangling the DOM

By parsing HTML events and manipulating the AST before passing it to React

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

* Use MatrixClientContext in Pill now that we are in the main React tree

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

* Add missing import

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

* Break import cycles

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

* Iterate

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

* Iterate

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

* Iterate

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

* Minimise

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

* Iterate

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

* Iterate

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

* Docs

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-26 20:25:03 +00:00
Michael Telatynski
89e22e00fb Fix MFileBody fileName not considering filename (#29589)
* Fix MFileBody fileName not considering `filename`

* Update MFileBody.tsx
2025-03-26 19:00:22 +00:00
Florian Duros
bbd798ef36 New room list: add notification decoration (#29552)
* chore: update @compound-web

* feat(notification decoration): add NotificationDecoration component

* feat(room list item): get notification state in view model

* feat(room list item): use notification decoration in RoomListItemView

* test(notification decoration): add tests

* test(room list item view model): add a11yLabel tests

* test(room list item): update tests

* test(e2e): add decoration tests
2025-03-26 13:32:02 +00:00
ElementRobot
f3f05874fa Localazy Download, including changes to 'only send to verified' (#29592)
* [create-pull-request] automated change

* Update tests for copy changes on 'only send to verified'

* Update one more test snapshot for new wording of exclude unverified

* Update screenshots

---------

Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
2025-03-26 13:00:17 +00:00
ElementRobot
d9091bcba9 [create-pull-request] automated change (#29591)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-03-26 06:16:24 +00:00
RiotRobot
68692c5af5 Reset matrix-js-sdk back to develop branch 2025-03-25 14:54:43 +00:00
RiotRobot
03dc093e89 Merge branch 'master' into develop 2025-03-25 14:54:30 +00:00
RiotRobot
c68157ec46 v1.11.96 2025-03-25 14:51:30 +00:00
RiotRobot
4fc8b8915b Upgrade dependency to matrix-js-sdk@37.2.0 2025-03-25 14:44:39 +00:00
R Midhun Suresh
690d623dcf New Room List: Move tests that are in the wrong location (#29584)
* Move test to room-list-filter-sort.spec.ts

This test should reside with all the other filter related tests.

* Move sorting tests out of filter describe block

These two tests somehow ended up in the wrong block!

* Fix lint
2025-03-25 12:18:58 +00:00
Michael Telatynski
102a1ddb9e Fix token expiry racing with login causing wrong error to be shown (#29566)
* Fix token expiry racing with login causing wrong error to be shown

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

* Add tests

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

* yay jest

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

* Iterate

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

* Iterate

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-25 12:07:07 +00:00
Michael Telatynski
99ea51c6f2 Update mxLoginWithAccessToken to grab deviceId from /whoami (#29571)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-25 10:32:27 +00:00
R Midhun Suresh
3f1e56b715 RoomListStore: Unread filter should match rooms that were marked as unread (#29580)
* Unread filter should match rooms marked as unread

* Re-insert room into skip list on account data

So that filters are re-calculated when rooms are marked as unread.

* Write test
2025-03-24 16:51:42 +00:00
Richard van der Hoff
f3653abe92 Fix bug which caused startup to hang if the clock was wound back since a previous session (#29558)
* SessionLock: reduce the stale time

30 seconds staring at a spinner while we wait for a stale lock to expire is
rather painful. Pretty sure 15 seconds will be fine.

* SessionLock: deal with the clock having been wound back

If a previous session terminated uncleanly, and then the clock is wound back,
we could be waiting a very long time for the previous session's claim to
expire.

We can fix this by simply treating a future claim the same as "now", and
waiting for the normal stale timeout.

* fixup! SessionLock: deal with the clock having been wound back
2025-03-24 16:43:53 +00:00
Florian Duros
a6e8d512d0 Room notification state: add clearer methods, documentation and deprecation (#29564)
* feat(notification state): add clearer methods, documentation and deprecation

* test(room notification state): add tests for new attributes

* doc: more explicit documentation for `hasUnreadCount`

* doc: add link to `RoomNotificationState.isMention` in `hasMentions` doc

* refactor: change `isSilent` to `hasAnyNotificationOrActivity`

* refactor: add `invited` to `determineUnreadState` and use it in `NotificationState` & `RoomNotificationState`

* test: update `RoomNotificationState` test to use `invited`

* test: update other tests to add `invited`

* refactor: remove count check in `isNotification`
2025-03-24 16:00:47 +00:00
Will Hunt
13c4ab2cf4 Add support for hiding videos (#29496)
* start hide

* Move useSettingsValueWithSetter to useSettings

* Add new setting showMediaEventIds

* Add a migration path

* Add an action button to hide settings.

* Tweaks to MImageBody to support new setting.

* Fixup and add tests

* add description for migration

* docs fixes

* add type

* i18n

* appese prettier

* Add tests for HideActionButton

* lint

* lint

* First pass at support for previewing/hiding images.

* Add a test for video files.

* First pass at supporting hiding video files.

* Use a hook for media visibility.

* Drop setting hook usage.

* Fixup MImageBody test

* Fixup tests

* Support functional components for message body rendering.

* Add a comment

* Move props into IProps

* Use new wrapping logic

* lint

* fixup

* allow for a delay for the image to render

* remove .only

* lint

* Fix jest test

* Fixup tests.

* make tests happy

* Improve comments

* review fixes

* unbreak test
2025-03-24 14:38:34 +00:00
Robin
74da64db63 Use an outline icon for the report room button (#29573)
For visual consistency
2025-03-24 14:03:34 +00:00
ElementRobot
e5d37a324d Localazy Download (#29577)
* [create-pull-request] automated change

* Fix playwright tests

* Update tests to reflect string changes

---------

Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
Co-authored-by: R Midhun Suresh <hi@midhun.dev>
Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
2025-03-24 12:02:20 +00:00
Andy Balaam
d0c1610bd2 Use 'Dismiss' to close pinned identity changes, instead of 'Ok' (#29569) 2025-03-24 09:20:26 +00:00
ElementRobot
64e2a843c3 [create-pull-request] automated change (#29574)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-03-22 06:15:38 +00:00
Julien CLEMENT
fba59381a0 Generate/load pickle key on SSO (#29568)
* Generate/load pickle key when logged in with SSO

* add comments

* Refactor pickle key loading/creation

* Coding style fixes and fix racy loadOrCreatePickleKey

* fix outdated documentation comment

* fix prettier

Signed-off-by: Julien CLEMENT <julien.clement@epita.fr>

---------

Signed-off-by: Julien CLEMENT <julien.clement@epita.fr>
2025-03-21 19:10:34 +00:00
Will Hunt
e1970df704 Add report room dialog button/dialog. (#29513)
* Add report room dialog button/dialog.

* Update copy

* fixup tests / lint

* Fix title in test.

* update snapshot

* Add unit tests for dialog

* lint
2025-03-21 17:08:37 +00:00
R Midhun Suresh
b54122884c RoomListViewModel: Make the active room sticky in the list (#29551)
* Add new hook for sticky room

This hook takes the filtered, sorted rooms and returns a new list of
rooms such that the active room is kept in the same index even when the
list has changes.

* Use new hook in view model

* Add tests

* Use single * in comments
2025-03-21 12:11:59 +00:00
RiotRobot
b5f8f2b9f5 v1.11.96-rc.0 2025-03-18 13:28:56 +00:00
RiotRobot
425adb147a Upgrade dependency to matrix-js-sdk@37.2.0-rc.0 2025-03-18 13:26:07 +00:00
436 changed files with 5100 additions and 2301 deletions

View File

@@ -51,6 +51,7 @@ jobs:
error|invalid_json
error|misconfigured
welcome_to_element
devtools|settings|elementCallUrl
rethemendex_lint:
name: "Rethemendex Check"

View File

@@ -1,3 +1,81 @@
Changes in [1.11.97](https://github.com/element-hq/element-web/releases/tag/v1.11.97) (2025-04-08)
==================================================================================================
## ✨ Features
* New room list: reduce padding between avatar and room list border ([#29634](https://github.com/element-hq/element-web/pull/29634)). Contributed by @florianduros.
* Bundle Element Call with Element Web packages ([#29309](https://github.com/element-hq/element-web/pull/29309)). Contributed by @t3chguy.
* Hide an event notification if it is redacted ([#29605](https://github.com/element-hq/element-web/pull/29605)). Contributed by @Half-Shot.
* Docker: Use nginx-unprivileged as base image ([#29353](https://github.com/element-hq/element-web/pull/29353)). Contributed by @AndrewFerr.
* Switch away from nesting React trees and mangling the DOM ([#29586](https://github.com/element-hq/element-web/pull/29586)). Contributed by @t3chguy.
* New room list: add notification decoration ([#29552](https://github.com/element-hq/element-web/pull/29552)). Contributed by @florianduros.
* RoomListStore: Unread filter should match rooms that were marked as unread ([#29580](https://github.com/element-hq/element-web/pull/29580)). Contributed by @MidhunSureshR.
* Add support for hiding videos ([#29496](https://github.com/element-hq/element-web/pull/29496)). Contributed by @Half-Shot.
* Use an outline icon for the report room button ([#29573](https://github.com/element-hq/element-web/pull/29573)). Contributed by @robintown.
* Generate/load pickle key on SSO ([#29568](https://github.com/element-hq/element-web/pull/29568)). Contributed by @Jujure.
* Add report room dialog button/dialog. ([#29513](https://github.com/element-hq/element-web/pull/29513)). Contributed by @Half-Shot.
* RoomListViewModel: Make the active room sticky in the list ([#29551](https://github.com/element-hq/element-web/pull/29551)). Contributed by @MidhunSureshR.
* Replace checkboxes with Compound checkboxes, and appropriately label each checkbox. ([#29363](https://github.com/element-hq/element-web/pull/29363)). Contributed by @Half-Shot.
* New room list: add selection decoration ([#29531](https://github.com/element-hq/element-web/pull/29531)). Contributed by @florianduros.
* Simplified Sliding Sync ([#28515](https://github.com/element-hq/element-web/pull/28515)). Contributed by @dbkr.
* Add ability to hide images after clicking "show image" ([#29467](https://github.com/element-hq/element-web/pull/29467)). Contributed by @Half-Shot.
## 🐛 Bug Fixes
* Fix scroll issues in memberlist ([#29392](https://github.com/element-hq/element-web/pull/29392)). Contributed by @MidhunSureshR.
* Ensure clicks on spoilers do not get handled by the hidden content ([#29618](https://github.com/element-hq/element-web/pull/29618)). Contributed by @t3chguy.
* New room list: add cursor pointer on room list item ([#29627](https://github.com/element-hq/element-web/pull/29627)). Contributed by @florianduros.
* Fix missing ambiguous url tooltips on Element Desktop ([#29619](https://github.com/element-hq/element-web/pull/29619)). Contributed by @t3chguy.
* New room list: fix spacing and padding ([#29607](https://github.com/element-hq/element-web/pull/29607)). Contributed by @florianduros.
* Make fetchdep check out matching branch name ([#29601](https://github.com/element-hq/element-web/pull/29601)). Contributed by @dbkr.
* Fix MFileBody fileName not considering `filename` ([#29589](https://github.com/element-hq/element-web/pull/29589)). Contributed by @t3chguy.
* Fix token expiry racing with login causing wrong error to be shown ([#29566](https://github.com/element-hq/element-web/pull/29566)). Contributed by @t3chguy.
* Fix bug which caused startup to hang if the clock was wound back since a previous session ([#29558](https://github.com/element-hq/element-web/pull/29558)). Contributed by @richvdh.
* RoomListViewModel: Reset any primary filter on secondary filter change ([#29562](https://github.com/element-hq/element-web/pull/29562)). Contributed by @MidhunSureshR.
* RoomListStore: Unread filter should only filter rooms having unread counts ([#29555](https://github.com/element-hq/element-web/pull/29555)). Contributed by @MidhunSureshR.
* In force-verify mode, prevent bypassing by cancelling device verification ([#29487](https://github.com/element-hq/element-web/pull/29487)). Contributed by @andybalaam.
* Add title attribute to user identifier ([#29547](https://github.com/element-hq/element-web/pull/29547)). Contributed by @arpitbatra123.
Changes in [1.11.96](https://github.com/element-hq/element-web/releases/tag/v1.11.96) (2025-03-25)
==================================================================================================
## ✨ Features
* RoomListViewModel: Track the index of the active room in the list ([#29519](https://github.com/element-hq/element-web/pull/29519)). Contributed by @MidhunSureshR.
* New room list: add empty state ([#29512](https://github.com/element-hq/element-web/pull/29512)). Contributed by @florianduros.
* Implement `MessagePreviewViewModel` ([#29514](https://github.com/element-hq/element-web/pull/29514)). Contributed by @MidhunSureshR.
* RoomListViewModel: Add functionality to toggle message preview setting ([#29511](https://github.com/element-hq/element-web/pull/29511)). Contributed by @MidhunSureshR.
* New room list: add more options menu on room list item ([#29445](https://github.com/element-hq/element-web/pull/29445)). Contributed by @florianduros.
* RoomListViewModel: Provide a way to resort the room list and track the active sort method ([#29499](https://github.com/element-hq/element-web/pull/29499)). Contributed by @MidhunSureshR.
* Change \*All rooms\* meta space name to \*All Chats\* ([#29498](https://github.com/element-hq/element-web/pull/29498)). Contributed by @florianduros.
* Add setting to hide avatars of rooms you have been invited to. ([#29497](https://github.com/element-hq/element-web/pull/29497)). Contributed by @Half-Shot.
* Room List Store: Save preferred sorting algorithm and use that on app launch ([#29493](https://github.com/element-hq/element-web/pull/29493)). Contributed by @MidhunSureshR.
* Add key storage toggle to Encryption settings ([#29310](https://github.com/element-hq/element-web/pull/29310)). Contributed by @dbkr.
* New room list: add primary filters ([#29481](https://github.com/element-hq/element-web/pull/29481)). Contributed by @florianduros.
* Implement MSC4142: Remove unintentional intentional mentions in replies ([#28209](https://github.com/element-hq/element-web/pull/28209)). Contributed by @tulir.
* White background for 'They do not match' button ([#29470](https://github.com/element-hq/element-web/pull/29470)). Contributed by @andybalaam.
* RoomListViewModel: Support secondary filters in the view model ([#29465](https://github.com/element-hq/element-web/pull/29465)). Contributed by @MidhunSureshR.
* RoomListViewModel: Support primary filters in the view model ([#29454](https://github.com/element-hq/element-web/pull/29454)). Contributed by @MidhunSureshR.
* Room List Store: Implement secondary filters ([#29458](https://github.com/element-hq/element-web/pull/29458)). Contributed by @MidhunSureshR.
* Room List Store: Implement rest of the primary filters ([#29444](https://github.com/element-hq/element-web/pull/29444)). Contributed by @MidhunSureshR.
* Room List Store: Support filters by implementing just the favourite filter ([#29433](https://github.com/element-hq/element-web/pull/29433)). Contributed by @MidhunSureshR.
* Move toggle switch for integration manager for a11y ([#29436](https://github.com/element-hq/element-web/pull/29436)). Contributed by @Half-Shot.
* New room list: basic flat list ([#29368](https://github.com/element-hq/element-web/pull/29368)). Contributed by @florianduros.
* Improve rageshake upload experience by providing useful error information ([#29378](https://github.com/element-hq/element-web/pull/29378)). Contributed by @Half-Shot.
* Add more functionality to the room list vm ([#29402](https://github.com/element-hq/element-web/pull/29402)). Contributed by @MidhunSureshR.
## 🐛 Bug Fixes
* New room list: fix compose menu action in space ([#29500](https://github.com/element-hq/element-web/pull/29500)). Contributed by @florianduros.
* Change ToggleHiddenEventVisibility \& GoToHome KeyBindingActions ([#29374](https://github.com/element-hq/element-web/pull/29374)). Contributed by @gy-mate.
* Fix Docker Healthcheck ([#29471](https://github.com/element-hq/element-web/pull/29471)). Contributed by @benbz.
* Room List Store: Fetch rooms after space store is ready + attach store to window ([#29453](https://github.com/element-hq/element-web/pull/29453)). Contributed by @MidhunSureshR.
* Room List Store: Fix bug where left rooms appear in room list ([#29452](https://github.com/element-hq/element-web/pull/29452)). Contributed by @MidhunSureshR.
* Add space to the bottom of the room summary actions below leave room ([#29270](https://github.com/element-hq/element-web/pull/29270)). Contributed by @langleyd.
* Show error screens in group calls ([#29254](https://github.com/element-hq/element-web/pull/29254)). Contributed by @robintown.
* Prevent user from accidentally triggering multiple identity resets ([#29388](https://github.com/element-hq/element-web/pull/29388)). Contributed by @uhoreg.
* Remove buggy tooltip on room intro \& homepage ([#29406](https://github.com/element-hq/element-web/pull/29406)). Contributed by @t3chguy.
Changes in [1.11.95](https://github.com/element-hq/element-web/releases/tag/v1.11.95) (2025-03-11)
==================================================================================================
## ✨ Features

View File

@@ -19,7 +19,10 @@ RUN /src/scripts/docker-package.sh
RUN cp /src/config.sample.json /src/webapp/config.json
# App
FROM nginx:alpine-slim
FROM nginxinc/nginx-unprivileged:alpine-slim
# Need root user to install packages & manipulate the usr directory
USER root
# Install jq and moreutils for sponge, both used by our entrypoints
RUN apk add jq moreutils
@@ -31,13 +34,6 @@ COPY --from=builder /src/webapp /app
COPY /docker/nginx-templates/* /etc/nginx/templates/
COPY /docker/docker-entrypoint.d/* /docker-entrypoint.d/
# Tell nginx to put its pidfile elsewhere, so it can run as non-root
RUN sed -i -e 's,/var/run/nginx.pid,/tmp/nginx.pid,' /etc/nginx/nginx.conf
# nginx user must own the cache and etc directory to write cache and tweak the nginx config
RUN chown -R nginx:0 /var/cache/nginx /etc/nginx
RUN chmod -R g+w /var/cache/nginx /etc/nginx
RUN rm -rf /usr/share/nginx/html \
&& ln -s /app /usr/share/nginx/html

View File

@@ -384,8 +384,6 @@ The VoIP and Jitsi options are:
5. `audio_stream_url`: Optional URL to pass to Jitsi to enable live streaming. This option is considered experimental and may be removed
at any time without notice.
6. `element_call`: Optional configuration for native group calls using Element Call, with the following subkeys:
- `url`: The URL of the Element Call instance to use for native group calls. This option is considered experimental
and may be removed at any time without notice. Defaults to `https://call.element.io`.
- `use_exclusively`: A boolean specifying whether Element Call should be used exclusively as the only VoIP stack in
the app, removing the ability to start legacy 1:1 calls or Jitsi calls. Defaults to `false`.
- `participant_limit`: The maximum number of users who can join a call; if

View File

@@ -40,6 +40,8 @@ export default {
// Used by webpack
"process",
"util",
// Embedded into webapp
"@element-hq/element-call-embedded",
],
ignoreBinaries: [
// Used in scripts & workflows

View File

@@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.11.95",
"version": "1.11.97",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -65,7 +65,8 @@
"test:playwright:screenshots": "playwright-screenshots --project=Chrome",
"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"
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js",
"postinstall": "patch-package"
},
"resolutions": {
"@playwright/test": "1.51.1",
@@ -92,7 +93,7 @@
"@types/png-chunks-extract": "^1.0.2",
"@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^4.0.0",
"@vector-im/compound-web": "^7.7.2",
"@vector-im/compound-web": "^7.9.0",
"@vector-im/matrix-wysiwyg": "2.38.2",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
@@ -107,6 +108,7 @@
"css-tree": "^3.0.0",
"diff-dom": "^5.0.0",
"diff-match-patch": "^1.0.5",
"domutils": "^3.2.2",
"emojibase-regex": "15.3.2",
"escape-html": "^1.0.3",
"file-saver": "^2.0.5",
@@ -115,12 +117,12 @@
"glob-to-regexp": "^0.4.1",
"highlight.js": "^11.3.1",
"html-entities": "^2.0.0",
"html-react-parser": "^5.2.2",
"is-ip": "^3.1.0",
"js-xxhash": "^4.0.0",
"jsrsasign": "^11.0.0",
"jszip": "^3.7.0",
"katex": "^0.16.0",
"linkify-element": "4.2.0",
"linkify-react": "4.2.0",
"linkify-string": "4.2.0",
"linkifyjs": "4.2.0",
@@ -128,7 +130,7 @@
"maplibre-gl": "^5.0.0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-js-sdk": "37.3.0",
"matrix-widget-api": "^1.10.0",
"memoize-one": "^6.0.0",
"mime": "^4.0.4",
@@ -144,6 +146,7 @@
"react-blurhash": "^0.3.0",
"react-dom": "^18.3.1",
"react-focus-lock": "^2.5.1",
"react-string-replace": "^1.1.1",
"react-transition-group": "^4.4.1",
"react-virtualized": "^9.22.5",
"rfc4648": "^1.4.0",
@@ -177,6 +180,7 @@
"@babel/preset-typescript": "^7.12.7",
"@babel/runtime": "^7.12.5",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@element-hq/element-call-embedded": "0.9.0",
"@element-hq/element-web-playwright-common": "^1.1.5",
"@peculiar/webcrypto": "^1.4.3",
"@playwright/test": "^1.50.1",
@@ -263,6 +267,7 @@
"minimist": "^1.2.6",
"modernizr": "^3.12.0",
"node-fetch": "^2.6.7",
"patch-package": "^8.0.0",
"playwright-core": "^1.51.0",
"postcss": "8.4.46",
"postcss-easings": "^4.0.0",

View File

@@ -0,0 +1,13 @@
diff --git a/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts b/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
index 917a7fc..a2710c6 100644
--- a/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
+++ b/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
@@ -37,7 +37,7 @@ export interface ModuleApi {
* @returns Whether the user submitted the dialog or closed it, and the model returned by the
* dialog component if submitted.
*/
- openDialog<M extends object, P extends DialogProps = DialogProps, C extends DialogContent<P> = DialogContent<P>>(initialTitleOrOptions: string | ModuleUiDialogOptions, body: (props: P, ref: React.RefObject<C>) => React.ReactNode, props?: Omit<P, keyof DialogProps>): Promise<{
+ openDialog<M extends object, P extends DialogProps = DialogProps, C extends DialogContent<P> = DialogContent<P>>(initialTitleOrOptions: string | ModuleUiDialogOptions, body: (props: P, ref: React.RefObject<C | null>) => React.ReactNode, props?: Omit<P, keyof DialogProps>): Promise<{
didOkOrSubmit: boolean;
model: M;
}>;

View File

@@ -0,0 +1,76 @@
diff --git a/node_modules/@types/react/index.d.ts b/node_modules/@types/react/index.d.ts
index 6ea73ef..cb51757 100644
--- a/node_modules/@types/react/index.d.ts
+++ b/node_modules/@types/react/index.d.ts
@@ -151,7 +151,7 @@ declare namespace React {
/**
* The current value of the ref.
*/
- readonly current: T | null;
+ current: T;
}
interface DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES {
@@ -186,7 +186,7 @@ declare namespace React {
* @see {@link RefObject}
*/
- type Ref<T> = RefCallback<T> | RefObject<T> | null;
+ type Ref<T> = RefCallback<T> | RefObject<T | null> | null;
/**
* A legacy implementation of refs where you can pass a string to a ref prop.
*
@@ -300,7 +300,7 @@ declare namespace React {
*
* @see {@link https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom React Docs}
*/
- ref?: LegacyRef<T> | undefined;
+ ref?: LegacyRef<T | null> | undefined;
}
/**
@@ -1234,7 +1234,7 @@ declare namespace React {
*
* @see {@link ForwardRefRenderFunction}
*/
- type ForwardedRef<T> = ((instance: T | null) => void) | MutableRefObject<T | null> | null;
+ type ForwardedRef<T> = ((instance: T | null) => void) | RefObject<T | null> | null;
/**
* The type of the function passed to {@link forwardRef}. This is considered different
@@ -1565,7 +1565,7 @@ declare namespace React {
[propertyName: string]: any;
}
- function createRef<T>(): RefObject<T>;
+ function createRef<T>(): RefObject<T | null>;
/**
* The type of the component returned from {@link forwardRef}.
@@ -1989,7 +1989,7 @@ declare namespace React {
* @version 16.8.0
* @see {@link https://react.dev/reference/react/useRef}
*/
- function useRef<T>(initialValue: T): MutableRefObject<T>;
+ function useRef<T>(initialValue: T): RefObject<T>;
// convenience overload for refs given as a ref prop as they typically start with a null value
/**
* `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
@@ -2004,7 +2004,7 @@ declare namespace React {
* @version 16.8.0
* @see {@link https://react.dev/reference/react/useRef}
*/
- function useRef<T>(initialValue: T | null): RefObject<T>;
+ function useRef<T>(initialValue: T | null): RefObject<T | null>;
// convenience overload for potentially undefined initialValue / call with 0 arguments
// has a default to stop it from defaulting to {} instead
/**
@@ -2017,7 +2017,7 @@ declare namespace React {
* @version 16.8.0
* @see {@link https://react.dev/reference/react/useRef}
*/
- function useRef<T = undefined>(initialValue?: undefined): MutableRefObject<T | undefined>;
+ function useRef<T>(initialValue: T | undefined): RefObject<T | undefined>;
/**
* The signature is identical to `useEffect`, but it fires synchronously after all DOM mutations.
* Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside

View File

@@ -162,6 +162,7 @@ test.describe("Cryptography", function () {
}
test("Can reset cross-signing keys", async ({ page, app, user: aliceCredentials }) => {
await app.client.bootstrapCrossSigning(aliceCredentials);
const secretStorageKey = await enableKeyBackup(app);
// Fetch the current cross-signing keys

View File

@@ -27,16 +27,22 @@ test.use({
test.describe("Dehydration", () => {
test.skip(isDendrite, "does not yet support dehydration v2");
test("'Set up secure backup' creates dehydrated device", async ({ page, user, app }, workerInfo) => {
// Create a backup (which will create SSSS, and dehydrated device)
test("Verify device and reset creates dehydrated device", async ({ page, user, credentials, app }, workerInfo) => {
// Verify the device by resetting the key (which will create SSSS, and dehydrated device)
const securityTab = await app.settings.openUserSettings("Security & Privacy");
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
await completeCreateSecretStorageDialog(page);
await app.closeDialog();
// Verify the device by resetting the key
const settings = await app.settings.openUserSettings("Encryption");
await settings.getByRole("button", { name: "Verify this device" }).click();
await page.getByRole("button", { name: "Proceed with reset" }).click();
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Copy" }).click();
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Done" }).click();
await expectDehydratedDeviceEnabled(app);

View File

@@ -324,7 +324,7 @@ test.describe("Cryptography", function () {
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
await lastE2eIcon.focus();
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
"Sender's verified identity has changed",
"Sender's verified identity was reset",
);
});
});

View File

@@ -52,6 +52,6 @@ test.describe("Invisible cryptography", () => {
/* should show an error for a message from a previously verified device */
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
const lastTile = page.locator(".mx_EventTile_last");
await expect(lastTile).toContainText("Sender's verified identity has changed");
await expect(lastTile).toContainText("Sender's verified identity was reset");
});
});

View File

@@ -292,17 +292,28 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
}
/**
* Open the security settings and enable secure key backup.
*
* Assumes that the current device has been cross-signed (which means that we skip a step where we set it up).
* Open the encryption settings and enable key storage and recovery
* Assumes that the current device has been verified
*
* Returns the recovery key
*/
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
await app.settings.openUserSettings("Security & Privacy");
await app.page.getByRole("button", { name: "Set up Secure Backup" }).click();
const encryptionTab = await app.settings.openUserSettings("Encryption");
return await completeCreateSecretStorageDialog(app.page);
const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
if (!(await keyStorageToggle.isChecked())) {
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
}
await encryptionTab.getByRole("button", { name: "Set up recovery" }).click();
await encryptionTab.getByRole("button", { name: "Continue" }).click();
const recoveryKey = await encryptionTab.getByTestId("recoveryKey").innerText();
await encryptionTab.getByRole("button", { name: "Continue" }).click();
await encryptionTab.getByRole("textbox").fill(recoveryKey);
await encryptionTab.getByRole("button", { name: "Finish set up" }).click();
await app.settings.closeDialog();
return recoveryKey;
}
/**

View File

@@ -36,17 +36,21 @@ test.describe("Room list filters and sort", () => {
return page.getByTestId("room-list");
}
let unReadDmId: string | undefined;
let unReadRoomId: string | undefined;
test.beforeEach(async ({ page, app, bot, user }) => {
await app.client.createRoom({ name: "empty room" });
const unReadDmId = await bot.createRoom({
unReadDmId = await bot.createRoom({
name: "unread dm",
invite: [user.userId],
is_direct: true,
});
await app.client.joinRoom(unReadDmId);
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
const unReadRoomId = await app.client.createRoom({ name: "unread room" });
unReadRoomId = await app.client.createRoom({ name: "unread room" });
await app.client.inviteUser(unReadRoomId, bot.credentials.userId);
await bot.joinRoom(unReadRoomId);
await bot.sendMessage(unReadRoomId, "I am a robot. Beep.");
@@ -88,6 +92,30 @@ test.describe("Room list filters and sort", () => {
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(3);
});
test("unread filter should only match unread rooms that have a count", async ({ page, app, bot }) => {
const roomListView = getRoomList(page);
// Let's configure unread dm room so that we only get notification for mentions and keywords
await app.viewRoomById(unReadDmId);
await app.settings.openRoomSettings("Notifications");
await page.getByText("@mentions & keywords").click();
await app.settings.closeDialog();
// Let's open a room other than unread room or unread dm
await roomListView.getByRole("gridcell", { name: "Open room favourite room" }).click();
// Let's make the bot send a new message in both rooms
await bot.sendMessage(unReadDmId, "Hello!");
await bot.sendMessage(unReadRoomId, "Hello!");
// Let's activate the unread filter now
await page.getByRole("option", { name: "Unread" }).click();
// Unread filter should only show unread room and not unread dm!
await expect(roomListView.getByRole("gridcell", { name: "Open room unread room" })).toBeVisible();
await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible();
});
});
test.describe("Empty room list", () => {

View File

@@ -13,6 +13,9 @@ test.describe("Room list", () => {
test.use({
displayName: "Alice",
labsFlags: ["feature_new_room_list"],
botCreateOpts: {
displayName: "BotBob",
},
});
/**
@@ -26,108 +29,199 @@ test.describe("Room list", () => {
test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
await app.closeNotificationToast();
for (let i = 0; i < 30; i++) {
await app.client.createRoom({ name: `room${i}` });
}
});
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list.png");
await roomListView.hover();
// Scroll to the end of the room list
await page.mouse.wheel(0, 1000);
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
});
test("should open the room when it is clicked", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
});
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomList(page);
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
await roomItem.hover();
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
const roomItemMenu = roomItem.getByRole("button", { name: "More Options" });
await roomItemMenu.click();
await expect(page).toMatchScreenshot("room-list-item-open-more-options.png");
// It should make the room favourited
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
// Check that the room is favourited
await roomItem.hover();
await roomItemMenu.click();
await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked();
// It should show the invite dialog
await page.getByRole("menuitem", { name: "invite" }).click();
await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible();
await app.closeDialog();
// It should leave the room
await roomItem.hover();
await roomItemMenu.click();
await page.getByRole("menuitem", { name: "leave room" }).click();
await expect(roomItem).not.toBeVisible();
});
test("should scroll to the current room", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.hover();
// Scroll to the end of the room list
await page.mouse.wheel(0, 1000);
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
const filters = page.getByRole("listbox", { name: "Room list filters" });
await filters.getByRole("option", { name: "People" }).click();
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();
await filters.getByRole("option", { name: "People" }).click();
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
});
test("unread filter should only match unread rooms that have a count", async ({ page, app, bot }) => {
const roomListView = getRoomList(page);
// Let's create a new room and invite the bot
const room1Id = await app.client.createRoom({
name: "Unread Room 1",
invite: [bot.credentials?.userId],
test.describe("Room list", () => {
test.beforeEach(async ({ page, app, user }) => {
for (let i = 0; i < 30; i++) {
await app.client.createRoom({ name: `room${i}` });
}
});
await bot.awaitRoomMembership(room1Id);
// Let's create another room as well
const room2Id = await app.client.createRoom({
name: "Unread Room 2",
invite: [bot.credentials?.userId],
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list.png");
await roomListView.hover();
// Scroll to the end of the room list
await page.mouse.wheel(0, 1000);
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
});
await bot.awaitRoomMembership(room2Id);
// Let's configure unread room 1 so that we only get notification for mentions and keywords
await app.viewRoomById(room1Id);
await app.settings.openRoomSettings("Notifications");
await page.getByText("@mentions & keywords").click();
await app.settings.closeDialog();
test("should open the room when it is clicked", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
});
// Let's open a room other than room 1 or room 2
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomList(page);
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
await roomItem.hover();
// Let's make the bot send a new message in both room 1 and room 2
await bot.sendMessage(room1Id, "Hello!");
await bot.sendMessage(room2Id, "Hello!");
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
const roomItemMenu = roomItem.getByRole("button", { name: "More Options" });
await roomItemMenu.click();
await expect(page).toMatchScreenshot("room-list-item-open-more-options.png");
// Let's activate the unread filter now
await page.getByRole("option", { name: "Unread" }).click();
// It should make the room favourited
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
// Unread filter should only show room 2!!
await expect(roomListView.getByRole("gridcell", { name: "Open room Unread Room 2" })).toBeVisible();
await expect(roomListView.getByRole("gridcell", { name: "Open room Unread Room 1" })).not.toBeVisible();
// Check that the room is favourited
await roomItem.hover();
await roomItemMenu.click();
await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked();
// It should show the invite dialog
await page.getByRole("menuitem", { name: "invite" }).click();
await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible();
await app.closeDialog();
// It should leave the room
await roomItem.hover();
await roomItemMenu.click();
await page.getByRole("menuitem", { name: "leave room" }).click();
await expect(roomItem).not.toBeVisible();
});
test("should scroll to the current room", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.hover();
// Scroll to the end of the room list
await page.mouse.wheel(0, 1000);
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
const filters = page.getByRole("listbox", { name: "Room list filters" });
await filters.getByRole("option", { name: "People" }).click();
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();
await filters.getByRole("option", { name: "People" }).click();
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
});
});
test.describe("Notification decoration", () => {
test("should render the invitation decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page);
await bot.createRoom({
name: "invited room",
invite: [user.userId],
is_direct: true,
});
const invitedRoom = roomListView.getByRole("gridcell", { name: "invited room" });
await expect(invitedRoom).toBeVisible();
await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png");
});
test("should render the regular decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page);
const roomId = await app.client.createRoom({ name: "2 notifications" });
await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId);
await bot.sendMessage(roomId, "I am a robot. Beep.");
await bot.sendMessage(roomId, "I am a robot. Beep.");
const room = roomListView.getByRole("gridcell", { name: "2 notifications" });
await expect(room).toBeVisible();
await expect(room.getByTestId("notification-decoration")).toHaveText("2");
await expect(room).toMatchScreenshot("room-list-item-notification.png");
});
test("should render the mention decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page);
const roomId = await app.client.createRoom({ name: "mention" });
await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId);
const clientBot = await bot.prepareClient();
await clientBot.evaluate(
async (client, { roomId, userId }) => {
await client.sendMessage(roomId, {
// @ts-ignore ignore usage of MsgType.text
"msgtype": "m.text",
"body": "User",
"format": "org.matrix.custom.html",
"formatted_body": `<a href="https://matrix.to/#/${userId}">User</a>`,
"m.mentions": {
user_ids: [userId],
},
});
},
{ roomId, userId: user.userId },
);
await bot.sendMessage(roomId, "I am a robot. Beep.");
const room = roomListView.getByRole("gridcell", { name: "mention" });
await expect(room).toBeVisible();
await expect(room).toMatchScreenshot("room-list-item-mention.png");
});
test("should render an activity decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page);
const otherRoomId = await app.client.createRoom({ name: "other room" });
const roomId = await app.client.createRoom({ name: "activity" });
await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId);
await app.viewRoomById(roomId);
await app.settings.openRoomSettings("Notifications");
await page.getByText("@mentions & keywords").click();
await app.settings.closeDialog();
await app.settings.openUserSettings("Notifications");
await page.getByText("Show all activity in the room list (dots or number of unread messages)").click();
await app.settings.closeDialog();
// Switch to the other room to avoid the notification to be cleared
await app.viewRoomById(otherRoomId);
await bot.sendMessage(roomId, "I am a robot. Beep.");
const room = roomListView.getByRole("gridcell", { name: "activity" });
await expect(room.getByTestId("notification-decoration")).toBeVisible();
await expect(room).toMatchScreenshot("room-list-item-activity.png");
});
test("should render a mark as unread decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page);
const roomId = await app.client.createRoom({ name: "mark as unread" });
await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId);
const room = roomListView.getByRole("gridcell", { name: "mark as unread" });
await room.hover();
await room.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "mark as unread" }).click();
// Remove hover on the room list item
await roomListView.hover();
await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png");
});
test("should render silent decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page);
const roomId = await app.client.createRoom({ name: "silent" });
await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId);
await app.viewRoomById(roomId);
await app.settings.openRoomSettings("Notifications");
await page.getByText("Off").click();
await app.settings.closeDialog();
const room = roomListView.getByRole("gridcell", { name: "silent" });
await expect(room.getByTestId("notification-decoration")).toBeVisible();
await expect(room).toMatchScreenshot("room-list-item-silent.png");
});
});
});

View File

@@ -73,4 +73,33 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await revokeAccessTokenPromise;
await revokeRefreshTokenPromise;
});
test(
"it should log out the user & wipe data when logging out via MAS",
{ tag: "@screenshot" },
async ({ mas, page, mailpitClient }, testInfo) => {
// We use this over the `user` fixture to ensure we get an OIDC session rather than a compatibility one
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
const userId = `alice_${testInfo.testId}`;
await registerAccountMas(page, mailpitClient, userId, "alice@email.com", "Pa$sW0rD!");
await expect(page.getByText("Welcome")).toBeVisible();
await page.goto("about:blank");
// @ts-expect-error
const result = await mas.manage("kill-sessions", userId);
expect(result.output).toContain("Ended 1 active OAuth 2.0 session");
await page.goto("http://localhost:8080");
await expect(
page.getByText("For security, this session has been signed out. Please sign in again."),
).toBeVisible();
await expect(page).toMatchScreenshot("token-expired.png", { includeDialogBackground: true });
const localStorageKeys = await page.evaluate(() => Object.keys(localStorage));
expect(localStorageKeys).toHaveLength(0);
},
);
});

View File

@@ -1,5 +1,5 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024, 2025 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
@@ -10,6 +10,7 @@ import { type Locator, type Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { checkRoomSummaryCard, viewRoomSummaryByName } from "./utils";
import { isDendrite } from "../../plugins/homeserver/dendrite";
const ROOM_NAME = "Test room";
const ROOM_NAME_LONG =
@@ -133,6 +134,17 @@ test.describe("RightPanel", () => {
await page.getByLabel("Room info").nth(1).click();
await checkRoomSummaryCard(page, ROOM_NAME);
});
test.describe("room reporting", () => {
test.skip(isDendrite, "Dendrite does not implement room reporting");
test("should handle reporting a room", async ({ page, app }) => {
await viewRoomSummaryByName(page, app, ROOM_NAME);
await page.getByRole("menuitem", { name: "Report room" }).click();
const dialog = await page.getByRole("dialog", { name: "Report Room" });
await dialog.getByLabel("reason").fill("This room should be reported");
await dialog.getByRole("button", { name: "Send report" }).click();
await expect(page.getByText("Your report was sent.")).toBeVisible();
});
});
});
test.describe("in spaces", () => {

View File

@@ -28,7 +28,10 @@ test.describe("Preferences user settings tab", () => {
const tab = await app.settings.openUserSettings("Preferences");
// Assert that the top heading is rendered
await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible();
await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png");
await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png", {
// masked due to daylight saving time
mask: [tab.locator("#mx_dropdownUserTimezone_value")],
});
});
test("should be able to change the app language", { tag: ["@no-firefox", "@no-webkit"] }, async ({ uut, user }) => {

View File

@@ -28,6 +28,8 @@ const NEW_AVATAR = fs.readFileSync("playwright/sample-files/element.png");
const OLD_NAME = "Alan";
const NEW_NAME = "Alan (away)";
const VIDEO_FILE = fs.readFileSync("playwright/sample-files/5secvid.webm");
const getEventTilesWithBodies = (page: Page): Locator => {
return page.locator(".mx_EventTile").filter({ has: page.locator(".mx_EventTile_body") });
};
@@ -916,7 +918,27 @@ test.describe("Timeline", () => {
await page.getByRole("button", { name: "Hide" }).click();
// Check that the image is now hidden.
await expect(page.getByRole("link", { name: "Show image" })).toBeVisible();
await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
});
test("should be able to hide a video", async ({ page, app, room, context }) => {
await app.viewRoomById(room.roomId);
const upload = await app.client.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" });
await app.client.sendEvent(room.roomId, null, "m.room.message" as EventType, {
msgtype: "m.video" as MsgType,
body: "bbb.webm",
url: upload.content_uri,
});
await app.timeline.scrollToBottom();
const imgTile = page.locator(".mx_MVideoBody").first();
await expect(imgTile).toBeVisible();
await imgTile.hover();
await page.getByRole("button", { name: "Hide" }).click();
// Check that the video is now hidden.
await expect(page.getByRole("button", { name: "Show video" })).toBeVisible();
await expect(page.locator("video")).not.toBeVisible();
});
});
@@ -1319,4 +1341,44 @@ test.describe("Timeline", () => {
);
});
});
test.describe("spoilers", { tag: "@screenshot" }, () => {
test("clicking a spoiler containing the pill de-spoilers on 1st click, then follows link on 2nd", async ({
page,
user,
app,
room,
}) => {
// View room
await page.goto(`/#/room/${room.roomId}`);
// Send a spoilered pill
await app.client.sendMessage(room.roomId, {
msgtype: "m.text",
body: user.userId,
format: "org.matrix.custom.html",
formatted_body: `<span data-mx-spoiler>https://matrix.to/#/${user.userId}</span>`,
});
const screenshotOptions = {
css: `
.mx_MessageTimestamp {
display: none !important;
}
`,
};
const eventTile = page.locator(".mx_RoomView_body .mx_EventTile_last");
await expect(eventTile).toMatchScreenshot("spoiler.png", screenshotOptions);
const rightPanelButton = page.getByText("Share profile");
const pill = page.locator(".mx_UserPill");
await pill.click({ force: true }); // force to click the spoiler wrapper instead
await expect(eventTile).toMatchScreenshot("spoiler-uncovered.png", screenshotOptions);
await expect(rightPanelButton).not.toBeVisible(); // assert the right panel is not yet open
await pill.click();
await expect(rightPanelButton).toBeVisible(); // assert the right panel is open
});
});
});

View File

@@ -114,7 +114,7 @@ export class ElementAppPage {
* @param isRightPanel whether to select the right panel composer, otherwise the main timeline composer
*/
public getComposerField(isRightPanel?: boolean): Locator {
return this.getComposer(isRightPanel).locator("[contenteditable]");
return this.getComposer(isRightPanel).locator("div[contenteditable]");
}
/**

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

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

View File

@@ -152,6 +152,7 @@
@import "./views/dialogs/_ModalWidgetDialog.pcss";
@import "./views/dialogs/_PollCreateDialog.pcss";
@import "./views/dialogs/_RegistrationEmailPromptDialog.pcss";
@import "./views/dialogs/_ReportRoomDialog.pcss";
@import "./views/dialogs/_RoomSettingsDialog.pcss";
@import "./views/dialogs/_RoomSettingsDialogBridges.pcss";
@import "./views/dialogs/_RoomUpgradeDialog.pcss";
@@ -226,6 +227,7 @@
@import "./views/messages/_DisambiguatedProfile.pcss";
@import "./views/messages/_EventTileBubble.pcss";
@import "./views/messages/_HiddenBody.pcss";
@import "./views/messages/_HiddenMediaPlaceholder.pcss";
@import "./views/messages/_JumpToDatePicker.pcss";
@import "./views/messages/_LegacyCallEvent.pcss";
@import "./views/messages/_MEmoteBody.pcss";

View File

@@ -0,0 +1,16 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
.mx_ReportRoomDialog {
textarea {
font: var(--cpd-font-body-md-regular);
border: 1px solid var(--cpd-color-border-interactive-primary);
background: var(--cpd-color-bg-canvas-default);
border-radius: 0.5rem;
padding: var(--cpd-space-3x) var(--cpd-space-4x);
}
}

View File

@@ -0,0 +1,29 @@
.mx_HiddenMediaPlaceholder {
border: none;
width: 100%;
height: 100%;
inset: 0;
/* To center the text in the middle of the frame */
display: flex;
align-items: center;
justify-content: center;
text-align: center;
cursor: pointer;
background-color: $header-panel-bg-color;
> div {
color: $accent;
/* Icon alignment */
display: flex;
> svg {
margin-top: auto;
margin-bottom: auto;
}
}
}
.mx_EventTile:hover .mx_HiddenMediaPlaceholder {
background-color: $background;
}

View File

@@ -79,39 +79,3 @@ Please see LICENSE files in the repository root for full details.
color: $imagebody-giflabel-color;
pointer-events: none;
}
.mx_HiddenImagePlaceholder {
position: absolute;
inset: 0;
/* To center the text in the middle of the frame */
display: flex;
align-items: center;
justify-content: center;
text-align: center;
cursor: pointer;
background-color: $header-panel-bg-color;
.mx_HiddenImagePlaceholder_button {
color: $accent;
span.mx_HiddenImagePlaceholder_eye {
margin-right: 8px;
background-color: $accent;
mask-image: url("$(res)/img/element-icons/eye.svg");
display: inline-block;
width: 18px;
height: 14px;
}
span:not(.mx_HiddenImagePlaceholder_eye) {
vertical-align: text-bottom;
}
}
}
.mx_EventTile:hover .mx_HiddenImagePlaceholder {
background-color: $background;
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024, 2025 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
@@ -101,6 +101,6 @@ Please see LICENSE files in the repository root for full details.
margin: $spacing-12 0 $spacing-4;
}
.mx_RoomSummaryCard_leave {
.mx_RoomSummaryCard_bottomOptions {
margin: 0 0 var(--cpd-space-8x);
}

View File

@@ -16,18 +16,22 @@
*/
.mx_RoomListItemView {
all: unset;
cursor: pointer;
&:hover {
background-color: var(--cpd-color-bg-action-secondary-hovered);
.mx_RoomListItemView_content {
padding-right: var(--cpd-space-1-5x);
}
}
.mx_RoomListItemView_container {
padding-left: var(--cpd-space-3x);
padding-left: var(--cpd-space-2x);
font: var(--cpd-font-body-md-regular);
height: 100%;
.mx_RoomListItemView_content {
padding-right: var(--cpd-space-3x);
height: 100%;
flex: 1;
/* The border is only under the room name and the future hover menu */
@@ -46,8 +50,24 @@
.mx_RoomListItemView_menu_open {
background-color: var(--cpd-color-bg-action-secondary-hovered);
.mx_RoomListItemView_content {
padding-right: var(--cpd-space-1-5x);
}
}
.mx_RoomListItemView_selected {
background-color: var(--cpd-color-bg-action-secondary-pressed);
}
.mx_RoomListItemView_notification_decoration {
.mx_RoomListItemView_content {
padding-right: var(--cpd-space-2x);
}
}
.mx_RoomListItemView_empty {
.mx_RoomListItemView_content {
padding-right: var(--cpd-space-3x);
}
}

View File

@@ -816,11 +816,13 @@ $left-gutter: 64px;
.mx_EventTile_spoiler_content {
filter: blur(5px) saturate(0.1) sepia(1);
transition-duration: 0.5s;
pointer-events: none;
}
&.visible > .mx_EventTile_spoiler_content {
filter: none;
user-select: auto;
pointer-events: auto;
}
}

View File

@@ -45,10 +45,7 @@ getPRInfo() {
# Some CIs don't give us enough info, so we just get the PR number and ask the
# GH API for more info - "fork:branch". Some give us this directly.
if [ -n "$BUILDKITE_BRANCH" ]; then
# BuildKite
head=$BUILDKITE_BRANCH
elif [ -n "$PR_NUMBER" ]; then
if [ -n "$PR_NUMBER" ]; then
# GitHub
getPRInfo $PR_NUMBER
elif [ -n "$REVIEW_ID" ]; then
@@ -79,11 +76,14 @@ if [[ "$GITHUB_EVENT_NAME" == "merge_group" ]]; then
clone $deforg $defrepo ${withoutPrefix%%/pr-*}
fi
# Try the target branch of the push or PR.
if [ -n "$GITHUB_BASE_REF" ]; then
clone $deforg $defrepo $GITHUB_BASE_REF
elif [ -n "$BUILDKITE_PULL_REQUEST_BASE_BRANCH" ]; then
clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH
# Try the target branch of the push or PR, or the branch that was pushed to
# (ie. the 'master' branch should use matching 'master' dependencies)
base_or_branch=$GITHUB_BASE_REF
if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then
base_or_branch=${GITHUB_REF}
fi
if [ -n "$base_or_branch" ]; then
clone $deforg $defrepo $base_or_branch
fi
# Try HEAD which is the branch name in Netlify (not BRANCH which is pull/xxxx/head for PR builds)

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { type JSXElementConstructor } from "react";
import { type JSX, type JSXElementConstructor } from "react";
export type { NonEmptyArray, XOR, Writeable } from "matrix-js-sdk/src/matrix";

View File

@@ -16,6 +16,7 @@ import {
type SSOAction,
encodeUnpaddedBase64,
type OidcRegistrationClientMetadata,
MatrixEventEvent,
} from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
@@ -228,6 +229,16 @@ export default abstract class BasePlatform {
window.focus();
};
const closeHandler = (): void => notification.close();
// Clear a notification from a redacted event.
if (ev) {
ev.once(MatrixEventEvent.BeforeRedaction, closeHandler);
notification.onclose = () => {
ev.off(MatrixEventEvent.BeforeRedaction, closeHandler);
};
}
return notification;
}

View File

@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type LegacyRef, type ReactNode } from "react";
import React, { type JSX, type LegacyRef, type ReactNode } from "react";
import sanitizeHtml, { type IOptions } from "sanitize-html";
import classNames from "classnames";
import katex from "katex";
@@ -25,7 +25,7 @@ import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils";
import { sanitizeHtmlParams, transformTags } from "./Linkify";
import { graphemeSegmenter } from "./utils/strings";
export { Linkify, linkifyElement, linkifyAndSanitizeHtml } from "./Linkify";
export { Linkify, linkifyAndSanitizeHtml } from "./Linkify";
// Anything outside the basic multilingual plane will be a surrogate pair
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
@@ -365,53 +365,6 @@ function analyseEvent(content: IContent, highlights: Optional<string[]>, opts: E
}
}
export function bodyToDiv(
content: IContent,
highlights: Optional<string[]>,
opts: EventRenderOpts = {},
ref?: React.Ref<HTMLDivElement>,
): ReactNode {
const { strippedBody, formattedBody, emojiBodyElements, className } = bodyToNode(content, highlights, opts);
return formattedBody ? (
<div
key="body"
ref={ref}
className={className}
dangerouslySetInnerHTML={{ __html: formattedBody }}
dir="auto"
/>
) : (
<div key="body" ref={ref} className={className} dir="auto">
{emojiBodyElements || strippedBody}
</div>
);
}
export function bodyToSpan(
content: IContent,
highlights: Optional<string[]>,
opts: EventRenderOpts = {},
ref?: React.Ref<HTMLSpanElement>,
includeDir = true,
): ReactNode {
const { strippedBody, formattedBody, emojiBodyElements, className } = bodyToNode(content, highlights, opts);
return formattedBody ? (
<span
key="body"
ref={ref}
className={className}
dangerouslySetInnerHTML={{ __html: formattedBody }}
dir={includeDir ? "auto" : undefined}
/>
) : (
<span key="body" ref={ref} className={className} dir={includeDir ? "auto" : undefined}>
{emojiBodyElements || strippedBody}
</span>
);
}
interface BodyToNodeReturn {
strippedBody: string;
formattedBody?: string;
@@ -419,7 +372,11 @@ interface BodyToNodeReturn {
className: string;
}
function bodyToNode(content: IContent, highlights: Optional<string[]>, opts: EventRenderOpts = {}): BodyToNodeReturn {
export function bodyToNode(
content: IContent,
highlights: Optional<string[]>,
opts: EventRenderOpts = {},
): BodyToNodeReturn {
const eventInfo = analyseEvent(content, highlights, opts);
let emojiBody = false;

View File

@@ -117,7 +117,6 @@ export interface IConfigOptions {
obey_asserted_identity?: boolean; // MSC3086
};
element_call: {
url?: string;
guest_spa_url?: string;
use_exclusively?: boolean;
participant_limit?: number;

View File

@@ -149,6 +149,7 @@ interface ILoadSessionOpts {
ignoreGuest?: boolean;
defaultDeviceDisplayName?: string;
fragmentQueryParams?: QueryDict;
abortSignal?: AbortSignal;
}
/**
@@ -196,7 +197,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
if (enableGuest && guestHsUrl && fragmentQueryParams.guest_user_id && fragmentQueryParams.guest_access_token) {
logger.log("Using guest access credentials");
return doSetLoggedIn(
await doSetLoggedIn(
{
userId: fragmentQueryParams.guest_user_id as string,
accessToken: fragmentQueryParams.guest_access_token as string,
@@ -206,7 +207,8 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
},
true,
false,
).then(() => true);
);
return true;
}
const success = await restoreSessionFromStorage({
ignoreGuest: Boolean(opts.ignoreGuest),
@@ -225,6 +227,11 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
// fall back to welcome screen
return false;
} catch (e) {
// We may be aborted e.g. because our token expired, so don't show an error here
if (opts.abortSignal?.aborted) {
return false;
}
if (e instanceof AbortLoginAndRebuildStorage) {
// If we're aborting login because of a storage inconsistency, we don't
// need to show the general failure dialog. Instead, just go back to welcome.
@@ -236,7 +243,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
return false;
}
return handleLoadSessionFailure(e);
return handleLoadSessionFailure(e, opts);
}
}
@@ -406,6 +413,39 @@ export function attemptTokenLogin(
});
}
/**
* Load the pickle key inside the credentials or create it if it does not exist for this device.
*
* @param credentials Holds the device to load/store the pickle key
*
* @returns {Promise} promise which resolves to the loaded or generated pickle key or undefined if
* none was loaded nor generated
*/
async function loadOrCreatePickleKey(credentials: IMatrixClientCreds): Promise<string | undefined> {
// Try to load the pickle key
const userId = credentials.userId;
const deviceId = credentials.deviceId;
let pickleKey = (await PlatformPeg.get()?.getPickleKey(userId, deviceId ?? "")) ?? undefined;
if (!pickleKey) {
// Create it if it did not exist
pickleKey =
userId && deviceId
? ((await PlatformPeg.get()?.createPickleKey(userId, deviceId)) ?? undefined)
: undefined;
if (pickleKey) {
logger.log(`Created pickle key for ${credentials.userId}|${credentials.deviceId}`);
} else {
logger.log("Pickle key not created");
}
} else {
logger.log(
`Pickle key already exists for ${credentials.userId}|${credentials.deviceId} do not create a new one`,
);
}
return pickleKey;
}
/**
* Called after a successful token login or OIDC authorization.
* Clear storage then save new credentials in storage
@@ -413,6 +453,8 @@ export function attemptTokenLogin(
*/
async function onSuccessfulDelegatedAuthLogin(credentials: IMatrixClientCreds): Promise<void> {
await clearStorage();
// SSO does not go through setLoggedIn so we need to load/create the pickle key here too
credentials.pickleKey = await loadOrCreatePickleKey(credentials);
await persistCredentials(credentials);
// remember that we just logged in
@@ -621,7 +663,7 @@ export async function restoreSessionFromStorage(opts?: { ignoreGuest?: boolean }
}
}
async function handleLoadSessionFailure(e: unknown): Promise<boolean> {
async function handleLoadSessionFailure(e: unknown, loadSessionOpts?: ILoadSessionOpts): Promise<boolean> {
logger.error("Unable to load session", e);
const modal = Modal.createDialog(SessionRestoreErrorDialog, {
@@ -636,7 +678,7 @@ async function handleLoadSessionFailure(e: unknown): Promise<boolean> {
}
// try, try again
return loadSession();
return loadSession(loadSessionOpts);
}
/**
@@ -655,18 +697,8 @@ async function handleLoadSessionFailure(e: unknown): Promise<boolean> {
export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<MatrixClient> {
credentials.freshLogin = true;
stopMatrixClient();
const pickleKey =
credentials.userId && credentials.deviceId
? await PlatformPeg.get()?.createPickleKey(credentials.userId, credentials.deviceId)
: null;
if (pickleKey) {
logger.log(`Created pickle key for ${credentials.userId}|${credentials.deviceId}`);
} else {
logger.log("Pickle key not created");
}
return doSetLoggedIn({ ...credentials, pickleKey: pickleKey ?? undefined }, true, true);
credentials.pickleKey = await loadOrCreatePickleKey(credentials);
return doSetLoggedIn(credentials, true, true);
}
/**
@@ -1124,12 +1156,13 @@ window.mxLoginWithAccessToken = async (hsUrl: string, accessToken: string): Prom
baseUrl: hsUrl,
accessToken,
});
const { user_id: userId } = await tempClient.whoami();
const { user_id: userId, device_id: deviceId } = await tempClient.whoami();
await doSetLoggedIn(
{
homeserverUrl: hsUrl,
accessToken,
userId,
deviceId,
},
true,
false,

View File

@@ -11,12 +11,7 @@ import sanitizeHtml, { type IOptions } from "sanitize-html";
import { merge } from "lodash";
import _Linkify from "linkify-react";
import {
_linkifyElement,
_linkifyString,
ELEMENT_URL_PATTERN,
options as linkifyMatrixOptions,
} from "./linkify-matrix";
import { _linkifyString, ELEMENT_URL_PATTERN, options as linkifyMatrixOptions } from "./linkify-matrix";
import SettingsStore from "./settings/SettingsStore";
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
import { mediaFromMxc } from "./customisations/Media";
@@ -223,17 +218,6 @@ export function linkifyString(str: string, options = linkifyMatrixOptions): stri
return _linkifyString(str, options);
}
/**
* Linkifies the given DOM element. This is a wrapper around 'linkifyjs/element'.
*
* @param {object} element DOM element to linkify
* @param {object} [options] Options for linkifyElement. Default: linkifyMatrixOptions
* @returns {object}
*/
export function linkifyElement(element: HTMLElement, options = linkifyMatrixOptions): HTMLElement {
return _linkifyElement(element, options);
}
/**
* Linkify the given string and sanitize the HTML afterwards.
*

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type Key, type MutableRefObject, type ReactElement, type RefCallback } from "react";
import React, { type Key, type RefObject, type ReactElement, type RefCallback } from "react";
interface IChildProps {
style: React.CSSProperties;
@@ -20,7 +20,7 @@ interface IProps {
// a list of state objects to apply to each child node in turn
startStyles: React.CSSProperties[];
innerRef?: MutableRefObject<any>;
innerRef?: RefObject<any>;
}
function isReactElement(c: ReturnType<(typeof React.Children)["toArray"]>[number]): c is ReactElement {

View File

@@ -238,25 +238,25 @@ export function determineUnreadState(
room?: Room,
threadId?: string,
includeThreads?: boolean,
): { level: NotificationLevel; symbol: string | null; count: number } {
): { level: NotificationLevel; symbol: string | null; count: number; invited: boolean } {
if (!room) {
return { symbol: null, count: 0, level: NotificationLevel.None };
return { symbol: null, count: 0, level: NotificationLevel.None, invited: false };
}
if (getUnsentMessages(room, threadId).length > 0) {
return { symbol: "!", count: 1, level: NotificationLevel.Unsent };
return { symbol: "!", count: 1, level: NotificationLevel.Unsent, invited: false };
}
if (getEffectiveMembership(room.getMyMembership()) === EffectiveMembership.Invite) {
return { symbol: "!", count: 1, level: NotificationLevel.Highlight };
return { symbol: "!", count: 1, level: NotificationLevel.Highlight, invited: true };
}
if (SettingsStore.getValue("feature_ask_to_join") && isKnockDenied(room)) {
return { symbol: "!", count: 1, level: NotificationLevel.Highlight };
return { symbol: "!", count: 1, level: NotificationLevel.Highlight, invited: false };
}
if (getRoomNotifsState(room.client, room.roomId) === RoomNotifState.Mute) {
return { symbol: null, count: 0, level: NotificationLevel.None };
return { symbol: null, count: 0, level: NotificationLevel.None, invited: false };
}
const redNotifs = getUnreadNotificationCount(
@@ -269,12 +269,12 @@ export function determineUnreadState(
const trueCount = greyNotifs || redNotifs;
if (redNotifs > 0) {
return { symbol: null, count: trueCount, level: NotificationLevel.Highlight };
return { symbol: null, count: trueCount, level: NotificationLevel.Highlight, invited: false };
}
const markedUnreadState = getMarkedUnreadState(room);
if (greyNotifs > 0 || markedUnreadState) {
return { symbol: null, count: trueCount, level: NotificationLevel.Notification };
return { symbol: null, count: trueCount, level: NotificationLevel.Notification, invited: false };
}
// We don't have any notified messages, but we might have unread messages. Let's find out.
@@ -293,5 +293,6 @@ export function determineUnreadState(
symbol: null,
count: trueCount,
level: hasUnread ? NotificationLevel.Activity : NotificationLevel.None,
invited: false,
};
}

View File

@@ -30,7 +30,6 @@ export const DEFAULTS: DeepReadonly<IConfigOptions> = {
preferred_domain: "meet.element.io",
},
element_call: {
url: "https://call.element.io",
use_exclusively: false,
participant_limit: 8,
brand: "Element Call",

View File

@@ -354,7 +354,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
* nodeRef = inputRef when inputRef argument is provided.
*/
export const useRovingTabIndex = <T extends HTMLElement>(
inputRef?: RefObject<T>,
inputRef?: RefObject<T | null>,
): [FocusHandler, boolean, RefCallback<T>, RefObject<T | null>] => {
const context = useContext(RovingTabIndexContext);

View File

@@ -6,13 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type RefObject } from "react";
import React, { type JSX, type RefObject } from "react";
import AccessibleButton, { type ButtonProps } from "../../components/views/elements/AccessibleButton";
import { useRovingTabIndex } from "../RovingTabIndex";
type Props<T extends keyof HTMLElementTagNameMap> = Omit<ButtonProps<T>, "tabIndex"> & {
inputRef?: RefObject<HTMLElementTagNameMap[T]>;
inputRef?: RefObject<HTMLElementTagNameMap[T] | null>;
focusOnMouseOver?: boolean;
};

View File

@@ -6,14 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { type ReactElement, type RefCallback } from "react";
import { type ReactElement, type RefCallback, type RefObject } from "react";
import type React from "react";
import { useRovingTabIndex } from "../RovingTabIndex";
import { type FocusHandler, type Ref } from "./types";
import { type FocusHandler } from "./types";
interface IProps {
inputRef?: Ref;
inputRef?: RefObject<HTMLElement | null>;
children(renderProps: {
onFocus: FocusHandler;
isActive: boolean;

View File

@@ -6,8 +6,4 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { type RefObject } from "react";
export type Ref = RefObject<HTMLElement>;
export type FocusHandler = () => void;

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type ReactNode } from "react";
import React, { type JSX, type ReactNode } from "react";
import { Text, Heading, Button, Separator } from "@vector-im/compound-web";
import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out";

View File

@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import React, { type JSX } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from "../../../../MatrixClientPeg";

View File

@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { createRef } from "react";
import React, { type JSX, createRef } from "react";
import FileSaver from "file-saver";
import { logger } from "matrix-js-sdk/src/logger";
import { type AuthDict } from "matrix-js-sdk/src/matrix";

View File

@@ -178,7 +178,9 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
type="password"
disabled={disableForm}
autoComplete="new-password"
fieldRef={(field) => (this.fieldPassword = field)}
fieldRef={(field) => {
this.fieldPassword = field;
}}
/>
</div>
<div className="mx_E2eKeysDialog_inputRow">
@@ -195,7 +197,9 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
type="password"
disabled={disableForm}
autoComplete="new-password"
fieldRef={(field) => (this.fieldPasswordConfirm = field)}
fieldRef={(field) => {
this.fieldPasswordConfirm = field;
}}
/>
</div>
</div>

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
*/
import classNames from "classnames";
import React, { type HTMLAttributes, type ReactHTML, type ReactNode, type WheelEvent } from "react";
import React, { type HTMLAttributes, type JSX, type ReactNode, type WheelEvent } from "react";
type DynamicHtmlElementProps<T extends keyof JSX.IntrinsicElements> =
JSX.IntrinsicElements[T] extends HTMLAttributes<object> ? DynamicElementProps<T> : DynamicElementProps<"div">;
@@ -27,10 +27,10 @@ export type IProps<T extends keyof JSX.IntrinsicElements> = Omit<DynamicHtmlElem
export default class AutoHideScrollbar<T extends keyof JSX.IntrinsicElements> extends React.Component<IProps<T>> {
public static defaultProps = {
element: "div" as keyof ReactHTML,
element: "div" as keyof HTMLElementTagNameMap,
};
public readonly containerRef: React.RefObject<HTMLDivElement> = React.createRef();
public readonly containerRef = React.createRef<HTMLDivElement>();
public componentDidMount(): void {
if (this.containerRef.current && this.props.onScroll) {

View File

@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type CSSProperties, type RefObject, type SyntheticEvent, useRef, useState } from "react";
import React, { type JSX, type CSSProperties, type RefObject, type SyntheticEvent, useRef, useState } from "react";
import ReactDOM from "react-dom";
import classNames from "classnames";
import FocusLock from "react-focus-lock";
@@ -440,7 +440,7 @@ export default class ContextMenu extends React.PureComponent<React.PropsWithChil
);
}
public render(): React.ReactChild {
public render(): JSX.Element {
if (this.props.mountAsChild) {
// Render as a child of the current parent
return this.renderMenu();
@@ -582,13 +582,15 @@ export const alwaysAboveRightOf = (
type ContextMenuTuple<T> = [
boolean,
RefObject<T>,
RefObject<T | null>,
(ev?: SyntheticEvent) => void,
(ev?: SyntheticEvent) => void,
(val: boolean) => void,
];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
export const useContextMenu = <T extends any = HTMLElement>(inputRef?: RefObject<T>): ContextMenuTuple<T> => {
export const useContextMenu = <T extends HTMLElement = HTMLElement>(
inputRef?: RefObject<T | null>,
): ContextMenuTuple<T> => {
let button = useRef<T>(null);
if (inputRef) {
// if we are given a ref, use it instead of ours

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import classNames from "classnames";
import React, { type FunctionComponent, type Key, type PropsWithChildren, type ReactNode } from "react";
import React, { type JSX, type FunctionComponent, type Key, type PropsWithChildren, type ReactNode } from "react";
import { MenuItemRadio } from "../../accessibility/context_menu/MenuItemRadio";
import { type ButtonEvent } from "../views/elements/AccessibleButton";

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import * as React from "react";
import React, { type JSX } from "react";
import { useContext, useState } from "react";
import AutoHideScrollbar from "./AutoHideScrollbar";

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { createRef } from "react";
import React, { createRef, type JSX } from "react";
import AutoHideScrollbar, { type IProps as AutoHideScrollbarProps } from "./AutoHideScrollbar";
import UIStore, { UI_EVENTS } from "../../stores/UIStore";

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import * as React from "react";
import React, { type JSX } from "react";
import { createRef } from "react";
import classNames from "classnames";

View File

@@ -124,9 +124,9 @@ class LoggedInView extends React.Component<IProps, IState> {
public static displayName = "LoggedInView";
protected readonly _matrixClient: MatrixClient;
protected readonly _roomView: React.RefObject<RoomView>;
protected readonly _resizeContainer: React.RefObject<HTMLDivElement>;
protected readonly resizeHandler: React.RefObject<HTMLDivElement>;
protected readonly _roomView: React.RefObject<RoomView | null>;
protected readonly _resizeContainer: React.RefObject<HTMLDivElement | null>;
protected readonly resizeHandler: React.RefObject<HTMLDivElement | null>;
protected layoutWatcherRef?: string;
protected compactLayoutWatcherRef?: string;
protected backgroundImageWatcherRef?: string;

View File

@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type ReactNode } from "react";
import React, { type JSX, type ReactNode } from "react";
import { type NumberSize, Resizable } from "re-resizable";
import { type Direction } from "re-resizable/lib/resizer";
import { type WebPanelResize } from "@matrix-org/analytics-events/types/typescript/WebPanelResize";

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { createRef, lazy } from "react";
import React, { type JSX, createRef, lazy } from "react";
import {
ClientEvent,
createClient,
@@ -235,6 +235,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private themeWatcher?: ThemeWatcher;
private fontWatcher?: FontWatcher;
private readonly stores: SdkContextClass;
private loadSessionAbortController = new AbortController();
public constructor(props: IProps) {
super(props);
@@ -327,7 +328,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// When the session loads it'll be detected as soft logged out and a dispatch
// will be sent out to say that, triggering this MatrixChat to show the soft
// logout page.
Lifecycle.loadSession();
Lifecycle.loadSession({ abortSignal: this.loadSessionAbortController.signal });
return;
}
@@ -552,6 +553,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
guestHsUrl: this.getServerProperties().serverConfig.hsUrl,
guestIsUrl: this.getServerProperties().serverConfig.isUrl,
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
abortSignal: this.loadSessionAbortController.signal,
});
})
.then((loadedSession) => {
@@ -1565,26 +1567,33 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
dis.fire(Action.FocusSendMessageComposer);
});
cli.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
cli.on(HttpApiEvent.SessionLoggedOut, (errObj) => {
this.loadSessionAbortController.abort(errObj);
this.loadSessionAbortController = new AbortController();
if (Lifecycle.isLoggingOut()) return;
// A modal might have been open when we were logged out by the server
Modal.forceCloseAllModals();
if (errObj.httpStatus === 401 && errObj.data && errObj.data["soft_logout"]) {
if (errObj.httpStatus === 401 && errObj.data?.["soft_logout"]) {
logger.warn("Soft logout issued by server - avoiding data deletion");
Lifecycle.softLogout();
return;
}
dis.dispatch(
{
action: "logout",
},
true,
);
// The above dispatch closes all modals, so open the modal after calling it synchronously
Modal.createDialog(ErrorDialog, {
title: _t("auth|session_logged_out_title"),
description: _t("auth|session_logged_out_description"),
});
dis.dispatch({
action: "logout",
});
});
cli.on(HttpApiEvent.NoConsent, function (message, consentUri) {
Modal.createDialog(

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { createRef, type ReactNode, type TransitionEvent } from "react";
import React, { type JSX, createRef, type ReactNode, type TransitionEvent } from "react";
import classNames from "classnames";
import {
type Room,
@@ -292,6 +292,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
this.props.room?.currentState.off(RoomStateEvent.Update, this.calculateRoomMembersCount);
SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef);
this.readReceiptMap = {};
this.resizeObserver.disconnect();
}
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
@@ -800,7 +801,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
isRedacted={mxEv.isRedacted()}
replacingEventId={mxEv.replacingEventId()}
editState={isEditing ? this.props.editState : undefined}
onHeightChanged={this.onHeightChanged}
resizeObserver={this.resizeObserver}
readReceipts={readReceipts}
readReceiptMap={this.readReceiptMap}
showUrlPreview={this.props.showUrlPreview}
@@ -953,15 +954,13 @@ export default class MessagePanel extends React.Component<IProps, IState> {
this.eventTiles[eventId] = node;
};
// once dynamic content in the events load, make the scrollPanel check the
// scroll offsets.
// Once dynamic content in the events load, make the scrollPanel check the scroll offsets.
public onHeightChanged = (): void => {
const scrollPanel = this.scrollPanel.current;
if (scrollPanel) {
scrollPanel.checkScroll();
}
this.scrollPanel.current?.checkScroll();
};
private resizeObserver = new ResizeObserver(this.onHeightChanged);
private onTypingShown = (): void => {
const scrollPanel = this.scrollPanel.current;
// this will make the timeline grow, so checkScroll

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import React, { type JSX } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import NotificationsIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications";

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { createRef } from "react";
import React, { type JSX, createRef } from "react";
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
import { lerp } from "../../utils/AnimationUtils";

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type MutableRefObject, type ReactNode, useRef } from "react";
import React, { type RefObject, type ReactNode, useRef } from "react";
import { CallEvent, CallState, type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { logger } from "matrix-js-sdk/src/logger";
import { type Optional } from "matrix-events-sdk";
@@ -34,7 +34,7 @@ const SHOW_CALL_IN_STATES = [
];
interface IProps {
movePersistedElement: MutableRefObject<(() => void) | undefined>;
movePersistedElement: RefObject<(() => void) | null>;
}
interface IState {
@@ -280,7 +280,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
}
export const PipContainer: React.FC = () => {
const movePersistedElement = useRef<() => void>();
const movePersistedElement = useRef<() => void>(null);
return <PipContainerInner movePersistedElement={movePersistedElement} />;
};

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import React, { type JSX, forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import {
type ISearchResults,
type IThreadBundledRelationship,
@@ -59,7 +59,7 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
const aborted = useRef(false);
// A map from room ID to permalink creator
const permalinkCreators = useMemo(() => new Map<string, RoomPermalinkCreator>(), []);
const innerRef = useRef<ScrollPanel | null>();
const innerRef = useRef<ScrollPanel>(null);
useEffect(() => {
return () => {
@@ -198,12 +198,6 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
}
}
// once dynamic content in the search results load, make the scrollPanel check
// the scroll offsets.
const onHeightChanged = (): void => {
innerRef.current?.checkScroll();
};
const onRef = (e: ScrollPanel | null): void => {
if (typeof ref === "function") {
ref(e);
@@ -302,7 +296,6 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
searchHighlights={highlights ?? []}
resultLink={resultLink}
permalinkCreator={permalinkCreator}
onHeightChanged={onHeightChanged}
/>,
);

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type ReactNode } from "react";
import React, { type JSX, type ReactNode } from "react";
import {
ClientEvent,
EventStatus,

View File

@@ -256,7 +256,7 @@ interface LocalRoomViewProps {
localRoom: LocalRoom;
resizeNotifier: ResizeNotifier;
permalinkCreator: RoomPermalinkCreator;
roomView: RefObject<HTMLElement>;
roomView: RefObject<HTMLElement | null>;
onFileDrop: (dataTransfer: DataTransfer) => Promise<void>;
mainSplitContentType: MainSplitContentType;
}

View File

@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React, {
type JSX,
type Dispatch,
type KeyboardEvent,
type KeyboardEventHandler,
@@ -636,7 +637,7 @@ const useIntersectionObserver = (callback: () => void): ((element: HTMLDivElemen
}
};
const observerRef = useRef<IntersectionObserver>();
const observerRef = useRef<IntersectionObserver>(undefined);
return (element: HTMLDivElement) => {
if (observerRef.current) {
observerRef.current.disconnect();

View File

@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import { EventType, RoomType, JoinRule, Preset, type Room, RoomEvent } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { logger } from "matrix-js-sdk/src/logger";
import React, { useCallback, useContext, useRef, useState } from "react";
import React, { type JSX, useCallback, useContext, useRef, useState } from "react";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import createRoom, { type IOpts } from "../../createRoom";

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import classNames from "classnames";
import React, { type DetailedHTMLProps, type HTMLAttributes, type ReactNode } from "react";
import React, { type JSX, type DetailedHTMLProps, type HTMLAttributes, type ReactNode } from "react";
interface Props extends DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> {
className?: string;

View File

@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import * as React from "react";
import React, { type JSX } from "react";
import classNames from "classnames";
import { _t, type TranslationKey } from "../../languageHandler";

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { createRef, type KeyboardEvent } from "react";
import React, { type JSX, createRef, type KeyboardEvent } from "react";
import {
type Thread,
THREAD_RELATION_TYPE,

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { createRef, type ReactNode } from "react";
import React, { type JSX, createRef, type ReactNode } from "react";
import { type Room } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "../../MatrixClientPeg";
@@ -81,7 +81,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
private dispatcherRef?: string;
private themeWatcherRef?: string;
private readonly dndWatcherRef?: string;
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
private buttonRef = createRef<HTMLButtonElement>();
public constructor(props: IProps) {
super(props);

View File

@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import React, { type JSX } from "react";
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import SyntaxHighlight from "../views/elements/SyntaxHighlight";

View File

@@ -21,7 +21,7 @@ import SdkConfig from "../../SdkConfig";
import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx";
interface Props {
roomView: RefObject<HTMLElement>;
roomView: RefObject<HTMLElement | null>;
resizeNotifier: ResizeNotifier;
inviteEvent: MatrixEvent;
}

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import React, { type JSX } from "react";
import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";

View File

@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type ReactNode } from "react";
import React, { type JSX, type ReactNode } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { sleep } from "matrix-js-sdk/src/utils";
import { LockSolidIcon, CheckIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
@@ -388,7 +388,9 @@ export default class ForgotPassword extends React.Component<Props, State> {
label={_td("auth|change_password_new_label")}
value={this.state.password}
minScore={PASSWORD_MIN_SCORE}
fieldRef={(field) => (this.fieldPassword = field)}
fieldRef={(field) => {
this.fieldPassword = field;
}}
onChange={this.onInputChanged.bind(this, "password")}
autoComplete="new-password"
/>
@@ -399,7 +401,9 @@ export default class ForgotPassword extends React.Component<Props, State> {
labelInvalid={_td("auth|reset_password|passwords_mismatch")}
value={this.state.password2}
password={this.state.password}
fieldRef={(field) => (this.fieldPasswordConfirm = field)}
fieldRef={(field) => {
this.fieldPasswordConfirm = field;
}}
onChange={this.onInputChanged.bind(this, "password2")}
autoComplete="new-password"
/>

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type ReactNode } from "react";
import React, { type JSX, type ReactNode } from "react";
import classNames from "classnames";
import { logger } from "matrix-js-sdk/src/logger";
import { type SSOFlow, SSOAction } from "matrix-js-sdk/src/matrix";

View File

@@ -20,7 +20,7 @@ import {
SSOAction,
type RegisterResponse,
} from "matrix-js-sdk/src/matrix";
import React, { Fragment, type ReactNode } from "react";
import React, { type JSX, Fragment, type ReactNode } from "react";
import classNames from "classnames";
import { logger } from "matrix-js-sdk/src/logger";

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import React, { type JSX } from "react";
import SplashPage from "../SplashPage";
import { _t } from "../../../languageHandler";

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import React, { type JSX } from "react";
import { type KeyBackupInfo, type VerificationRequest } from "matrix-js-sdk/src/crypto-api";
import { logger } from "matrix-js-sdk/src/logger";
import { type SecretStorageKeyDescription } from "matrix-js-sdk/src/secret-storage";

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type ChangeEvent, type SyntheticEvent } from "react";
import React, { type JSX, type ChangeEvent, type SyntheticEvent } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { type Optional } from "matrix-events-sdk";
import { type LoginFlow, MatrixError, SSOAction, type SSOFlow } from "matrix-js-sdk/src/matrix";

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { Fragment, type PropsWithChildren, type ReactNode, useContext } from "react";
import React, { type JSX, Fragment, type PropsWithChildren, type ReactNode, useContext } from "react";
import { AuthHeaderContext } from "./AuthHeaderContext";

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import { isEqual } from "lodash";
import React, { type ComponentProps, type PropsWithChildren, type Reducer, useReducer } from "react";
import React, { type JSX, type ComponentProps, type PropsWithChildren, type Reducer, useReducer } from "react";
import { AuthHeaderContext } from "./AuthHeaderContext";
import { type AuthHeaderModifier } from "./AuthHeaderModifier";

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import classNames from "classnames";
import React, { useMemo } from "react";
import React, { type JSX, useMemo } from "react";
type FlexProps = {
/**

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import classNames from "classnames";
import React, { type ComponentProps, type JSXElementConstructor, useMemo } from "react";
import React, { type JSX, type ComponentProps, type JSXElementConstructor, useMemo } from "react";
type FlexProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> = {
/**

View File

@@ -5,13 +5,16 @@
* Please see LICENSE files in the repository root for full details.
*/
import { useCallback } from "react";
import { useCallback, useMemo } from "react";
import { type Room } from "matrix-js-sdk/src/matrix";
import dispatcher from "../../../dispatcher/dispatcher";
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../../dispatcher/actions";
import { hasAccessToOptionsMenu } from "./utils";
import { _t } from "../../../languageHandler";
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
export interface RoomListItemViewState {
/**
@@ -22,6 +25,14 @@ export interface RoomListItemViewState {
* Open the room having given roomId.
*/
openRoom: () => void;
/**
* The a11y label for the room list item.
*/
a11yLabel: string;
/**
* The notification state of the room.
*/
notificationState: RoomNotificationState;
}
/**
@@ -31,6 +42,8 @@ export interface RoomListItemViewState {
export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
// incoming: Check notification menu rights
const showHoverMenu = hasAccessToOptionsMenu(room);
const notificationState = useMemo(() => RoomNotificationStateStore.instance.getRoomState(room), [room]);
const a11yLabel = getA11yLabel(room, notificationState);
// Actions
@@ -43,7 +56,38 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
}, [room]);
return {
notificationState,
showHoverMenu,
openRoom,
a11yLabel,
};
}
/**
* Get the a11y label for the room list item
* @param room
* @param notificationState
*/
function getA11yLabel(room: Room, notificationState: RoomNotificationState): string {
if (notificationState.isUnsetMessage) {
return _t("a11y|room_messsage_not_sent", {
roomName: room.name,
});
} else if (notificationState.invited) {
return _t("a11y|room_n_unread_invite", {
roomName: room.name,
});
} else if (notificationState.isMention) {
return _t("a11y|room_n_unread_messages_mentions", {
roomName: room.name,
count: notificationState.count,
});
} else if (notificationState.hasUnreadCount) {
return _t("a11y|room_n_unread_messages", {
roomName: room.name,
count: notificationState.count,
});
} else {
return _t("room_list|room|open_room", { roomName: room.name });
}
}

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