mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-09 01:30:57 +00:00
Compare commits
44 Commits
v1.11.85
...
t3chguy/as
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
901921848e | ||
|
|
66cee20639 | ||
|
|
7775f10263 | ||
|
|
cb5f84cc49 | ||
|
|
8657048949 | ||
|
|
a355292a7f | ||
|
|
24fabfff89 | ||
|
|
0b6ed44390 | ||
|
|
a6ce6dc7ab | ||
|
|
aeabf3b188 | ||
|
|
c9d9c421bc | ||
|
|
d7d96b6b8b | ||
|
|
2631b908b6 | ||
|
|
502cc91dfe | ||
|
|
38e5eeea00 | ||
|
|
1ccbdb21e9 | ||
|
|
b1ef099cd6 | ||
|
|
00d46f1c8f | ||
|
|
8e304713a2 | ||
|
|
0899165d9e | ||
|
|
2d9982f9f0 | ||
|
|
b8fd98ab3c | ||
|
|
a27dfa1825 | ||
|
|
a3ece9d902 | ||
|
|
23613ac37e | ||
|
|
195337d865 | ||
|
|
4bb9f2ed7b | ||
|
|
e0ffddf3eb | ||
|
|
386b782f2a | ||
|
|
c23c9dfacb | ||
|
|
5c45ca5e3c | ||
|
|
fbc96f458c | ||
|
|
a7b3337c39 | ||
|
|
7daa8b232b | ||
|
|
7408a83618 | ||
|
|
8a743a9f89 | ||
|
|
3e8f55f883 | ||
|
|
e606b5f5d4 | ||
|
|
a4e4ac45c4 | ||
|
|
2ff51c218f | ||
|
|
76b9c27250 | ||
|
|
13bfb51d8c | ||
|
|
99542c9eda | ||
|
|
95c3027657 |
@@ -266,6 +266,9 @@ module.exports = {
|
||||
parserOptions: {
|
||||
project: ["./playwright/tsconfig.json"],
|
||||
},
|
||||
rules: {
|
||||
"react-hooks/rules-of-hooks": ["off"],
|
||||
},
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
3
.github/workflows/release_prepare.yml
vendored
3
.github/workflows/release_prepare.yml
vendored
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
20
|
||||
22
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)",
|
||||
|
||||
12
package.json
12
package.json
@@ -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",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/playwright:v1.48.0-jammy
|
||||
FROM mcr.microsoft.com/playwright:v1.48.2-jammy
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
18
res/css/views/rooms/_EventPreview.pcss
Normal file
18
res/css/views/rooms/_EventPreview.pcss
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
2
res/jitsi_external_api.min.js
vendored
2
res/jitsi_external_api.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -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,
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
136
src/Modal.tsx
136
src/Modal.tsx
@@ -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(<></>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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")),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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() });
|
||||
|
||||
@@ -134,6 +134,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.unmounted = false;
|
||||
this.initLoginLogic(this.props.serverConfig);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ export default class PowerSelector<K extends undefined | string> extends React.C
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.unmounted = false;
|
||||
this.initStateFromProps();
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.unmounted = false;
|
||||
this.initialize();
|
||||
this.trySetExpandableQuotes();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
138
src/components/views/rooms/EventPreview.tsx
Normal file
138
src/components/views/rooms/EventPreview.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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()}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user