Compare commits

...

7 Commits

Author SHA1 Message Date
Michael Telatynski
03d8b8c58f Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-30 17:55:55 +00:00
Michael Telatynski
f62de51328 Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-30 17:36:31 +00:00
Michael Telatynski
628e3afd9f Switch ModalManager to the React 18 createRoot API
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-30 17:14:30 +00:00
Michael Telatynski
c016f305e3 Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-30 15:27:29 +00:00
Michael Telatynski
eb10355542 Move state update listeners from constructor to componentDidMount
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-30 14:47:55 +00:00
Michael Telatynski
f980e1cab0 Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-30 14:46:13 +00:00
Michael Telatynski
65c46a73fe Remove boilerplate around dispatcher and settings watchers
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-30 13:53:20 +00:00
107 changed files with 520 additions and 478 deletions

View File

@@ -37,6 +37,7 @@ export default class AsyncWrapper extends React.Component<IProps, IState> {
public state: IState = {}; public state: IState = {};
public componentDidMount(): void { public componentDidMount(): void {
this.unmounted = false;
this.props.prom this.props.prom
.then((result) => { .then((result) => {
if (this.unmounted) return; if (this.unmounted) return;

View File

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

View File

@@ -8,9 +8,9 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React, { StrictMode } from "react"; import React, { StrictMode } from "react";
import ReactDOM from "react-dom"; import { createRoot, Root } from "react-dom/client";
import classNames from "classnames"; 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 { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
import { Glass, TooltipProvider } from "@vector-im/compound-web"; import { Glass, TooltipProvider } from "@vector-im/compound-web";
@@ -69,6 +69,16 @@ type HandlerMap = {
type ModalCloseReason = "backgroundClick"; 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> { export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMap> {
private counter = 0; private counter = 0;
// The modal to prioritise over all others. If this is set, only show // 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. // Neither the static nor priority modal will be in this list.
private modals: IModal<any>[] = []; private modals: IModal<any>[] = [];
private static getOrCreateContainer(): HTMLElement { private static root?: Root;
let container = document.getElementById(DIALOG_CONTAINER_ID); private static getOrCreateRoot(): Root {
if (!ModalManager.root) {
if (!container) { const container = getOrCreateContainer(DIALOG_CONTAINER_ID);
container = document.createElement("div"); ModalManager.root = createRoot(container);
container.id = DIALOG_CONTAINER_ID; }
document.body.appendChild(container); return ModalManager.root;
} }
return container; private static staticRoot?: Root;
private static getOrCreateStaticRoot(): Root {
if (!ModalManager.staticRoot) {
const container = getOrCreateContainer(STATIC_DIALOG_CONTAINER_ID);
ModalManager.staticRoot = createRoot(container);
} }
return ModalManager.staticRoot;
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);
}
return container;
} }
public constructor() { public constructor() {
@@ -389,19 +393,14 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
} }
private async reRender(): Promise<void> { 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 (this.modals.length === 0 && !this.priorityModal && !this.staticModal) {
// If there is no modal to render, make all of Element available // If there is no modal to render, make all of Element available
// to screen reader users again // to screen reader users again
dis.dispatch({ dis.dispatch({
action: "aria_unhide_main_app", action: "aria_unhide_main_app",
}); });
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer()); ModalManager.getOrCreateRoot().render(<></>);
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer()); ModalManager.getOrCreateStaticRoot().render(<></>);
return; return;
} }
@@ -432,10 +431,10 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
</StrictMode> </StrictMode>
); );
ReactDOM.render(staticDialog, ModalManager.getOrCreateStaticContainer()); ModalManager.getOrCreateStaticRoot().render(staticDialog);
} else { } else {
// This is safe to call repeatedly if we happen to do that // This is safe to call repeatedly if we happen to do that
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer()); ModalManager.getOrCreateStaticRoot().render(<></>);
} }
const modal = this.getCurrentModal(); const modal = this.getCurrentModal();
@@ -461,10 +460,10 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
</StrictMode> </StrictMode>
); );
setTimeout(() => ReactDOM.render(dialog, ModalManager.getOrCreateContainer()), 0); ModalManager.getOrCreateRoot().render(dialog);
} else { } else {
// This is safe to call repeatedly if we happen to do that // This is safe to call repeatedly if we happen to do that
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer()); ModalManager.getOrCreateRoot().render(<></>);
} }
} }
} }

View File

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

View File

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

View File

@@ -137,8 +137,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
// signing key upload as well. This avoids hitting the server to // signing key upload as well. This avoids hitting the server to
// test auth flows, which may be slow under high load. // test auth flows, which may be slow under high load.
canUploadKeysWithPasswordOnly = true; canUploadKeysWithPasswordOnly = true;
} else {
this.queryKeyUploadAuth();
} }
this.state = { this.state = {
@@ -159,7 +157,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
passPhraseKeySelected, passPhraseKeySelected,
accountPassword, accountPassword,
}; };
}
public componentDidMount(): void {
if (this.state.canUploadKeysWithPasswordOnly === null) {
this.queryKeyUploadAuth();
}
const cli = MatrixClientPeg.safeGet();
cli.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatusChange); cli.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatusChange);
this.getInitialPhase(); this.getInitialPhase();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -231,10 +231,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private prevWindowWidth: number; private prevWindowWidth: number;
private voiceBroadcastResumer?: VoiceBroadcastResumer; private voiceBroadcastResumer?: VoiceBroadcastResumer;
private readonly loggedInView: React.RefObject<LoggedInViewType>; private readonly loggedInView = createRef<LoggedInViewType>();
private readonly dispatcherRef: string; private dispatcherRef?: string;
private readonly themeWatcher: ThemeWatcher; private themeWatcher?: ThemeWatcher;
private readonly fontWatcher: FontWatcher; private fontWatcher?: FontWatcher;
private readonly stores: SdkContextClass; private readonly stores: SdkContextClass;
public constructor(props: IProps) { public constructor(props: IProps) {
@@ -256,8 +256,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
ready: false, ready: false,
}; };
this.loggedInView = createRef();
SdkConfig.put(this.props.config); SdkConfig.put(this.props.config);
// Used by _viewRoom before getting state from sync // Used by _viewRoom before getting state from sync
@@ -282,32 +280,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
this.prevWindowWidth = UIStore.instance.windowWidth || 1000; 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. // 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. // we don't do it as react state as i'm scared about triggering needless react refreshes.
this.subTitleStatus = ""; 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 +452,29 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
public componentDidMount(): void { 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); window.addEventListener("resize", this.onWindowResized);
} }
@@ -497,8 +496,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
public componentWillUnmount(): void { public componentWillUnmount(): void {
Lifecycle.stopMatrixClient(); Lifecycle.stopMatrixClient();
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
this.themeWatcher.stop(); this.themeWatcher?.stop();
this.fontWatcher.stop(); this.fontWatcher?.stop();
UIStore.destroy(); UIStore.destroy();
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize); this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
window.removeEventListener("resize", this.onWindowResized); window.removeEventListener("resize", this.onWindowResized);
@@ -1011,7 +1010,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.setStateForNewView(newState); this.setStateForNewView(newState);
ThemeController.isLogin = true; ThemeController.isLogin = true;
this.themeWatcher.recheck(); this.themeWatcher?.recheck();
this.notifyNewScreen(isMobileRegistration ? "mobile_register" : "register"); this.notifyNewScreen(isMobileRegistration ? "mobile_register" : "register");
} }
@@ -1088,7 +1087,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}, },
() => { () => {
ThemeController.isLogin = false; ThemeController.isLogin = false;
this.themeWatcher.recheck(); this.themeWatcher?.recheck();
this.notifyNewScreen("room/" + presentedId, replaceLast); this.notifyNewScreen("room/" + presentedId, replaceLast);
}, },
); );
@@ -1113,7 +1112,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}); });
this.notifyNewScreen("welcome"); this.notifyNewScreen("welcome");
ThemeController.isLogin = true; ThemeController.isLogin = true;
this.themeWatcher.recheck(); this.themeWatcher?.recheck();
} }
private viewLogin(otherState?: any): void { private viewLogin(otherState?: any): void {
@@ -1123,7 +1122,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}); });
this.notifyNewScreen("login"); this.notifyNewScreen("login");
ThemeController.isLogin = true; ThemeController.isLogin = true;
this.themeWatcher.recheck(); this.themeWatcher?.recheck();
} }
private viewHome(justRegistered = false): void { private viewHome(justRegistered = false): void {
@@ -1136,7 +1135,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.setPage(PageType.HomePage); this.setPage(PageType.HomePage);
this.notifyNewScreen("home"); this.notifyNewScreen("home");
ThemeController.isLogin = false; ThemeController.isLogin = false;
this.themeWatcher.recheck(); this.themeWatcher?.recheck();
} }
private viewUser(userId: string, subAction: string): void { private viewUser(userId: string, subAction: string): void {
@@ -1357,7 +1356,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
*/ */
private async onLoggedIn(): Promise<void> { private async onLoggedIn(): Promise<void> {
ThemeController.isLogin = false; ThemeController.isLogin = false;
this.themeWatcher.recheck(); this.themeWatcher?.recheck();
StorageManager.tryPersistStorage(); StorageManager.tryPersistStorage();
await this.onShowPostLoginScreen(); await this.onShowPostLoginScreen();

View File

@@ -240,13 +240,13 @@ export default class MessagePanel extends React.Component<IProps, IState> {
private readReceiptsByUserId: Map<string, IReadReceiptForUser> = new Map(); private readReceiptsByUserId: Map<string, IReadReceiptForUser> = new Map();
private readonly _showHiddenEvents: boolean; private readonly _showHiddenEvents: boolean;
private isMounted = false; private unmounted = false;
private readMarkerNode = createRef<HTMLLIElement>(); private readMarkerNode = createRef<HTMLLIElement>();
private whoIsTyping = createRef<WhoIsTypingTile>(); private whoIsTyping = createRef<WhoIsTypingTile>();
public scrollPanel = createRef<ScrollPanel>(); public scrollPanel = createRef<ScrollPanel>();
private readonly showTypingNotificationsWatcherRef: string; private showTypingNotificationsWatcherRef?: string;
private eventTiles: Record<string, UnwrappedEventTile> = {}; 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. // 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 // 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. // RoomContext, however we still need a fallback for roomless MessagePanels.
this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline"); this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline");
}
public componentDidMount(): void {
this.unmounted = false;
this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting( this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting(
"showTypingNotifications", "showTypingNotifications",
null, null,
this.onShowTypingNotificationsChange, this.onShowTypingNotificationsChange,
); );
}
public componentDidMount(): void {
this.calculateRoomMembersCount(); this.calculateRoomMembersCount();
this.props.room?.currentState.on(RoomStateEvent.Update, this.calculateRoomMembersCount); this.props.room?.currentState.on(RoomStateEvent.Update, this.calculateRoomMembersCount);
this.isMounted = true;
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
this.isMounted = false; this.unmounted = true;
this.props.room?.currentState.off(RoomStateEvent.Update, this.calculateRoomMembersCount); this.props.room?.currentState.off(RoomStateEvent.Update, this.calculateRoomMembersCount);
SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef);
this.readReceiptMap = {}; this.readReceiptMap = {};
@@ -441,7 +440,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
} }
private isUnmounting = (): boolean => { private isUnmounting = (): boolean => {
return !this.isMounted; return this.unmounted;
}; };
public get showHiddenEvents(): boolean { public get showHiddenEvents(): boolean {

View File

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

View File

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

View File

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

View File

@@ -351,8 +351,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private static e2eStatusCache = new Map<string, E2EStatus>(); private static e2eStatusCache = new Map<string, E2EStatus>();
private readonly askToJoinEnabled: boolean; private readonly askToJoinEnabled: boolean;
private readonly dispatcherRef: string; private dispatcherRef?: string;
private settingWatchers: string[]; private settingWatchers: string[] = [];
private unmounted = false; private unmounted = false;
private permalinkCreators: Record<string, RoomPermalinkCreator> = {}; private permalinkCreators: Record<string, RoomPermalinkCreator> = {};
@@ -418,62 +418,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
promptAskToJoin: false, promptAskToJoin: false,
viewRoomOpts: { buttons: [] }, 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 => { private onIsResizing = (resizing: boolean): void => {
@@ -904,6 +848,66 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
} }
public componentDidMount(): void { 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); this.onRoomViewStoreUpdate(true);
const call = this.getCallForRoom(); const call = this.getCallForRoom();

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,12 +24,11 @@ export default class ToastContainer extends React.Component<{}, IState> {
toasts: ToastStore.sharedInstance().getToasts(), toasts: ToastStore.sharedInstance().getToasts(),
countSeen: ToastStore.sharedInstance().getCountSeen(), countSeen: ToastStore.sharedInstance().getCountSeen(),
}; };
}
// Start listening here rather than in componentDidMount because public componentDidMount(): void {
// 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.
ToastStore.sharedInstance().on("update", this.onToastStoreUpdate); ToastStore.sharedInstance().on("update", this.onToastStoreUpdate);
this.onToastStoreUpdate();
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -64,6 +64,11 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
this.unmounted = false; this.unmounted = false;
this.issueRef = React.createRef(); 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 // 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 // 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 { public componentWillUnmount(): void {
this.unmounted = true; this.unmounted = true;
} }

View File

@@ -113,14 +113,6 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
nameIsValid: false, nameIsValid: false,
canChangeEncryption: 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 { private roomCreateOptions(): IOpts {
@@ -160,6 +152,15 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
} }
public componentDidMount(): void { 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 // move focus to first field when showing dialog
this.nameField.current?.focus(); this.nameField.current?.focus();
} }

View File

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

View File

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

View File

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

View File

@@ -81,9 +81,10 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
this.state = { this.state = {
backupStatus: BackupStatus.LOADING, backupStatus: BackupStatus.LOADING,
}; };
}
// we can't call setState() immediately, so wait a beat public componentDidMount(): void {
window.setTimeout(() => this.startLoadBackupStatus(), 0); this.startLoadBackupStatus();
} }
/** kick off the asynchronous calls to populate `state.backupStatus` in the background */ /** kick off the asynchronous calls to populate `state.backupStatus` in the background */

View File

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

View File

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

View File

@@ -134,29 +134,20 @@ export default class AppTile extends React.Component<IProps, IState> {
private iframe?: HTMLIFrameElement; // ref to the iframe (callback style) private iframe?: HTMLIFrameElement; // ref to the iframe (callback style)
private allowedWidgetsWatchRef?: string; private allowedWidgetsWatchRef?: string;
private persistKey: string; private persistKey: string;
private sgWidget: StopGapWidget | null; private sgWidget?: StopGapWidget;
private dispatcherRef?: string; private dispatcherRef?: string;
private unmounted = false; private unmounted = false;
public constructor(props: IProps, context: ContextType<typeof MatrixClientContext>) { public constructor(props: IProps, context: ContextType<typeof MatrixClientContext>) {
super(props, context); 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 // The key used for PersistedElement
this.persistKey = getPersistKey(WidgetUtils.getWidgetUid(this.props.app)); this.persistKey = getPersistKey(WidgetUtils.getWidgetUid(this.props.app));
try { try {
this.sgWidget = new StopGapWidget(this.props); this.sgWidget = new StopGapWidget(this.props);
this.setupSgListeners();
} catch (e) { } catch (e) {
logger.log("Failed to construct widget", e); logger.log("Failed to construct widget", e);
this.sgWidget = null; this.sgWidget = undefined;
} }
this.state = this.getNewState(props); this.state = this.getNewState(props);
@@ -303,6 +294,20 @@ export default class AppTile extends React.Component<IProps, IState> {
} }
public componentDidMount(): void { 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 // Only fetch IM token on mount if we're showing and have permission to load
if (this.sgWidget && this.state.hasPermissionToLoad) { if (this.sgWidget && this.state.hasPermissionToLoad) {
this.startWidget(); this.startWidget();
@@ -340,13 +345,13 @@ export default class AppTile extends React.Component<IProps, IState> {
} }
// Widget action listeners // Widget action listeners
if (this.dispatcherRef) dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
if (this.props.room) { if (this.props.room) {
this.context.off(RoomEvent.MyMembership, this.onMyMembership); 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); OwnProfileStore.instance.removeListener(UPDATE_EVENT, this.onUserReady);
} }
@@ -374,7 +379,7 @@ export default class AppTile extends React.Component<IProps, IState> {
this.startWidget(); this.startWidget();
} catch (e) { } catch (e) {
logger.error("Failed to construct widget", 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 { 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 // 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 // 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")} /> <AppWarning errorMsg={_t("widget|error_loading")} />
</div> </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 // only possible for room widgets, can assert this.props.room here
const isEncrypted = this.context.isRoomEncrypted(this.props.room.roomId); const isEncrypted = this.context.isRoomEncrypted(this.props.room.roomId);
appTileBody = ( appTileBody = (
@@ -677,7 +682,7 @@ export default class AppTile extends React.Component<IProps, IState> {
<AppWarning errorMsg={_t("widget|error_mixed_content")} /> <AppWarning errorMsg={_t("widget|error_mixed_content")} />
</div> </div>
); );
} else { } else if (this.sgWidget) {
appTileBody = ( appTileBody = (
<> <>
<div className={appTileBodyClass} style={appTileBodyStyles}> <div className={appTileBodyClass} style={appTileBodyStyles}>

View File

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

View File

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

View File

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

View File

@@ -79,7 +79,7 @@ interface IProps {
*/ */
export default class PersistedElement extends React.Component<IProps> { export default class PersistedElement extends React.Component<IProps> {
private resizeObserver: ResizeObserver; private resizeObserver: ResizeObserver;
private dispatcherRef: string; private dispatcherRef?: string;
private childContainer?: HTMLDivElement; private childContainer?: HTMLDivElement;
private child?: HTMLDivElement; private child?: HTMLDivElement;
@@ -87,13 +87,6 @@ export default class PersistedElement extends React.Component<IProps> {
super(props); super(props);
this.resizeObserver = new ResizeObserver(this.repositionChild); 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; 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 { 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.updateChild();
this.renderApp(); this.renderApp();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,7 +59,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; public declare context: React.ContextType<typeof RoomContext>;
private unmounted = true; private unmounted = false;
private image = createRef<HTMLImageElement>(); private image = createRef<HTMLImageElement>();
private timeout?: number; private timeout?: number;
private sizeWatcher?: string; private sizeWatcher?: string;
@@ -367,7 +367,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
this.unmounted = true; this.unmounted = true;
MatrixClientPeg.get()?.off(ClientEvent.Sync, this.reconnectedListener); MatrixClientPeg.get()?.off(ClientEvent.Sync, this.reconnectedListener);
this.clearBlurhashTimeout(); this.clearBlurhashTimeout();
if (this.sizeWatcher) SettingsStore.unwatchSetting(this.sizeWatcher); SettingsStore.unwatchSetting(this.sizeWatcher);
if (this.state.isAnimated && this.state.thumbUrl) { if (this.state.isAnimated && this.state.thumbUrl) {
URL.revokeObjectURL(this.state.thumbUrl); URL.revokeObjectURL(this.state.thumbUrl);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -386,6 +386,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
} }
public componentDidMount(): void { public componentDidMount(): void {
this.unmounted = false;
this.suppressReadReceiptAnimation = false; this.suppressReadReceiptAnimation = false;
const client = MatrixClientPeg.safeGet(); const client = MatrixClientPeg.safeGet();
if (!this.props.forExport) { if (!this.props.forExport) {

View File

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

View File

@@ -134,9 +134,6 @@ export class MessageComposer extends React.Component<IProps, IState> {
super(props, context); super(props, context);
this.context = context; // otherwise React will only set it prior to render due to type def above 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"); const isWysiwygLabEnabled = SettingsStore.getValue<boolean>("feature_wysiwyg_composer");
let isRichTextEnabled = true; let isRichTextEnabled = true;
let initialComposerContent = ""; let initialComposerContent = "";
@@ -145,13 +142,6 @@ export class MessageComposer extends React.Component<IProps, IState> {
if (wysiwygState) { if (wysiwygState) {
isRichTextEnabled = wysiwygState.isRichText; isRichTextEnabled = wysiwygState.isRichText;
initialComposerContent = wysiwygState.content; 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++; 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 { private get editorStateKey(): string {
@@ -248,6 +233,25 @@ export class MessageComposer extends React.Component<IProps, IState> {
} }
public componentDidMount(): void { 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.dispatcherRef = dis.register(this.onAction);
this.waitForOwnMember(); this.waitForOwnMember();
UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current!); UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current!);
@@ -331,7 +335,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
public componentWillUnmount(): void { public componentWillUnmount(): void {
VoiceRecordingStore.instance.off(UPDATE_EVENT, this.onVoiceStoreUpdate); 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.stopTrackingElementDimensions(`MessageComposer${this.instanceId}`);
UIStore.instance.removeListener(`MessageComposer${this.instanceId}`, this.onResize); UIStore.instance.removeListener(`MessageComposer${this.instanceId}`, this.onResize);

View File

@@ -44,15 +44,23 @@ interface IState {
} }
export default class NotificationBadge extends React.PureComponent<XOR<IProps, IClickableProps>, IState> { export default class NotificationBadge extends React.PureComponent<XOR<IProps, IClickableProps>, IState> {
private countWatcherRef: string; private countWatcherRef?: string;
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
this.props.notification.on(NotificationStateEvents.Update, this.onNotificationUpdate);
this.state = { this.state = {
showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId), 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( this.countWatcherRef = SettingsStore.watchSetting(
"Notifications.alwaysShowBadgeCounts", "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 { public componentWillUnmount(): void {
SettingsStore.unwatchSetting(this.countWatcherRef); SettingsStore.unwatchSetting(this.countWatcherRef);
this.props.notification.off(NotificationStateEvents.Update, this.onNotificationUpdate); this.props.notification.off(NotificationStateEvents.Update, this.onNotificationUpdate);

View File

@@ -60,7 +60,7 @@ const RoomBreadcrumbTile: React.FC<{ room: Room; onClick: (ev: ButtonEvent) => v
}; };
export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState> { export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState> {
private isMounted = true; private unmounted = false;
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
@@ -69,17 +69,20 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
doAnimation: true, // technically we want animation on mount, but it won't be perfect doAnimation: true, // technically we want animation on mount, but it won't be perfect
skipFirst: false, // render the thing, as boring as it is skipFirst: false, // render the thing, as boring as it is
}; };
}
public componentDidMount(): void {
this.unmounted = false;
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
this.isMounted = false; this.unmounted = true;
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
} }
private onBreadcrumbsUpdate = (): void => { 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 // 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 // tell it to not animate, then to animate a moment later. This causes two updates

View File

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

View File

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

View File

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

View File

@@ -255,7 +255,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
private readonly editorRef = createRef<BasicMessageComposer>(); private readonly editorRef = createRef<BasicMessageComposer>();
private model: EditorModel; private model: EditorModel;
private currentlyComposedEditorState: SerializedPart[] | null = null; private currentlyComposedEditorState: SerializedPart[] | null = null;
private dispatcherRef: string; private dispatcherRef?: string;
private sendHistoryManager: SendHistoryManager; private sendHistoryManager: SendHistoryManager;
public static defaultProps = { public static defaultProps = {
@@ -275,15 +275,17 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
); );
} }
window.addEventListener("beforeunload", this.saveStoredEditorState);
const partCreator = new CommandPartCreator(this.props.room, this.props.mxClient); const partCreator = new CommandPartCreator(this.props.room, this.props.mxClient);
const parts = this.restoreStoredEditorState(partCreator) || []; const parts = this.restoreStoredEditorState(partCreator) || [];
this.model = new EditorModel(parts, partCreator); this.model = new EditorModel(parts, partCreator);
this.dispatcherRef = dis.register(this.onAction);
this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, "mx_cider_history_"); this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, "mx_cider_history_");
} }
public componentDidMount(): void {
window.addEventListener("beforeunload", this.saveStoredEditorState);
this.dispatcherRef = dis.register(this.onAction);
}
public componentDidUpdate(prevProps: ISendMessageComposerProps): void { public componentDidUpdate(prevProps: ISendMessageComposerProps): void {
const replyingToThread = this.props.relation?.key === THREAD_RELATION_TYPE.name; const replyingToThread = this.props.relation?.key === THREAD_RELATION_TYPE.name;
const differentEventTarget = this.props.relation?.event_id !== prevProps.relation?.event_id; const differentEventTarget = this.props.relation?.event_id !== prevProps.relation?.event_id;

View File

@@ -141,10 +141,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
if (client) client.removeListener(ClientEvent.AccountData, this.updateWidget); if (client) client.removeListener(ClientEvent.AccountData, this.updateWidget);
RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
window.removeEventListener("resize", this.onResize); window.removeEventListener("resize", this.onResize);
if (this.dispatcherRef) {
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
} }
}
public componentDidUpdate(): void { public componentDidUpdate(): void {
this.sendVisibilityToWidget(this.props.isStickerPickerOpen); this.sendVisibilityToWidget(this.props.isStickerPickerOpen);

View File

@@ -45,6 +45,7 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
} }
public componentDidMount(): void { public componentDidMount(): void {
this.unmounted = false;
const cli = MatrixClientPeg.safeGet(); const cli = MatrixClientPeg.safeGet();
cli.on(ClientEvent.AccountData, this.onAccountData); cli.on(ClientEvent.AccountData, this.onAccountData);
cli.on(CryptoEvent.UserTrustStatusChanged, this.onStatusChanged); cli.on(CryptoEvent.UserTrustStatusChanged, this.onStatusChanged);

View File

@@ -11,7 +11,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import type ExportE2eKeysDialog from "../../../async-components/views/dialogs/security/ExportE2eKeysDialog"; import type ExportE2eKeysDialog from "../../../async-components/views/dialogs/security/ExportE2eKeysDialog";
import type ImportE2eKeysDialog from "../../../async-components/views/dialogs/security/ImportE2eKeysDialog"; import type ImportE2eKeysDialog from "../../../async-components/views/dialogs/security/ImportE2eKeysDialog";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
@@ -20,6 +19,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import SettingsFlag from "../elements/SettingsFlag"; import SettingsFlag from "../elements/SettingsFlag";
import { SettingLevel } from "../../../settings/SettingLevel"; import { SettingLevel } from "../../../settings/SettingLevel";
import SettingsSubsection, { SettingsSubsectionText } from "./shared/SettingsSubsection"; import SettingsSubsection, { SettingsSubsectionText } from "./shared/SettingsSubsection";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps {} interface IProps {}
@@ -33,17 +33,24 @@ interface IState {
} }
export default class CryptographyPanel extends React.Component<IProps, IState> { export default class CryptographyPanel extends React.Component<IProps, IState> {
public constructor(props: IProps) { public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>;
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props); super(props);
const client = MatrixClientPeg.safeGet(); if (!context.getCrypto()) {
const crypto = client.getCrypto();
if (!crypto) {
this.state = { deviceIdentityKey: null }; this.state = { deviceIdentityKey: null };
} else { } else {
this.state = { deviceIdentityKey: undefined }; this.state = { deviceIdentityKey: undefined };
crypto }
.getOwnDeviceKeys() }
public componentDidMount(): void {
if (this.state.deviceIdentityKey === undefined) {
this.context
.getCrypto()
?.getOwnDeviceKeys()
.then((keys) => { .then((keys) => {
this.setState({ deviceIdentityKey: keys.ed25519 }); this.setState({ deviceIdentityKey: keys.ed25519 });
}) })
@@ -55,7 +62,7 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
} }
public render(): React.ReactNode { public render(): React.ReactNode {
const client = MatrixClientPeg.safeGet(); const client = this.context;
const deviceId = client.deviceId; const deviceId = client.deviceId;
let identityKey = this.state.deviceIdentityKey; let identityKey = this.state.deviceIdentityKey;
if (identityKey === undefined) { if (identityKey === undefined) {
@@ -126,7 +133,7 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog") as unknown as Promise< import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog") as unknown as Promise<
typeof ExportE2eKeysDialog typeof ExportE2eKeysDialog
>, >,
{ matrixClient: MatrixClientPeg.safeGet() }, { matrixClient: this.context },
); );
}; };
@@ -135,12 +142,12 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
import("../../../async-components/views/dialogs/security/ImportE2eKeysDialog") as unknown as Promise< import("../../../async-components/views/dialogs/security/ImportE2eKeysDialog") as unknown as Promise<
typeof ImportE2eKeysDialog typeof ImportE2eKeysDialog
>, >,
{ matrixClient: MatrixClientPeg.safeGet() }, { matrixClient: this.context },
); );
}; };
private updateBlacklistDevicesFlag = (checked: boolean): void => { private updateBlacklistDevicesFlag = (checked: boolean): void => {
const crypto = MatrixClientPeg.safeGet().getCrypto(); const crypto = this.context.getCrypto();
if (crypto) crypto.globalBlacklistUnverifiedDevices = checked; if (crypto) crypto.globalBlacklistUnverifiedDevices = checked;
}; };
} }

View File

@@ -55,6 +55,7 @@ export default class FontScalingPanel extends React.Component<IProps, IState> {
} }
public async componentDidMount(): Promise<void> { public async componentDidMount(): Promise<void> {
this.unmounted = false;
// Fetch the current user profile for the message preview // Fetch the current user profile for the message preview
const client = MatrixClientPeg.safeGet(); const client = MatrixClientPeg.safeGet();
const userId = client.getSafeUserId(); const userId = client.getSafeUserId();
@@ -79,10 +80,8 @@ export default class FontScalingPanel extends React.Component<IProps, IState> {
public componentWillUnmount(): void { public componentWillUnmount(): void {
this.unmounted = true; this.unmounted = true;
if (this.layoutWatcherRef) {
SettingsStore.unwatchSetting(this.layoutWatcherRef); SettingsStore.unwatchSetting(this.layoutWatcherRef);
} }
}
/** /**
* Save the new font size * Save the new font size

View File

@@ -52,7 +52,7 @@ export default class IntegrationManager extends React.Component<IProps, IState>
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
if (this.dispatcherRef) dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
document.removeEventListener("keydown", this.onKeyDown); document.removeEventListener("keydown", this.onKeyDown);
} }

View File

@@ -206,7 +206,7 @@ const NotificationActivitySettings = (): JSX.Element => {
* The old, deprecated notifications tab view, only displayed if the user has the labs flag disabled. * The old, deprecated notifications tab view, only displayed if the user has the labs flag disabled.
*/ */
export default class Notifications extends React.PureComponent<IProps, IState> { export default class Notifications extends React.PureComponent<IProps, IState> {
private settingWatchers: string[]; private settingWatchers: string[] = [];
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
@@ -220,7 +220,17 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
clearingNotifications: false, clearingNotifications: false,
ruleIdsWithError: {}, ruleIdsWithError: {},
}; };
}
private get isInhibited(): boolean {
// Caution: The master rule's enabled state is inverted from expectation. When
// the master rule is *enabled* it means all other rules are *disabled* (or
// inhibited). Conversely, when the master rule is *disabled* then all other rules
// are *enabled* (or operate fine).
return !!this.state.masterPushRule?.enabled;
}
public componentDidMount(): void {
this.settingWatchers = [ this.settingWatchers = [
SettingsStore.watchSetting("notificationsEnabled", null, (...[, , , , value]) => SettingsStore.watchSetting("notificationsEnabled", null, (...[, , , , value]) =>
this.setState({ desktopNotifications: value as boolean }), this.setState({ desktopNotifications: value as boolean }),
@@ -235,17 +245,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
this.setState({ audioNotifications: value as boolean }), this.setState({ audioNotifications: value as boolean }),
), ),
]; ];
}
private get isInhibited(): boolean {
// Caution: The master rule's enabled state is inverted from expectation. When
// the master rule is *enabled* it means all other rules are *disabled* (or
// inhibited). Conversely, when the master rule is *disabled* then all other rules
// are *enabled* (or operate fine).
return !!this.state.masterPushRule?.enabled;
}
public componentDidMount(): void {
// noinspection JSIgnoredPromiseFromCall // noinspection JSIgnoredPromiseFromCall
this.refreshFromServer(); this.refreshFromServer();
this.refreshFromAccountData(); this.refreshFromAccountData();

View File

@@ -83,6 +83,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
} }
public componentDidMount(): void { public componentDidMount(): void {
this.unmounted = false;
this.loadBackupStatus(); this.loadBackupStatus();
MatrixClientPeg.safeGet().on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus); MatrixClientPeg.safeGet().on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);

View File

@@ -101,7 +101,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
if (this.dispatcherRef) dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
} }
private onAction = (payload: ActionPayload): void => { private onAction = (payload: ActionPayload): void => {

View File

@@ -53,9 +53,11 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors");
this.state = {}; this.state = {};
}
public componentDidMount(): void {
const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors");
// we handle lack of this object gracefully later, so don't worry about it failing here. // we handle lack of this object gracefully later, so don't worry about it failing here.
const room = this.props.room; const room = this.props.room;

View File

@@ -129,7 +129,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
if (this.dispatcherRef) dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
MatrixClientPeg.safeGet().removeListener(RoomEvent.MyMembership, this.onMyMembership); MatrixClientPeg.safeGet().removeListener(RoomEvent.MyMembership, this.onMyMembership);
} }

View File

@@ -205,7 +205,9 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
collapsed, collapsed,
childSpaces: this.childSpaces, childSpaces: this.childSpaces,
}; };
}
public componentDidMount(): void {
SpaceStore.instance.on(this.props.space.roomId, this.onSpaceUpdate); SpaceStore.instance.on(this.props.space.roomId, this.onSpaceUpdate);
this.props.space.on(RoomEvent.Name, this.onRoomNameChange); this.props.space.on(RoomEvent.Name, this.onRoomNameChange);
} }

View File

@@ -110,11 +110,10 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
sidebarFeeds: sidebar, sidebarFeeds: sidebar,
sidebarShown: true, sidebarShown: true,
}; };
this.updateCallListeners(null, this.props.call);
} }
public componentDidMount(): void { public componentDidMount(): void {
this.updateCallListeners(null, this.props.call);
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
document.addEventListener("keydown", this.onNativeKeyDown); document.addEventListener("keydown", this.onNativeKeyDown);
} }
@@ -126,7 +125,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
document.removeEventListener("keydown", this.onNativeKeyDown); document.removeEventListener("keydown", this.onNativeKeyDown);
this.updateCallListeners(this.props.call, null); this.updateCallListeners(this.props.call, null);
if (this.dispatcherRef) dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
} }
public static getDerivedStateFromProps(props: IProps): Partial<IState> { public static getDerivedStateFromProps(props: IProps): Partial<IState> {

View File

@@ -46,7 +46,8 @@ export class MatrixDispatcher {
/** /**
* Removes a callback based on its token. * Removes a callback based on its token.
*/ */
public unregister(id: DispatchToken): void { public unregister(id?: DispatchToken): void {
if (!id) return;
invariant(this.callbacks.has(id), `Dispatcher.unregister(...): '${id}' does not map to a registered callback.`); invariant(this.callbacks.has(id), `Dispatcher.unregister(...): '${id}' does not map to a registered callback.`);
this.callbacks.delete(id); this.callbacks.delete(id);
} }

View File

@@ -22,12 +22,12 @@ import { Action } from "../dispatcher/actions";
// TODO: Move this and related files to the js-sdk or something once finalized. // TODO: Move this and related files to the js-sdk or something once finalized.
export class Mjolnir { export class Mjolnir {
private static instance: Mjolnir | null = null; private static instance?: Mjolnir;
private _lists: BanList[] = []; // eslint-disable-line @typescript-eslint/naming-convention private _lists: BanList[] = []; // eslint-disable-line @typescript-eslint/naming-convention
private _roomIds: string[] = []; // eslint-disable-line @typescript-eslint/naming-convention private _roomIds: string[] = []; // eslint-disable-line @typescript-eslint/naming-convention
private mjolnirWatchRef: string | null = null; private mjolnirWatchRef?: string;
private dispatcherRef: string | null = null; private dispatcherRef?: string;
public get roomIds(): string[] { public get roomIds(): string[] {
return this._roomIds; return this._roomIds;
@@ -61,15 +61,11 @@ export class Mjolnir {
} }
public stop(): void { public stop(): void {
if (this.mjolnirWatchRef) {
SettingsStore.unwatchSetting(this.mjolnirWatchRef); SettingsStore.unwatchSetting(this.mjolnirWatchRef);
this.mjolnirWatchRef = null; this.mjolnirWatchRef = undefined;
}
if (this.dispatcherRef) {
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
this.dispatcherRef = null; this.dispatcherRef = undefined;
}
MatrixClientPeg.get()?.removeListener(RoomStateEvent.Events, this.onEvent); MatrixClientPeg.get()?.removeListener(RoomStateEvent.Events, this.onEvent);
} }

View File

@@ -643,8 +643,8 @@ export class ElementCall extends Call {
public static readonly MEMBER_EVENT_TYPE = new NamespacedValue(null, EventType.GroupCallMemberPrefix); public static readonly MEMBER_EVENT_TYPE = new NamespacedValue(null, EventType.GroupCallMemberPrefix);
public readonly STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60; // 1 hour public readonly STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60; // 1 hour
private settingsStoreCallEncryptionWatcher: string | null = null; private settingsStoreCallEncryptionWatcher?: string;
private terminationTimer: number | null = null; private terminationTimer?: number;
private _layout = Layout.Tile; private _layout = Layout.Tile;
public get layout(): Layout { public get layout(): Layout {
return this._layout; return this._layout;
@@ -938,13 +938,9 @@ export class ElementCall extends Call {
this.session.off(MatrixRTCSessionEvent.MembershipsChanged, this.onMembershipChanged); this.session.off(MatrixRTCSessionEvent.MembershipsChanged, this.onMembershipChanged);
this.client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, this.onRTCSessionEnded); this.client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, this.onRTCSessionEnded);
if (this.settingsStoreCallEncryptionWatcher) {
SettingsStore.unwatchSetting(this.settingsStoreCallEncryptionWatcher); SettingsStore.unwatchSetting(this.settingsStoreCallEncryptionWatcher);
}
if (this.terminationTimer !== null) {
clearTimeout(this.terminationTimer); clearTimeout(this.terminationTimer);
this.terminationTimer = null; this.terminationTimer = undefined;
}
super.destroy(); super.destroy();
} }

View File

@@ -195,7 +195,8 @@ export default class SettingsStore {
* @param {string} watcherReference The watcher reference (received from #watchSetting) * @param {string} watcherReference The watcher reference (received from #watchSetting)
* to cancel. * to cancel.
*/ */
public static unwatchSetting(watcherReference: string): void { public static unwatchSetting(watcherReference?: string): void {
if (!watcherReference) return;
if (!SettingsStore.watchers.has(watcherReference)) { if (!SettingsStore.watchers.has(watcherReference)) {
logger.warn(`Ending non-existent watcher ID ${watcherReference}`); logger.warn(`Ending non-existent watcher ID ${watcherReference}`);
return; return;

View File

@@ -28,11 +28,7 @@ export class FontWatcher implements IWatcher {
*/ */
public static readonly DEFAULT_DELTA = 0; public static readonly DEFAULT_DELTA = 0;
private dispatcherRef: string | null; private dispatcherRef?: string;
public constructor() {
this.dispatcherRef = null;
}
public async start(): Promise<void> { public async start(): Promise<void> {
this.updateFont(); this.updateFont();
@@ -148,7 +144,6 @@ export class FontWatcher implements IWatcher {
} }
public stop(): void { public stop(): void {
if (!this.dispatcherRef) return;
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
} }

View File

@@ -18,9 +18,9 @@ import { ActionPayload } from "../../dispatcher/payloads";
import { SettingLevel } from "../SettingLevel"; import { SettingLevel } from "../SettingLevel";
export default class ThemeWatcher { export default class ThemeWatcher {
private themeWatchRef: string | null; private themeWatchRef?: string;
private systemThemeWatchRef: string | null; private systemThemeWatchRef?: string;
private dispatcherRef: string | null; private dispatcherRef?: string;
private preferDark: MediaQueryList; private preferDark: MediaQueryList;
private preferLight: MediaQueryList; private preferLight: MediaQueryList;
@@ -29,10 +29,6 @@ export default class ThemeWatcher {
private currentTheme: string; private currentTheme: string;
public constructor() { public constructor() {
this.themeWatchRef = null;
this.systemThemeWatchRef = null;
this.dispatcherRef = null;
// we have both here as each may either match or not match, so by having both // we have both here as each may either match or not match, so by having both
// we can get the tristate of dark/light/unsupported // we can get the tristate of dark/light/unsupported
this.preferDark = (<any>global).matchMedia("(prefers-color-scheme: dark)"); this.preferDark = (<any>global).matchMedia("(prefers-color-scheme: dark)");
@@ -55,9 +51,9 @@ export default class ThemeWatcher {
this.preferDark.removeEventListener("change", this.onChange); this.preferDark.removeEventListener("change", this.onChange);
this.preferLight.removeEventListener("change", this.onChange); this.preferLight.removeEventListener("change", this.onChange);
this.preferHighContrast.removeEventListener("change", this.onChange); this.preferHighContrast.removeEventListener("change", this.onChange);
if (this.systemThemeWatchRef) SettingsStore.unwatchSetting(this.systemThemeWatchRef); SettingsStore.unwatchSetting(this.systemThemeWatchRef);
if (this.themeWatchRef) SettingsStore.unwatchSetting(this.themeWatchRef); SettingsStore.unwatchSetting(this.themeWatchRef);
if (this.dispatcherRef) dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
} }
private onChange = (): void => { private onChange = (): void => {

View File

@@ -65,7 +65,7 @@ export abstract class AsyncStore<T extends object> extends EventEmitter {
* Stops the store's listening functions, such as the listener to the dispatcher. * Stops the store's listening functions, such as the listener to the dispatcher.
*/ */
protected stop(): void { protected stop(): void {
if (this.dispatcherRef) this.dispatcher.unregister(this.dispatcherRef); this.dispatcher.unregister(this.dispatcherRef);
} }
/** /**

View File

@@ -23,7 +23,7 @@ export abstract class AsyncStoreWithClient<T extends object> extends AsyncStore<
const asyncStore = this; // eslint-disable-line @typescript-eslint/no-this-alias const asyncStore = this; // eslint-disable-line @typescript-eslint/no-this-alias
this.readyStore = new (class extends ReadyWatchingStore { this.readyStore = new (class extends ReadyWatchingStore {
public get mxClient(): MatrixClient | null { public get mxClient(): MatrixClient | null {
return this.matrixClient; return this.matrixClient ?? null;
} }
protected async onReady(): Promise<any> { protected async onReady(): Promise<any> {

View File

@@ -142,7 +142,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
this.matrixClient.removeListener(BeaconEvent.Destroy, this.onDestroyBeacon); this.matrixClient.removeListener(BeaconEvent.Destroy, this.onDestroyBeacon);
this.matrixClient.removeListener(RoomStateEvent.Members, this.onRoomStateMembers); this.matrixClient.removeListener(RoomStateEvent.Members, this.onRoomStateMembers);
} }
SettingsStore.unwatchSetting(this.dynamicWatcherRef ?? ""); SettingsStore.unwatchSetting(this.dynamicWatcherRef);
this.clearBeacons(); this.clearBeacons();
} }

View File

@@ -16,8 +16,8 @@ import { Action } from "../dispatcher/actions";
import { MatrixDispatcher } from "../dispatcher/dispatcher"; import { MatrixDispatcher } from "../dispatcher/dispatcher";
export abstract class ReadyWatchingStore extends EventEmitter implements IDestroyable { export abstract class ReadyWatchingStore extends EventEmitter implements IDestroyable {
protected matrixClient: MatrixClient | null = null; protected matrixClient?: MatrixClient;
private dispatcherRef: string | null = null; private dispatcherRef?: string;
public constructor(protected readonly dispatcher: MatrixDispatcher) { public constructor(protected readonly dispatcher: MatrixDispatcher) {
super(); super();
@@ -35,7 +35,7 @@ export abstract class ReadyWatchingStore extends EventEmitter implements IDestro
} }
public get mxClient(): MatrixClient | null { public get mxClient(): MatrixClient | null {
return this.matrixClient; // for external readonly access return this.matrixClient ?? null; // for external readonly access
} }
public useUnitTestClient(cli: MatrixClient): void { public useUnitTestClient(cli: MatrixClient): void {
@@ -43,7 +43,7 @@ export abstract class ReadyWatchingStore extends EventEmitter implements IDestro
} }
public destroy(): void { public destroy(): void {
if (this.dispatcherRef !== null) this.dispatcher.unregister(this.dispatcherRef); this.dispatcher.unregister(this.dispatcherRef);
} }
protected async onReady(): Promise<void> { protected async onReady(): Promise<void> {
@@ -80,7 +80,7 @@ export abstract class ReadyWatchingStore extends EventEmitter implements IDestro
} else if (payload.action === "on_client_not_viable" || payload.action === Action.OnLoggedOut) { } else if (payload.action === "on_client_not_viable" || payload.action === Action.OnLoggedOut) {
if (this.matrixClient) { if (this.matrixClient) {
await this.onNotReady(); await this.onNotReady();
this.matrixClient = null; this.matrixClient = undefined;
} }
} }
}; };

View File

@@ -91,9 +91,9 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
this.byRoom = new MapWithDefault(() => new Map()); this.byRoom = new MapWithDefault(() => new Map());
this.matrixClient?.off(RoomStateEvent.Events, this.updateRoomFromState); this.matrixClient?.off(RoomStateEvent.Events, this.updateRoomFromState);
if (this.pinnedRef) SettingsStore.unwatchSetting(this.pinnedRef); SettingsStore.unwatchSetting(this.pinnedRef);
if (this.layoutRef) SettingsStore.unwatchSetting(this.layoutRef); SettingsStore.unwatchSetting(this.layoutRef);
if (this.dynamicRef) SettingsStore.unwatchSetting(this.dynamicRef); SettingsStore.unwatchSetting(this.dynamicRef);
WidgetStore.instance.off(UPDATE_EVENT, this.updateFromWidgetStore); WidgetStore.instance.off(UPDATE_EVENT, this.updateFromWidgetStore);
} }

View File

@@ -39,6 +39,7 @@ jest.mock("matrix-js-sdk/src/logger");
jest.mock("../../src/dispatcher/dispatcher", () => ({ jest.mock("../../src/dispatcher/dispatcher", () => ({
dispatch: jest.fn(), dispatch: jest.fn(),
register: jest.fn(), register: jest.fn(),
unregister: jest.fn(),
})); }));
jest.mock("../../src/SecurityManager", () => ({ jest.mock("../../src/SecurityManager", () => ({

View File

@@ -62,6 +62,7 @@ import { DRAFT_LAST_CLEANUP_KEY } from "../../../../src/DraftCleaner";
import { UIFeature } from "../../../../src/settings/UIFeature"; import { UIFeature } from "../../../../src/settings/UIFeature";
import AutoDiscoveryUtils from "../../../../src/utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils from "../../../../src/utils/AutoDiscoveryUtils";
import { ValidatedServerConfig } from "../../../../src/utils/ValidatedServerConfig"; import { ValidatedServerConfig } from "../../../../src/utils/ValidatedServerConfig";
import Modal from "../../../../src/Modal.tsx";
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({ jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
completeAuthorizationCodeGrant: jest.fn(), completeAuthorizationCodeGrant: jest.fn(),
@@ -1514,7 +1515,9 @@ describe("<MatrixChat />", () => {
describe("when key backup failed", () => { describe("when key backup failed", () => {
it("should show the new recovery method dialog", async () => { it("should show the new recovery method dialog", async () => {
const spy = jest.spyOn(Modal, "createDialogAsync");
jest.mock("../../../../src/async-components/views/dialogs/security/NewRecoveryMethodDialog", () => ({ jest.mock("../../../../src/async-components/views/dialogs/security/NewRecoveryMethodDialog", () => ({
__test: true,
__esModule: true, __esModule: true,
default: () => <span>mocked dialog</span>, default: () => <span>mocked dialog</span>,
})); }));
@@ -1526,7 +1529,8 @@ describe("<MatrixChat />", () => {
}); });
await flushPromises(); await flushPromises();
mockClient.emit(CryptoEvent.KeyBackupFailed, "error code"); mockClient.emit(CryptoEvent.KeyBackupFailed, "error code");
await waitFor(() => expect(screen.getByText("mocked dialog")).toBeInTheDocument()); await waitFor(() => expect(spy).toHaveBeenCalledTimes(1));
expect(await spy.mock.lastCall![0]).toEqual(expect.objectContaining({ __test: true }));
}); });
}); });
}); });

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react"; import React from "react";
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import { act, render, RenderResult, screen } from "jest-matrix-react"; import { act, render, RenderResult, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { MatrixClient, createClient } from "matrix-js-sdk/src/matrix"; import { MatrixClient, createClient } from "matrix-js-sdk/src/matrix";
@@ -47,14 +47,12 @@ describe("<ForgotPassword>", () => {
}; };
const click = async (element: Element): Promise<void> => { const click = async (element: Element): Promise<void> => {
await act(async () => {
await userEvent.click(element, { delay: null }); await userEvent.click(element, { delay: null });
});
}; };
const itShouldCloseTheDialogAndShowThePasswordInput = (): void => { const itShouldCloseTheDialogAndShowThePasswordInput = (): void => {
it("should close the dialog and show the password input", () => { it("should close the dialog and show the password input", async () => {
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument(); await waitFor(() => expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument());
expect(screen.getByText("Reset your password")).toBeInTheDocument(); expect(screen.getByText("Reset your password")).toBeInTheDocument();
}); });
}; };
@@ -314,7 +312,7 @@ describe("<ForgotPassword>", () => {
}); });
}); });
it("should send the new password and show the click validation link dialog", () => { it("should send the new password and show the click validation link dialog", async () => {
expect(client.setPassword).toHaveBeenCalledWith( expect(client.setPassword).toHaveBeenCalledWith(
{ {
type: "m.login.email.identity", type: "m.login.email.identity",
@@ -326,15 +324,15 @@ describe("<ForgotPassword>", () => {
testPassword, testPassword,
false, false,
); );
expect(screen.getByText("Verify your email to continue")).toBeInTheDocument(); await expect(
screen.findByText("Verify your email to continue"),
).resolves.toBeInTheDocument();
expect(screen.getByText(testEmail)).toBeInTheDocument(); expect(screen.getByText(testEmail)).toBeInTheDocument();
}); });
describe("and dismissing the dialog by clicking the background", () => { describe("and dismissing the dialog by clicking the background", () => {
beforeEach(async () => { beforeEach(async () => {
await act(async () => { await userEvent.click(await screen.findByTestId("dialog-background"), { delay: null });
await userEvent.click(screen.getByTestId("dialog-background"), { delay: null });
});
await waitEnoughCyclesForModal({ await waitEnoughCyclesForModal({
useFakeTimers: true, useFakeTimers: true,
}); });
@@ -345,7 +343,7 @@ describe("<ForgotPassword>", () => {
describe("and dismissing the dialog", () => { describe("and dismissing the dialog", () => {
beforeEach(async () => { beforeEach(async () => {
await click(screen.getByLabelText("Close dialog")); await click(await screen.findByLabelText("Close dialog"));
await waitEnoughCyclesForModal({ await waitEnoughCyclesForModal({
useFakeTimers: true, useFakeTimers: true,
}); });
@@ -356,14 +354,16 @@ describe("<ForgotPassword>", () => {
describe("and clicking »Re-enter email address«", () => { describe("and clicking »Re-enter email address«", () => {
beforeEach(async () => { beforeEach(async () => {
await click(screen.getByText("Re-enter email address")); await click(await screen.findByText("Re-enter email address"));
await waitEnoughCyclesForModal({ await waitEnoughCyclesForModal({
useFakeTimers: true, useFakeTimers: true,
}); });
}); });
it("should close the dialog and go back to the email input", () => { it("should close the dialog and go back to the email input", async () => {
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument(); await waitFor(() =>
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument(),
);
expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument(); expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument();
}); });
}); });
@@ -397,11 +397,11 @@ describe("<ForgotPassword>", () => {
}); });
it("should show the sign out warning dialog", async () => { it("should show the sign out warning dialog", async () => {
expect( await expect(
screen.getByText( screen.findByText(
"Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.", "Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.",
), ),
).toBeInTheDocument(); ).resolves.toBeInTheDocument();
// confirm dialog // confirm dialog
await click(screen.getByText("Continue")); await click(screen.getByText("Continue"));

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