Compare commits

...

44 Commits

Author SHA1 Message Date
Michael Telatynski
901921848e Improve coverage
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-06 13:53:27 +00:00
Michael Telatynski
66cee20639 Improve coverage
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-06 09:56:39 +00:00
Michael Telatynski
7775f10263 Improve coverage
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-06 09:23:10 +00:00
Michael Telatynski
cb5f84cc49 Fix test
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-05 18:39:13 +00:00
Michael Telatynski
8657048949 Use React Suspense when rendering async modals
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-05 17:48:28 +00:00
Michael Telatynski
a355292a7f Merge remote-tracking branch 'origin/develop' into develop 2024-11-05 14:03:17 +00:00
Michael Telatynski
24fabfff89 Fix post release docker check
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-05 14:02:25 +00:00
RiotRobot
0b6ed44390 Reset matrix-js-sdk back to develop branch 2024-11-05 13:59:24 +00:00
RiotRobot
a6ce6dc7ab Merge branch 'master' into develop 2024-11-05 13:59:15 +00:00
Michael Telatynski
aeabf3b188 Show message type prefix in thread root & reply previews (#28361)
* Extract EventPreview from PinnedMessageBanner

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

* Show message type prefix in thread root previews

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

* Show message type prefix in thread reply preview

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

* Update tests

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>
2024-11-05 11:34:42 +00:00
Michael Telatynski
c9d9c421bc Move navigator message listener registration to be synchronously attached (#28340)
to silence Chrome warning

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-05 11:04:14 +00:00
ElementRobot
d7d96b6b8b [create-pull-request] automated change (#28377)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2024-11-05 06:19:43 +00:00
Hubert Chathi
2631b908b6 Update the display of decryption failures due to failed trust requirement (#28300)
* update the display of decryption failures due to failed trust requirement

* add test for not showing shield
2024-11-04 12:46:38 +00:00
Michael Telatynski
502cc91dfe Switch ModalManager to the React 18 createRoot API (#28336)
* Remove boilerplate around dispatcher and settings watchers

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

* Iterate

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

* Move state update listeners from constructor to componentDidMount

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

* Iterate

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

* Switch ModalManager to the React 18 createRoot API

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>
2024-11-04 11:34:00 +00:00
Michael Telatynski
38e5eeea00 Fix markdown escaping wrongly passing html through (#28363)
* Fix markdown escaping wrongly passing html through

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

* Add comment

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-04 11:31:44 +00:00
Michael Telatynski
1ccbdb21e9 Wire up analytics for Legacy/EC/Jitsi voip options (#28348)
* Wire up analytics for Legacy/EC/Jitsi voip options

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

* Update @matrix-org/analytics-events

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-04 09:49:41 +00:00
Michael Telatynski
b1ef099cd6 Show developer jump to event button on all events with associations (#28351)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-04 09:49:35 +00:00
Michael Telatynski
00d46f1c8f Remove unused Security customisations module (#28350)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-04 09:49:20 +00:00
ElementRobot
8e304713a2 [create-pull-request] automated change (#28372)
Co-authored-by: t3chguy <t3chguy@users.noreply.github.com>
2024-11-04 06:25:18 +00:00
Michael Telatynski
0899165d9e Move state update listeners from constructor to componentDidMount (#28341)
* Move state update listeners from constructor to componentDidMount

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>
2024-11-01 17:39:08 +00:00
Michael Telatynski
2d9982f9f0 Remove boilerplate around dispatcher and settings watchers (#28338)
* Remove boilerplate around dispatcher and settings watchers

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>
2024-11-01 15:15:04 +00:00
Richard Gibson
b8fd98ab3c Specify a local address when exposing ports with Docker (#20891)
* Specify a local address when exposing ports with Docker

cf. https://docs.docker.com/engine/reference/commandline/run/#publish-or-expose-port--p---expose

Signed-off-by: Richard Gibson <richard.gibson@gmail.com>

* Provide and explain docker run examples with and without confinement to localhost

Signed-off-by: Richard Gibson <richard.gibson@gmail.com>

* Update README.md

Co-authored-by: Richard Gibson <richard.gibson@gmail.com>

---------

Signed-off-by: Richard Gibson <richard.gibson@gmail.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-01 12:59:16 +00:00
ElementRobot
a27dfa1825 [create-pull-request] automated change (#28360)
Co-authored-by: t3chguy <t3chguy@users.noreply.github.com>
2024-11-01 06:26:53 +00:00
ElementRobot
a3ece9d902 [create-pull-request] automated change (#28359)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2024-11-01 06:22:26 +00:00
ElementRobot
23613ac37e [create-pull-request] automated change (#28119)
Co-authored-by: Johennes <1137962+Johennes@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-31 13:56:41 +00:00
Michael Telatynski
195337d865 Pass nodeRef to CSSTransition to avoid ReactDOM.findDOMNode (#28339)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-31 11:21:19 +00:00
chagai95
4bb9f2ed7b Documentation fix: missing comma and I would also take the ellipsis away (#17233)
* Missing coma and I would also take the ellipsis away

took me a while to find the mistake and "comment" and I think it'll take other even longer...

* use json5

---------

Co-authored-by: Travis Ralston <travisr@matrix.org>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-10-31 10:54:46 +00:00
ElementRobot
e0ffddf3eb [create-pull-request] automated change (#28345)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2024-10-31 06:21:39 +00:00
Florian Duros
386b782f2a Remove "Upgrade your encryption" flow in CreateSecretStorageDialog (#28290)
* Remove "Upgrade your encryption" flow

* Rename and remove tests

* Remove `BackupTrustInfo`

* Get keybackup when bootstraping the secret storage.

* Update src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-10-30 11:22:05 +00:00
Hugh Nimmo-Smith
c23c9dfacb Use new CryptoApi.encryptToDeviceMessages() to send encrypted to-device messages from widgets (#28315) 2024-10-30 09:37:23 +00:00
renovate[bot]
5c45ca5e3c Update react-types (#28323)
* Update react-types

* Iterate

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-29 16:22:16 +00:00
renovate[bot]
fbc96f458c Update dependency @sentry/browser to v8.35.0 (#28326)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 16:19:08 +00:00
renovate[bot]
a7b3337c39 Update dependency eslint-plugin-react-hooks to v5 (#28330)
* Update dependency eslint-plugin-react-hooks to v5

* Iterate

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-29 15:56:57 +00:00
renovate[bot]
7daa8b232b Update typescript-eslint monorepo to v8.11.0 (#28328)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 15:49:45 +00:00
renovate[bot]
7408a83618 Update dependency node to v22 (#28329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 15:39:34 +00:00
renovate[bot]
8a743a9f89 Update dependency uuid to v11 (#28332)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 15:32:24 +00:00
renovate[bot]
3e8f55f883 Update dependency @vector-im/compound-design-tokens to v1.9.0 (#28327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 14:45:24 +00:00
renovate[bot]
e606b5f5d4 Update dependency @matrix-org/analytics-events to ^0.28.0 (#28325)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 14:44:42 +00:00
renovate[bot]
a4e4ac45c4 Update playwright to v1.48.2 (#28322)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 14:42:30 +00:00
renovate[bot]
2ff51c218f Update dependency stylelint-scss to v6.8.1 (#28321)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 14:41:35 +00:00
renovate[bot]
76b9c27250 Update dependency @testing-library/jest-dom to v6.6.2 (#28319)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 14:41:05 +00:00
renovate[bot]
13bfb51d8c Update dependency axe-core to v4.10.2 (#28320)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 14:40:49 +00:00
renovate[bot]
99542c9eda Update dependency @formatjs/intl-segmenter to v11.7.1 (#28324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 14:35:52 +00:00
Michael Telatynski
95c3027657 Fix release_prepare.yml
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-29 12:51:39 +00:00
187 changed files with 1752 additions and 2018 deletions

View File

@@ -266,6 +266,9 @@ module.exports = {
parserOptions: {
project: ["./playwright/tsconfig.json"],
},
rules: {
"react-hooks/rules-of-hooks": ["off"],
},
},
],
settings: {

View File

@@ -49,7 +49,7 @@ jobs:
ref: master
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
check-name: "Docker Buildx (vanilla)"
check-name: "Docker Buildx"
allowed-conclusions: success
- name: Wait for debian package

View File

@@ -20,6 +20,9 @@ on:
jobs:
prepare:
runs-on: ubuntu-24.04
env:
# The order is specified bottom-up to avoid any races for allchange
REPOS: matrix-js-sdk element-web element-desktop
steps:
- name: Checkout Element Desktop
uses: actions/checkout@v4

View File

@@ -1 +1 @@
20
22

View File

@@ -1,5 +1,5 @@
# Builder
FROM --platform=$BUILDPLATFORM node:20-bullseye as builder
FROM --platform=$BUILDPLATFORM node:22-bullseye as builder
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
ARG USE_CUSTOM_SDKS=false

View File

@@ -11,8 +11,8 @@ Customisations will be removed from the codebase in a future release.
Element Web and the React SDK support "customisation points" that can be used to
easily add custom logic specific to a particular deployment of Element Web.
An example of this is the [security customisations
module](https://github.com/element-hq/element-web/blob/develop/src/customisations/Security.ts).
An example of this is the [media customisations
module](https://github.com/element-hq/element-web/blob/develop/src/customisations/Media.ts).
This module in the React SDK only defines some empty functions and their types:
it does not do anything by default.
@@ -21,14 +21,14 @@ Web so that you can add your own code. Even though the default module is part of
the React SDK, you can still override it from the Element Web layer:
1. Copy the default customisation module to
`element-web/src/customisations/YourNameSecurity.ts`
`element-web/src/customisations/YourNameMedia.ts`
2. Edit customisations points and make sure export the ones you actually want to
activate
3. Create/add an entry to `customisations.json` next to the webpack config:
```json
{
"src/customisations/Security.ts": "src/customisations/YourNameSecurity.ts"
"src/customisations/Media.ts": "src/customisations/YourNameMedia.ts"
}
```

View File

@@ -41,7 +41,15 @@ The Docker image can be used to serve element-web as a web server. The easiest w
it is to use the prebuilt image:
```bash
docker run -p 80:80 vectorim/element-web
docker run --rm -p 127.0.0.1:80:80 vectorim/element-web
```
A server can also be made available to clients outside the local host by omitting the
explicit local address as described in
[docker run documentation](https://docs.docker.com/engine/reference/commandline/run/#publish-or-expose-port--p---expose):
```bash
docker run --rm -p 80:80 vectorim/element-web
```
To supply your own custom `config.json`, map a volume to `/app/config.json`. For example,
@@ -49,7 +57,7 @@ if your custom config was located at `/etc/element-web/config.json` then your Do
would be:
```bash
docker run -p 80:80 -v /etc/element-web/config.json:/app/config.json vectorim/element-web
docker run --rm -p 127.0.0.1:80:80 -v /etc/element-web/config.json:/app/config.json vectorim/element-web
```
To build the image yourself:

View File

@@ -29,7 +29,7 @@ default theme, you would use `default_theme: "custom-Electric Blue"`.
e.g. in config.json:
```
```json5
"setting_defaults": {
"custom_themes": [
{
@@ -59,6 +59,10 @@ e.g. in config.json:
"timeline-text-color": "#2e2f32",
"timeline-text-secondary-color": "#61708b",
"timeline-highlights-color": "#f3f8fd",
// These should both be 8 values long
"username-colors": ["#ff0000", /*...*/],
"avatar-background-colors": ["#cc0000", /*...*/]
},
"compound": {
"--cpd-color-icon-accent-tertiary": "var(--cpd-color-blue-800)",

View File

@@ -84,7 +84,7 @@
"dependencies": {
"@babel/runtime": "^7.12.5",
"@formatjs/intl-segmenter": "^11.5.7",
"@matrix-org/analytics-events": "^0.27.0",
"@matrix-org/analytics-events": "^0.29.0",
"@matrix-org/emojibase-bindings": "^1.3.3",
"@matrix-org/react-sdk-module-api": "^2.4.0",
"@matrix-org/spec": "^1.7.0",
@@ -126,7 +126,7 @@
"maplibre-gl": "^2.0.0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "34.10.0",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^1.9.0",
"memoize-one": "^6.0.0",
"oidc-client-ts": "^3.0.1",
@@ -148,7 +148,7 @@
"tar-js": "^0.3.0",
"temporal-polyfill": "^0.2.5",
"ua-parser-js": "^1.0.2",
"uuid": "^10.0.0",
"uuid": "^11.0.0",
"what-input": "^5.2.10"
},
"devDependencies": {
@@ -208,7 +208,7 @@
"@types/qrcode": "^1.3.5",
"@types/react": "18.3.3",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "18.3.0",
"@types/react-dom": "18.3.1",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "2.13.0",
"@types/sdp-transform": "^2.4.6",
@@ -219,7 +219,7 @@
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"axe-core": "4.10.0",
"axe-core": "4.10.2",
"babel-jest": "^29.0.0",
"babel-loader": "^9.0.0",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
@@ -242,7 +242,7 @@
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-matrix-org": "^2.0.2",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-unicorn": "^56.0.0",
"express": "^4.18.2",
"fake-indexeddb": "^6.0.0",

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/playwright:v1.48.0-jammy
FROM mcr.microsoft.com/playwright:v1.48.2-jammy
WORKDIR /work

View File

@@ -51,6 +51,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("Verified identity has changed");
await expect(lastTile).toContainText("Sender's verified identity has changed");
});
});

View File

@@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
// Docker tag to use for synapse docker image.
// We target a specific digest as every now and then a Synapse update will break our CI.
// This digest is updated by the playwright-image-updates.yaml workflow periodically.
const DOCKER_TAG = "develop@sha256:eee8e1551640da28f017b93dbf2264a89a092709d12475c785a52c91cf580c6a";
const DOCKER_TAG = "develop@sha256:df06607d21965639cb7dd72724fd610731c13bed95d3334746f53668a36c6cda";
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
const templateDir = path.join(__dirname, "templates", opts.template);

View File

@@ -282,6 +282,7 @@
@import "./views/rooms/_EmojiButton.pcss";
@import "./views/rooms/_EntityTile.pcss";
@import "./views/rooms/_EventBubbleTile.pcss";
@import "./views/rooms/_EventPreview.pcss";
@import "./views/rooms/_EventTile.pcss";
@import "./views/rooms/_HistoryTile.pcss";
@import "./views/rooms/_IRCLayout.pcss";

View File

@@ -11,22 +11,11 @@ Please see LICENSE files in the repository root for full details.
font-style: italic;
}
/* Formatting for the "Verified identity has changed" error */
.mx_DecryptionFailureVerifiedIdentityChanged > span {
/* Show it in red */
color: var(--cpd-color-text-critical-primary);
background-color: var(--cpd-color-bg-critical-subtle);
/* With a red border */
border: 1px solid var(--cpd-color-border-critical-subtle);
border-radius: $font-16px;
/* Some space inside the border */
padding: var(--cpd-space-1x) var(--cpd-space-3x) var(--cpd-space-1x) var(--cpd-space-2x);
/* some space between the (!) icon and text */
/* Formatting for errors due to sender trust requirement failures */
.mx_DecryptionFailureSenderTrustRequirement > span {
/* some space between the (/) icon and text */
display: inline-flex;
gap: var(--cpd-space-2x);
gap: var(--cpd-space-1x);
/* Center vertically */
align-items: center;

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2024 New Vector Ltd.
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*/
.mx_EventPreview {
font: var(--cpd-font-body-sm-regular);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.mx_EventPreview_prefix {
font: var(--cpd-font-body-sm-semibold);
}
}

View File

@@ -81,15 +81,7 @@
.mx_PinnedMessageBanner_message {
grid-area: message;
font: var(--cpd-font-body-sm-regular);
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.mx_PinnedMessageBanner_prefix {
font: var(--cpd-font-body-sm-semibold);
}
}
.mx_PinnedMessageBanner_redactedMessage {

File diff suppressed because one or more lines are too long

View File

@@ -10,13 +10,13 @@ Please see LICENSE files in the repository root for full details.
import {
IAddThreePidOnlyBody,
IAuthData,
IRequestMsisdnTokenResponse,
IRequestTokenResponse,
MatrixClient,
MatrixError,
HTTPError,
IThreepid,
UIAResponse,
} from "matrix-js-sdk/src/matrix";
import Modal from "./Modal";
@@ -179,7 +179,9 @@ export default class AddThreepid {
* with a "message" property which contains a human-readable message detailing why
* the request failed.
*/
public async checkEmailLinkClicked(): Promise<[success?: boolean, result?: IAuthData | Error | null]> {
public async checkEmailLinkClicked(): Promise<
[success?: boolean, result?: UIAResponse<IAddThreePidOnlyBody> | Error | null]
> {
try {
if (this.bind) {
const authClient = new IdentityAuthClient();
@@ -220,7 +222,7 @@ export default class AddThreepid {
continueKind: "primary",
},
};
const { finished } = Modal.createDialog(InteractiveAuthDialog<{}>, {
const { finished } = Modal.createDialog(InteractiveAuthDialog<IAddThreePidOnlyBody>, {
title: _t("settings|general|add_email_dialog_title"),
matrixClient: this.matrixClient,
authData: err.data,
@@ -263,7 +265,9 @@ export default class AddThreepid {
* with a "message" property which contains a human-readable message detailing why
* the request failed.
*/
public async haveMsisdnToken(msisdnToken: string): Promise<[success?: boolean, result?: IAuthData | Error | null]> {
public async haveMsisdnToken(
msisdnToken: string,
): Promise<[success?: boolean, result?: UIAResponse<IAddThreePidOnlyBody> | Error | null]> {
const authClient = new IdentityAuthClient();
if (this.submitUrl) {
@@ -319,7 +323,7 @@ export default class AddThreepid {
continueKind: "primary",
},
};
const { finished } = Modal.createDialog(InteractiveAuthDialog<{}>, {
const { finished } = Modal.createDialog(InteractiveAuthDialog<IAddThreePidOnlyBody>, {
title: _t("settings|general|add_msisdn_dialog_title"),
matrixClient: this.matrixClient,
authData: err.data,

View File

@@ -6,24 +6,19 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { ComponentType, PropsWithChildren } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import React, { ReactNode, Suspense } from "react";
import { _t } from "./languageHandler";
import BaseDialog from "./components/views/dialogs/BaseDialog";
import DialogButtons from "./components/views/elements/DialogButtons";
import Spinner from "./components/views/elements/Spinner";
type AsyncImport<T> = { default: T };
interface IProps {
// A promise which resolves with the real component
prom: Promise<ComponentType<any> | AsyncImport<ComponentType<any>>>;
onFinished(): void;
children: ReactNode;
}
interface IState {
component?: ComponentType<PropsWithChildren<any>>;
error?: Error;
}
@@ -32,55 +27,26 @@ interface IState {
* spinner until the real component loads.
*/
export default class AsyncWrapper extends React.Component<IProps, IState> {
private unmounted = false;
public static getDerivedStateFromError(error: Error): IState {
return { error };
}
public state: IState = {};
public componentDidMount(): void {
this.props.prom
.then((result) => {
if (this.unmounted) return;
// Take the 'default' member if it's there, then we support
// passing in just an import()ed module, since ES6 async import
// always returns a module *namespace*.
const component = (result as AsyncImport<ComponentType>).default
? (result as AsyncImport<ComponentType>).default
: (result as ComponentType);
this.setState({ component });
})
.catch((e) => {
logger.warn("AsyncWrapper promise failed", e);
this.setState({ error: e });
});
}
public componentWillUnmount(): void {
this.unmounted = true;
}
private onWrapperCancelClick = (): void => {
this.props.onFinished();
};
public render(): React.ReactNode {
if (this.state.component) {
const Component = this.state.component;
return <Component {...this.props} />;
} else if (this.state.error) {
if (this.state.error) {
return (
<BaseDialog onFinished={this.props.onFinished} title={_t("common|error")}>
{_t("failed_load_async_component")}
<DialogButtons
primaryButton={_t("action|dismiss")}
onPrimaryButtonClick={this.onWrapperCancelClick}
onPrimaryButtonClick={this.props.onFinished}
hasCancel={false}
/>
</BaseDialog>
);
} else {
// show a spinner until the component is loaded.
return <Spinner />;
}
return <Suspense fallback={<Spinner />}>{this.props.children}</Suspense>;
}
}

View File

@@ -113,13 +113,9 @@ export default class DeviceListener {
this.client.removeListener(ClientEvent.Sync, this.onSync);
this.client.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
}
if (this.deviceClientInformationSettingWatcherRef) {
SettingsStore.unwatchSetting(this.deviceClientInformationSettingWatcherRef);
}
if (this.dispatcherRef) {
dis.unregister(this.dispatcherRef);
this.dispatcherRef = undefined;
}
SettingsStore.unwatchSetting(this.deviceClientInformationSettingWatcherRef);
dis.unregister(this.dispatcherRef);
this.dispatcherRef = undefined;
this.dismissed.clear();
this.dismissedThisDeviceToast = false;
this.keyBackupInfo = null;

View File

@@ -383,6 +383,9 @@ export default class Markdown {
if (isMultiLine(node) && node.next) this.lit("\n\n");
};
return renderer.render(this.parsed);
// We inhibit the default escape function as we escape the entire output string to correctly handle backslashes
renderer.esc = (input: string) => input;
return escape(renderer.render(this.parsed));
}
}

View File

@@ -8,9 +8,9 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { StrictMode } from "react";
import ReactDOM from "react-dom";
import { createRoot, Root } from "react-dom/client";
import classNames from "classnames";
import { IDeferred, defer, sleep } from "matrix-js-sdk/src/utils";
import { IDeferred, defer } from "matrix-js-sdk/src/utils";
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
import { Glass, TooltipProvider } from "@vector-im/compound-web";
@@ -69,6 +69,16 @@ type HandlerMap = {
type ModalCloseReason = "backgroundClick";
function getOrCreateContainer(id: string): HTMLDivElement {
let container = document.getElementById(id) as HTMLDivElement | null;
if (!container) {
container = document.createElement("div");
container.id = id;
document.body.appendChild(container);
}
return container;
}
export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMap> {
private counter = 0;
// The modal to prioritise over all others. If this is set, only show
@@ -83,28 +93,22 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
// Neither the static nor priority modal will be in this list.
private modals: IModal<any>[] = [];
private static getOrCreateContainer(): HTMLElement {
let container = document.getElementById(DIALOG_CONTAINER_ID);
if (!container) {
container = document.createElement("div");
container.id = DIALOG_CONTAINER_ID;
document.body.appendChild(container);
private static root?: Root;
private static getOrCreateRoot(): Root {
if (!ModalManager.root) {
const container = getOrCreateContainer(DIALOG_CONTAINER_ID);
ModalManager.root = createRoot(container);
}
return container;
return ModalManager.root;
}
private static getOrCreateStaticContainer(): HTMLElement {
let container = document.getElementById(STATIC_DIALOG_CONTAINER_ID);
if (!container) {
container = document.createElement("div");
container.id = STATIC_DIALOG_CONTAINER_ID;
document.body.appendChild(container);
private static staticRoot?: Root;
private static getOrCreateStaticRoot(): Root {
if (!ModalManager.staticRoot) {
const container = getOrCreateContainer(STATIC_DIALOG_CONTAINER_ID);
ModalManager.staticRoot = createRoot(container);
}
return container;
return ModalManager.staticRoot;
}
public constructor() {
@@ -132,32 +136,6 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
return !!this.priorityModal || !!this.staticModal || this.modals.length > 0;
}
public createDialog<C extends ComponentType>(
Element: C,
props?: ComponentProps<C>,
className?: string,
isPriorityModal = false,
isStaticModal = false,
options: IOptions<C> = {},
): IHandle<C> {
return this.createDialogAsync<C>(
Promise.resolve(Element),
props,
className,
isPriorityModal,
isStaticModal,
options,
);
}
public appendDialog<C extends ComponentType>(
Element: C,
props?: ComponentProps<C>,
className?: string,
): IHandle<C> {
return this.appendDialogAsync<C>(Promise.resolve(Element), props, className);
}
/**
* DEPRECATED.
* This is used only for tests. They should be using forceCloseAllModals but that
@@ -192,8 +170,13 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
this.reRender();
}
/**
* @typeParam A - the arguments to the onFinished callback
* @typeParam P - the props of the component
* @typeParam C - the component type
*/
private buildModal<C extends ComponentType>(
prom: Promise<C>,
Component: C,
props?: ComponentProps<C>,
className?: string,
options?: IOptions<C>,
@@ -218,9 +201,12 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
// otherwise we'll get confused.
const modalCount = this.counter++;
// FIXME: If a dialog uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the dialog from a button click!
modal.elem = <AsyncWrapper key={modalCount} prom={prom} {...props} onFinished={closeDialog} />;
// Typescript doesn't like us passing props as any here, but we know that they are well typed due to the rigorous generics.
modal.elem = (
<AsyncWrapper key={modalCount} onFinished={closeDialog}>
<Component {...(props as any)} onFinished={closeDialog} />
</AsyncWrapper>
);
modal.close = closeDialog;
return { modal, closeDialog, onFinishedProm };
@@ -287,29 +273,30 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
* require(['<module>'], cb);
* }
*
* @param {Promise} prom a promise which resolves with a React component
* which will be displayed as the modal view.
* @param component The component to render as a dialog. This component must accept an `onFinished` prop function as
* per the type {@link ComponentType}. If loading a component with esoteric dependencies consider
* using React.lazy to async load the component.
* e.g. `lazy(() => import('./MyComponent'))`
*
* @param {Object} props properties to pass to the displayed
* component. (We will also pass an 'onFinished' property.)
* @param props properties to pass to the displayed component. (We will also pass an 'onFinished' property.)
*
* @param {String} className CSS class to apply to the modal wrapper
* @param className CSS class to apply to the modal wrapper
*
* @param {boolean} isPriorityModal if true, this modal will be displayed regardless
* @param isPriorityModal if true, this modal will be displayed regardless
* of other modals that are currently in the stack.
* Also, when closed, all modals will be removed
* from the stack.
* @param {boolean} isStaticModal if true, this modal will be displayed under other
* @param isStaticModal if true, this modal will be displayed under other
* modals in the stack. When closed, all modals will
* also be removed from the stack. This is not compatible
* with being a priority modal. Only one modal can be
* static at a time.
* @param {Object} options? extra options for the dialog
* @param {onBeforeClose} options.onBeforeClose a callback to decide whether to close the dialog
* @returns {object} Object with 'close' parameter being a function that will close the dialog
* @param options? extra options for the dialog
* @param options.onBeforeClose a callback to decide whether to close the dialog
* @returns Object with 'close' parameter being a function that will close the dialog
*/
public createDialogAsync<C extends ComponentType>(
prom: Promise<C>,
public createDialog<C extends ComponentType>(
component: C,
props?: ComponentProps<C>,
className?: string,
isPriorityModal = false,
@@ -317,7 +304,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
options: IOptions<C> = {},
): IHandle<C> {
const beforeModal = this.getCurrentModal();
const { modal, closeDialog, onFinishedProm } = this.buildModal<C>(prom, props, className, options);
const { modal, closeDialog, onFinishedProm } = this.buildModal<C>(component, props, className, options);
if (isPriorityModal) {
// XXX: This is destructive
this.priorityModal = modal;
@@ -337,13 +324,13 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
};
}
private appendDialogAsync<C extends ComponentType>(
prom: Promise<C>,
public appendDialog<C extends ComponentType>(
component: C,
props?: ComponentProps<C>,
className?: string,
): IHandle<C> {
const beforeModal = this.getCurrentModal();
const { modal, closeDialog, onFinishedProm } = this.buildModal<C>(prom, props, className, {});
const { modal, closeDialog, onFinishedProm } = this.buildModal<C>(component, props, className, {});
this.modals.push(modal);
@@ -389,19 +376,14 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
}
private async reRender(): Promise<void> {
// TODO: We should figure out how to remove this weird sleep. It also makes testing harder
//
// await next tick because sometimes ReactDOM can race with itself and cause the modal to wrongly stick around
await sleep(0);
if (this.modals.length === 0 && !this.priorityModal && !this.staticModal) {
// If there is no modal to render, make all of Element available
// to screen reader users again
dis.dispatch({
action: "aria_unhide_main_app",
});
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer());
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer());
ModalManager.getOrCreateRoot().render(<></>);
ModalManager.getOrCreateStaticRoot().render(<></>);
return;
}
@@ -432,10 +414,10 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
</StrictMode>
);
ReactDOM.render(staticDialog, ModalManager.getOrCreateStaticContainer());
ModalManager.getOrCreateStaticRoot().render(staticDialog);
} else {
// This is safe to call repeatedly if we happen to do that
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer());
ModalManager.getOrCreateStaticRoot().render(<></>);
}
const modal = this.getCurrentModal();
@@ -461,10 +443,10 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
</StrictMode>
);
setTimeout(() => ReactDOM.render(dialog, ModalManager.getOrCreateContainer()), 0);
ModalManager.getOrCreateRoot().render(dialog);
} else {
// This is safe to call repeatedly if we happen to do that
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer());
ModalManager.getOrCreateRoot().render(<></>);
}
}
}

View File

@@ -326,7 +326,7 @@ export class PosthogAnalytics {
if (this.enabled) {
this.posthog.reset();
}
if (this.watchSettingRef) SettingsStore.unwatchSetting(this.watchSettingRef);
SettingsStore.unwatchSetting(this.watchSettingRef);
this.setAnonymity(Anonymity.Disabled);
}

View File

@@ -20,9 +20,9 @@ import { ActionPayload } from "./dispatcher/payloads";
const UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
class Presence {
private unavailableTimer: Timer | null = null;
private dispatcherRef: string | null = null;
private state: SetPresence | null = null;
private unavailableTimer?: Timer;
private dispatcherRef?: string;
private state?: SetPresence;
/**
* Start listening the user activity to evaluate his presence state.
@@ -46,14 +46,10 @@ class Presence {
* Stop tracking user activity
*/
public stop(): void {
if (this.dispatcherRef) {
dis.unregister(this.dispatcherRef);
this.dispatcherRef = null;
}
if (this.unavailableTimer) {
this.unavailableTimer.abort();
this.unavailableTimer = null;
}
dis.unregister(this.dispatcherRef);
this.dispatcherRef = undefined;
this.unavailableTimer?.abort();
this.unavailableTimer = undefined;
}
/**
@@ -61,7 +57,7 @@ class Presence {
* @returns {string} the presence state (see PRESENCE enum)
*/
public getState(): SetPresence | null {
return this.state;
return this.state ?? null;
}
private onAction = (payload: ActionPayload): void => {

View File

@@ -6,11 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { lazy } from "react";
import { ICryptoCallbacks, SecretStorage } from "matrix-js-sdk/src/matrix";
import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey } from "matrix-js-sdk/src/crypto-api";
import { logger } from "matrix-js-sdk/src/logger";
import type CreateSecretStorageDialog from "./async-components/views/dialogs/security/CreateSecretStorageDialog";
import Modal from "./Modal";
import { MatrixClientPeg } from "./MatrixClientPeg";
import { _t } from "./languageHandler";
@@ -232,10 +232,8 @@ async function doAccessSecretStorage(func: () => Promise<void>, forceReset: bool
if (createNew) {
// This dialog calls bootstrap itself after guiding the user through
// passphrase creation.
const { finished } = Modal.createDialogAsync(
import("./async-components/views/dialogs/security/CreateSecretStorageDialog") as unknown as Promise<
typeof CreateSecretStorageDialog
>,
const { finished } = Modal.createDialog(
lazy(() => import("./async-components/views/dialogs/security/CreateSecretStorageDialog")),
{
forceReset,
},

View File

@@ -11,7 +11,7 @@ import React, { createRef } from "react";
import FileSaver from "file-saver";
import { logger } from "matrix-js-sdk/src/logger";
import { AuthDict, CrossSigningKeys, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix";
import { CryptoEvent, BackupTrustInfo, GeneratedSecretStorageKey, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
import classNames from "classnames";
import CheckmarkIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
@@ -25,7 +25,6 @@ import StyledRadioButton from "../../../../components/views/elements/StyledRadio
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
import {
getSecureBackupSetupMethods,
isSecureBackupRequired,
@@ -45,7 +44,6 @@ enum Phase {
Loading = "loading",
LoadError = "load_error",
ChooseKeyPassphrase = "choose_key_passphrase",
Migrate = "migrate",
Passphrase = "passphrase",
PassphraseConfirm = "passphrase_confirm",
ShowKey = "show_key",
@@ -72,24 +70,6 @@ interface IState {
downloaded: boolean;
setPassphrase: boolean;
/** Information on the current key backup version, as returned by the server.
*
* `null` could mean any of:
* * we haven't yet requested the data from the server.
* * we were unable to reach the server.
* * the server returned key backup version data we didn't understand or was malformed.
* * there is actually no backup on the server.
*/
backupInfo: KeyBackupInfo | null;
/**
* Information on whether the backup in `backupInfo` is correctly signed, and whether we have the right key to
* decrypt it.
*
* `undefined` if `backupInfo` is null, or if crypto is not enabled in the client.
*/
backupTrustInfo: BackupTrustInfo | undefined;
// does the server offer a UI auth flow with just m.login.password
// for /keys/device_signing/upload?
canUploadKeysWithPasswordOnly: boolean | null;
@@ -137,20 +117,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
// signing key upload as well. This avoids hitting the server to
// test auth flows, which may be slow under high load.
canUploadKeysWithPasswordOnly = true;
} else {
this.queryKeyUploadAuth();
}
const keyFromCustomisations = ModuleRunner.instance.extensions.cryptoSetup.createSecretStorageKey();
const phase = keyFromCustomisations ? Phase.Loading : Phase.ChooseKeyPassphrase;
this.state = {
phase: Phase.Loading,
phase,
passPhrase: "",
passPhraseValid: false,
passPhraseConfirm: "",
copied: false,
downloaded: false,
setPassphrase: false,
backupInfo: null,
backupTrustInfo: undefined,
// does the server offer a UI auth flow with just m.login.password
// for /keys/device_signing/upload?
accountPasswordCorrect: null,
@@ -159,61 +138,23 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
passPhraseKeySelected,
accountPassword,
};
cli.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatusChange);
this.getInitialPhase();
}
public componentWillUnmount(): void {
MatrixClientPeg.get()?.removeListener(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatusChange);
}
private getInitialPhase(): void {
public componentDidMount(): void {
const keyFromCustomisations = ModuleRunner.instance.extensions.cryptoSetup.createSecretStorageKey();
if (keyFromCustomisations) {
logger.log("CryptoSetupExtension: Created key via extension, jumping to bootstrap step");
this.recoveryKey = {
privateKey: keyFromCustomisations,
};
this.bootstrapSecretStorage();
return;
}
if (keyFromCustomisations) this.initExtension(keyFromCustomisations);
this.fetchBackupInfo();
if (this.state.canUploadKeysWithPasswordOnly === null) {
this.queryKeyUploadAuth();
}
}
/**
* Attempt to get information on the current backup from the server, and update the state.
*
* Updates {@link IState.backupInfo} and {@link IState.backupTrustInfo}, and picks an appropriate phase for
* {@link IState.phase}.
*
* @returns If the backup data was retrieved successfully, the trust info for the backup. Otherwise, undefined.
*/
private async fetchBackupInfo(): Promise<BackupTrustInfo | undefined> {
try {
const cli = MatrixClientPeg.safeGet();
const backupInfo = await cli.getKeyBackupVersion();
const backupTrustInfo =
// we may not have started crypto yet, in which case we definitely don't trust the backup
backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined;
const { forceReset } = this.props;
const phase = backupInfo && !forceReset ? Phase.Migrate : Phase.ChooseKeyPassphrase;
this.setState({
phase,
backupInfo,
backupTrustInfo,
});
return backupTrustInfo;
} catch (e) {
console.error("Error fetching backup data from server", e);
this.setState({ phase: Phase.LoadError });
return undefined;
}
private initExtension(keyFromCustomisations: Uint8Array): void {
logger.log("CryptoSetupExtension: Created key via extension, jumping to bootstrap step");
this.recoveryKey = {
privateKey: keyFromCustomisations,
};
this.bootstrapSecretStorage();
}
private async queryKeyUploadAuth(): Promise<void> {
@@ -237,10 +178,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
}
}
private onKeyBackupStatusChange = (): void => {
if (this.state.phase === Phase.Migrate) this.fetchBackupInfo();
};
private onKeyPassphraseChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({
passPhraseKeySelected: e.target.value,
@@ -265,15 +202,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
}
};
private onMigrateFormSubmit = (e: React.FormEvent): void => {
e.preventDefault();
if (this.state.backupTrustInfo?.trusted) {
this.bootstrapSecretStorage();
} else {
this.restoreBackup();
}
};
private onCopyClick = (): void => {
const successful = copyNode(this.recoveryKeyNode.current);
if (successful) {
@@ -340,16 +268,28 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
};
private bootstrapSecretStorage = async (): Promise<void> => {
const cli = MatrixClientPeg.safeGet();
const crypto = cli.getCrypto()!;
const { forceReset } = this.props;
let backupInfo;
// First, unless we know we want to do a reset, we see if there is an existing key backup
if (!forceReset) {
try {
this.setState({ phase: Phase.Loading });
backupInfo = await cli.getKeyBackupVersion();
} catch (e) {
logger.error("Error fetching backup data from server", e);
this.setState({ phase: Phase.LoadError });
return;
}
}
this.setState({
phase: Phase.Storing,
error: undefined,
});
const cli = MatrixClientPeg.safeGet();
const crypto = cli.getCrypto()!;
const { forceReset } = this.props;
try {
if (forceReset) {
logger.log("Forcing secret storage reset");
@@ -371,8 +311,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
});
await crypto.bootstrapSecretStorage({
createSecretStorageKey: async () => this.recoveryKey!,
keyBackupInfo: this.state.backupInfo!,
setupNewKeyBackup: !this.state.backupInfo,
setupNewKeyBackup: !backupInfo,
});
}
await initialiseDehydration(true);
@@ -381,20 +320,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
phase: Phase.Stored,
});
} catch (e) {
if (
this.state.canUploadKeysWithPasswordOnly &&
e instanceof MatrixError &&
e.httpStatus === 401 &&
e.data.flows
) {
this.setState({
accountPassword: "",
accountPasswordCorrect: false,
phase: Phase.Migrate,
});
} else {
this.setState({ error: true });
}
this.setState({ error: true });
logger.error("Error bootstrapping secret storage", e);
}
};
@@ -403,27 +329,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
this.props.onFinished(false);
};
private restoreBackup = async (): Promise<void> => {
const { finished } = Modal.createDialog(
RestoreKeyBackupDialog,
{
showSummary: false,
},
undefined,
/* priority = */ false,
/* static = */ false,
);
await finished;
const backupTrustInfo = await this.fetchBackupInfo();
if (backupTrustInfo?.trusted && this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
this.bootstrapSecretStorage();
}
};
private onLoadRetryClick = (): void => {
this.setState({ phase: Phase.Loading });
this.fetchBackupInfo();
this.bootstrapSecretStorage();
};
private onShowKeyContinueClick = (): void => {
@@ -495,12 +402,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
});
};
private onAccountPasswordChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({
accountPassword: e.target.value,
});
};
private renderOptionKey(): JSX.Element {
return (
<StyledRadioButton
@@ -565,55 +466,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
);
}
private renderPhaseMigrate(): JSX.Element {
let authPrompt;
let nextCaption = _t("action|next");
if (this.state.canUploadKeysWithPasswordOnly) {
authPrompt = (
<div>
<div>{_t("settings|key_backup|setup_secure_backup|requires_password_confirmation")}</div>
<div>
<Field
id="mx_CreateSecretStorageDialog_password"
type="password"
label={_t("common|password")}
value={this.state.accountPassword}
onChange={this.onAccountPasswordChange}
forceValidity={this.state.accountPasswordCorrect === false ? false : undefined}
autoFocus={true}
/>
</div>
</div>
);
} else if (!this.state.backupTrustInfo?.trusted) {
authPrompt = (
<div>
<div>{_t("settings|key_backup|setup_secure_backup|requires_key_restore")}</div>
</div>
);
nextCaption = _t("action|restore");
} else {
authPrompt = <p>{_t("settings|key_backup|setup_secure_backup|requires_server_authentication")}</p>;
}
return (
<form onSubmit={this.onMigrateFormSubmit}>
<p>{_t("settings|key_backup|setup_secure_backup|session_upgrade_description")}</p>
<div>{authPrompt}</div>
<DialogButtons
primaryButton={nextCaption}
onPrimaryButtonClick={this.onMigrateFormSubmit}
hasCancel={false}
primaryDisabled={!!this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
>
<button type="button" className="danger" onClick={this.onCancelClick}>
{_t("action|skip")}
</button>
</DialogButtons>
</form>
);
}
private renderPhasePassPhrase(): JSX.Element {
return (
<form onSubmit={this.onPassPhraseNextClick}>
@@ -829,8 +681,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
switch (phase) {
case Phase.ChooseKeyPassphrase:
return _t("encryption|set_up_toast_title");
case Phase.Migrate:
return _t("settings|key_backup|setup_secure_backup|title_upgrade_encryption");
case Phase.Passphrase:
return _t("settings|key_backup|setup_secure_backup|title_set_phrase");
case Phase.PassphraseConfirm:
@@ -889,9 +739,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
case Phase.ChooseKeyPassphrase:
content = this.renderPhaseChooseKeyPassphrase();
break;
case Phase.Migrate:
content = this.renderPhaseMigrate();
break;
case Phase.Passphrase:
content = this.renderPhasePassPhrase();
break;

View File

@@ -56,6 +56,10 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
};
}
public componentDidMount(): void {
this.unmounted = false;
}
public componentWillUnmount(): void {
this.unmounted = true;
}

View File

@@ -64,6 +64,10 @@ export default class ImportE2eKeysDialog extends React.Component<IProps, IState>
};
}
public componentDidMount(): void {
this.unmounted = false;
}
public componentWillUnmount(): void {
this.unmounted = true;
}

View File

@@ -28,7 +28,7 @@ interface NewRecoveryMethodDialogProps {
onFinished(): void;
}
// Export as default instead of a named export so that it can be dynamically imported with `Modal.createDialogAsync`
// Export as default instead of a named export so that it can be dynamically imported with React lazy
/**
* Dialog to inform the user that a new recovery method has been detected.

View File

@@ -7,11 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import React, { lazy } from "react";
import dis from "../../../../dispatcher/dispatcher";
import { _t } from "../../../../languageHandler";
import Modal, { ComponentType } from "../../../../Modal";
import Modal from "../../../../Modal";
import { Action } from "../../../../dispatcher/actions";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
@@ -28,8 +28,8 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent<IPr
private onSetupClick = (): void => {
this.props.onFinished();
Modal.createDialogAsync(
import("./CreateKeyBackupDialog") as unknown as Promise<ComponentType>,
Modal.createDialog(
lazy(() => import("./CreateKeyBackupDialog")),
undefined,
undefined,
/* priority = */ false,

View File

@@ -38,7 +38,7 @@ export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>;
private unmounted = false;
private dispatcherRef: string | null = null;
private dispatcherRef?: string;
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context);
@@ -100,7 +100,7 @@ export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
public componentWillUnmount(): void {
this.unmounted = true;
if (this.dispatcherRef !== null) dis.unregister(this.dispatcherRef);
dis.unregister(this.dispatcherRef);
}
private onAction = (payload: ActionPayload): void => {

View File

@@ -90,8 +90,8 @@ interface IState {
export default class InteractiveAuthComponent<T> extends React.Component<InteractiveAuthProps<T>, IState> {
private readonly authLogic: InteractiveAuth<T>;
private readonly intervalId: number | null = null;
private readonly stageComponent = createRef<IStageComponent>();
private intervalId: number | null = null;
private unmounted = false;
@@ -126,15 +126,17 @@ export default class InteractiveAuthComponent<T> extends React.Component<Interac
AuthType.SsoUnstable,
],
});
}
public componentDidMount(): void {
this.unmounted = false;
if (this.props.poll) {
this.intervalId = window.setInterval(() => {
this.authLogic.poll();
}, 2000);
}
}
public componentDidMount(): void {
this.authLogic
.attemptAuth()
.then(async (result) => {

View File

@@ -67,10 +67,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
activeSpace: SpaceStore.instance.activeSpace,
showBreadcrumbs: LeftPanel.breadcrumbsMode,
};
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
}
private static get breadcrumbsMode(): BreadcrumbsMode {
@@ -78,6 +74,10 @@ export default class LeftPanel extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
if (this.listContainerRef.current) {
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
// Using the passive option to not block the main thread

View File

@@ -228,9 +228,9 @@ class LoggedInView extends React.Component<IProps, IState> {
this._matrixClient.removeListener(ClientEvent.Sync, this.onSync);
this._matrixClient.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
OwnProfileStore.instance.off(UPDATE_EVENT, this.refreshBackgroundImage);
if (this.layoutWatcherRef) SettingsStore.unwatchSetting(this.layoutWatcherRef);
if (this.compactLayoutWatcherRef) SettingsStore.unwatchSetting(this.compactLayoutWatcherRef);
if (this.backgroundImageWatcherRef) SettingsStore.unwatchSetting(this.backgroundImageWatcherRef);
SettingsStore.unwatchSetting(this.layoutWatcherRef);
SettingsStore.unwatchSetting(this.compactLayoutWatcherRef);
SettingsStore.unwatchSetting(this.backgroundImageWatcherRef);
this.timezoneProfileUpdateRef?.forEach((s) => SettingsStore.unwatchSetting(s));
this.resizer?.detach();
}

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { createRef } from "react";
import React, { createRef, lazy } from "react";
import {
ClientEvent,
createClient,
@@ -28,8 +28,6 @@ import { TooltipProvider } from "@vector-im/compound-web";
// what-input helps improve keyboard accessibility
import "what-input";
import type NewRecoveryMethodDialog from "../../async-components/views/dialogs/security/NewRecoveryMethodDialog";
import type RecoveryMethodRemovedDialog from "../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog";
import PosthogTrackers from "../../PosthogTrackers";
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
import { IMatrixClientCreds, MatrixClientPeg } from "../../MatrixClientPeg";
@@ -231,10 +229,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private prevWindowWidth: number;
private voiceBroadcastResumer?: VoiceBroadcastResumer;
private readonly loggedInView: React.RefObject<LoggedInViewType>;
private readonly dispatcherRef: string;
private readonly themeWatcher: ThemeWatcher;
private readonly fontWatcher: FontWatcher;
private readonly loggedInView = createRef<LoggedInViewType>();
private dispatcherRef?: string;
private themeWatcher?: ThemeWatcher;
private fontWatcher?: FontWatcher;
private readonly stores: SdkContextClass;
public constructor(props: IProps) {
@@ -256,8 +254,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
ready: false,
};
this.loggedInView = createRef();
SdkConfig.put(this.props.config);
// Used by _viewRoom before getting state from sync
@@ -282,32 +278,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
this.prevWindowWidth = UIStore.instance.windowWidth || 1000;
UIStore.instance.on(UI_EVENTS.Resize, this.handleResize);
// For PersistentElement
this.state.resizeNotifier.on("middlePanelResized", this.dispatchTimelineResize);
RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatusIndicator);
this.dispatcherRef = dis.register(this.onAction);
this.themeWatcher = new ThemeWatcher();
this.fontWatcher = new FontWatcher();
this.themeWatcher.start();
this.fontWatcher.start();
// object field used for tracking the status info appended to the title tag.
// we don't do it as react state as i'm scared about triggering needless react refreshes.
this.subTitleStatus = "";
initSentry(SdkConfig.get("sentry"));
if (!checkSessionLockFree()) {
// another instance holds the lock; confirm its theft before proceeding
setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0);
} else {
this.startInitSession();
}
}
/**
@@ -476,6 +450,29 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
public componentDidMount(): void {
UIStore.instance.on(UI_EVENTS.Resize, this.handleResize);
// For PersistentElement
this.state.resizeNotifier.on("middlePanelResized", this.dispatchTimelineResize);
RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatusIndicator);
this.dispatcherRef = dis.register(this.onAction);
this.themeWatcher = new ThemeWatcher();
this.fontWatcher = new FontWatcher();
this.themeWatcher.start();
this.fontWatcher.start();
initSentry(SdkConfig.get("sentry"));
if (!checkSessionLockFree()) {
// another instance holds the lock; confirm its theft before proceeding
setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0);
} else {
this.startInitSession();
}
window.addEventListener("resize", this.onWindowResized);
}
@@ -497,8 +494,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
public componentWillUnmount(): void {
Lifecycle.stopMatrixClient();
dis.unregister(this.dispatcherRef);
this.themeWatcher.stop();
this.fontWatcher.stop();
this.themeWatcher?.stop();
this.fontWatcher?.stop();
UIStore.destroy();
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
window.removeEventListener("resize", this.onWindowResized);
@@ -1011,7 +1008,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.setStateForNewView(newState);
ThemeController.isLogin = true;
this.themeWatcher.recheck();
this.themeWatcher?.recheck();
this.notifyNewScreen(isMobileRegistration ? "mobile_register" : "register");
}
@@ -1088,7 +1085,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
},
() => {
ThemeController.isLogin = false;
this.themeWatcher.recheck();
this.themeWatcher?.recheck();
this.notifyNewScreen("room/" + presentedId, replaceLast);
},
);
@@ -1113,7 +1110,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
this.notifyNewScreen("welcome");
ThemeController.isLogin = true;
this.themeWatcher.recheck();
this.themeWatcher?.recheck();
}
private viewLogin(otherState?: any): void {
@@ -1123,7 +1120,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
this.notifyNewScreen("login");
ThemeController.isLogin = true;
this.themeWatcher.recheck();
this.themeWatcher?.recheck();
}
private viewHome(justRegistered = false): void {
@@ -1136,7 +1133,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.setPage(PageType.HomePage);
this.notifyNewScreen("home");
ThemeController.isLogin = false;
this.themeWatcher.recheck();
this.themeWatcher?.recheck();
}
private viewUser(userId: string, subAction: string): void {
@@ -1357,7 +1354,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
*/
private async onLoggedIn(): Promise<void> {
ThemeController.isLogin = false;
this.themeWatcher.recheck();
this.themeWatcher?.recheck();
StorageManager.tryPersistStorage();
await this.onShowPostLoginScreen();
@@ -1650,16 +1647,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
if (haveNewVersion) {
Modal.createDialogAsync(
import(
"../../async-components/views/dialogs/security/NewRecoveryMethodDialog"
) as unknown as Promise<typeof NewRecoveryMethodDialog>,
Modal.createDialog(
lazy(() => import("../../async-components/views/dialogs/security/NewRecoveryMethodDialog")),
);
} else {
Modal.createDialogAsync(
import(
"../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog"
) as unknown as Promise<typeof RecoveryMethodRemovedDialog>,
Modal.createDialog(
lazy(() => import("../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog")),
);
}
});

View File

@@ -240,13 +240,13 @@ export default class MessagePanel extends React.Component<IProps, IState> {
private readReceiptsByUserId: Map<string, IReadReceiptForUser> = new Map();
private readonly _showHiddenEvents: boolean;
private isMounted = false;
private unmounted = false;
private readMarkerNode = createRef<HTMLLIElement>();
private whoIsTyping = createRef<WhoIsTypingTile>();
public scrollPanel = createRef<ScrollPanel>();
private readonly showTypingNotificationsWatcherRef: string;
private showTypingNotificationsWatcherRef?: string;
private eventTiles: Record<string, UnwrappedEventTile> = {};
// A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination.
@@ -267,22 +267,21 @@ export default class MessagePanel extends React.Component<IProps, IState> {
// and we check this in a hot code path. This is also cached in our
// RoomContext, however we still need a fallback for roomless MessagePanels.
this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline");
}
public componentDidMount(): void {
this.unmounted = false;
this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting(
"showTypingNotifications",
null,
this.onShowTypingNotificationsChange,
);
}
public componentDidMount(): void {
this.calculateRoomMembersCount();
this.props.room?.currentState.on(RoomStateEvent.Update, this.calculateRoomMembersCount);
this.isMounted = true;
}
public componentWillUnmount(): void {
this.isMounted = false;
this.unmounted = true;
this.props.room?.currentState.off(RoomStateEvent.Update, this.calculateRoomMembersCount);
SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef);
this.readReceiptMap = {};
@@ -441,7 +440,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
}
private isUnmounting = (): boolean => {
return !this.isMounted;
return this.unmounted;
};
public get showHiddenEvents(): boolean {

View File

@@ -25,7 +25,9 @@ export default class NonUrgentToastContainer extends React.PureComponent<IProps,
this.state = {
toasts: NonUrgentToastStore.instance.components,
};
}
public componentDidMount(): void {
NonUrgentToastStore.instance.on(UPDATE_EVENT, this.onUpdateToasts);
}

View File

@@ -22,11 +22,9 @@ interface IProps {
}
export default class RoomSearch extends React.PureComponent<IProps> {
private readonly dispatcherRef: string;
public constructor(props: IProps) {
super(props);
private dispatcherRef?: string;
public componentDidMount(): void {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
}

View File

@@ -103,6 +103,8 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
}
public componentDidMount(): void {
this.unmounted = false;
const client = this.context;
client.on(ClientEvent.Sync, this.onSyncStateChange);
client.on(RoomEvent.LocalEchoUpdated, this.onRoomLocalEchoUpdated);

View File

@@ -351,8 +351,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private static e2eStatusCache = new Map<string, E2EStatus>();
private readonly askToJoinEnabled: boolean;
private readonly dispatcherRef: string;
private settingWatchers: string[];
private dispatcherRef?: string;
private settingWatchers: string[] = [];
private unmounted = false;
private permalinkCreators: Record<string, RoomPermalinkCreator> = {};
@@ -418,62 +418,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
promptAskToJoin: false,
viewRoomOpts: { buttons: [] },
};
this.dispatcherRef = dis.register(this.onAction);
context.client.on(ClientEvent.Room, this.onRoom);
context.client.on(RoomEvent.Timeline, this.onRoomTimeline);
context.client.on(RoomEvent.TimelineReset, this.onRoomTimelineReset);
context.client.on(RoomEvent.Name, this.onRoomName);
context.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
context.client.on(RoomStateEvent.Update, this.onRoomStateUpdate);
context.client.on(RoomEvent.MyMembership, this.onMyMembership);
context.client.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
context.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
context.client.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
context.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
// Start listening for RoomViewStore updates
context.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
context.rightPanelStore.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
context.widgetStore.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
CallStore.instance.on(CallStoreEvent.ConnectedCalls, this.onConnectedCalls);
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
this.settingWatchers = [
SettingsStore.watchSetting("layout", null, (...[, , , value]) =>
this.setState({ layout: value as Layout }),
),
SettingsStore.watchSetting("lowBandwidth", null, (...[, , , value]) =>
this.setState({ lowBandwidth: value as boolean }),
),
SettingsStore.watchSetting("alwaysShowTimestamps", null, (...[, , , value]) =>
this.setState({ alwaysShowTimestamps: value as boolean }),
),
SettingsStore.watchSetting("showTwelveHourTimestamps", null, (...[, , , value]) =>
this.setState({ showTwelveHourTimestamps: value as boolean }),
),
SettingsStore.watchSetting(TimezoneHandler.USER_TIMEZONE_KEY, null, (...[, , , value]) =>
this.setState({ userTimezone: value as string }),
),
SettingsStore.watchSetting("readMarkerInViewThresholdMs", null, (...[, , , value]) =>
this.setState({ readMarkerInViewThresholdMs: value as number }),
),
SettingsStore.watchSetting("readMarkerOutOfViewThresholdMs", null, (...[, , , value]) =>
this.setState({ readMarkerOutOfViewThresholdMs: value as number }),
),
SettingsStore.watchSetting("showHiddenEventsInTimeline", null, (...[, , , value]) =>
this.setState({ showHiddenEvents: value as boolean }),
),
SettingsStore.watchSetting("urlPreviewsEnabled", null, this.onUrlPreviewsEnabledChange),
SettingsStore.watchSetting("urlPreviewsEnabled_e2ee", null, this.onUrlPreviewsEnabledChange),
SettingsStore.watchSetting("feature_dynamic_room_predecessors", null, (...[, , , value]) =>
this.setState({ msc3946ProcessDynamicPredecessor: value as boolean }),
),
];
}
private onIsResizing = (resizing: boolean): void => {
@@ -904,6 +848,66 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
public componentDidMount(): void {
this.unmounted = false;
this.dispatcherRef = dis.register(this.onAction);
if (this.context.client) {
this.context.client.on(ClientEvent.Room, this.onRoom);
this.context.client.on(RoomEvent.Timeline, this.onRoomTimeline);
this.context.client.on(RoomEvent.TimelineReset, this.onRoomTimelineReset);
this.context.client.on(RoomEvent.Name, this.onRoomName);
this.context.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
this.context.client.on(RoomStateEvent.Update, this.onRoomStateUpdate);
this.context.client.on(RoomEvent.MyMembership, this.onMyMembership);
this.context.client.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
this.context.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
this.context.client.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
this.context.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
}
// Start listening for RoomViewStore updates
this.context.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
this.context.rightPanelStore.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
this.context.widgetStore.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
CallStore.instance.on(CallStoreEvent.ConnectedCalls, this.onConnectedCalls);
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
this.settingWatchers = [
SettingsStore.watchSetting("layout", null, (...[, , , value]) =>
this.setState({ layout: value as Layout }),
),
SettingsStore.watchSetting("lowBandwidth", null, (...[, , , value]) =>
this.setState({ lowBandwidth: value as boolean }),
),
SettingsStore.watchSetting("alwaysShowTimestamps", null, (...[, , , value]) =>
this.setState({ alwaysShowTimestamps: value as boolean }),
),
SettingsStore.watchSetting("showTwelveHourTimestamps", null, (...[, , , value]) =>
this.setState({ showTwelveHourTimestamps: value as boolean }),
),
SettingsStore.watchSetting(TimezoneHandler.USER_TIMEZONE_KEY, null, (...[, , , value]) =>
this.setState({ userTimezone: value as string }),
),
SettingsStore.watchSetting("readMarkerInViewThresholdMs", null, (...[, , , value]) =>
this.setState({ readMarkerInViewThresholdMs: value as number }),
),
SettingsStore.watchSetting("readMarkerOutOfViewThresholdMs", null, (...[, , , value]) =>
this.setState({ readMarkerOutOfViewThresholdMs: value as number }),
),
SettingsStore.watchSetting("showHiddenEventsInTimeline", null, (...[, , , value]) =>
this.setState({ showHiddenEvents: value as boolean }),
),
SettingsStore.watchSetting("urlPreviewsEnabled", null, this.onUrlPreviewsEnabledChange),
SettingsStore.watchSetting("urlPreviewsEnabled_e2ee", null, this.onUrlPreviewsEnabledChange),
SettingsStore.watchSetting("feature_dynamic_room_predecessors", null, (...[, , , value]) =>
this.setState({ msc3946ProcessDynamicPredecessor: value as boolean }),
),
];
this.onRoomViewStoreUpdate(true);
const call = this.getCallForRoom();

View File

@@ -191,12 +191,12 @@ export default class ScrollPanel extends React.Component<IProps> {
public constructor(props: IProps) {
super(props);
this.props.resizeNotifier?.on("middlePanelResizedNoisy", this.onResize);
this.resetScrollState();
}
public componentDidMount(): void {
this.unmounted = false;
this.props.resizeNotifier?.on("middlePanelResizedNoisy", this.onResize);
this.checkScroll();
}

View File

@@ -599,7 +599,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>;
private readonly dispatcherRef: string;
private dispatcherRef?: string;
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context);
@@ -621,12 +621,11 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
showRightPanel: RightPanelStore.instance.isOpenForRoom(this.props.space.roomId),
myMembership: this.props.space.getMyMembership(),
};
this.dispatcherRef = defaultDispatcher.register(this.onAction);
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
}
public componentDidMount(): void {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
this.context.on(RoomEvent.MyMembership, this.onMyMembership);
}

View File

@@ -77,8 +77,8 @@ export default class ThreadView extends React.Component<IProps, IState> {
public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>;
private dispatcherRef: string | null = null;
private readonly layoutWatcherRef: string;
private dispatcherRef?: string;
private layoutWatcherRef?: string;
private timelinePanel = createRef<TimelinePanel>();
private card = createRef<HTMLDivElement>();
@@ -91,7 +91,6 @@ export default class ThreadView extends React.Component<IProps, IState> {
this.setEventId(this.props.mxEvent);
const thread = this.props.room.getThread(this.eventId) ?? undefined;
this.setupThreadListeners(thread);
this.state = {
layout: SettingsStore.getValue("layout"),
narrow: false,
@@ -100,13 +99,15 @@ export default class ThreadView extends React.Component<IProps, IState> {
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
}),
};
}
public componentDidMount(): void {
this.setupThreadListeners(this.state.thread);
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[, , , value]) =>
this.setState({ layout: value as Layout }),
);
}
public componentDidMount(): void {
if (this.state.thread) {
this.postThreadUpdate(this.state.thread);
}
@@ -118,7 +119,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
}
public componentWillUnmount(): void {
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
dis.unregister(this.dispatcherRef);
const roomId = this.props.mxEvent.getRoomId();
SettingsStore.unwatchSetting(this.layoutWatcherRef);

View File

@@ -248,7 +248,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
private lastRMSentEventId: string | null | undefined = undefined;
private readonly messagePanel = createRef<MessagePanel>();
private readonly dispatcherRef: string;
private dispatcherRef?: string;
private timelineWindow?: TimelineWindow;
private overlayTimelineWindow?: TimelineWindow;
private unmounted = false;
@@ -291,6 +291,10 @@ class TimelinePanel extends React.Component<IProps, IState> {
readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
};
}
public componentDidMount(): void {
this.unmounted = false;
this.dispatcherRef = dis.register(this.onAction);
const cli = MatrixClientPeg.safeGet();
@@ -312,9 +316,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
cli.on(ClientEvent.Sync, this.onSync);
this.props.timelineSet.room?.on(ThreadEvent.Update, this.onThreadUpdate);
}
public componentDidMount(): void {
if (this.props.manageReadReceipts) {
this.updateReadReceiptOnUserActivity();
}

View File

@@ -24,12 +24,11 @@ export default class ToastContainer extends React.Component<{}, IState> {
toasts: ToastStore.sharedInstance().getToasts(),
countSeen: ToastStore.sharedInstance().getCountSeen(),
};
}
// Start listening here rather than in componentDidMount because
// toasts may dismiss themselves in their didMount if they find
// they're already irrelevant by the time they're mounted, and
// our own componentDidMount is too late.
public componentDidMount(): void {
ToastStore.sharedInstance().on("update", this.onToastStoreUpdate);
this.onToastStoreUpdate();
}
public componentWillUnmount(): void {

View File

@@ -46,7 +46,7 @@ function isUploadPayload(payload: ActionPayload): payload is UploadPayload {
export default class UploadBar extends React.PureComponent<IProps, IState> {
private dispatcherRef: Optional<string>;
private mounted = false;
private unmounted = false;
public constructor(props: IProps) {
super(props);
@@ -57,12 +57,12 @@ export default class UploadBar extends React.PureComponent<IProps, IState> {
}
public componentDidMount(): void {
this.unmounted = false;
this.dispatcherRef = dis.register(this.onAction);
this.mounted = true;
}
public componentWillUnmount(): void {
this.mounted = false;
this.unmounted = true;
dis.unregister(this.dispatcherRef!);
}
@@ -83,7 +83,7 @@ export default class UploadBar extends React.PureComponent<IProps, IState> {
}
private onAction = (payload: ActionPayload): void => {
if (!this.mounted) return;
if (this.unmounted) return;
if (isUploadPayload(payload)) {
this.setState(this.calculateState());
}

View File

@@ -96,9 +96,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
selectedSpace: SpaceStore.instance.activeSpaceRoom,
showLiveAvatarAddon: this.context.voiceBroadcastRecordingsStore.hasCurrent(),
};
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
}
private get hasHomePage(): boolean {
@@ -112,6 +109,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
};
public componentDidMount(): void {
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
this.context.voiceBroadcastRecordingsStore.on(
VoiceBroadcastRecordingsStoreEvent.CurrentChanged,
this.onCurrentVoiceBroadcastRecordingChanged,
@@ -121,9 +120,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
}
public componentWillUnmount(): void {
if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef);
if (this.dndWatcherRef) SettingsStore.unwatchSetting(this.dndWatcherRef);
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
SettingsStore.unwatchSetting(this.themeWatcherRef);
SettingsStore.unwatchSetting(this.dndWatcherRef);
defaultDispatcher.unregister(this.dispatcherRef);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
this.context.voiceBroadcastRecordingsStore.off(

View File

@@ -29,11 +29,15 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
public constructor(props: IProps) {
super(props);
const store = SetupEncryptionStore.sharedInstance();
store.on("update", this.onStoreUpdate);
store.start();
this.state = { phase: store.phase, lostKeys: store.lostKeys() };
}
public componentDidMount(): void {
const store = SetupEncryptionStore.sharedInstance();
store.on("update", this.onStoreUpdate);
}
private onStoreUpdate = (): void => {
const store = SetupEncryptionStore.sharedInstance();
this.setState({ phase: store.phase, lostKeys: store.lostKeys() });

View File

@@ -134,6 +134,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
}
public componentDidMount(): void {
this.unmounted = false;
this.initLoginLogic(this.props.serverConfig);
}

View File

@@ -39,7 +39,6 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
public constructor(props: IProps) {
super(props);
const store = SetupEncryptionStore.sharedInstance();
store.on("update", this.onStoreUpdate);
store.start();
this.state = {
phase: store.phase,
@@ -52,6 +51,11 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
};
}
public componentDidMount(): void {
const store = SetupEncryptionStore.sharedInstance();
store.on("update", this.onStoreUpdate);
}
private onStoreUpdate = (): void => {
const store = SetupEncryptionStore.sharedInstance();
if (store.phase === Phase.Finished) {

View File

@@ -41,7 +41,9 @@ export default abstract class AudioPlayerBase<T extends IProps = IProps> extends
this.state = {
playbackPhase: this.props.playback.currentState,
};
}
public componentDidMount(): void {
// We don't need to de-register: the class handles this for us internally
this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate);

View File

@@ -27,10 +27,6 @@ export default class Clock extends React.Component<Props> {
formatFn: formatSeconds,
};
public constructor(props: Props) {
super(props);
}
public shouldComponentUpdate(nextProps: Readonly<Props>): boolean {
const currentFloor = Math.floor(this.props.seconds);
const nextFloor = Math.floor(nextProps.seconds);

View File

@@ -33,6 +33,9 @@ export default class DurationClock extends React.PureComponent<IProps, IState> {
// member property to track "did we get a duration".
durationSeconds: this.props.playback.clockInfo.durationSeconds,
};
}
public componentDidMount(): void {
this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate);
}

View File

@@ -26,10 +26,6 @@ type Props = Omit<ButtonProps<"div">, "title" | "onClick" | "disabled" | "elemen
* to be displayed in reference to a recording.
*/
export default class PlayPauseButton extends React.PureComponent<Props> {
public constructor(props: Props) {
super(props);
}
private onClick = (): void => {
// noinspection JSIgnoredPromiseFromCall
this.toggleState();

View File

@@ -43,6 +43,9 @@ export default class PlaybackClock extends React.PureComponent<IProps, IState> {
durationSeconds: this.props.playback.clockInfo.durationSeconds,
playbackPhase: PlaybackState.Stopped, // assume not started, so full clock
};
}
public componentDidMount(): void {
this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate);
this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate);
}

View File

@@ -34,7 +34,9 @@ export default class PlaybackWaveform extends React.PureComponent<IProps, IState
heights: this.toHeights(this.props.playback.waveform),
progress: 0, // default no progress
};
}
public componentDidMount(): void {
this.props.playback.waveformData.onUpdate(this.onWaveformUpdate);
this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate);
}

View File

@@ -55,7 +55,9 @@ export default class SeekBar extends React.PureComponent<IProps, IState> {
this.state = {
percentage: percentageOf(this.props.playback.timeSeconds, 0, this.props.playback.durationSeconds),
};
}
public componentDidMount(): void {
// We don't need to de-register: the class handles this for us internally
this.props.playback.liveData.onUpdate(() => this.animationFrameFn.mark());
}

View File

@@ -801,7 +801,6 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
this.ssoUrl = props.matrixClient.getFallbackAuthUrl(this.props.loginType, this.props.authSessionId);
this.popupWindow = null;
window.addEventListener("message", this.onReceiveMessage);
this.state = {
phase: SSOAuthEntry.PHASE_PREAUTH,
@@ -810,6 +809,7 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
}
public componentDidMount(): void {
window.addEventListener("message", this.onReceiveMessage);
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
}
@@ -918,10 +918,10 @@ export class FallbackAuthEntry<T = {}> extends React.Component<IAuthEntryProps &
// we have to make the user click a button, as browsers will block
// the popup if we open it immediately.
this.popupWindow = null;
window.addEventListener("message", this.onReceiveMessage);
}
public componentDidMount(): void {
window.addEventListener("message", this.onReceiveMessage);
this.props.onPhaseChange(DEFAULT_PHASE);
}

View File

@@ -41,10 +41,6 @@ interface Props {
export default class LoginWithQRFlow extends React.Component<Props> {
private checkCodeInput = createRef<HTMLInputElement>();
public constructor(props: Props) {
super(props);
}
private handleClick = (type: Click): ((e: React.FormEvent) => Promise<void>) => {
return async (e: React.FormEvent): Promise<void> => {
e.preventDefault();

View File

@@ -20,10 +20,6 @@ interface IProps {
* menu.
*/
export default class GenericElementContextMenu extends React.Component<IProps> {
public constructor(props: IProps) {
super(props);
}
public componentDidMount(): void {
window.addEventListener("resize", this.resize);
}

View File

@@ -17,10 +17,6 @@ interface IProps extends IContextMenuProps {
}
export default class LegacyCallContextMenu extends React.Component<IProps> {
public constructor(props: IProps) {
super(props);
}
public onHoldClick = (): void => {
this.props.call.setRemoteOnHold(true);
this.props.onFinished();

View File

@@ -507,7 +507,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
}
let jumpToRelatedEventButton: JSX.Element | undefined;
const relatedEventId = mxEvent.relationEventId;
const relatedEventId = mxEvent.getAssociatedId();
if (relatedEventId && SettingsStore.getValue("developerMode")) {
jumpToRelatedEventButton = (
<IconizedContextMenuOption

View File

@@ -64,6 +64,11 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
this.unmounted = false;
this.issueRef = React.createRef();
}
public componentDidMount(): void {
this.unmounted = false;
this.issueRef.current?.focus();
// Get all of the extra info dumped to the console when someone is about
// to send debug logs. Since this is a fire and forget action, we do
@@ -76,10 +81,6 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
});
}
public componentDidMount(): void {
this.issueRef.current?.focus();
}
public componentWillUnmount(): void {
this.unmounted = true;
}

View File

@@ -113,14 +113,6 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
nameIsValid: false,
canChangeEncryption: false,
};
checkUserIsAllowedToChangeEncryption(cli, Preset.PrivateChat).then(({ allowChange, forcedValue }) =>
this.setState((state) => ({
canChangeEncryption: allowChange,
// override with forcedValue if it is set
isEncrypted: forcedValue ?? state.isEncrypted,
})),
);
}
private roomCreateOptions(): IOpts {
@@ -160,6 +152,15 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
const cli = MatrixClientPeg.safeGet();
checkUserIsAllowedToChangeEncryption(cli, Preset.PrivateChat).then(({ allowChange, forcedValue }) =>
this.setState((state) => ({
canChangeEncryption: allowChange,
// override with forcedValue if it is set
isEncrypted: forcedValue ?? state.isEncrypted,
})),
);
// move focus to first field when showing dialog
this.nameField.current?.focus();
}

View File

@@ -58,7 +58,9 @@ export default class DeactivateAccountDialog extends React.Component<IProps, ISt
authData: null, // for UIA
authEnabled: true, // see usages for information
};
}
public componentDidMount(): void {
this.initAuth(/* shouldErase= */ false);
}

View File

@@ -63,6 +63,9 @@ export default class IncomingSasDialog extends React.Component<IProps, IState> {
opponentProfileError: null,
sas: null,
};
}
public componentDidMount(): void {
this.props.verifier.on(VerifierEvent.ShowSas, this.onVerifierShowSas);
this.props.verifier.on(VerifierEvent.Cancel, this.onVerifierCancel);
this.fetchOpponentProfile();

View File

@@ -397,6 +397,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
}
public componentDidMount(): void {
this.unmounted = false;
this.encryptionByDefault = privateShouldBeEncrypted(MatrixClientPeg.safeGet());
if (this.props.initialText) {

View File

@@ -7,12 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import React, { lazy } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import type CreateKeyBackupDialog from "../../../async-components/views/dialogs/security/CreateKeyBackupDialog";
import type ExportE2eKeysDialog from "../../../async-components/views/dialogs/security/ExportE2eKeysDialog";
import Modal from "../../../Modal";
import dis from "../../../dispatcher/dispatcher";
import { _t } from "../../../languageHandler";
@@ -81,9 +79,10 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
this.state = {
backupStatus: BackupStatus.LOADING,
};
}
// we can't call setState() immediately, so wait a beat
window.setTimeout(() => this.startLoadBackupStatus(), 0);
public componentDidMount(): void {
this.startLoadBackupStatus();
}
/** kick off the asynchronous calls to populate `state.backupStatus` in the background */
@@ -115,10 +114,8 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
}
private onExportE2eKeysClicked = (): void => {
Modal.createDialogAsync(
import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog") as unknown as Promise<
typeof ExportE2eKeysDialog
>,
Modal.createDialog(
lazy(() => import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog")),
{
matrixClient: MatrixClientPeg.safeGet(),
},
@@ -146,10 +143,8 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
/* static = */ true,
);
} else {
Modal.createDialogAsync(
import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog") as unknown as Promise<
typeof CreateKeyBackupDialog
>,
Modal.createDialog(
lazy(() => import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog")),
undefined,
undefined,
/* priority = */ false,

View File

@@ -80,9 +80,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
}
public componentWillUnmount(): void {
if (this.dispatcherRef) {
dis.unregister(this.dispatcherRef);
}
dis.unregister(this.dispatcherRef);
MatrixClientPeg.get()?.removeListener(RoomEvent.Name, this.onRoomName);
MatrixClientPeg.get()?.removeListener(RoomStateEvent.Events, this.onStateEvent);

View File

@@ -32,6 +32,9 @@ export default class VerificationRequestDialog extends React.Component<IProps, I
this.state = {
verificationRequest: this.props.verificationRequest,
};
}
public componentDidMount(): void {
this.props.verificationRequestPromise?.then((r) => {
this.setState({ verificationRequest: r });
});

View File

@@ -134,29 +134,20 @@ export default class AppTile extends React.Component<IProps, IState> {
private iframe?: HTMLIFrameElement; // ref to the iframe (callback style)
private allowedWidgetsWatchRef?: string;
private persistKey: string;
private sgWidget: StopGapWidget | null;
private sgWidget?: StopGapWidget;
private dispatcherRef?: string;
private unmounted = false;
public constructor(props: IProps, context: ContextType<typeof MatrixClientContext>) {
super(props, context);
// Tiles in miniMode are floating, and therefore not docked
if (!this.props.miniMode) {
ActiveWidgetStore.instance.dockWidget(
this.props.app.id,
isAppWidget(this.props.app) ? this.props.app.roomId : null,
);
}
// The key used for PersistedElement
this.persistKey = getPersistKey(WidgetUtils.getWidgetUid(this.props.app));
try {
this.sgWidget = new StopGapWidget(this.props);
this.setupSgListeners();
} catch (e) {
logger.log("Failed to construct widget", e);
this.sgWidget = null;
this.sgWidget = undefined;
}
this.state = this.getNewState(props);
@@ -303,6 +294,20 @@ export default class AppTile extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
this.unmounted = false;
// Tiles in miniMode are floating, and therefore not docked
if (!this.props.miniMode) {
ActiveWidgetStore.instance.dockWidget(
this.props.app.id,
isAppWidget(this.props.app) ? this.props.app.roomId : null,
);
}
if (this.sgWidget) {
this.setupSgListeners();
}
// Only fetch IM token on mount if we're showing and have permission to load
if (this.sgWidget && this.state.hasPermissionToLoad) {
this.startWidget();
@@ -340,13 +345,13 @@ export default class AppTile extends React.Component<IProps, IState> {
}
// Widget action listeners
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
dis.unregister(this.dispatcherRef);
if (this.props.room) {
this.context.off(RoomEvent.MyMembership, this.onMyMembership);
}
if (this.allowedWidgetsWatchRef) SettingsStore.unwatchSetting(this.allowedWidgetsWatchRef);
SettingsStore.unwatchSetting(this.allowedWidgetsWatchRef);
OwnProfileStore.instance.removeListener(UPDATE_EVENT, this.onUserReady);
}
@@ -374,7 +379,7 @@ export default class AppTile extends React.Component<IProps, IState> {
this.startWidget();
} catch (e) {
logger.error("Failed to construct widget", e);
this.sgWidget = null;
this.sgWidget = undefined;
}
}
@@ -607,7 +612,7 @@ export default class AppTile extends React.Component<IProps, IState> {
};
public render(): React.ReactNode {
let appTileBody: JSX.Element;
let appTileBody: JSX.Element | undefined;
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
// because that would allow the iframe to programmatically remove the sandbox attribute, but
@@ -650,7 +655,7 @@ export default class AppTile extends React.Component<IProps, IState> {
<AppWarning errorMsg={_t("widget|error_loading")} />
</div>
);
} else if (!this.state.hasPermissionToLoad && this.props.room) {
} else if (!this.state.hasPermissionToLoad && this.props.room && this.sgWidget) {
// only possible for room widgets, can assert this.props.room here
const isEncrypted = this.context.isRoomEncrypted(this.props.room.roomId);
appTileBody = (
@@ -677,7 +682,7 @@ export default class AppTile extends React.Component<IProps, IState> {
<AppWarning errorMsg={_t("widget|error_mixed_content")} />
</div>
);
} else {
} else if (this.sgWidget) {
appTileBody = (
<>
<div className={appTileBodyClass} style={appTileBodyStyles}>

View File

@@ -41,10 +41,6 @@ export interface ExistingSourceIProps {
}
export class ExistingSource extends React.Component<ExistingSourceIProps> {
public constructor(props: ExistingSourceIProps) {
super(props);
}
private onClick = (): void => {
this.props.onSelect(this.props.source);
};

View File

@@ -127,7 +127,9 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
// the current search query
searchQuery: "",
};
}
public componentDidMount(): void {
// Listen for all clicks on the document so we can close the
// menu when the user clicks somewhere else
document.addEventListener("click", this.onDocumentClick, false);

View File

@@ -15,10 +15,6 @@ interface IProps extends Omit<React.ComponentProps<typeof TextWithTooltip>, "tab
}
export default class LinkWithTooltip extends React.Component<IProps> {
public constructor(props: IProps) {
super(props);
}
public render(): React.ReactNode {
const { children, tooltip, ...props } = this.props;

View File

@@ -79,7 +79,7 @@ interface IProps {
*/
export default class PersistedElement extends React.Component<IProps> {
private resizeObserver: ResizeObserver;
private dispatcherRef: string;
private dispatcherRef?: string;
private childContainer?: HTMLDivElement;
private child?: HTMLDivElement;
@@ -87,13 +87,6 @@ export default class PersistedElement extends React.Component<IProps> {
super(props);
this.resizeObserver = new ResizeObserver(this.repositionChild);
// Annoyingly, a resize observer is insufficient, since we also care
// about when the element moves on the screen without changing its
// dimensions. Doesn't look like there's a ResizeObserver equivalent
// for this, so we bodge it by listening for document resize and
// the timeline_resize action.
window.addEventListener("resize", this.repositionChild);
this.dispatcherRef = dis.register(this.onAction);
if (this.props.moveRef) this.props.moveRef.current = this.repositionChild;
}
@@ -132,6 +125,14 @@ export default class PersistedElement extends React.Component<IProps> {
};
public componentDidMount(): void {
// Annoyingly, a resize observer is insufficient, since we also care
// about when the element moves on the screen without changing its
// dimensions. Doesn't look like there's a ResizeObserver equivalent
// for this, so we bodge it by listening for document resize and
// the timeline_resize action.
window.addEventListener("resize", this.repositionChild);
this.dispatcherRef = dis.register(this.onAction);
this.updateChild();
this.renderApp();
}

View File

@@ -68,6 +68,7 @@ export default class PowerSelector<K extends undefined | string> extends React.C
}
public componentDidMount(): void {
this.unmounted = false;
this.initStateFromProps();
}

View File

@@ -89,6 +89,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
this.unmounted = false;
this.initialize();
this.trySetExpandableQuotes();
}

View File

@@ -16,10 +16,6 @@ interface IProps extends HTMLAttributes<HTMLSpanElement> {
}
export default class TextWithTooltip extends React.Component<IProps> {
public constructor(props: IProps) {
super(props);
}
public render(): React.ReactNode {
const { className, children, tooltip, tooltipProps } = this.props;

View File

@@ -37,6 +37,9 @@ class ReactionPicker extends React.Component<IProps, IState> {
this.state = {
selectedEmojis: new Set(Object.keys(this.getReactions())),
};
}
public componentDidMount(): void {
this.addListeners();
}

View File

@@ -58,7 +58,9 @@ export default class DateSeparator extends React.Component<IProps, IState> {
this.state = {
jumpToDateEnabled: SettingsStore.getValue("feature_jump_to_date"),
};
}
public componentDidMount(): void {
// We're using a watcher so the date headers in the timeline are updated
// when the lab setting is toggled.
this.settingWatcherRef = SettingsStore.watchSetting(
@@ -71,7 +73,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
}
public componentWillUnmount(): void {
if (this.settingWatcherRef) SettingsStore.unwatchSetting(this.settingWatcherRef);
SettingsStore.unwatchSetting(this.settingWatcherRef);
}
private onContextMenuOpenClick = (e: ButtonEvent): void => {

View File

@@ -10,7 +10,7 @@ import classNames from "classnames";
import React, { forwardRef, ForwardRefExoticComponent, useContext } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api";
import { WarningIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { BlockIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler";
import { IBodyProps } from "./IBodyProps";
@@ -41,7 +41,7 @@ function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined):
case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED:
return (
<span>
<WarningIcon className="mx_Icon mx_Icon_16" />
<BlockIcon className="mx_Icon mx_Icon_16" />
{_t("timeline|decryption_failure|sender_identity_previously_verified")}
</span>
);
@@ -49,7 +49,12 @@ function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined):
case DecryptionFailureCode.UNSIGNED_SENDER_DEVICE:
// TODO: event should be hidden instead of showing this error.
// To be revisited as part of https://github.com/element-hq/element-meta/issues/2449
return _t("timeline|decryption_failure|sender_unsigned_device");
return (
<span>
<BlockIcon className="mx_Icon mx_Icon_16" />
{_t("timeline|decryption_failure|sender_unsigned_device")}
</span>
);
}
return _t("timeline|decryption_failure|unable_to_decrypt");
}
@@ -58,7 +63,8 @@ function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined):
function errorClassName(mxEvent: MatrixEvent): string | null {
switch (mxEvent.decryptionFailureReason) {
case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED:
return "mx_DecryptionFailureVerifiedIdentityChanged";
case DecryptionFailureCode.UNSIGNED_SENDER_DEVICE:
return "mx_DecryptionFailureSenderTrustRequirement";
default:
return null;

View File

@@ -59,8 +59,9 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>;
private unmounted = true;
private unmounted = false;
private image = createRef<HTMLImageElement>();
private placeholder = createRef<HTMLDivElement>();
private timeout?: number;
private sizeWatcher?: string;
@@ -367,7 +368,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
this.unmounted = true;
MatrixClientPeg.get()?.off(ClientEvent.Sync, this.reconnectedListener);
this.clearBlurhashTimeout();
if (this.sizeWatcher) SettingsStore.unwatchSetting(this.sizeWatcher);
SettingsStore.unwatchSetting(this.sizeWatcher);
if (this.state.isAnimated && this.state.thumbUrl) {
URL.revokeObjectURL(this.state.thumbUrl);
}
@@ -453,7 +454,11 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
"mx_MImageBody_placeholder--blurhash": this.props.mxEvent.getContent().info?.[BLURHASH_FIELD],
});
placeholder = <div className={classes}>{this.getPlaceholder(maxWidth, maxHeight)}</div>;
placeholder = (
<div className={classes} ref={this.placeholder}>
{this.getPlaceholder(maxWidth, maxHeight)}
</div>
);
}
let showPlaceholder = Boolean(placeholder);
@@ -499,8 +504,19 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
if (!this.props.forExport) {
placeholder = (
<SwitchTransition mode="out-in">
<CSSTransition classNames="mx_rtg--fade" key={`img-${showPlaceholder}`} timeout={300}>
{showPlaceholder ? placeholder : <></> /* Transition always expects a child */}
<CSSTransition
classNames="mx_rtg--fade"
key={`img-${showPlaceholder}`}
timeout={300}
nodeRef={this.placeholder}
>
{
showPlaceholder ? (
placeholder
) : (
<div ref={this.placeholder} />
) /* Transition always expects a child */
}
</CSSTransition>
</SwitchTransition>
);

View File

@@ -21,10 +21,6 @@ interface IProps {
}
export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
public constructor(props: IProps) {
super(props);
}
public render(): React.ReactNode {
const url = this.props.mxEvent.getContent()["url"];
const prevUrl = this.props.mxEvent.getPrevContent()["url"];

View File

@@ -75,6 +75,10 @@ export default class MLocationBody extends React.Component<IBodyProps, IState> {
this.context.on(ClientEvent.Sync, this.reconnectedListener);
};
public componentDidMount(): void {
this.unmounted = false;
}
public componentWillUnmount(): void {
this.unmounted = true;
this.context.off(ClientEvent.Sync, this.reconnectedListener);

View File

@@ -175,7 +175,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
}
public componentWillUnmount(): void {
if (this.sizeWatcher) SettingsStore.unwatchSetting(this.sizeWatcher);
SettingsStore.unwatchSetting(this.sizeWatcher);
}
private videoOnPlay = async (): Promise<void> => {

View File

@@ -100,14 +100,10 @@ export default class TimelineCard extends React.Component<IProps, IState> {
public componentWillUnmount(): void {
SdkContextClass.instance.roomViewStore.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
if (this.readReceiptsSettingWatcher) {
SettingsStore.unwatchSetting(this.readReceiptsSettingWatcher);
}
if (this.layoutWatcherRef) {
SettingsStore.unwatchSetting(this.layoutWatcherRef);
}
SettingsStore.unwatchSetting(this.readReceiptsSettingWatcher);
SettingsStore.unwatchSetting(this.layoutWatcherRef);
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
dis.unregister(this.dispatcherRef);
}
private onRoomViewStoreUpdate = async (_initial?: boolean): Promise<void> => {

View File

@@ -68,11 +68,13 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
};
this.resizer = this.createResizer();
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
}
public componentDidMount(): void {
this.unmounted = false;
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
ScalarMessaging.startListening();
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps);
this.dispatcherRef = dis.register(this.onAction);
@@ -82,7 +84,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
this.unmounted = true;
ScalarMessaging.stopListening();
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps);
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
dis.unregister(this.dispatcherRef);
if (this.resizeContainer) {
this.resizer.detach();
}

View File

@@ -128,10 +128,10 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
private lastCaret!: DocumentOffset;
private lastSelection: ReturnType<typeof cloneSelection> | null = null;
private readonly useMarkdownHandle: string;
private readonly emoticonSettingHandle: string;
private readonly shouldShowPillAvatarSettingHandle: string;
private readonly surroundWithHandle: string;
private useMarkdownHandle?: string;
private emoticonSettingHandle?: string;
private shouldShowPillAvatarSettingHandle?: string;
private surroundWithHandle?: string;
private readonly historyManager = new HistoryManager();
public constructor(props: IProps) {
@@ -145,28 +145,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
const ua = navigator.userAgent.toLowerCase();
this.isSafari = ua.includes("safari/") && !ua.includes("chrome/");
this.useMarkdownHandle = SettingsStore.watchSetting(
"MessageComposerInput.useMarkdown",
null,
this.configureUseMarkdown,
);
this.emoticonSettingHandle = SettingsStore.watchSetting(
"MessageComposerInput.autoReplaceEmoji",
null,
this.configureEmoticonAutoReplace,
);
this.configureEmoticonAutoReplace();
this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting(
"Pill.shouldShowPillAvatar",
null,
this.configureShouldShowPillAvatar,
);
this.surroundWithHandle = SettingsStore.watchSetting(
"MessageComposerInput.surroundWith",
null,
this.surroundWithSettingChanged,
);
}
public componentDidUpdate(prevProps: IProps): void {
@@ -737,6 +716,27 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
}
public componentDidMount(): void {
this.useMarkdownHandle = SettingsStore.watchSetting(
"MessageComposerInput.useMarkdown",
null,
this.configureUseMarkdown,
);
this.emoticonSettingHandle = SettingsStore.watchSetting(
"MessageComposerInput.autoReplaceEmoji",
null,
this.configureEmoticonAutoReplace,
);
this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting(
"Pill.shouldShowPillAvatar",
null,
this.configureShouldShowPillAvatar,
);
this.surroundWithHandle = SettingsStore.watchSetting(
"MessageComposerInput.surroundWith",
null,
this.surroundWithSettingChanged,
);
const model = this.props.model;
model.setUpdateCallback(this.updateEditorState);
const partCreator = model.partCreator;

View File

@@ -124,7 +124,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
public declare context: React.ContextType<typeof RoomContext>;
private readonly editorRef = createRef<BasicMessageComposer>();
private readonly dispatcherRef: string;
private dispatcherRef?: string;
private readonly replyToEvent?: MatrixEvent;
private model!: EditorModel;
@@ -140,7 +140,9 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
this.state = {
saveDisabled: !isRestored || !this.isContentModified(editContent["m.new_content"]!),
};
}
public componentDidMount(): void {
window.addEventListener("beforeunload", this.saveStoredEditorState);
this.dispatcherRef = dis.register(this.onAction);
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2024 New Vector Ltd.
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*/
import React, { HTMLProps, JSX, useContext, useState } from "react";
import { IContent, M_POLL_START, MatrixEvent, MatrixEventEvent, MsgType } from "matrix-js-sdk/src/matrix";
import classNames from "classnames";
import { _t } from "../../../languageHandler";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter.ts";
/**
* The props for the {@link EventPreview} component.
*/
interface Props extends HTMLProps<HTMLSpanElement> {
/**
* The event to display the preview for
*/
mxEvent: MatrixEvent;
}
/**
* A component that displays a preview for the given event.
* Wraps both `useEventPreview` & `EventPreviewTile`.
*/
export function EventPreview({ mxEvent, className, ...props }: Props): JSX.Element | null {
const preview = useEventPreview(mxEvent);
if (!preview) return null;
return <EventPreviewTile {...props} preview={preview} className={className} />;
}
/**
* The props for the {@link EventPreviewTile} component.
*/
interface EventPreviewTileProps extends HTMLProps<HTMLSpanElement> {
/**
* The preview to display
*/
preview: Preview;
}
/**
* A component that displays a preview given the output from `useEventPreview`.
*/
export function EventPreviewTile({
preview: [preview, prefix],
className,
...props
}: EventPreviewTileProps): JSX.Element | null {
const classes = classNames("mx_EventPreview", className);
if (!prefix)
return (
<span {...props} className={classes} title={preview}>
{preview}
</span>
);
return (
<span {...props} className={classes}>
{_t(
"event_preview|preview",
{
prefix,
preview,
},
{
bold: (sub) => <span className="mx_EventPreview_prefix">{sub}</span>,
},
)}
</span>
);
}
type Preview = [preview: string, prefix: string | null];
/**
* Hooks to generate a preview for the event.
* @param mxEvent
*/
export function useEventPreview(mxEvent: MatrixEvent | undefined): Preview | null {
const cli = useContext(MatrixClientContext);
// track the content as a means to regenerate the preview upon edits & decryption
const [content, setContent] = useState<IContent | undefined>(mxEvent?.getContent());
useTypedEventEmitter(mxEvent ?? undefined, MatrixEventEvent.Replaced, () => {
setContent(mxEvent!.getContent());
});
const awaitDecryption = mxEvent?.shouldAttemptDecryption() || mxEvent?.isBeingDecrypted();
useTypedEventEmitter(awaitDecryption ? (mxEvent ?? undefined) : undefined, MatrixEventEvent.Decrypted, () => {
setContent(mxEvent!.getContent());
});
return useAsyncMemo(
async () => {
if (!mxEvent || mxEvent.isRedacted() || mxEvent.isDecryptionFailure()) return null;
await cli.decryptEventIfNeeded(mxEvent);
return [
MessagePreviewStore.instance.generatePreviewForEvent(mxEvent),
getPreviewPrefix(mxEvent.getType(), content?.msgtype as MsgType),
];
},
[mxEvent, content],
null,
);
}
/**
* Get the prefix for the preview based on the type and the message type.
* @param type
* @param msgType
*/
function getPreviewPrefix(type: string, msgType: MsgType): string | null {
switch (type) {
case M_POLL_START.name:
return _t("event_preview|prefix|poll");
default:
}
switch (msgType) {
case MsgType.Audio:
return _t("event_preview|prefix|audio");
case MsgType.Image:
return _t("event_preview|prefix|image");
case MsgType.Video:
return _t("event_preview|prefix|video");
case MsgType.File:
return _t("event_preview|prefix|file");
default:
return null;
}
}

View File

@@ -28,6 +28,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { CallErrorCode } from "matrix-js-sdk/src/webrtc/call";
import {
CryptoEvent,
DecryptionFailureCode,
EventShieldColour,
EventShieldReason,
UserVerificationStatus,
@@ -60,7 +61,6 @@ import { IReadReceiptPosition } from "./ReadReceiptMarker";
import MessageActionBar from "../messages/MessageActionBar";
import ReactionsRow from "../messages/ReactionsRow";
import { getEventDisplayInfo } from "../../../utils/EventRenderingUtils";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
import { ButtonEvent } from "../elements/AccessibleButton";
@@ -82,6 +82,7 @@ import { EventTileThreadToolbar } from "./EventTile/EventTileThreadToolbar";
import { getLateEventInfo } from "../../structures/grouper/LateEventGrouper";
import PinningUtils from "../../../utils/PinningUtils";
import { PinnedMessageBadge } from "../messages/PinnedMessageBadge";
import { EventPreview } from "./EventPreview";
export type GetRelationsForEvent = (
eventId: string,
@@ -386,6 +387,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
}
public componentDidMount(): void {
this.unmounted = false;
this.suppressReadReceiptAnimation = false;
const client = MatrixClientPeg.safeGet();
if (!this.props.forExport) {
@@ -718,7 +720,14 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
// event could not be decrypted
if (ev.isDecryptionFailure()) {
return <E2ePadlockDecryptionFailure />;
switch (ev.decryptionFailureReason) {
// These two errors get icons from DecryptionFailureBody, so we hide the padlock icon
case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED:
case DecryptionFailureCode.UNSIGNED_SENDER_DEVICE:
return null;
default:
return <E2ePadlockDecryptionFailure />;
}
}
if (this.state.shieldColour !== EventShieldColour.NONE) {
@@ -1332,7 +1341,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
) : this.props.mxEvent.isDecryptionFailure() ? (
<DecryptionFailureBody mxEvent={this.props.mxEvent} />
) : (
MessagePreviewStore.instance.generatePreviewForEvent(this.props.mxEvent)
<EventPreview mxEvent={this.props.mxEvent} />
)}
</div>
{this.renderThreadPanelSummary()}

View File

@@ -72,7 +72,7 @@ interface IState {
export default class MemberList extends React.Component<IProps, IState> {
private readonly showPresence: boolean;
private mounted = false;
private unmounted = false;
public static contextType = SDKContext;
public declare context: React.ContextType<typeof SDKContext>;
@@ -82,8 +82,6 @@ export default class MemberList extends React.Component<IProps, IState> {
super(props, context);
this.state = this.getMembersState([], []);
this.showPresence = context?.memberListStore.isPresenceEnabled() ?? true;
this.mounted = true;
this.listenForMembersChanges();
}
private listenForMembersChanges(): void {
@@ -102,11 +100,13 @@ export default class MemberList extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
this.unmounted = false;
this.listenForMembersChanges();
this.updateListNow(true);
}
public componentWillUnmount(): void {
this.mounted = false;
this.unmounted = true;
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener(RoomStateEvent.Update, this.onRoomStateUpdate);
@@ -205,7 +205,7 @@ export default class MemberList extends React.Component<IProps, IState> {
// XXX: exported for tests
public async updateListNow(showLoadingSpinner?: boolean): Promise<void> {
if (!this.mounted) {
if (this.unmounted) {
return;
}
if (showLoadingSpinner) {
@@ -215,7 +215,7 @@ export default class MemberList extends React.Component<IProps, IState> {
this.props.roomId,
this.props.searchQuery,
);
if (!this.mounted) {
if (this.unmounted) {
return;
}
this.setState({

View File

@@ -134,9 +134,6 @@ export class MessageComposer extends React.Component<IProps, IState> {
super(props, context);
this.context = context; // otherwise React will only set it prior to render due to type def above
VoiceRecordingStore.instance.on(UPDATE_EVENT, this.onVoiceStoreUpdate);
window.addEventListener("beforeunload", this.saveWysiwygEditorState);
const isWysiwygLabEnabled = SettingsStore.getValue<boolean>("feature_wysiwyg_composer");
let isRichTextEnabled = true;
let initialComposerContent = "";
@@ -145,13 +142,6 @@ export class MessageComposer extends React.Component<IProps, IState> {
if (wysiwygState) {
isRichTextEnabled = wysiwygState.isRichText;
initialComposerContent = wysiwygState.content;
if (wysiwygState.replyEventId) {
dis.dispatch({
action: "reply_to_event",
event: this.props.room.findEventById(wysiwygState.replyEventId),
context: this.context.timelineRenderingType,
});
}
}
}
@@ -171,11 +161,6 @@ export class MessageComposer extends React.Component<IProps, IState> {
};
this.instanceId = instanceCount++;
SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null);
SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null);
SettingsStore.monitorSetting(Features.VoiceBroadcast, null);
SettingsStore.monitorSetting("feature_wysiwyg_composer", null);
}
private get editorStateKey(): string {
@@ -248,6 +233,25 @@ export class MessageComposer extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
VoiceRecordingStore.instance.on(UPDATE_EVENT, this.onVoiceStoreUpdate);
window.addEventListener("beforeunload", this.saveWysiwygEditorState);
if (this.state.isWysiwygLabEnabled) {
const wysiwygState = this.restoreWysiwygEditorState();
if (wysiwygState?.replyEventId) {
dis.dispatch({
action: "reply_to_event",
event: this.props.room.findEventById(wysiwygState.replyEventId),
context: this.context.timelineRenderingType,
});
}
}
SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null);
SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null);
SettingsStore.monitorSetting(Features.VoiceBroadcast, null);
SettingsStore.monitorSetting("feature_wysiwyg_composer", null);
this.dispatcherRef = dis.register(this.onAction);
this.waitForOwnMember();
UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current!);
@@ -331,7 +335,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
public componentWillUnmount(): void {
VoiceRecordingStore.instance.off(UPDATE_EVENT, this.onVoiceStoreUpdate);
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
dis.unregister(this.dispatcherRef);
UIStore.instance.stopTrackingElementDimensions(`MessageComposer${this.instanceId}`);
UIStore.instance.removeListener(`MessageComposer${this.instanceId}`, this.onResize);

View File

@@ -44,15 +44,23 @@ interface IState {
}
export default class NotificationBadge extends React.PureComponent<XOR<IProps, IClickableProps>, IState> {
private countWatcherRef: string;
private countWatcherRef?: string;
public constructor(props: IProps) {
super(props);
this.props.notification.on(NotificationStateEvents.Update, this.onNotificationUpdate);
this.state = {
showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId),
};
}
private get roomId(): string | null {
// We should convert this to null for safety with the SettingsStore
return this.props.roomId || null;
}
public componentDidMount(): void {
this.props.notification.on(NotificationStateEvents.Update, this.onNotificationUpdate);
this.countWatcherRef = SettingsStore.watchSetting(
"Notifications.alwaysShowBadgeCounts",
@@ -61,11 +69,6 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
);
}
private get roomId(): string | null {
// We should convert this to null for safety with the SettingsStore
return this.props.roomId || null;
}
public componentWillUnmount(): void {
SettingsStore.unwatchSetting(this.countWatcherRef);
this.props.notification.off(NotificationStateEvents.Update, this.onNotificationUpdate);

View File

@@ -6,10 +6,10 @@
* Please see LICENSE files in the repository root for full details.
*/
import React, { JSX, useEffect, useMemo, useState } from "react";
import React, { JSX, useEffect, useState } from "react";
import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin-solid";
import { Button } from "@vector-im/compound-web";
import { M_POLL_START, MatrixEvent, MsgType, Room } from "matrix-js-sdk/src/matrix";
import { Room } from "matrix-js-sdk/src/matrix";
import classNames from "classnames";
import { usePinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents";
@@ -19,12 +19,12 @@ import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePha
import { useEventEmitter } from "../../../hooks/useEventEmitter";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
import dis from "../../../dispatcher/dispatcher";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../../dispatcher/actions";
import MessageEvent from "../messages/MessageEvent";
import PosthogTrackers from "../../../PosthogTrackers.ts";
import { EventPreview } from "./EventPreview.tsx";
/**
* The props for the {@link PinnedMessageBanner} component.
@@ -105,7 +105,11 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan
)}
</div>
)}
<EventPreview pinnedEvent={pinnedEvent} />
<EventPreview
mxEvent={pinnedEvent}
className="mx_PinnedMessageBanner_message"
data-testid="banner-message"
/>
{/* In case of redacted event, we want to display the nice sentence of the message event like in the timeline or in the pinned message list */}
{shouldUseMessageEvent && (
<div className="mx_PinnedMessageBanner_redactedMessage">
@@ -124,84 +128,6 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan
);
}
/**
* The props for the {@link EventPreview} component.
*/
interface EventPreviewProps {
/**
* The pinned event to display the preview for
*/
pinnedEvent: MatrixEvent;
}
/**
* A component that displays a preview for the pinned event.
*/
function EventPreview({ pinnedEvent }: EventPreviewProps): JSX.Element | null {
const preview = useEventPreview(pinnedEvent);
if (!preview) return null;
const prefix = getPreviewPrefix(pinnedEvent.getType(), pinnedEvent.getContent().msgtype as MsgType);
if (!prefix)
return (
<span className="mx_PinnedMessageBanner_message" data-testid="banner-message">
{preview}
</span>
);
return (
<span className="mx_PinnedMessageBanner_message" data-testid="banner-message">
{_t(
"room|pinned_message_banner|preview",
{
prefix,
preview,
},
{
bold: (sub) => <span className="mx_PinnedMessageBanner_prefix">{sub}</span>,
},
)}
</span>
);
}
/**
* Hooks to generate a preview for the pinned event.
* @param pinnedEvent
*/
function useEventPreview(pinnedEvent: MatrixEvent | null): string | null {
return useMemo(() => {
if (!pinnedEvent || pinnedEvent.isRedacted() || pinnedEvent.isDecryptionFailure()) return null;
return MessagePreviewStore.instance.generatePreviewForEvent(pinnedEvent);
}, [pinnedEvent]);
}
/**
* Get the prefix for the preview based on the type and the message type.
* @param type
* @param msgType
*/
function getPreviewPrefix(type: string, msgType: MsgType): string | null {
switch (type) {
case M_POLL_START.name:
return _t("room|pinned_message_banner|prefix|poll");
default:
}
switch (msgType) {
case MsgType.Audio:
return _t("room|pinned_message_banner|prefix|audio");
case MsgType.Image:
return _t("room|pinned_message_banner|prefix|image");
case MsgType.Video:
return _t("room|pinned_message_banner|prefix|video");
case MsgType.File:
return _t("room|pinned_message_banner|prefix|file");
default:
return null;
}
}
const MAX_INDICATORS = 3;
/**

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import React, { createRef } from "react";
import { Room } from "matrix-js-sdk/src/matrix";
import { CSSTransition } from "react-transition-group";
@@ -60,7 +60,8 @@ const RoomBreadcrumbTile: React.FC<{ room: Room; onClick: (ev: ButtonEvent) => v
};
export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState> {
private isMounted = true;
private unmounted = false;
private toolbar = createRef<HTMLDivElement>();
public constructor(props: IProps) {
super(props);
@@ -69,17 +70,20 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
doAnimation: true, // technically we want animation on mount, but it won't be perfect
skipFirst: false, // render the thing, as boring as it is
};
}
public componentDidMount(): void {
this.unmounted = false;
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
}
public componentWillUnmount(): void {
this.isMounted = false;
this.unmounted = true;
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
}
private onBreadcrumbsUpdate = (): void => {
if (!this.isMounted) return;
if (this.unmounted) return;
// We need to trick the CSSTransition component into updating, which means we need to
// tell it to not animate, then to animate a moment later. This causes two updates
@@ -113,8 +117,18 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
if (tiles.length > 0) {
// NOTE: The CSSTransition timeout MUST match the timeout in our CSS!
return (
<CSSTransition appear={true} in={this.state.doAnimation} timeout={640} classNames="mx_RoomBreadcrumbs">
<Toolbar className="mx_RoomBreadcrumbs" aria-label={_t("room_list|breadcrumbs_label")}>
<CSSTransition
appear={true}
in={this.state.doAnimation}
timeout={640}
classNames="mx_RoomBreadcrumbs"
nodeRef={this.toolbar}
>
<Toolbar
className="mx_RoomBreadcrumbs"
aria-label={_t("room_list|breadcrumbs_label")}
ref={this.toolbar}
>
{tiles.slice(this.state.skipFirst ? 1 : 0)}
</Toolbar>
</CSSTransition>

View File

@@ -27,7 +27,7 @@ import { useRoomMemberCount, useRoomMembers } from "../../../hooks/useRoomMember
import { _t } from "../../../languageHandler";
import { Flex } from "../../utils/Flex";
import { Box } from "../../utils/Box";
import { getPlatformCallTypeChildren, getPlatformCallTypeLabel, useRoomCall } from "../../../hooks/room/useRoomCall";
import { getPlatformCallTypeProps, useRoomCall } from "../../../hooks/room/useRoomCall";
import { useRoomThreadNotifications } from "../../../hooks/room/useRoomThreadNotifications";
import { useGlobalNotificationState } from "../../../hooks/useGlobalNotificationState";
import SdkConfig from "../../../SdkConfig";
@@ -167,18 +167,21 @@ export default function RoomHeader({
side="left"
align="start"
>
{callOptions.map((option) => (
<MenuItem
key={option}
label={getPlatformCallTypeLabel(option)}
aria-label={getPlatformCallTypeLabel(option)}
children={getPlatformCallTypeChildren(option)}
className="mx_RoomHeader_videoCallOption"
onClick={(ev) => videoCallClick(ev, option)}
Icon={VideoCallIcon}
onSelect={() => {} /* Dummy handler since we want the click event.*/}
/>
))}
{callOptions.map((option) => {
const { label, children } = getPlatformCallTypeProps(option);
return (
<MenuItem
key={option}
label={label}
aria-label={label}
children={children}
className="mx_RoomHeader_videoCallOption"
onClick={(ev) => videoCallClick(ev, option)}
Icon={VideoCallIcon}
onSelect={() => {} /* Dummy handler since we want the click event.*/}
/>
);
})}
</Menu>
) : (
<IconButton

View File

@@ -446,7 +446,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
public componentWillUnmount(): void {
SpaceStore.instance.off(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
defaultDispatcher.unregister(this.dispatcherRef);
SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
}

View File

@@ -248,7 +248,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
}
public componentWillUnmount(): void {
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
defaultDispatcher.unregister(this.dispatcherRef);
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
RoomListStore.instance.off(LISTS_LOADING_EVENT, this.onListsLoading);
this.tilesRef.current?.removeEventListener("scroll", this.onScrollPrevent);

View File

@@ -94,7 +94,6 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
// generatePreview() will return nothing if the user has previews disabled
messagePreview: null,
};
this.generatePreview();
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
this.roomProps = EchoChamber.forRoom(this.props.room);
@@ -147,6 +146,8 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
}
public componentDidMount(): void {
this.generatePreview();
// when we're first rendered (or our sublist is expanded) make sure we are visible if we're active
if (this.state.selected) {
this.scrollIntoView();
@@ -175,7 +176,7 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
this.onRoomPreviewChanged,
);
this.props.room.off(RoomEvent.Name, this.onRoomNameUpdate);
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
defaultDispatcher.unregister(this.dispatcherRef);
this.notificationState.off(NotificationStateEvents.Update, this.onNotificationUpdate);
this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
CallStore.instance.off(CallStoreEvent.Call, this.onCallChanged);

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