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 componentDidMount(): void {
this.unmounted = false;
this.props.prom
.then((result) => {
if (this.unmounted) return;

View File

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

View File

@@ -8,9 +8,9 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { StrictMode } from "react";
import ReactDOM from "react-dom";
import { createRoot, Root } from "react-dom/client";
import classNames from "classnames";
import { IDeferred, defer, sleep } from "matrix-js-sdk/src/utils";
import { IDeferred, defer } from "matrix-js-sdk/src/utils";
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
import { Glass, TooltipProvider } from "@vector-im/compound-web";
@@ -69,6 +69,16 @@ type HandlerMap = {
type ModalCloseReason = "backgroundClick";
function getOrCreateContainer(id: string): HTMLDivElement {
let container = document.getElementById(id) as HTMLDivElement | null;
if (!container) {
container = document.createElement("div");
container.id = id;
document.body.appendChild(container);
}
return container;
}
export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMap> {
private counter = 0;
// The modal to prioritise over all others. If this is set, only show
@@ -83,28 +93,22 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
// Neither the static nor priority modal will be in this list.
private modals: IModal<any>[] = [];
private static getOrCreateContainer(): HTMLElement {
let container = document.getElementById(DIALOG_CONTAINER_ID);
if (!container) {
container = document.createElement("div");
container.id = DIALOG_CONTAINER_ID;
document.body.appendChild(container);
private static root?: Root;
private static getOrCreateRoot(): Root {
if (!ModalManager.root) {
const container = getOrCreateContainer(DIALOG_CONTAINER_ID);
ModalManager.root = createRoot(container);
}
return container;
return ModalManager.root;
}
private static getOrCreateStaticContainer(): HTMLElement {
let container = document.getElementById(STATIC_DIALOG_CONTAINER_ID);
if (!container) {
container = document.createElement("div");
container.id = STATIC_DIALOG_CONTAINER_ID;
document.body.appendChild(container);
private static staticRoot?: Root;
private static getOrCreateStaticRoot(): Root {
if (!ModalManager.staticRoot) {
const container = getOrCreateContainer(STATIC_DIALOG_CONTAINER_ID);
ModalManager.staticRoot = createRoot(container);
}
return container;
return ModalManager.staticRoot;
}
public constructor() {
@@ -389,19 +393,14 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
}
private async reRender(): Promise<void> {
// TODO: We should figure out how to remove this weird sleep. It also makes testing harder
//
// await next tick because sometimes ReactDOM can race with itself and cause the modal to wrongly stick around
await sleep(0);
if (this.modals.length === 0 && !this.priorityModal && !this.staticModal) {
// If there is no modal to render, make all of Element available
// to screen reader users again
dis.dispatch({
action: "aria_unhide_main_app",
});
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer());
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer());
ModalManager.getOrCreateRoot().render(<></>);
ModalManager.getOrCreateStaticRoot().render(<></>);
return;
}
@@ -432,10 +431,10 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
</StrictMode>
);
ReactDOM.render(staticDialog, ModalManager.getOrCreateStaticContainer());
ModalManager.getOrCreateStaticRoot().render(staticDialog);
} else {
// This is safe to call repeatedly if we happen to do that
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer());
ModalManager.getOrCreateStaticRoot().render(<></>);
}
const modal = this.getCurrentModal();
@@ -461,10 +460,10 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
</StrictMode>
);
setTimeout(() => ReactDOM.render(dialog, ModalManager.getOrCreateContainer()), 0);
ModalManager.getOrCreateRoot().render(dialog);
} else {
// This is safe to call repeatedly if we happen to do that
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer());
ModalManager.getOrCreateRoot().render(<></>);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -231,10 +231,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private prevWindowWidth: number;
private voiceBroadcastResumer?: VoiceBroadcastResumer;
private readonly loggedInView: React.RefObject<LoggedInViewType>;
private readonly dispatcherRef: string;
private readonly themeWatcher: ThemeWatcher;
private readonly fontWatcher: FontWatcher;
private readonly loggedInView = createRef<LoggedInViewType>();
private dispatcherRef?: string;
private themeWatcher?: ThemeWatcher;
private fontWatcher?: FontWatcher;
private readonly stores: SdkContextClass;
public constructor(props: IProps) {
@@ -256,8 +256,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
ready: false,
};
this.loggedInView = createRef();
SdkConfig.put(this.props.config);
// Used by _viewRoom before getting state from sync
@@ -282,32 +280,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
this.prevWindowWidth = UIStore.instance.windowWidth || 1000;
UIStore.instance.on(UI_EVENTS.Resize, this.handleResize);
// For PersistentElement
this.state.resizeNotifier.on("middlePanelResized", this.dispatchTimelineResize);
RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatusIndicator);
this.dispatcherRef = dis.register(this.onAction);
this.themeWatcher = new ThemeWatcher();
this.fontWatcher = new FontWatcher();
this.themeWatcher.start();
this.fontWatcher.start();
// object field used for tracking the status info appended to the title tag.
// we don't do it as react state as i'm scared about triggering needless react refreshes.
this.subTitleStatus = "";
initSentry(SdkConfig.get("sentry"));
if (!checkSessionLockFree()) {
// another instance holds the lock; confirm its theft before proceeding
setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0);
} else {
this.startInitSession();
}
}
/**
@@ -476,6 +452,29 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
public componentDidMount(): void {
UIStore.instance.on(UI_EVENTS.Resize, this.handleResize);
// For PersistentElement
this.state.resizeNotifier.on("middlePanelResized", this.dispatchTimelineResize);
RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatusIndicator);
this.dispatcherRef = dis.register(this.onAction);
this.themeWatcher = new ThemeWatcher();
this.fontWatcher = new FontWatcher();
this.themeWatcher.start();
this.fontWatcher.start();
initSentry(SdkConfig.get("sentry"));
if (!checkSessionLockFree()) {
// another instance holds the lock; confirm its theft before proceeding
setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0);
} else {
this.startInitSession();
}
window.addEventListener("resize", this.onWindowResized);
}
@@ -497,8 +496,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
public componentWillUnmount(): void {
Lifecycle.stopMatrixClient();
dis.unregister(this.dispatcherRef);
this.themeWatcher.stop();
this.fontWatcher.stop();
this.themeWatcher?.stop();
this.fontWatcher?.stop();
UIStore.destroy();
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
window.removeEventListener("resize", this.onWindowResized);
@@ -1011,7 +1010,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.setStateForNewView(newState);
ThemeController.isLogin = true;
this.themeWatcher.recheck();
this.themeWatcher?.recheck();
this.notifyNewScreen(isMobileRegistration ? "mobile_register" : "register");
}
@@ -1088,7 +1087,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
},
() => {
ThemeController.isLogin = false;
this.themeWatcher.recheck();
this.themeWatcher?.recheck();
this.notifyNewScreen("room/" + presentedId, replaceLast);
},
);
@@ -1113,7 +1112,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
this.notifyNewScreen("welcome");
ThemeController.isLogin = true;
this.themeWatcher.recheck();
this.themeWatcher?.recheck();
}
private viewLogin(otherState?: any): void {
@@ -1123,7 +1122,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
this.notifyNewScreen("login");
ThemeController.isLogin = true;
this.themeWatcher.recheck();
this.themeWatcher?.recheck();
}
private viewHome(justRegistered = false): void {
@@ -1136,7 +1135,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.setPage(PageType.HomePage);
this.notifyNewScreen("home");
ThemeController.isLogin = false;
this.themeWatcher.recheck();
this.themeWatcher?.recheck();
}
private viewUser(userId: string, subAction: string): void {
@@ -1357,7 +1356,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
*/
private async onLoggedIn(): Promise<void> {
ThemeController.isLogin = false;
this.themeWatcher.recheck();
this.themeWatcher?.recheck();
StorageManager.tryPersistStorage();
await this.onShowPostLoginScreen();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -60,7 +60,7 @@ const RoomBreadcrumbTile: React.FC<{ room: Room; onClick: (ev: ButtonEvent) => v
};
export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState> {
private isMounted = true;
private unmounted = false;
public constructor(props: IProps) {
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
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

View File

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

View File

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

View File

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

View File

@@ -255,7 +255,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
private readonly editorRef = createRef<BasicMessageComposer>();
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<ISendMessageComposerPro
);
}
window.addEventListener("beforeunload", this.saveStoredEditorState);
const partCreator = new CommandPartCreator(this.props.room, this.props.mxClient);
const parts = this.restoreStoredEditorState(partCreator) || [];
this.model = new EditorModel(parts, partCreator);
this.dispatcherRef = dis.register(this.onAction);
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 {
const replyingToThread = this.props.relation?.key === THREAD_RELATION_TYPE.name;
const differentEventTarget = this.props.relation?.event_id !== prevProps.relation?.event_id;

View File

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

View File

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

View File

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

View File

@@ -52,7 +52,7 @@ export default class IntegrationManager extends React.Component<IProps, IState>
}
public componentWillUnmount(): void {
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
dis.unregister(this.dispatcherRef);
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.
*/
export default class Notifications extends React.PureComponent<IProps, IState> {
private settingWatchers: string[];
private settingWatchers: string[] = [];
public constructor(props: IProps) {
super(props);
@@ -220,7 +220,17 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
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<IProps, IState> {
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();

View File

@@ -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);

View File

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

View File

@@ -53,9 +53,11 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
public constructor(props: IProps) {
super(props);
const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors");
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.
const room = this.props.room;

View File

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

View File

@@ -205,7 +205,9 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
collapsed,
childSpaces: this.childSpaces,
};
}
public componentDidMount(): void {
SpaceStore.instance.on(this.props.space.roomId, this.onSpaceUpdate);
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,
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);
}
@@ -126,7 +125,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
document.removeEventListener("keydown", this.onNativeKeyDown);
this.updateCallListeners(this.props.call, null);
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
dis.unregister(this.dispatcherRef);
}
public static getDerivedStateFromProps(props: IProps): Partial<IState> {

View File

@@ -46,7 +46,8 @@ export class MatrixDispatcher {
/**
* 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.`);
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.
export class Mjolnir {
private static instance: Mjolnir | null = null;
private static instance?: Mjolnir;
private _lists: BanList[] = []; // eslint-disable-line @typescript-eslint/naming-convention
private _roomIds: string[] = []; // eslint-disable-line @typescript-eslint/naming-convention
private mjolnirWatchRef: string | null = null;
private dispatcherRef: string | null = null;
private mjolnirWatchRef?: string;
private dispatcherRef?: string;
public get roomIds(): string[] {
return this._roomIds;
@@ -61,15 +61,11 @@ export class Mjolnir {
}
public stop(): void {
if (this.mjolnirWatchRef) {
SettingsStore.unwatchSetting(this.mjolnirWatchRef);
this.mjolnirWatchRef = null;
}
SettingsStore.unwatchSetting(this.mjolnirWatchRef);
this.mjolnirWatchRef = undefined;
if (this.dispatcherRef) {
dis.unregister(this.dispatcherRef);
this.dispatcherRef = null;
}
dis.unregister(this.dispatcherRef);
this.dispatcherRef = undefined;
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 readonly STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60; // 1 hour
private settingsStoreCallEncryptionWatcher: string | null = null;
private terminationTimer: number | null = null;
private settingsStoreCallEncryptionWatcher?: string;
private terminationTimer?: number;
private _layout = Layout.Tile;
public get layout(): Layout {
return this._layout;
@@ -938,13 +938,9 @@ export class ElementCall extends Call {
this.session.off(MatrixRTCSessionEvent.MembershipsChanged, this.onMembershipChanged);
this.client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, this.onRTCSessionEnded);
if (this.settingsStoreCallEncryptionWatcher) {
SettingsStore.unwatchSetting(this.settingsStoreCallEncryptionWatcher);
}
if (this.terminationTimer !== null) {
clearTimeout(this.terminationTimer);
this.terminationTimer = null;
}
SettingsStore.unwatchSetting(this.settingsStoreCallEncryptionWatcher);
clearTimeout(this.terminationTimer);
this.terminationTimer = undefined;
super.destroy();
}

View File

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

View File

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

View File

@@ -18,9 +18,9 @@ import { ActionPayload } from "../../dispatcher/payloads";
import { SettingLevel } from "../SettingLevel";
export default class ThemeWatcher {
private themeWatchRef: string | null;
private systemThemeWatchRef: string | null;
private dispatcherRef: string | null;
private themeWatchRef?: string;
private systemThemeWatchRef?: string;
private dispatcherRef?: string;
private preferDark: MediaQueryList;
private preferLight: MediaQueryList;
@@ -29,10 +29,6 @@ export default class ThemeWatcher {
private currentTheme: string;
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 can get the tristate of dark/light/unsupported
this.preferDark = (<any>global).matchMedia("(prefers-color-scheme: dark)");
@@ -55,9 +51,9 @@ export default class ThemeWatcher {
this.preferDark.removeEventListener("change", this.onChange);
this.preferLight.removeEventListener("change", this.onChange);
this.preferHighContrast.removeEventListener("change", this.onChange);
if (this.systemThemeWatchRef) SettingsStore.unwatchSetting(this.systemThemeWatchRef);
if (this.themeWatchRef) SettingsStore.unwatchSetting(this.themeWatchRef);
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
SettingsStore.unwatchSetting(this.systemThemeWatchRef);
SettingsStore.unwatchSetting(this.themeWatchRef);
dis.unregister(this.dispatcherRef);
}
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.
*/
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
this.readyStore = new (class extends ReadyWatchingStore {
public get mxClient(): MatrixClient | null {
return this.matrixClient;
return this.matrixClient ?? null;
}
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(RoomStateEvent.Members, this.onRoomStateMembers);
}
SettingsStore.unwatchSetting(this.dynamicWatcherRef ?? "");
SettingsStore.unwatchSetting(this.dynamicWatcherRef);
this.clearBeacons();
}

View File

@@ -16,8 +16,8 @@ import { Action } from "../dispatcher/actions";
import { MatrixDispatcher } from "../dispatcher/dispatcher";
export abstract class ReadyWatchingStore extends EventEmitter implements IDestroyable {
protected matrixClient: MatrixClient | null = null;
private dispatcherRef: string | null = null;
protected matrixClient?: MatrixClient;
private dispatcherRef?: string;
public constructor(protected readonly dispatcher: MatrixDispatcher) {
super();
@@ -35,7 +35,7 @@ export abstract class ReadyWatchingStore extends EventEmitter implements IDestro
}
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 {
@@ -43,7 +43,7 @@ export abstract class ReadyWatchingStore extends EventEmitter implements IDestro
}
public destroy(): void {
if (this.dispatcherRef !== null) this.dispatcher.unregister(this.dispatcherRef);
this.dispatcher.unregister(this.dispatcherRef);
}
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) {
if (this.matrixClient) {
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.matrixClient?.off(RoomStateEvent.Events, this.updateRoomFromState);
if (this.pinnedRef) SettingsStore.unwatchSetting(this.pinnedRef);
if (this.layoutRef) SettingsStore.unwatchSetting(this.layoutRef);
if (this.dynamicRef) SettingsStore.unwatchSetting(this.dynamicRef);
SettingsStore.unwatchSetting(this.pinnedRef);
SettingsStore.unwatchSetting(this.layoutRef);
SettingsStore.unwatchSetting(this.dynamicRef);
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", () => ({
dispatch: jest.fn(),
register: jest.fn(),
unregister: jest.fn(),
}));
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 AutoDiscoveryUtils from "../../../../src/utils/AutoDiscoveryUtils";
import { ValidatedServerConfig } from "../../../../src/utils/ValidatedServerConfig";
import Modal from "../../../../src/Modal.tsx";
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
completeAuthorizationCodeGrant: jest.fn(),
@@ -1514,7 +1515,9 @@ describe("<MatrixChat />", () => {
describe("when key backup failed", () => {
it("should show the new recovery method dialog", async () => {
const spy = jest.spyOn(Modal, "createDialogAsync");
jest.mock("../../../../src/async-components/views/dialogs/security/NewRecoveryMethodDialog", () => ({
__test: true,
__esModule: true,
default: () => <span>mocked dialog</span>,
}));
@@ -1526,7 +1529,8 @@ describe("<MatrixChat />", () => {
});
await flushPromises();
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 { 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 { MatrixClient, createClient } from "matrix-js-sdk/src/matrix";
@@ -47,14 +47,12 @@ describe("<ForgotPassword>", () => {
};
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 => {
it("should close the dialog and show the password input", () => {
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument();
it("should close the dialog and show the password input", async () => {
await waitFor(() => expect(screen.queryByText("Verify your email to continue")).not.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(
{
type: "m.login.email.identity",
@@ -326,15 +324,15 @@ describe("<ForgotPassword>", () => {
testPassword,
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();
});
describe("and dismissing the dialog by clicking the background", () => {
beforeEach(async () => {
await act(async () => {
await userEvent.click(screen.getByTestId("dialog-background"), { delay: null });
});
await userEvent.click(await screen.findByTestId("dialog-background"), { delay: null });
await waitEnoughCyclesForModal({
useFakeTimers: true,
});
@@ -345,7 +343,7 @@ describe("<ForgotPassword>", () => {
describe("and dismissing the dialog", () => {
beforeEach(async () => {
await click(screen.getByLabelText("Close dialog"));
await click(await screen.findByLabelText("Close dialog"));
await waitEnoughCyclesForModal({
useFakeTimers: true,
});
@@ -356,14 +354,16 @@ describe("<ForgotPassword>", () => {
describe("and clicking »Re-enter email address«", () => {
beforeEach(async () => {
await click(screen.getByText("Re-enter email address"));
await click(await screen.findByText("Re-enter email address"));
await waitEnoughCyclesForModal({
useFakeTimers: true,
});
});
it("should close the dialog and go back to the email input", () => {
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument();
it("should close the dialog and go back to the email input", async () => {
await waitFor(() =>
expect(screen.queryByText("Verify your email to continue")).not.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 () => {
expect(
screen.getByText(
await expect(
screen.findByText(
"Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.",
),
).toBeInTheDocument();
).resolves.toBeInTheDocument();
// confirm dialog
await click(screen.getByText("Continue"));

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