diff --git a/src/AsyncWrapper.tsx b/src/AsyncWrapper.tsx index 179d42668e..cec814df17 100644 --- a/src/AsyncWrapper.tsx +++ b/src/AsyncWrapper.tsx @@ -37,6 +37,7 @@ export default class AsyncWrapper extends React.Component { public state: IState = {}; public componentDidMount(): void { + this.unmounted = false; this.props.prom .then((result) => { if (this.unmounted) return; diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 3022aa6b8d..1e87b5b826 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -117,8 +117,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent }; } + public componentDidMount(): void { + this.unmounted = false; + } + public componentWillUnmount(): void { this.unmounted = true; } diff --git a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx index fa41d53a45..d08259f2cb 100644 --- a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx +++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx @@ -64,6 +64,10 @@ export default class ImportE2eKeysDialog extends React.Component }; } + public componentDidMount(): void { + this.unmounted = false; + } + public componentWillUnmount(): void { this.unmounted = true; } diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index 91e52a1905..4b0f060952 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -90,8 +90,8 @@ interface IState { export default class InteractiveAuthComponent extends React.Component, IState> { private readonly authLogic: InteractiveAuth; - private readonly intervalId: number | null = null; private readonly stageComponent = createRef(); + private intervalId: number | null = null; private unmounted = false; @@ -126,15 +126,17 @@ export default class InteractiveAuthComponent extends React.Component { this.authLogic.poll(); }, 2000); } - } - public componentDidMount(): void { this.authLogic .attemptAuth() .then(async (result) => { diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index f8cd0184d4..49d0f570a5 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -67,10 +67,6 @@ export default class LeftPanel extends React.Component { activeSpace: SpaceStore.instance.activeSpace, showBreadcrumbs: LeftPanel.breadcrumbsMode, }; - - BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); - RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); - SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace); } private static get breadcrumbsMode(): BreadcrumbsMode { @@ -78,6 +74,10 @@ export default class LeftPanel extends React.Component { } public componentDidMount(): void { + BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); + RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); + SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace); + if (this.listContainerRef.current) { UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current); // Using the passive option to not block the main thread diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index d0edcccd4f..80a648b5d5 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -231,10 +231,10 @@ export default class MatrixChat extends React.PureComponent { private prevWindowWidth: number; private voiceBroadcastResumer?: VoiceBroadcastResumer; - private readonly loggedInView: React.RefObject; - private readonly dispatcherRef: string; - private readonly themeWatcher: ThemeWatcher; - private readonly fontWatcher: FontWatcher; + private readonly loggedInView = createRef(); + private dispatcherRef?: string; + private themeWatcher?: ThemeWatcher; + private fontWatcher?: FontWatcher; private readonly stores: SdkContextClass; public constructor(props: IProps) { @@ -256,8 +256,6 @@ export default class MatrixChat extends React.PureComponent { ready: false, }; - this.loggedInView = createRef(); - SdkConfig.put(this.props.config); // Used by _viewRoom before getting state from sync @@ -282,32 +280,10 @@ export default class MatrixChat extends React.PureComponent { } this.prevWindowWidth = UIStore.instance.windowWidth || 1000; - UIStore.instance.on(UI_EVENTS.Resize, this.handleResize); - - // For PersistentElement - this.state.resizeNotifier.on("middlePanelResized", this.dispatchTimelineResize); - - RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatusIndicator); - - this.dispatcherRef = dis.register(this.onAction); - - this.themeWatcher = new ThemeWatcher(); - this.fontWatcher = new FontWatcher(); - this.themeWatcher.start(); - this.fontWatcher.start(); // object field used for tracking the status info appended to the title tag. // we don't do it as react state as i'm scared about triggering needless react refreshes. this.subTitleStatus = ""; - - initSentry(SdkConfig.get("sentry")); - - if (!checkSessionLockFree()) { - // another instance holds the lock; confirm its theft before proceeding - setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0); - } else { - this.startInitSession(); - } } /** @@ -476,6 +452,29 @@ export default class MatrixChat extends React.PureComponent { } public componentDidMount(): void { + UIStore.instance.on(UI_EVENTS.Resize, this.handleResize); + + // For PersistentElement + this.state.resizeNotifier.on("middlePanelResized", this.dispatchTimelineResize); + + RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatusIndicator); + + this.dispatcherRef = dis.register(this.onAction); + + this.themeWatcher = new ThemeWatcher(); + this.fontWatcher = new FontWatcher(); + this.themeWatcher.start(); + this.fontWatcher.start(); + + initSentry(SdkConfig.get("sentry")); + + if (!checkSessionLockFree()) { + // another instance holds the lock; confirm its theft before proceeding + setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0); + } else { + this.startInitSession(); + } + window.addEventListener("resize", this.onWindowResized); } @@ -497,8 +496,8 @@ export default class MatrixChat extends React.PureComponent { public componentWillUnmount(): void { Lifecycle.stopMatrixClient(); dis.unregister(this.dispatcherRef); - this.themeWatcher.stop(); - this.fontWatcher.stop(); + this.themeWatcher?.stop(); + this.fontWatcher?.stop(); UIStore.destroy(); this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize); window.removeEventListener("resize", this.onWindowResized); @@ -1011,7 +1010,7 @@ export default class MatrixChat extends React.PureComponent { this.setStateForNewView(newState); ThemeController.isLogin = true; - this.themeWatcher.recheck(); + this.themeWatcher?.recheck(); this.notifyNewScreen(isMobileRegistration ? "mobile_register" : "register"); } @@ -1088,7 +1087,7 @@ export default class MatrixChat extends React.PureComponent { }, () => { ThemeController.isLogin = false; - this.themeWatcher.recheck(); + this.themeWatcher?.recheck(); this.notifyNewScreen("room/" + presentedId, replaceLast); }, ); @@ -1113,7 +1112,7 @@ export default class MatrixChat extends React.PureComponent { }); this.notifyNewScreen("welcome"); ThemeController.isLogin = true; - this.themeWatcher.recheck(); + this.themeWatcher?.recheck(); } private viewLogin(otherState?: any): void { @@ -1123,7 +1122,7 @@ export default class MatrixChat extends React.PureComponent { }); this.notifyNewScreen("login"); ThemeController.isLogin = true; - this.themeWatcher.recheck(); + this.themeWatcher?.recheck(); } private viewHome(justRegistered = false): void { @@ -1136,7 +1135,7 @@ export default class MatrixChat extends React.PureComponent { this.setPage(PageType.HomePage); this.notifyNewScreen("home"); ThemeController.isLogin = false; - this.themeWatcher.recheck(); + this.themeWatcher?.recheck(); } private viewUser(userId: string, subAction: string): void { @@ -1357,7 +1356,7 @@ export default class MatrixChat extends React.PureComponent { */ private async onLoggedIn(): Promise { ThemeController.isLogin = false; - this.themeWatcher.recheck(); + this.themeWatcher?.recheck(); StorageManager.tryPersistStorage(); await this.onShowPostLoginScreen(); diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 7383e06f07..b26de2e645 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -240,13 +240,13 @@ export default class MessagePanel extends React.Component { private readReceiptsByUserId: Map = new Map(); private readonly _showHiddenEvents: boolean; - private isMounted = false; + private unmounted = false; private readMarkerNode = createRef(); private whoIsTyping = createRef(); public scrollPanel = createRef(); - private readonly showTypingNotificationsWatcherRef: string; + private showTypingNotificationsWatcherRef?: string; private eventTiles: Record = {}; // 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 { // and we check this in a hot code path. This is also cached in our // RoomContext, however we still need a fallback for roomless MessagePanels. this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline"); + } + public componentDidMount(): void { + this.unmounted = false; this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting( "showTypingNotifications", null, this.onShowTypingNotificationsChange, ); - } - - public componentDidMount(): void { this.calculateRoomMembersCount(); this.props.room?.currentState.on(RoomStateEvent.Update, this.calculateRoomMembersCount); - this.isMounted = true; } public componentWillUnmount(): void { - this.isMounted = false; + this.unmounted = true; this.props.room?.currentState.off(RoomStateEvent.Update, this.calculateRoomMembersCount); SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); this.readReceiptMap = {}; @@ -441,7 +440,7 @@ export default class MessagePanel extends React.Component { } private isUnmounting = (): boolean => { - return !this.isMounted; + return this.unmounted; }; public get showHiddenEvents(): boolean { diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx index 2eca6db934..d01bf78959 100644 --- a/src/components/structures/NonUrgentToastContainer.tsx +++ b/src/components/structures/NonUrgentToastContainer.tsx @@ -25,7 +25,9 @@ export default class NonUrgentToastContainer extends React.PureComponent { - private readonly dispatcherRef: string; - - public constructor(props: IProps) { - super(props); + private dispatcherRef?: string; + public componentDidMount(): void { this.dispatcherRef = defaultDispatcher.register(this.onAction); } diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index bd236f2286..76f3b0c229 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -103,6 +103,8 @@ export default class RoomStatusBar extends React.PureComponent { } public componentDidMount(): void { + this.unmounted = false; + const client = this.context; client.on(ClientEvent.Sync, this.onSyncStateChange); client.on(RoomEvent.LocalEchoUpdated, this.onRoomLocalEchoUpdated); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 486a7fb652..520760713c 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -351,8 +351,8 @@ export class RoomView extends React.Component { private static e2eStatusCache = new Map(); private readonly askToJoinEnabled: boolean; - private readonly dispatcherRef: string; - private settingWatchers: string[]; + private dispatcherRef?: string; + private settingWatchers: string[] = []; private unmounted = false; private permalinkCreators: Record = {}; @@ -418,62 +418,6 @@ export class RoomView extends React.Component { promptAskToJoin: false, viewRoomOpts: { buttons: [] }, }; - - this.dispatcherRef = dis.register(this.onAction); - context.client.on(ClientEvent.Room, this.onRoom); - context.client.on(RoomEvent.Timeline, this.onRoomTimeline); - context.client.on(RoomEvent.TimelineReset, this.onRoomTimelineReset); - context.client.on(RoomEvent.Name, this.onRoomName); - context.client.on(RoomStateEvent.Events, this.onRoomStateEvents); - context.client.on(RoomStateEvent.Update, this.onRoomStateUpdate); - context.client.on(RoomEvent.MyMembership, this.onMyMembership); - context.client.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus); - context.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged); - context.client.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged); - context.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted); - // Start listening for RoomViewStore updates - context.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate); - - context.rightPanelStore.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); - - WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate); - context.widgetStore.on(UPDATE_EVENT, this.onWidgetStoreUpdate); - - CallStore.instance.on(CallStoreEvent.ConnectedCalls, this.onConnectedCalls); - - this.props.resizeNotifier.on("isResizing", this.onIsResizing); - - this.settingWatchers = [ - SettingsStore.watchSetting("layout", null, (...[, , , value]) => - this.setState({ layout: value as Layout }), - ), - SettingsStore.watchSetting("lowBandwidth", null, (...[, , , value]) => - this.setState({ lowBandwidth: value as boolean }), - ), - SettingsStore.watchSetting("alwaysShowTimestamps", null, (...[, , , value]) => - this.setState({ alwaysShowTimestamps: value as boolean }), - ), - SettingsStore.watchSetting("showTwelveHourTimestamps", null, (...[, , , value]) => - this.setState({ showTwelveHourTimestamps: value as boolean }), - ), - SettingsStore.watchSetting(TimezoneHandler.USER_TIMEZONE_KEY, null, (...[, , , value]) => - this.setState({ userTimezone: value as string }), - ), - SettingsStore.watchSetting("readMarkerInViewThresholdMs", null, (...[, , , value]) => - this.setState({ readMarkerInViewThresholdMs: value as number }), - ), - SettingsStore.watchSetting("readMarkerOutOfViewThresholdMs", null, (...[, , , value]) => - this.setState({ readMarkerOutOfViewThresholdMs: value as number }), - ), - SettingsStore.watchSetting("showHiddenEventsInTimeline", null, (...[, , , value]) => - this.setState({ showHiddenEvents: value as boolean }), - ), - SettingsStore.watchSetting("urlPreviewsEnabled", null, this.onUrlPreviewsEnabledChange), - SettingsStore.watchSetting("urlPreviewsEnabled_e2ee", null, this.onUrlPreviewsEnabledChange), - SettingsStore.watchSetting("feature_dynamic_room_predecessors", null, (...[, , , value]) => - this.setState({ msc3946ProcessDynamicPredecessor: value as boolean }), - ), - ]; } private onIsResizing = (resizing: boolean): void => { @@ -904,6 +848,66 @@ export class RoomView extends React.Component { } public componentDidMount(): void { + this.unmounted = false; + + this.dispatcherRef = dis.register(this.onAction); + if (this.context.client) { + this.context.client.on(ClientEvent.Room, this.onRoom); + this.context.client.on(RoomEvent.Timeline, this.onRoomTimeline); + this.context.client.on(RoomEvent.TimelineReset, this.onRoomTimelineReset); + this.context.client.on(RoomEvent.Name, this.onRoomName); + this.context.client.on(RoomStateEvent.Events, this.onRoomStateEvents); + this.context.client.on(RoomStateEvent.Update, this.onRoomStateUpdate); + this.context.client.on(RoomEvent.MyMembership, this.onMyMembership); + this.context.client.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus); + this.context.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged); + this.context.client.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged); + this.context.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted); + } + // Start listening for RoomViewStore updates + this.context.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate); + + this.context.rightPanelStore.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); + + WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate); + this.context.widgetStore.on(UPDATE_EVENT, this.onWidgetStoreUpdate); + + CallStore.instance.on(CallStoreEvent.ConnectedCalls, this.onConnectedCalls); + + this.props.resizeNotifier.on("isResizing", this.onIsResizing); + + this.settingWatchers = [ + SettingsStore.watchSetting("layout", null, (...[, , , value]) => + this.setState({ layout: value as Layout }), + ), + SettingsStore.watchSetting("lowBandwidth", null, (...[, , , value]) => + this.setState({ lowBandwidth: value as boolean }), + ), + SettingsStore.watchSetting("alwaysShowTimestamps", null, (...[, , , value]) => + this.setState({ alwaysShowTimestamps: value as boolean }), + ), + SettingsStore.watchSetting("showTwelveHourTimestamps", null, (...[, , , value]) => + this.setState({ showTwelveHourTimestamps: value as boolean }), + ), + SettingsStore.watchSetting(TimezoneHandler.USER_TIMEZONE_KEY, null, (...[, , , value]) => + this.setState({ userTimezone: value as string }), + ), + SettingsStore.watchSetting("readMarkerInViewThresholdMs", null, (...[, , , value]) => + this.setState({ readMarkerInViewThresholdMs: value as number }), + ), + SettingsStore.watchSetting("readMarkerOutOfViewThresholdMs", null, (...[, , , value]) => + this.setState({ readMarkerOutOfViewThresholdMs: value as number }), + ), + SettingsStore.watchSetting("showHiddenEventsInTimeline", null, (...[, , , value]) => + this.setState({ showHiddenEvents: value as boolean }), + ), + SettingsStore.watchSetting("urlPreviewsEnabled", null, this.onUrlPreviewsEnabledChange), + SettingsStore.watchSetting("urlPreviewsEnabled_e2ee", null, this.onUrlPreviewsEnabledChange), + SettingsStore.watchSetting("feature_dynamic_room_predecessors", null, (...[, , , value]) => + this.setState({ msc3946ProcessDynamicPredecessor: value as boolean }), + ), + ]; + this.onRoomViewStoreUpdate(true); const call = this.getCallForRoom(); diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx index d072c322ce..b354f6b005 100644 --- a/src/components/structures/ScrollPanel.tsx +++ b/src/components/structures/ScrollPanel.tsx @@ -191,12 +191,12 @@ export default class ScrollPanel extends React.Component { public constructor(props: IProps) { super(props); - this.props.resizeNotifier?.on("middlePanelResizedNoisy", this.onResize); - this.resetScrollState(); } public componentDidMount(): void { + this.unmounted = false; + this.props.resizeNotifier?.on("middlePanelResizedNoisy", this.onResize); this.checkScroll(); } diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 4f0c895233..3ea2a03c1a 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -599,7 +599,7 @@ export default class SpaceRoomView extends React.PureComponent { public static contextType = MatrixClientContext; public declare context: React.ContextType; - private readonly dispatcherRef: string; + private dispatcherRef?: string; public constructor(props: IProps, context: React.ContextType) { super(props, context); @@ -621,12 +621,11 @@ export default class SpaceRoomView extends React.PureComponent { showRightPanel: RightPanelStore.instance.isOpenForRoom(this.props.space.roomId), myMembership: this.props.space.getMyMembership(), }; - - this.dispatcherRef = defaultDispatcher.register(this.onAction); - RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); } public componentDidMount(): void { + this.dispatcherRef = defaultDispatcher.register(this.onAction); + RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); this.context.on(RoomEvent.MyMembership, this.onMyMembership); } diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 8d2a286de1..be538a6669 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -78,7 +78,7 @@ export default class ThreadView extends React.Component { public declare context: React.ContextType; private dispatcherRef?: string; - private readonly layoutWatcherRef: string; + private layoutWatcherRef?: string; private timelinePanel = createRef(); private card = createRef(); @@ -91,7 +91,6 @@ export default class ThreadView extends React.Component { this.setEventId(this.props.mxEvent); const thread = this.props.room.getThread(this.eventId) ?? undefined; - this.setupThreadListeners(thread); this.state = { layout: SettingsStore.getValue("layout"), narrow: false, @@ -100,13 +99,15 @@ export default class ThreadView extends React.Component { return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status; }), }; + } + + public componentDidMount(): void { + this.setupThreadListeners(this.state.thread); this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[, , , value]) => this.setState({ layout: value as Layout }), ); - } - public componentDidMount(): void { if (this.state.thread) { this.postThreadUpdate(this.state.thread); } diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 846fc56d17..68b65965f5 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -248,7 +248,7 @@ class TimelinePanel extends React.Component { private lastRMSentEventId: string | null | undefined = undefined; private readonly messagePanel = createRef(); - private readonly dispatcherRef: string; + private dispatcherRef?: string; private timelineWindow?: TimelineWindow; private overlayTimelineWindow?: TimelineWindow; private unmounted = false; @@ -291,6 +291,10 @@ class TimelinePanel extends React.Component { readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"), readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"), }; + } + + public componentDidMount(): void { + this.unmounted = false; this.dispatcherRef = dis.register(this.onAction); const cli = MatrixClientPeg.safeGet(); @@ -312,9 +316,7 @@ class TimelinePanel extends React.Component { cli.on(ClientEvent.Sync, this.onSync); this.props.timelineSet.room?.on(ThreadEvent.Update, this.onThreadUpdate); - } - public componentDidMount(): void { if (this.props.manageReadReceipts) { this.updateReadReceiptOnUserActivity(); } diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index 8c572442a0..3e5b4a4474 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -24,12 +24,11 @@ export default class ToastContainer extends React.Component<{}, IState> { toasts: ToastStore.sharedInstance().getToasts(), countSeen: ToastStore.sharedInstance().getCountSeen(), }; + } - // Start listening here rather than in componentDidMount because - // toasts may dismiss themselves in their didMount if they find - // they're already irrelevant by the time they're mounted, and - // our own componentDidMount is too late. + public componentDidMount(): void { ToastStore.sharedInstance().on("update", this.onToastStoreUpdate); + this.onToastStoreUpdate(); } public componentWillUnmount(): void { diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx index 93ce6d6bf2..01ecae96dc 100644 --- a/src/components/structures/UploadBar.tsx +++ b/src/components/structures/UploadBar.tsx @@ -46,7 +46,7 @@ function isUploadPayload(payload: ActionPayload): payload is UploadPayload { export default class UploadBar extends React.PureComponent { private dispatcherRef: Optional; - private mounted = false; + private unmounted = false; public constructor(props: IProps) { super(props); @@ -57,12 +57,12 @@ export default class UploadBar extends React.PureComponent { } public componentDidMount(): void { + this.unmounted = false; this.dispatcherRef = dis.register(this.onAction); - this.mounted = true; } public componentWillUnmount(): void { - this.mounted = false; + this.unmounted = true; dis.unregister(this.dispatcherRef!); } @@ -83,7 +83,7 @@ export default class UploadBar extends React.PureComponent { } private onAction = (payload: ActionPayload): void => { - if (!this.mounted) return; + if (this.unmounted) return; if (isUploadPayload(payload)) { this.setState(this.calculateState()); } diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 84bd93cc36..b2c7990746 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -96,9 +96,6 @@ export default class UserMenu extends React.Component { selectedSpace: SpaceStore.instance.activeSpaceRoom, showLiveAvatarAddon: this.context.voiceBroadcastRecordingsStore.hasCurrent(), }; - - OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); - SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); } private get hasHomePage(): boolean { @@ -112,6 +109,8 @@ export default class UserMenu extends React.Component { }; public componentDidMount(): void { + OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); + SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); this.context.voiceBroadcastRecordingsStore.on( VoiceBroadcastRecordingsStoreEvent.CurrentChanged, this.onCurrentVoiceBroadcastRecordingChanged, diff --git a/src/components/structures/auth/CompleteSecurity.tsx b/src/components/structures/auth/CompleteSecurity.tsx index a74e07692d..ec65a62cef 100644 --- a/src/components/structures/auth/CompleteSecurity.tsx +++ b/src/components/structures/auth/CompleteSecurity.tsx @@ -29,11 +29,15 @@ export default class CompleteSecurity extends React.Component { public constructor(props: IProps) { super(props); const store = SetupEncryptionStore.sharedInstance(); - store.on("update", this.onStoreUpdate); store.start(); this.state = { phase: store.phase, lostKeys: store.lostKeys() }; } + public componentDidMount(): void { + const store = SetupEncryptionStore.sharedInstance(); + store.on("update", this.onStoreUpdate); + } + private onStoreUpdate = (): void => { const store = SetupEncryptionStore.sharedInstance(); this.setState({ phase: store.phase, lostKeys: store.lostKeys() }); diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index abbba0f970..0a14450e63 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -134,6 +134,7 @@ export default class LoginComponent extends React.PureComponent } public componentDidMount(): void { + this.unmounted = false; this.initLoginLogic(this.props.serverConfig); } diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 666313321a..32528fc7e3 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -39,7 +39,6 @@ export default class SetupEncryptionBody extends React.Component public constructor(props: IProps) { super(props); const store = SetupEncryptionStore.sharedInstance(); - store.on("update", this.onStoreUpdate); store.start(); this.state = { phase: store.phase, @@ -52,6 +51,11 @@ export default class SetupEncryptionBody extends React.Component }; } + public componentDidMount(): void { + const store = SetupEncryptionStore.sharedInstance(); + store.on("update", this.onStoreUpdate); + } + private onStoreUpdate = (): void => { const store = SetupEncryptionStore.sharedInstance(); if (store.phase === Phase.Finished) { diff --git a/src/components/views/audio_messages/AudioPlayerBase.tsx b/src/components/views/audio_messages/AudioPlayerBase.tsx index 70e30dccca..601611e422 100644 --- a/src/components/views/audio_messages/AudioPlayerBase.tsx +++ b/src/components/views/audio_messages/AudioPlayerBase.tsx @@ -41,7 +41,9 @@ export default abstract class AudioPlayerBase extends this.state = { playbackPhase: this.props.playback.currentState, }; + } + public componentDidMount(): void { // We don't need to de-register: the class handles this for us internally this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate); diff --git a/src/components/views/audio_messages/Clock.tsx b/src/components/views/audio_messages/Clock.tsx index 56661c7a0c..ca72d29a05 100644 --- a/src/components/views/audio_messages/Clock.tsx +++ b/src/components/views/audio_messages/Clock.tsx @@ -27,10 +27,6 @@ export default class Clock extends React.Component { formatFn: formatSeconds, }; - public constructor(props: Props) { - super(props); - } - public shouldComponentUpdate(nextProps: Readonly): boolean { const currentFloor = Math.floor(this.props.seconds); const nextFloor = Math.floor(nextProps.seconds); diff --git a/src/components/views/audio_messages/DurationClock.tsx b/src/components/views/audio_messages/DurationClock.tsx index e495098144..3794ab9a4f 100644 --- a/src/components/views/audio_messages/DurationClock.tsx +++ b/src/components/views/audio_messages/DurationClock.tsx @@ -33,6 +33,9 @@ export default class DurationClock extends React.PureComponent { // member property to track "did we get a duration". durationSeconds: this.props.playback.clockInfo.durationSeconds, }; + } + + public componentDidMount(): void { this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate); } diff --git a/src/components/views/audio_messages/PlayPauseButton.tsx b/src/components/views/audio_messages/PlayPauseButton.tsx index 1053a89eea..1cd2d168b4 100644 --- a/src/components/views/audio_messages/PlayPauseButton.tsx +++ b/src/components/views/audio_messages/PlayPauseButton.tsx @@ -26,10 +26,6 @@ type Props = Omit, "title" | "onClick" | "disabled" | "elemen * to be displayed in reference to a recording. */ export default class PlayPauseButton extends React.PureComponent { - public constructor(props: Props) { - super(props); - } - private onClick = (): void => { // noinspection JSIgnoredPromiseFromCall this.toggleState(); diff --git a/src/components/views/audio_messages/PlaybackClock.tsx b/src/components/views/audio_messages/PlaybackClock.tsx index 8de3cb71e6..b3d736758b 100644 --- a/src/components/views/audio_messages/PlaybackClock.tsx +++ b/src/components/views/audio_messages/PlaybackClock.tsx @@ -43,6 +43,9 @@ export default class PlaybackClock extends React.PureComponent { durationSeconds: this.props.playback.clockInfo.durationSeconds, playbackPhase: PlaybackState.Stopped, // assume not started, so full clock }; + } + + public componentDidMount(): void { this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate); this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate); } diff --git a/src/components/views/audio_messages/PlaybackWaveform.tsx b/src/components/views/audio_messages/PlaybackWaveform.tsx index 5f59289879..0f95f7084b 100644 --- a/src/components/views/audio_messages/PlaybackWaveform.tsx +++ b/src/components/views/audio_messages/PlaybackWaveform.tsx @@ -34,7 +34,9 @@ export default class PlaybackWaveform extends React.PureComponent { this.state = { percentage: percentageOf(this.props.playback.timeSeconds, 0, this.props.playback.durationSeconds), }; + } + public componentDidMount(): void { // We don't need to de-register: the class handles this for us internally this.props.playback.liveData.onUpdate(() => this.animationFrameFn.mark()); } diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 44ccd3a30e..b1360f5560 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -801,7 +801,6 @@ export class SSOAuthEntry extends React.Component extends React.Component { private checkCodeInput = createRef(); - public constructor(props: Props) { - super(props); - } - private handleClick = (type: Click): ((e: React.FormEvent) => Promise) => { return async (e: React.FormEvent): Promise => { e.preventDefault(); diff --git a/src/components/views/context_menus/GenericElementContextMenu.tsx b/src/components/views/context_menus/GenericElementContextMenu.tsx index 42ed8ce5be..afb39d6ebe 100644 --- a/src/components/views/context_menus/GenericElementContextMenu.tsx +++ b/src/components/views/context_menus/GenericElementContextMenu.tsx @@ -20,10 +20,6 @@ interface IProps { * menu. */ export default class GenericElementContextMenu extends React.Component { - public constructor(props: IProps) { - super(props); - } - public componentDidMount(): void { window.addEventListener("resize", this.resize); } diff --git a/src/components/views/context_menus/LegacyCallContextMenu.tsx b/src/components/views/context_menus/LegacyCallContextMenu.tsx index 817b4632e8..e6bb191df8 100644 --- a/src/components/views/context_menus/LegacyCallContextMenu.tsx +++ b/src/components/views/context_menus/LegacyCallContextMenu.tsx @@ -17,10 +17,6 @@ interface IProps extends IContextMenuProps { } export default class LegacyCallContextMenu extends React.Component { - public constructor(props: IProps) { - super(props); - } - public onHoldClick = (): void => { this.props.call.setRemoteOnHold(true); this.props.onFinished(); diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx index 22a7efacb9..373f30d3ae 100644 --- a/src/components/views/dialogs/BugReportDialog.tsx +++ b/src/components/views/dialogs/BugReportDialog.tsx @@ -64,6 +64,11 @@ export default class BugReportDialog extends React.Component { this.unmounted = false; this.issueRef = React.createRef(); + } + + public componentDidMount(): void { + this.unmounted = false; + this.issueRef.current?.focus(); // Get all of the extra info dumped to the console when someone is about // to send debug logs. Since this is a fire and forget action, we do @@ -76,10 +81,6 @@ export default class BugReportDialog extends React.Component { }); } - public componentDidMount(): void { - this.issueRef.current?.focus(); - } - public componentWillUnmount(): void { this.unmounted = true; } diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx index c5a8080e3f..990efdda71 100644 --- a/src/components/views/dialogs/CreateRoomDialog.tsx +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -113,14 +113,6 @@ export default class CreateRoomDialog extends React.Component { nameIsValid: false, canChangeEncryption: false, }; - - checkUserIsAllowedToChangeEncryption(cli, Preset.PrivateChat).then(({ allowChange, forcedValue }) => - this.setState((state) => ({ - canChangeEncryption: allowChange, - // override with forcedValue if it is set - isEncrypted: forcedValue ?? state.isEncrypted, - })), - ); } private roomCreateOptions(): IOpts { @@ -160,6 +152,15 @@ export default class CreateRoomDialog extends React.Component { } public componentDidMount(): void { + const cli = MatrixClientPeg.safeGet(); + checkUserIsAllowedToChangeEncryption(cli, Preset.PrivateChat).then(({ allowChange, forcedValue }) => + this.setState((state) => ({ + canChangeEncryption: allowChange, + // override with forcedValue if it is set + isEncrypted: forcedValue ?? state.isEncrypted, + })), + ); + // move focus to first field when showing dialog this.nameField.current?.focus(); } diff --git a/src/components/views/dialogs/DeactivateAccountDialog.tsx b/src/components/views/dialogs/DeactivateAccountDialog.tsx index fbcd26d38f..d68c931cc1 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.tsx +++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx @@ -58,7 +58,9 @@ export default class DeactivateAccountDialog extends React.Component { opponentProfileError: null, sas: null, }; + } + + public componentDidMount(): void { this.props.verifier.on(VerifierEvent.ShowSas, this.onVerifierShowSas); this.props.verifier.on(VerifierEvent.Cancel, this.onVerifierCancel); this.fetchOpponentProfile(); diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 8e1d49c138..35e04fb12e 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -397,6 +397,7 @@ export default class InviteDialog extends React.PureComponent { this.state = { backupStatus: BackupStatus.LOADING, }; + } - // we can't call setState() immediately, so wait a beat - window.setTimeout(() => this.startLoadBackupStatus(), 0); + public componentDidMount(): void { + this.startLoadBackupStatus(); } /** kick off the asynchronous calls to populate `state.backupStatus` in the background */ diff --git a/src/components/views/dialogs/VerificationRequestDialog.tsx b/src/components/views/dialogs/VerificationRequestDialog.tsx index 50644ccf30..d2ea83f2af 100644 --- a/src/components/views/dialogs/VerificationRequestDialog.tsx +++ b/src/components/views/dialogs/VerificationRequestDialog.tsx @@ -32,6 +32,9 @@ export default class VerificationRequestDialog extends React.Component { this.setState({ verificationRequest: r }); }); diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 8f5aa732be..dae452fd5d 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -134,29 +134,20 @@ export default class AppTile extends React.Component { private iframe?: HTMLIFrameElement; // ref to the iframe (callback style) private allowedWidgetsWatchRef?: string; private persistKey: string; - private sgWidget: StopGapWidget | null; + private sgWidget?: StopGapWidget; private dispatcherRef?: string; private unmounted = false; public constructor(props: IProps, context: ContextType) { super(props, context); - // Tiles in miniMode are floating, and therefore not docked - if (!this.props.miniMode) { - ActiveWidgetStore.instance.dockWidget( - this.props.app.id, - isAppWidget(this.props.app) ? this.props.app.roomId : null, - ); - } - // The key used for PersistedElement this.persistKey = getPersistKey(WidgetUtils.getWidgetUid(this.props.app)); try { this.sgWidget = new StopGapWidget(this.props); - this.setupSgListeners(); } catch (e) { logger.log("Failed to construct widget", e); - this.sgWidget = null; + this.sgWidget = undefined; } this.state = this.getNewState(props); @@ -303,6 +294,20 @@ export default class AppTile extends React.Component { } public componentDidMount(): void { + this.unmounted = false; + + // Tiles in miniMode are floating, and therefore not docked + if (!this.props.miniMode) { + ActiveWidgetStore.instance.dockWidget( + this.props.app.id, + isAppWidget(this.props.app) ? this.props.app.roomId : null, + ); + } + + if (this.sgWidget) { + this.setupSgListeners(); + } + // Only fetch IM token on mount if we're showing and have permission to load if (this.sgWidget && this.state.hasPermissionToLoad) { this.startWidget(); @@ -374,7 +379,7 @@ export default class AppTile extends React.Component { this.startWidget(); } catch (e) { logger.error("Failed to construct widget", e); - this.sgWidget = null; + this.sgWidget = undefined; } } @@ -607,7 +612,7 @@ export default class AppTile extends React.Component { }; public render(): React.ReactNode { - let appTileBody: JSX.Element; + let appTileBody: JSX.Element | undefined; // Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin // because that would allow the iframe to programmatically remove the sandbox attribute, but @@ -650,7 +655,7 @@ export default class AppTile extends React.Component { ); - } else if (!this.state.hasPermissionToLoad && this.props.room) { + } else if (!this.state.hasPermissionToLoad && this.props.room && this.sgWidget) { // only possible for room widgets, can assert this.props.room here const isEncrypted = this.context.isRoomEncrypted(this.props.room.roomId); appTileBody = ( @@ -677,7 +682,7 @@ export default class AppTile extends React.Component { ); - } else { + } else if (this.sgWidget) { appTileBody = ( <>
diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx index 26b759bcb2..e1f1def836 100644 --- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx +++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx @@ -41,10 +41,6 @@ export interface ExistingSourceIProps { } export class ExistingSource extends React.Component { - public constructor(props: ExistingSourceIProps) { - super(props); - } - private onClick = (): void => { this.props.onSelect(this.props.source); }; diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index c8802cd880..a7ce84163c 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -127,7 +127,9 @@ export default class Dropdown extends React.Component { // the current search query searchQuery: "", }; + } + public componentDidMount(): void { // Listen for all clicks on the document so we can close the // menu when the user clicks somewhere else document.addEventListener("click", this.onDocumentClick, false); diff --git a/src/components/views/elements/LinkWithTooltip.tsx b/src/components/views/elements/LinkWithTooltip.tsx index a9ca2606ae..016297d9f1 100644 --- a/src/components/views/elements/LinkWithTooltip.tsx +++ b/src/components/views/elements/LinkWithTooltip.tsx @@ -15,10 +15,6 @@ interface IProps extends Omit, "tab } export default class LinkWithTooltip extends React.Component { - public constructor(props: IProps) { - super(props); - } - public render(): React.ReactNode { const { children, tooltip, ...props } = this.props; diff --git a/src/components/views/elements/PersistedElement.tsx b/src/components/views/elements/PersistedElement.tsx index 20584f3794..2c87c8e7c6 100644 --- a/src/components/views/elements/PersistedElement.tsx +++ b/src/components/views/elements/PersistedElement.tsx @@ -79,7 +79,7 @@ interface IProps { */ export default class PersistedElement extends React.Component { private resizeObserver: ResizeObserver; - private dispatcherRef: string; + private dispatcherRef?: string; private childContainer?: HTMLDivElement; private child?: HTMLDivElement; @@ -87,13 +87,6 @@ export default class PersistedElement extends React.Component { super(props); this.resizeObserver = new ResizeObserver(this.repositionChild); - // Annoyingly, a resize observer is insufficient, since we also care - // about when the element moves on the screen without changing its - // dimensions. Doesn't look like there's a ResizeObserver equivalent - // for this, so we bodge it by listening for document resize and - // the timeline_resize action. - window.addEventListener("resize", this.repositionChild); - this.dispatcherRef = dis.register(this.onAction); if (this.props.moveRef) this.props.moveRef.current = this.repositionChild; } @@ -132,6 +125,14 @@ export default class PersistedElement extends React.Component { }; public componentDidMount(): void { + // Annoyingly, a resize observer is insufficient, since we also care + // about when the element moves on the screen without changing its + // dimensions. Doesn't look like there's a ResizeObserver equivalent + // for this, so we bodge it by listening for document resize and + // the timeline_resize action. + window.addEventListener("resize", this.repositionChild); + this.dispatcherRef = dis.register(this.onAction); + this.updateChild(); this.renderApp(); } diff --git a/src/components/views/elements/PowerSelector.tsx b/src/components/views/elements/PowerSelector.tsx index b600b2ba96..385d932b87 100644 --- a/src/components/views/elements/PowerSelector.tsx +++ b/src/components/views/elements/PowerSelector.tsx @@ -68,6 +68,7 @@ export default class PowerSelector extends React.C } public componentDidMount(): void { + this.unmounted = false; this.initStateFromProps(); } diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx index 4eb3707031..71846d6065 100644 --- a/src/components/views/elements/ReplyChain.tsx +++ b/src/components/views/elements/ReplyChain.tsx @@ -89,6 +89,7 @@ export default class ReplyChain extends React.Component { } public componentDidMount(): void { + this.unmounted = false; this.initialize(); this.trySetExpandableQuotes(); } diff --git a/src/components/views/elements/TextWithTooltip.tsx b/src/components/views/elements/TextWithTooltip.tsx index 34346cbe25..b589ce3635 100644 --- a/src/components/views/elements/TextWithTooltip.tsx +++ b/src/components/views/elements/TextWithTooltip.tsx @@ -16,10 +16,6 @@ interface IProps extends HTMLAttributes { } export default class TextWithTooltip extends React.Component { - public constructor(props: IProps) { - super(props); - } - public render(): React.ReactNode { const { className, children, tooltip, tooltipProps } = this.props; diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx index 2c2eb442a0..b62df99e25 100644 --- a/src/components/views/emojipicker/ReactionPicker.tsx +++ b/src/components/views/emojipicker/ReactionPicker.tsx @@ -37,6 +37,9 @@ class ReactionPicker extends React.Component { this.state = { selectedEmojis: new Set(Object.keys(this.getReactions())), }; + } + + public componentDidMount(): void { this.addListeners(); } diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index 7996b3bebe..6aed04d8f9 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -58,7 +58,9 @@ export default class DateSeparator extends React.Component { this.state = { jumpToDateEnabled: SettingsStore.getValue("feature_jump_to_date"), }; + } + public componentDidMount(): void { // We're using a watcher so the date headers in the timeline are updated // when the lab setting is toggled. this.settingWatcherRef = SettingsStore.watchSetting( diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 332777be5e..d8cac8e28b 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -59,7 +59,7 @@ export default class MImageBody extends React.Component { public static contextType = RoomContext; public declare context: React.ContextType; - private unmounted = true; + private unmounted = false; private image = createRef(); private placeholder = createRef(); private timeout?: number; diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx index a547a78f94..bec4f56164 100644 --- a/src/components/views/messages/MJitsiWidgetEvent.tsx +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -21,10 +21,6 @@ interface IProps { } export default class MJitsiWidgetEvent extends React.PureComponent { - public constructor(props: IProps) { - super(props); - } - public render(): React.ReactNode { const url = this.props.mxEvent.getContent()["url"]; const prevUrl = this.props.mxEvent.getPrevContent()["url"]; diff --git a/src/components/views/messages/MLocationBody.tsx b/src/components/views/messages/MLocationBody.tsx index 742587e0a7..b226476fa8 100644 --- a/src/components/views/messages/MLocationBody.tsx +++ b/src/components/views/messages/MLocationBody.tsx @@ -75,6 +75,10 @@ export default class MLocationBody extends React.Component { this.context.on(ClientEvent.Sync, this.reconnectedListener); }; + public componentDidMount(): void { + this.unmounted = false; + } + public componentWillUnmount(): void { this.unmounted = true; this.context.off(ClientEvent.Sync, this.reconnectedListener); diff --git a/src/components/views/rooms/AppsDrawer.tsx b/src/components/views/rooms/AppsDrawer.tsx index 1d768cae35..c02bfe8cf2 100644 --- a/src/components/views/rooms/AppsDrawer.tsx +++ b/src/components/views/rooms/AppsDrawer.tsx @@ -68,11 +68,13 @@ export default class AppsDrawer extends React.Component { }; this.resizer = this.createResizer(); - - this.props.resizeNotifier.on("isResizing", this.onIsResizing); } public componentDidMount(): void { + this.unmounted = false; + + this.props.resizeNotifier.on("isResizing", this.onIsResizing); + ScalarMessaging.startListening(); WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps); this.dispatcherRef = dis.register(this.onAction); diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 0add0c1027..5f033de238 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -128,10 +128,10 @@ export default class BasicMessageEditor extends React.Component private lastCaret!: DocumentOffset; private lastSelection: ReturnType | null = null; - private readonly useMarkdownHandle: string; - private readonly emoticonSettingHandle: string; - private readonly shouldShowPillAvatarSettingHandle: string; - private readonly surroundWithHandle: string; + private useMarkdownHandle?: string; + private emoticonSettingHandle?: string; + private shouldShowPillAvatarSettingHandle?: string; + private surroundWithHandle?: string; private readonly historyManager = new HistoryManager(); public constructor(props: IProps) { @@ -145,28 +145,7 @@ export default class BasicMessageEditor extends React.Component const ua = navigator.userAgent.toLowerCase(); this.isSafari = ua.includes("safari/") && !ua.includes("chrome/"); - - this.useMarkdownHandle = SettingsStore.watchSetting( - "MessageComposerInput.useMarkdown", - null, - this.configureUseMarkdown, - ); - this.emoticonSettingHandle = SettingsStore.watchSetting( - "MessageComposerInput.autoReplaceEmoji", - null, - this.configureEmoticonAutoReplace, - ); this.configureEmoticonAutoReplace(); - this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting( - "Pill.shouldShowPillAvatar", - null, - this.configureShouldShowPillAvatar, - ); - this.surroundWithHandle = SettingsStore.watchSetting( - "MessageComposerInput.surroundWith", - null, - this.surroundWithSettingChanged, - ); } public componentDidUpdate(prevProps: IProps): void { @@ -737,6 +716,27 @@ export default class BasicMessageEditor extends React.Component } public componentDidMount(): void { + this.useMarkdownHandle = SettingsStore.watchSetting( + "MessageComposerInput.useMarkdown", + null, + this.configureUseMarkdown, + ); + this.emoticonSettingHandle = SettingsStore.watchSetting( + "MessageComposerInput.autoReplaceEmoji", + null, + this.configureEmoticonAutoReplace, + ); + this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting( + "Pill.shouldShowPillAvatar", + null, + this.configureShouldShowPillAvatar, + ); + this.surroundWithHandle = SettingsStore.watchSetting( + "MessageComposerInput.surroundWith", + null, + this.surroundWithSettingChanged, + ); + const model = this.props.model; model.setUpdateCallback(this.updateEditorState); const partCreator = model.partCreator; diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index 06f189df59..d62a451b8b 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -124,7 +124,7 @@ class EditMessageComposer extends React.Component; private readonly editorRef = createRef(); - private readonly dispatcherRef: string; + private dispatcherRef?: string; private readonly replyToEvent?: MatrixEvent; private model!: EditorModel; @@ -140,7 +140,9 @@ class EditMessageComposer extends React.Component } public componentDidMount(): void { + this.unmounted = false; this.suppressReadReceiptAnimation = false; const client = MatrixClientPeg.safeGet(); if (!this.props.forExport) { diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index 993a2ba1f1..e503ce2363 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -72,7 +72,7 @@ interface IState { export default class MemberList extends React.Component { private readonly showPresence: boolean; - private mounted = false; + private unmounted = false; public static contextType = SDKContext; public declare context: React.ContextType; @@ -82,8 +82,6 @@ export default class MemberList extends React.Component { super(props, context); this.state = this.getMembersState([], []); this.showPresence = context?.memberListStore.isPresenceEnabled() ?? true; - this.mounted = true; - this.listenForMembersChanges(); } private listenForMembersChanges(): void { @@ -102,11 +100,13 @@ export default class MemberList extends React.Component { } public componentDidMount(): void { + this.unmounted = false; + this.listenForMembersChanges(); this.updateListNow(true); } public componentWillUnmount(): void { - this.mounted = false; + this.unmounted = true; const cli = MatrixClientPeg.get(); if (cli) { cli.removeListener(RoomStateEvent.Update, this.onRoomStateUpdate); @@ -205,7 +205,7 @@ export default class MemberList extends React.Component { // XXX: exported for tests public async updateListNow(showLoadingSpinner?: boolean): Promise { - if (!this.mounted) { + if (this.unmounted) { return; } if (showLoadingSpinner) { @@ -215,7 +215,7 @@ export default class MemberList extends React.Component { this.props.roomId, this.props.searchQuery, ); - if (!this.mounted) { + if (this.unmounted) { return; } this.setState({ diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index c8d1573ebc..69139fae5b 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -134,9 +134,6 @@ export class MessageComposer extends React.Component { super(props, context); this.context = context; // otherwise React will only set it prior to render due to type def above - VoiceRecordingStore.instance.on(UPDATE_EVENT, this.onVoiceStoreUpdate); - - window.addEventListener("beforeunload", this.saveWysiwygEditorState); const isWysiwygLabEnabled = SettingsStore.getValue("feature_wysiwyg_composer"); let isRichTextEnabled = true; let initialComposerContent = ""; @@ -145,13 +142,6 @@ export class MessageComposer extends React.Component { if (wysiwygState) { isRichTextEnabled = wysiwygState.isRichText; initialComposerContent = wysiwygState.content; - if (wysiwygState.replyEventId) { - dis.dispatch({ - action: "reply_to_event", - event: this.props.room.findEventById(wysiwygState.replyEventId), - context: this.context.timelineRenderingType, - }); - } } } @@ -171,11 +161,6 @@ export class MessageComposer extends React.Component { }; this.instanceId = instanceCount++; - - SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null); - SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null); - SettingsStore.monitorSetting(Features.VoiceBroadcast, null); - SettingsStore.monitorSetting("feature_wysiwyg_composer", null); } private get editorStateKey(): string { @@ -248,6 +233,25 @@ export class MessageComposer extends React.Component { } public componentDidMount(): void { + VoiceRecordingStore.instance.on(UPDATE_EVENT, this.onVoiceStoreUpdate); + + window.addEventListener("beforeunload", this.saveWysiwygEditorState); + if (this.state.isWysiwygLabEnabled) { + const wysiwygState = this.restoreWysiwygEditorState(); + if (wysiwygState?.replyEventId) { + dis.dispatch({ + action: "reply_to_event", + event: this.props.room.findEventById(wysiwygState.replyEventId), + context: this.context.timelineRenderingType, + }); + } + } + + SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null); + SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null); + SettingsStore.monitorSetting(Features.VoiceBroadcast, null); + SettingsStore.monitorSetting("feature_wysiwyg_composer", null); + this.dispatcherRef = dis.register(this.onAction); this.waitForOwnMember(); UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current!); diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index c4cc418db4..6825ea8e43 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -44,15 +44,23 @@ interface IState { } export default class NotificationBadge extends React.PureComponent, IState> { - private countWatcherRef: string; + private countWatcherRef?: string; public constructor(props: IProps) { super(props); - this.props.notification.on(NotificationStateEvents.Update, this.onNotificationUpdate); this.state = { showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId), }; + } + + private get roomId(): string | null { + // We should convert this to null for safety with the SettingsStore + return this.props.roomId || null; + } + + public componentDidMount(): void { + this.props.notification.on(NotificationStateEvents.Update, this.onNotificationUpdate); this.countWatcherRef = SettingsStore.watchSetting( "Notifications.alwaysShowBadgeCounts", @@ -61,11 +69,6 @@ export default class NotificationBadge extends React.PureComponent v }; export default class RoomBreadcrumbs extends React.PureComponent { - private isMounted = true; + private unmounted = false; private toolbar = createRef(); public constructor(props: IProps) { @@ -70,17 +70,20 @@ export default class RoomBreadcrumbs extends React.PureComponent doAnimation: true, // technically we want animation on mount, but it won't be perfect skipFirst: false, // render the thing, as boring as it is }; + } + public componentDidMount(): void { + this.unmounted = false; BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); } public componentWillUnmount(): void { - this.isMounted = false; + this.unmounted = true; BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); } private onBreadcrumbsUpdate = (): void => { - if (!this.isMounted) return; + if (this.unmounted) return; // We need to trick the CSSTransition component into updating, which means we need to // tell it to not animate, then to animate a moment later. This causes two updates diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index de14808f33..8351c176ff 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -94,7 +94,6 @@ export class RoomTile extends React.PureComponent { // generatePreview() will return nothing if the user has previews disabled messagePreview: null, }; - this.generatePreview(); this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room); this.roomProps = EchoChamber.forRoom(this.props.room); @@ -147,6 +146,8 @@ export class RoomTile extends React.PureComponent { } public componentDidMount(): void { + this.generatePreview(); + // when we're first rendered (or our sublist is expanded) make sure we are visible if we're active if (this.state.selected) { this.scrollIntoView(); diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 776963eb33..a12a09dcb7 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -255,7 +255,7 @@ export class SendMessageComposer extends React.Component(); private model: EditorModel; private currentlyComposedEditorState: SerializedPart[] | null = null; - private dispatcherRef: string; + private dispatcherRef?: string; private sendHistoryManager: SendHistoryManager; public static defaultProps = { @@ -275,15 +275,17 @@ export class SendMessageComposer extends React.Component { } public componentDidMount(): void { + this.unmounted = false; const cli = MatrixClientPeg.safeGet(); cli.on(ClientEvent.AccountData, this.onAccountData); cli.on(CryptoEvent.UserTrustStatusChanged, this.onStatusChanged); diff --git a/src/components/views/settings/CryptographyPanel.tsx b/src/components/views/settings/CryptographyPanel.tsx index 08917d215c..ae0436a9e5 100644 --- a/src/components/views/settings/CryptographyPanel.tsx +++ b/src/components/views/settings/CryptographyPanel.tsx @@ -11,7 +11,6 @@ import { logger } from "matrix-js-sdk/src/logger"; import type ExportE2eKeysDialog from "../../../async-components/views/dialogs/security/ExportE2eKeysDialog"; import type ImportE2eKeysDialog from "../../../async-components/views/dialogs/security/ImportE2eKeysDialog"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; import AccessibleButton from "../elements/AccessibleButton"; @@ -20,6 +19,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import SettingsFlag from "../elements/SettingsFlag"; import { SettingLevel } from "../../../settings/SettingLevel"; import SettingsSubsection, { SettingsSubsectionText } from "./shared/SettingsSubsection"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; interface IProps {} @@ -33,17 +33,24 @@ interface IState { } export default class CryptographyPanel extends React.Component { - public constructor(props: IProps) { + public static contextType = MatrixClientContext; + public declare context: React.ContextType; + + public constructor(props: IProps, context: React.ContextType) { super(props); - const client = MatrixClientPeg.safeGet(); - const crypto = client.getCrypto(); - if (!crypto) { + if (!context.getCrypto()) { this.state = { deviceIdentityKey: null }; } else { this.state = { deviceIdentityKey: undefined }; - crypto - .getOwnDeviceKeys() + } + } + + public componentDidMount(): void { + if (this.state.deviceIdentityKey === undefined) { + this.context + .getCrypto() + ?.getOwnDeviceKeys() .then((keys) => { this.setState({ deviceIdentityKey: keys.ed25519 }); }) @@ -55,7 +62,7 @@ export default class CryptographyPanel extends React.Component { } public render(): React.ReactNode { - const client = MatrixClientPeg.safeGet(); + const client = this.context; const deviceId = client.deviceId; let identityKey = this.state.deviceIdentityKey; if (identityKey === undefined) { @@ -126,7 +133,7 @@ export default class CryptographyPanel extends React.Component { import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog") as unknown as Promise< typeof ExportE2eKeysDialog >, - { matrixClient: MatrixClientPeg.safeGet() }, + { matrixClient: this.context }, ); }; @@ -135,12 +142,12 @@ export default class CryptographyPanel extends React.Component { import("../../../async-components/views/dialogs/security/ImportE2eKeysDialog") as unknown as Promise< typeof ImportE2eKeysDialog >, - { matrixClient: MatrixClientPeg.safeGet() }, + { matrixClient: this.context }, ); }; private updateBlacklistDevicesFlag = (checked: boolean): void => { - const crypto = MatrixClientPeg.safeGet().getCrypto(); + const crypto = this.context.getCrypto(); if (crypto) crypto.globalBlacklistUnverifiedDevices = checked; }; } diff --git a/src/components/views/settings/FontScalingPanel.tsx b/src/components/views/settings/FontScalingPanel.tsx index f6dedb3ffb..b7f7c64a3b 100644 --- a/src/components/views/settings/FontScalingPanel.tsx +++ b/src/components/views/settings/FontScalingPanel.tsx @@ -55,6 +55,7 @@ export default class FontScalingPanel extends React.Component { } public async componentDidMount(): Promise { + this.unmounted = false; // Fetch the current user profile for the message preview const client = MatrixClientPeg.safeGet(); const userId = client.getSafeUserId(); diff --git a/src/components/views/settings/Notifications.tsx b/src/components/views/settings/Notifications.tsx index 78660d4f9d..6890c7b5d3 100644 --- a/src/components/views/settings/Notifications.tsx +++ b/src/components/views/settings/Notifications.tsx @@ -206,7 +206,7 @@ const NotificationActivitySettings = (): JSX.Element => { * The old, deprecated notifications tab view, only displayed if the user has the labs flag disabled. */ export default class Notifications extends React.PureComponent { - private settingWatchers: string[]; + private settingWatchers: string[] = []; public constructor(props: IProps) { super(props); @@ -220,7 +220,17 @@ export default class Notifications extends React.PureComponent { clearingNotifications: false, 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 = [ SettingsStore.watchSetting("notificationsEnabled", null, (...[, , , , value]) => this.setState({ desktopNotifications: value as boolean }), @@ -235,17 +245,7 @@ export default class Notifications extends React.PureComponent { 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 this.refreshFromServer(); this.refreshFromAccountData(); diff --git a/src/components/views/settings/SecureBackupPanel.tsx b/src/components/views/settings/SecureBackupPanel.tsx index dac7425e3c..6a855c8ea8 100644 --- a/src/components/views/settings/SecureBackupPanel.tsx +++ b/src/components/views/settings/SecureBackupPanel.tsx @@ -83,6 +83,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { } public componentDidMount(): void { + this.unmounted = false; this.loadBackupStatus(); MatrixClientPeg.safeGet().on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus); diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx index c32ac5150b..337cead3a3 100644 --- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx @@ -53,9 +53,11 @@ export default class AdvancedRoomSettingsTab extends React.Component { collapsed, childSpaces: this.childSpaces, }; + } + public componentDidMount(): void { SpaceStore.instance.on(this.props.space.roomId, this.onSpaceUpdate); this.props.space.on(RoomEvent.Name, this.onRoomNameChange); } diff --git a/src/components/views/voip/LegacyCallView.tsx b/src/components/views/voip/LegacyCallView.tsx index 124d078782..aba3d60743 100644 --- a/src/components/views/voip/LegacyCallView.tsx +++ b/src/components/views/voip/LegacyCallView.tsx @@ -110,11 +110,10 @@ export default class LegacyCallView extends React.Component { sidebarFeeds: sidebar, sidebarShown: true, }; - - this.updateCallListeners(null, this.props.call); } public componentDidMount(): void { + this.updateCallListeners(null, this.props.call); this.dispatcherRef = dis.register(this.onAction); document.addEventListener("keydown", this.onNativeKeyDown); } diff --git a/test/unit-tests/components/views/settings/CryptographyPanel-test.tsx b/test/unit-tests/components/views/settings/CryptographyPanel-test.tsx index ce506461ee..0b699c1383 100644 --- a/test/unit-tests/components/views/settings/CryptographyPanel-test.tsx +++ b/test/unit-tests/components/views/settings/CryptographyPanel-test.tsx @@ -14,6 +14,7 @@ import { mocked } from "jest-mock"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import * as TestUtils from "../../../../test-utils"; import CryptographyPanel from "../../../../../src/components/views/settings/CryptographyPanel"; +import { withClientContextRenderOptions } from "../../../../test-utils"; describe("CryptographyPanel", () => { it("shows the session ID and key", async () => { @@ -28,7 +29,7 @@ describe("CryptographyPanel", () => { mocked(client.getCrypto()!.getOwnDeviceKeys).mockResolvedValue({ ed25519: sessionKey, curve25519: "1234" }); // When we render the CryptographyPanel - const rendered = render(); + const rendered = render(, withClientContextRenderOptions(client)); // Then it displays info about the user's session const codes = rendered.container.querySelectorAll("code"); @@ -52,7 +53,7 @@ describe("CryptographyPanel", () => { mocked(client.getCrypto()!.getOwnDeviceKeys).mockRejectedValue(new Error("bleh")); // When we render the CryptographyPanel - const rendered = render(); + const rendered = render(, withClientContextRenderOptions(client)); // Then it displays info about the user's session const codes = rendered.container.querySelectorAll("code");