diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index 97cb478512..9c3e7073d5 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -679,7 +679,7 @@ export default class LegacyCallHandler extends TypedEventEmitter { - SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow); - }, }, undefined, true, ); + + finished.then(([allow]) => { + SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow); + }); } private showMediaCaptureError(call: MatrixCall): void { diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index c1559886b4..6f78c01357 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -469,12 +469,15 @@ type TryAgainFunction = () => void; * @param tryAgain OPTIONAL function to call on try again button from error dialog */ function onFailedDelegatedAuthLogin(description: string | ReactNode, tryAgain?: TryAgainFunction): void { - Modal.createDialog(ErrorDialog, { + const { finished } = Modal.createDialog(ErrorDialog, { title: _t("auth|oidc|error_title"), description, button: _t("action|try_again"), + }); + + finished.then(([shouldTryAgain]) => { // if we have a tryAgain callback, call it the primary 'try again' button was clicked in the dialog - onFinished: tryAgain ? (shouldTryAgain?: boolean) => shouldTryAgain && tryAgain() : undefined, + if (shouldTryAgain) tryAgain?.(); }); } diff --git a/src/Modal.tsx b/src/Modal.tsx index ba5c6702bd..cd6c6a3255 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -30,12 +30,25 @@ export type ComponentType = }> | React.ComponentType; -// Generic type which returns the props of the Modal component with the onFinished being optional. +/** + * The parameter types of the `onFinished` callback property exposed by the component which forms the + * body of the dialog. + * + * @typeParam C - The type of the React component which forms the body of the dialog. + */ +type OnFinishedParams = Parameters["onFinished"]>; + +/** + * The properties exposed by the `props` argument to {@link Modal.createDialog}: the same as + * those exposed by the underlying component, with the exception of `onFinished`, which is provided by + * `createDialog`. + * + * @typeParam C - The type of the React component which forms the body of the dialog. + */ export type ComponentProps = Defaultize< Omit, "onFinished">, C["defaultProps"] -> & - Partial, "onFinished">>; +>; export interface IModal { elem: React.ReactNode; @@ -43,15 +56,44 @@ export interface IModal { beforeClosePromise?: Promise; closeReason?: ModalCloseReason; onBeforeClose?(reason?: ModalCloseReason): Promise; - onFinished: ComponentProps["onFinished"]; - close(...args: Parameters["onFinished"]>): void; + + /** + * Run the {@link deferred} with the given arguments, and close this modal. + * + * This method is passed as the `onFinished` callback to the underlying component, + * as well as being returned by {@link Modal.createDialog} to the caller. + */ + close(...args: OnFinishedParams | []): void; + hidden?: boolean; - deferred?: IDeferred["onFinished"]>>; + + /** A deferred to resolve when the dialog closes, with the results as provided by + * the call to {@link close} (normally from the `onFinished` callback). + */ + deferred?: IDeferred | []>; } +/** The result of {@link Modal.createDialog}. + * + * @typeParam C - The type of the React component which forms the body of the dialog. + */ export interface IHandle { - finished: Promise["onFinished"]>>; - close(...args: Parameters["onFinished"]>): void; + /** + * A promise which will resolve when the dialog closes. + * + * If the dialog body component calls the `onFinished` property, or the caller calls {@link close}, + * the promise resolves with an array holding the arguments to that call. + * + * If the dialog is closed by clicking in the background, the promise resolves with an empty array. + */ + finished: Promise | []>; + + /** + * A function which, if called, will close the dialog. + * + * @param args - Arguments to return to {@link finished}. + */ + close(...args: OnFinishedParams): void; } interface IOptions { @@ -164,7 +206,6 @@ export class ModalManager extends TypedEventEmitter["finished"]; } { const modal = { - onFinished: props?.onFinished, onBeforeClose: options?.onBeforeClose, className, @@ -196,8 +236,7 @@ export class ModalManager extends TypedEventEmitter; - // never call this from onFinished() otherwise it will loop - const [closeDialog, onFinishedProm] = this.getCloseFn(modal, props); + const [closeDialog, onFinishedProm] = this.getCloseFn(modal); // don't attempt to reuse the same AsyncWrapper for different dialogs, // otherwise we'll get confused. @@ -214,13 +253,10 @@ export class ModalManager extends TypedEventEmitter( - modal: IModal, - props?: ComponentProps, - ): [IHandle["close"], IHandle["finished"]] { - modal.deferred = defer["onFinished"]>>(); + private getCloseFn(modal: IModal): [IHandle["close"], IHandle["finished"]] { + modal.deferred = defer | []>(); return [ - async (...args: Parameters["onFinished"]>): Promise => { + async (...args: OnFinishedParams): Promise => { if (modal.beforeClosePromise) { await modal.beforeClosePromise; } else if (modal.onBeforeClose) { @@ -232,7 +268,6 @@ export class ModalManager extends TypedEventEmitter= 0) { this.modals.splice(i, 1); @@ -280,7 +315,8 @@ export class ModalManager extends TypedEventEmitter import('./MyComponent'))` * - * @param props properties to pass to the displayed component. (We will also pass an 'onFinished' property.) + * @param props properties to pass to the displayed component. (We will also pass an `onFinished` property; when + * called, that property will close the dialog and return the results to the caller via {@link IHandle.finished}.) * * @param className CSS class to apply to the modal wrapper * @@ -295,7 +331,7 @@ export class ModalManager extends TypedEventEmitter( component: C, diff --git a/src/Registration.tsx b/src/Registration.tsx index ea0264fab3..22eb6e15ff 100644 --- a/src/Registration.tsx +++ b/src/Registration.tsx @@ -62,14 +62,14 @@ export async function startAnyRegistrationFlow( , ] : [], - onFinished: (proceed) => { - if (proceed) { - dis.dispatch({ action: "start_login", screenAfterLogin: options.screen_after }); - } else if (options.go_home_on_cancel) { - dis.dispatch({ action: Action.ViewHomePage }); - } else if (options.go_welcome_on_cancel) { - dis.dispatch({ action: "view_welcome_page" }); - } - }, + }); + modal.finished.then(([proceed]) => { + if (proceed) { + dis.dispatch({ action: "start_login", screenAfterLogin: options.screen_after }); + } else if (options.go_home_on_cancel) { + dis.dispatch({ action: Action.ViewHomePage }); + } else if (options.go_welcome_on_cancel) { + dis.dispatch({ action: "view_welcome_page" }); + } }); } diff --git a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx index 9b9683b798..a2fef4e1c9 100644 --- a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx +++ b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx @@ -49,15 +49,8 @@ export default function NewRecoveryMethodDialog({ onFinished }: NewRecoveryMetho if (isKeyBackupEnabled) { onFinished(); } else { - Modal.createDialog( - RestoreKeyBackupDialog, - { - onFinished, - }, - undefined, - false, - true, - ); + const { finished } = Modal.createDialog(RestoreKeyBackupDialog, {}, undefined, false, true); + finished.then(onFinished); } } diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 83d6f5f54c..02a3a55c59 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1229,7 +1229,7 @@ export default class MatrixChat extends React.PureComponent { const warnings = this.leaveRoomWarnings(roomId); const isSpace = roomToLeave?.isSpaceRoom(); - Modal.createDialog(QuestionDialog, { + const { finished } = Modal.createDialog(QuestionDialog, { title: isSpace ? _t("space|leave_dialog_action") : _t("action|leave_room"), description: ( @@ -1245,16 +1245,17 @@ export default class MatrixChat extends React.PureComponent { ), button: _t("action|leave"), danger: warnings.length > 0, - onFinished: async (shouldLeave) => { - if (shouldLeave) { - await leaveRoomBehaviour(cli, roomId); + }); - dis.dispatch({ - action: Action.AfterLeaveRoom, - room_id: roomId, - }); - } - }, + finished.then(async ([shouldLeave]) => { + if (shouldLeave) { + await leaveRoomBehaviour(cli, roomId); + + dis.dispatch({ + action: Action.AfterLeaveRoom, + room_id: roomId, + }); + } }); } @@ -1558,7 +1559,7 @@ export default class MatrixChat extends React.PureComponent { }); }); cli.on(HttpApiEvent.NoConsent, function (message, consentUri) { - Modal.createDialog( + const { finished } = Modal.createDialog( QuestionDialog, { title: _t("terms|tac_title"), @@ -1569,16 +1570,16 @@ export default class MatrixChat extends React.PureComponent { ), button: _t("terms|tac_button"), cancelButton: _t("action|dismiss"), - onFinished: (confirmed) => { - if (confirmed) { - const wnd = window.open(consentUri, "_blank")!; - wnd.opener = null; - } - }, }, undefined, true, ); + finished.then(([confirmed]) => { + if (confirmed) { + const wnd = window.open(consentUri, "_blank")!; + wnd.opener = null; + } + }); }); DecryptionFailureTracker.instance diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 0a27dd4f4e..fbff50bd01 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1753,7 +1753,7 @@ export class RoomView extends React.Component { } if (reportRoom !== false) { - actions.push(this.context.client.reportRoom(this.state.room.roomId, reportRoom)); + actions.push(this.context.client.reportRoom(this.state.room.roomId, reportRoom!)); } actions.push(this.context.client.leave(this.state.room.roomId)); diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index a523754c41..8346a0ab31 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -1505,11 +1505,13 @@ class TimelinePanel extends React.Component { description = _t("timeline|load_error|unable_to_find"); } - Modal.createDialog(ErrorDialog, { + const { finished } = Modal.createDialog(ErrorDialog, { title: _t("timeline|load_error|title"), description, - onFinished, }); + if (onFinished) { + finished.then(onFinished); + } }; // if we already have the event in question, TimelineWindow.load diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index ee8dce4c58..b76a623e2c 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -90,14 +90,15 @@ export default class SetupEncryptionBody extends React.Component // We need to call onFinished now to close this dialog, and // again later to signal that the verification is complete. this.props.onFinished(); - Modal.createDialog(VerificationRequestDialog, { + const { finished: verificationFinished } = Modal.createDialog(VerificationRequestDialog, { verificationRequestPromise: requestPromise, member: cli.getUser(userId) ?? undefined, - onFinished: async (): Promise => { - const request = await requestPromise; - request.cancel(); - this.props.onFinished(); - }, + }); + + verificationFinished.then(async () => { + const request = await requestPromise; + request.cancel(); + this.props.onFinished(); }); }; diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 68fe65aedc..34fabe46c7 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -89,13 +89,12 @@ export default class SoftLogout extends React.Component { } private onClearAll = (): void => { - Modal.createDialog(ConfirmWipeDeviceDialog, { - onFinished: (wipeData) => { - if (!wipeData) return; + const { finished } = Modal.createDialog(ConfirmWipeDeviceDialog); + finished.then(([wipeData]) => { + if (!wipeData) return; - logger.log("Clearing data from soft-logged-out session"); - Lifecycle.logout(this.context.oidcClientStore); - }, + logger.log("Clearing data from soft-logged-out session"); + Lifecycle.logout(this.context.oidcClientStore); }); }; diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx index 783c15ed3f..88e9aa0615 100644 --- a/src/components/views/auth/RegistrationForm.tsx +++ b/src/components/views/auth/RegistrationForm.tsx @@ -127,19 +127,11 @@ export default class RegistrationForm extends React.PureComponent => { - if (confirmed && email !== undefined) { - this.setState( - { - email, - }, - () => { - this.doSubmit(ev); - }, - ); - } - }, + const { finished } = Modal.createDialog(RegistrationEmailPromptDialog); + finished.then(async ([confirmed, email]) => { + if (confirmed && email !== undefined) { + this.setState({ email }, () => this.doSubmit(ev)); + } }); } else { // user can't set an e-mail so don't prompt them to diff --git a/src/components/views/context_menus/DeveloperToolsOption.tsx b/src/components/views/context_menus/DeveloperToolsOption.tsx index fc77a2e284..53a5d7283d 100644 --- a/src/components/views/context_menus/DeveloperToolsOption.tsx +++ b/src/components/views/context_menus/DeveloperToolsOption.tsx @@ -25,7 +25,6 @@ export const DeveloperToolsOption: React.FC = ({ onFinished, roomId }) => Modal.createDialog( DevtoolsDialog, { - onFinished: () => {}, roomId: roomId, }, "mx_DevtoolsDialog_wrapper", diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index 627e0814e2..5b03d54e17 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -187,14 +187,15 @@ export const WidgetContextMenu: React.FC = ({ onDeleteClick(); } else if (roomId) { // Show delete confirmation dialog - Modal.createDialog(QuestionDialog, { + const { finished } = Modal.createDialog(QuestionDialog, { title: _t("widget|context_menu|delete"), description: _t("widget|context_menu|delete_warning"), button: _t("widget|context_menu|delete"), - onFinished: (confirmed) => { - if (!confirmed) return; - WidgetUtils.setRoomWidget(cli, roomId, app.id); - }, + }); + + finished.then(([confirmed]) => { + if (!confirmed) return; + WidgetUtils.setRoomWidget(cli, roomId, app.id); }); } diff --git a/src/components/views/dialogs/AnalyticsLearnMoreDialog.tsx b/src/components/views/dialogs/AnalyticsLearnMoreDialog.tsx index 90681556e8..d80f03b7d4 100644 --- a/src/components/views/dialogs/AnalyticsLearnMoreDialog.tsx +++ b/src/components/views/dialogs/AnalyticsLearnMoreDialog.tsx @@ -11,7 +11,7 @@ import React from "react"; import BaseDialog from "./BaseDialog"; import { _t } from "../../../languageHandler"; import DialogButtons from "../elements/DialogButtons"; -import Modal, { type ComponentProps } from "../../../Modal"; +import Modal, { type ComponentProps, type IHandle } from "../../../Modal"; import SdkConfig from "../../../SdkConfig"; import { getPolicyUrl } from "../../../toasts/AnalyticsToast"; import ExternalLink from "../elements/ExternalLink"; @@ -91,10 +91,10 @@ export const AnalyticsLearnMoreDialog: React.FC = ({ export const showDialog = ( props: Omit, "cookiePolicyUrl" | "analyticsOwner">, -): void => { +): IHandle => { const privacyPolicyUrl = getPolicyUrl(); const analyticsOwner = SdkConfig.get("analytics_owner") ?? SdkConfig.get("brand"); - Modal.createDialog( + return Modal.createDialog( AnalyticsLearnMoreDialog, { privacyPolicyUrl, diff --git a/src/components/views/dialogs/ConfirmRedactDialog.tsx b/src/components/views/dialogs/ConfirmRedactDialog.tsx index d3fba9b475..17bf75ccad 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.tsx +++ b/src/components/views/dialogs/ConfirmRedactDialog.tsx @@ -58,37 +58,32 @@ export function createRedactEventDialog({ const roomId = mxEvent.getRoomId(); if (!roomId) throw new Error(`cannot redact event ${mxEvent.getId()} without room ID`); - Modal.createDialog( - ConfirmRedactDialog, - { - event: mxEvent, - onFinished: async (proceed, reason): Promise => { - if (!proceed) return; + const { finished } = Modal.createDialog(ConfirmRedactDialog, { event: mxEvent }, "mx_Dialog_confirmredact"); - const cli = MatrixClientPeg.safeGet(); - const withRelTypes: Pick = {}; + finished.then(async ([proceed, reason]) => { + if (!proceed) return; - try { - onCloseDialog?.(); - await cli.redactEvent(roomId, eventId, undefined, { - ...(reason ? { reason } : {}), - ...withRelTypes, - }); - } catch (e: any) { - const code = e.errcode || e.statusCode; - // only show the dialog if failing for something other than a network error - // (e.g. no errcode or statusCode) as in that case the redactions end up in the - // detached queue and we show the room status bar to allow retry - if (typeof code !== "undefined") { - // display error message stating you couldn't delete this. - Modal.createDialog(ErrorDialog, { - title: _t("common|error"), - description: _t("redact|error", { code }), - }); - } - } - }, - }, - "mx_Dialog_confirmredact", - ); + const cli = MatrixClientPeg.safeGet(); + const withRelTypes: Pick = {}; + + try { + onCloseDialog?.(); + await cli.redactEvent(roomId, eventId, undefined, { + ...(reason ? { reason } : {}), + ...withRelTypes, + }); + } catch (e: any) { + const code = e.errcode || e.statusCode; + // only show the dialog if failing for something other than a network error + // (e.g. no errcode or statusCode) as in that case the redactions end up in the + // detached queue and we show the room status bar to allow retry + if (typeof code !== "undefined") { + // display error message stating you couldn't delete this. + Modal.createDialog(ErrorDialog, { + title: _t("common|error"), + description: _t("redact|error", { code }), + }); + } + } + }); } diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.tsx b/src/components/views/dialogs/SessionRestoreErrorDialog.tsx index 26baef9f2a..edf93558ff 100644 --- a/src/components/views/dialogs/SessionRestoreErrorDialog.tsx +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.tsx @@ -31,13 +31,13 @@ export default class SessionRestoreErrorDialog extends React.Component { }; private onClearStorageClick = (): void => { - Modal.createDialog(QuestionDialog, { + const { finished } = Modal.createDialog(QuestionDialog, { title: _t("action|sign_out"), description:
{_t("error|session_restore|clear_storage_description")}
, button: _t("action|sign_out"), danger: true, - onFinished: this.props.onFinished, }); + finished.then(([ok]) => this.props.onFinished(ok)); }; private onRefreshClick = (): void => { diff --git a/src/components/views/dialogs/SetEmailDialog.tsx b/src/components/views/dialogs/SetEmailDialog.tsx index 57d8ed12d4..5628df33ab 100644 --- a/src/components/views/dialogs/SetEmailDialog.tsx +++ b/src/components/views/dialogs/SetEmailDialog.tsx @@ -66,12 +66,12 @@ export default class SetEmailDialog extends React.Component { this.addThreepid = new AddThreepid(MatrixClientPeg.safeGet()); this.addThreepid.addEmailAddress(emailAddress).then( () => { - Modal.createDialog(QuestionDialog, { + const { finished } = Modal.createDialog(QuestionDialog, { title: _t("auth|set_email|verification_pending_title"), description: _t("auth|set_email|verification_pending_description"), button: _t("action|continue"), - onFinished: this.onEmailDialogFinished, }); + finished.then(([ok]) => this.onEmailDialogFinished(ok)); }, (err) => { this.setState({ emailBusy: false }); @@ -89,7 +89,7 @@ export default class SetEmailDialog extends React.Component { this.props.onFinished(false); }; - private onEmailDialogFinished = (ok: boolean): void => { + private onEmailDialogFinished = (ok?: boolean): void => { if (ok) { this.verifyEmailAddress(); } else { @@ -115,12 +115,12 @@ export default class SetEmailDialog extends React.Component { _t("settings|general|error_email_verification") + " " + _t("auth|set_email|verification_pending_description"); - Modal.createDialog(QuestionDialog, { + const { finished } = Modal.createDialog(QuestionDialog, { title: _t("auth|set_email|verification_pending_title"), description: message, button: _t("action|continue"), - onFinished: this.onEmailDialogFinished, }); + finished.then(([ok]) => this.onEmailDialogFinished(ok)); } else { logger.error("Unable to verify email address: " + err); Modal.createDialog(ErrorDialog, { diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx index 82714915cc..e46b89578a 100644 --- a/src/components/views/directory/NetworkDropdown.tsx +++ b/src/components/views/directory/NetworkDropdown.tsx @@ -222,10 +222,10 @@ export const NetworkDropdown: React.FC = ({ protocols, config, setConfig const [ok, newServer] = await finished; if (!ok) return; - if (!allServers.includes(newServer)) { - setUserDefinedServers([...userDefinedServers, newServer]); + if (!allServers.includes(newServer!)) { + setUserDefinedServers([...userDefinedServers, newServer!]); setConfig({ - roomServer: newServer, + roomServer: newServer!, }); } }} diff --git a/src/components/views/elements/PollCreateDialog.tsx b/src/components/views/elements/PollCreateDialog.tsx index 8e23074ca5..41a3bc9dfb 100644 --- a/src/components/views/elements/PollCreateDialog.tsx +++ b/src/components/views/elements/PollCreateDialog.tsx @@ -167,18 +167,18 @@ export default class PollCreateDialog extends ScrollableBaseModal this.props.onFinished(true)) .catch((e) => { console.error("Failed to post poll:", e); - Modal.createDialog(QuestionDialog, { + const { finished } = Modal.createDialog(QuestionDialog, { title: _t("poll|failed_send_poll_title"), description: _t("poll|failed_send_poll_description"), button: _t("action|try_again"), cancelButton: _t("action|cancel"), - onFinished: (tryAgain: boolean) => { - if (!tryAgain) { - this.cancel(); - } else { - this.setState({ busy: false, canSubmit: true }); - } - }, + }); + finished.then(([tryAgain]) => { + if (!tryAgain) { + this.cancel(); + } else { + this.setState({ busy: false, canSubmit: true }); + } }); }); } diff --git a/src/components/views/elements/ServerPicker.tsx b/src/components/views/elements/ServerPicker.tsx index 6740534711..430f205377 100644 --- a/src/components/views/elements/ServerPicker.tsx +++ b/src/components/views/elements/ServerPicker.tsx @@ -28,9 +28,10 @@ interface IProps { const showPickerDialog = ( title: string | undefined, serverConfig: ValidatedServerConfig, - onFinished: (config: ValidatedServerConfig) => void, + onFinished: (config?: ValidatedServerConfig) => void, ): void => { - Modal.createDialog(ServerPickerDialog, { title, serverConfig, onFinished }); + const { finished } = Modal.createDialog(ServerPickerDialog, { title, serverConfig }); + finished.then(([config]) => onFinished(config)); }; const onHelpClick = (): void => { diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index b51d2fa3f3..d0107b31ec 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -235,7 +235,7 @@ export default class TextualBody extends React.Component { scalarClient?.connect().then(() => { const completeUrl = scalarClient.getStarterLink(starterLink); const integrationsUrl = integrationManager!.uiUrl; - Modal.createDialog(QuestionDialog, { + const { finished } = Modal.createDialog(QuestionDialog, { title: _t("timeline|scalar_starter_link|dialog_title"), description: (
@@ -243,18 +243,19 @@ export default class TextualBody extends React.Component {
), button: _t("action|continue"), - onFinished(confirmed) { - if (!confirmed) { - return; - } - const width = window.screen.width > 1024 ? 1024 : window.screen.width; - const height = window.screen.height > 800 ? 800 : window.screen.height; - const left = (window.screen.width - width) / 2; - const top = (window.screen.height - height) / 2; - const features = `height=${height}, width=${width}, top=${top}, left=${left},`; - const wnd = window.open(completeUrl, "_blank", features)!; - wnd.opener = null; - }, + }); + + finished.then(([confirmed]) => { + if (!confirmed) { + return; + } + const width = window.screen.width > 1024 ? 1024 : window.screen.width; + const height = window.screen.height > 800 ? 800 : window.screen.height; + const left = (window.screen.width - width) / 2; + const top = (window.screen.height - height) / 2; + const features = `height=${height}, width=${width}, top=${top}, left=${left},`; + const wnd = window.open(completeUrl, "_blank", features)!; + wnd.opener = null; }); }); }; diff --git a/src/components/views/settings/EventIndexPanel.tsx b/src/components/views/settings/EventIndexPanel.tsx index 2cfcd70aed..fe19aa0580 100644 --- a/src/components/views/settings/EventIndexPanel.tsx +++ b/src/components/views/settings/EventIndexPanel.tsx @@ -119,15 +119,14 @@ export default class EventIndexPanel extends React.Component { - const { close } = Modal.createDialog(SeshatResetDialog, { - onFinished: async (success): Promise => { - if (success) { - await SettingsStore.setValue("enableEventIndexing", null, SettingLevel.DEVICE, false); - await EventIndexPeg.deleteEventIndex(); - await this.onEnable(); - close(); - } - }, + const { finished, close } = Modal.createDialog(SeshatResetDialog); + finished.then(async ([success]) => { + if (success) { + await SettingsStore.setValue("enableEventIndexing", null, SettingLevel.DEVICE, false); + await EventIndexPeg.deleteEventIndex(); + await this.onEnable(); + close(); + } }); }; diff --git a/src/components/views/settings/devices/deleteDevices.tsx b/src/components/views/settings/devices/deleteDevices.tsx index e2fc6c1abd..894e23e0f7 100644 --- a/src/components/views/settings/devices/deleteDevices.tsx +++ b/src/components/views/settings/devices/deleteDevices.tsx @@ -11,7 +11,6 @@ import { type AuthDict, type IAuthData } from "matrix-js-sdk/src/interactive-aut import { _t } from "../../../../languageHandler"; import Modal from "../../../../Modal"; -import { type InteractiveAuthCallback } from "../../../structures/InteractiveAuth"; import { SSOAuthEntry } from "../../auth/InteractiveAuthEntryComponents"; import InteractiveAuthDialog from "../../dialogs/InteractiveAuthDialog"; @@ -24,7 +23,7 @@ const makeDeleteRequest = export const deleteDevicesWithInteractiveAuth = async ( matrixClient: MatrixClient, deviceIds: string[], - onFinished: InteractiveAuthCallback, + onFinished: (success?: boolean) => Promise, ): Promise => { if (!deviceIds.length) { return; @@ -32,7 +31,7 @@ export const deleteDevicesWithInteractiveAuth = async ( try { await makeDeleteRequest(matrixClient, deviceIds)(null); // no interactive auth needed - await onFinished(true, undefined); + await onFinished(true); } catch (error) { if (!(error instanceof MatrixError) || error.httpStatus !== 401 || !error.data?.flows) { // doesn't look like an interactive-auth failure @@ -62,16 +61,16 @@ export const deleteDevicesWithInteractiveAuth = async ( continueKind: "danger", }, }; - Modal.createDialog(InteractiveAuthDialog, { + const { finished } = Modal.createDialog(InteractiveAuthDialog, { title: _t("common|authentication"), matrixClient: matrixClient, authData: error.data as IAuthData, - onFinished, makeRequest: makeDeleteRequest(matrixClient, deviceIds), aestheticsForStagePhases: { [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, }, }); + finished.then(([success]) => onFinished(success)); } }; diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index a4a94e8bab..89ae96f8a4 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -155,7 +155,7 @@ export default class SecurityRoomSettingsTab extends React.Component {sub}, }, ), - onFinished: (confirm) => { - if (!confirm) { - this.setState({ encrypted: false }); - return; - } + }); + finished.then(([confirm]) => { + if (!confirm) { + this.setState({ encrypted: false }); + return; + } - const beforeEncrypted = this.state.encrypted; - this.setState({ encrypted: true }); - this.context - .sendStateEvent(this.props.room.roomId, EventType.RoomEncryption, { - algorithm: MEGOLM_ENCRYPTION_ALGORITHM, - }) - .catch((e) => { - logger.error(e); - this.setState({ encrypted: beforeEncrypted }); - }); - }, + const beforeEncrypted = this.state.encrypted; + this.setState({ encrypted: true }); + this.context + .sendStateEvent(this.props.room.roomId, EventType.RoomEncryption, { + algorithm: MEGOLM_ENCRYPTION_ALGORITHM, + }) + .catch((e) => { + logger.error(e); + this.setState({ encrypted: beforeEncrypted }); + }); }); }; @@ -213,9 +213,9 @@ export default class SecurityRoomSettingsTab extends React.Component { diff --git a/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx index 83c073c3ce..014527f697 100644 --- a/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx @@ -165,10 +165,9 @@ const AccountUserSettingsTab: React.FC = ({ closeSettingsFn }) => { }, []); const onDeactivateClicked = useCallback((): void => { - Modal.createDialog(DeactivateAccountDialog, { - onFinished: (success) => { - if (success) closeSettingsFn(); - }, + const { finished } = Modal.createDialog(DeactivateAccountDialog); + finished.then(([success]) => { + if (success) closeSettingsFn(); }); }, [closeSettingsFn]); diff --git a/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx b/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx index 2303a1e50c..dea28628fb 100644 --- a/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @@ -247,7 +247,10 @@ function SetUpEncryptionPanel({ onFinish }: SetUpEncryptionPanelProps): JSX.Elem diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index c5d9385d7b..262f788daa 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -100,7 +100,7 @@ const useSignOut = ( } else { const deferredSuccess = defer(); await deleteDevicesWithInteractiveAuth(matrixClient, deviceIds, async (success) => { - deferredSuccess.resolve(success); + deferredSuccess.resolve(!!success); }); success = await deferredSuccess.promise; } @@ -203,7 +203,8 @@ const SessionManagerTab: React.FC<{ const shouldShowOtherSessions = otherSessionsCount > 0; const onVerifyCurrentDevice = (): void => { - Modal.createDialog(SetupEncryptionDialog, { onFinished: refreshDevices }); + const { finished } = Modal.createDialog(SetupEncryptionDialog); + finished.then(refreshDevices); }; const onTriggerDeviceVerification = useCallback( @@ -212,14 +213,14 @@ const SessionManagerTab: React.FC<{ return; } const verificationRequestPromise = requestDeviceVerification(deviceId); - Modal.createDialog(VerificationRequestDialog, { + const { finished } = Modal.createDialog(VerificationRequestDialog, { verificationRequestPromise, member: currentUserMember, - onFinished: async (): Promise => { - const request = await verificationRequestPromise; - request.cancel(); - await refreshDevices(); - }, + }); + finished.then(async () => { + const request = await verificationRequestPromise; + request.cancel(); + await refreshDevices(); }); }, [requestDeviceVerification, refreshDevices, currentUserMember], diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx index e3975c148b..12843c0b87 100644 --- a/src/components/views/toasts/VerificationRequestToast.tsx +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -124,18 +124,16 @@ export default class VerificationRequestToast extends React.PureComponent { - request.cancel(); - }, }, undefined, /* priority = */ false, /* static = */ true, ); + finished.then(() => request.cancel()); } await request.accept(); } catch (err) { diff --git a/src/slash-commands/utils.ts b/src/slash-commands/utils.ts index f1be124151..d9cf15d16a 100644 --- a/src/slash-commands/utils.ts +++ b/src/slash-commands/utils.ts @@ -49,16 +49,14 @@ export const singleMxcUpload = async (cli: MatrixClient): Promise const file = (ev as HTMLInputEvent).target.files?.[0]; if (!file) return; - Modal.createDialog(UploadConfirmDialog, { - file, - onFinished: async (shouldContinue): Promise => { - if (shouldContinue) { - const { content_uri: uri } = await cli.uploadContent(file); - resolve(uri); - } else { - resolve(null); - } - }, + const { finished } = Modal.createDialog(UploadConfirmDialog, { file }); + finished.then(async ([shouldContinue]) => { + if (shouldContinue) { + const { content_uri: uri } = await cli.uploadContent(file); + resolve(uri); + } else { + resolve(null); + } }); }; diff --git a/src/stores/ModalWidgetStore.ts b/src/stores/ModalWidgetStore.ts index 10f16d24d2..2a2fbdd8da 100644 --- a/src/stores/ModalWidgetStore.ts +++ b/src/stores/ModalWidgetStore.ts @@ -61,18 +61,18 @@ export class ModalWidgetStore extends AsyncStoreWithClient { widgetDefinition: { ...requestData }, widgetRoomId, sourceWidgetId: sourceWidget.id, - onFinished: (success, data) => { - this.closeModalWidget(sourceWidget, widgetRoomId, success && data ? data : { "m.exited": true }); - - this.openSourceWidgetId = null; - this.openSourceWidgetRoomId = null; - this.modalInstance = null; - }, }, undefined, /* priority = */ false, /* static = */ true, ); + this.modalInstance!.finished.then(([success, data]) => { + this.closeModalWidget(sourceWidget, widgetRoomId, success && data ? data : { "m.exited": true }); + + this.openSourceWidgetId = null; + this.openSourceWidgetRoomId = null; + this.modalInstance = null; + }); }; public closeModalWidget = ( diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 31d1d6edac..e0bc85746f 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -557,19 +557,17 @@ export class StopGapWidgetDriver extends WidgetDriver { observer.update({ state: OpenIDRequestState.PendingUserConfirmation }); - Modal.createDialog(WidgetOpenIDPermissionsDialog, { + const { finished } = Modal.createDialog(WidgetOpenIDPermissionsDialog, { widget: this.forWidget, widgetKind: this.forWidgetKind, inRoomId: this.inRoomId, - - onFinished: async (confirm): Promise => { - if (!confirm) { - return observer.update({ state: OpenIDRequestState.Blocked }); - } - - return observer.update({ state: OpenIDRequestState.Allowed, token: await getToken() }); - }, }); + const [confirm] = await finished; + if (!confirm) { + observer.update({ state: OpenIDRequestState.Blocked }); + } else { + observer.update({ state: OpenIDRequestState.Allowed, token: await getToken() }); + } } public async navigate(uri: string): Promise { diff --git a/src/toasts/AnalyticsToast.tsx b/src/toasts/AnalyticsToast.tsx index 63e0757520..351ccdf4b2 100644 --- a/src/toasts/AnalyticsToast.tsx +++ b/src/toasts/AnalyticsToast.tsx @@ -34,34 +34,36 @@ const onReject = (): void => { }; const onLearnMoreNoOptIn = (): void => { - showAnalyticsLearnMoreDialog({ - onFinished: (buttonClicked?: ButtonClicked) => { - if (buttonClicked === ButtonClicked.Primary) { - // user clicked "Enable" - onAccept(); - } - // otherwise, the user either clicked "Cancel", or closed the dialog without making a choice, - // leave the toast open - }, + const { finished } = showAnalyticsLearnMoreDialog({ primaryButton: _t("action|enable"), }); + + finished.then(([buttonClicked]) => { + if (buttonClicked === ButtonClicked.Primary) { + // user clicked "Enable" + onAccept(); + } + // otherwise, the user either clicked "Cancel", or closed the dialog without making a choice, + // leave the toast open + }); }; const onLearnMorePreviouslyOptedIn = (): void => { - showAnalyticsLearnMoreDialog({ - onFinished: (buttonClicked?: ButtonClicked) => { - if (buttonClicked === ButtonClicked.Primary) { - // user clicked "That's fine" - onAccept(); - } else if (buttonClicked === ButtonClicked.Cancel) { - // user clicked "Stop" - onReject(); - } - // otherwise, the user closed the dialog without making a choice, leave the toast open - }, + const { finished } = showAnalyticsLearnMoreDialog({ primaryButton: _t("analytics|accept_button"), cancelButton: _t("action|stop"), }); + + finished.then(([buttonClicked]) => { + if (buttonClicked === ButtonClicked.Primary) { + // user clicked "That's fine" + onAccept(); + } else if (buttonClicked === ButtonClicked.Cancel) { + // user clicked "Stop" + onReject(); + } + // otherwise, the user closed the dialog without making a choice, leave the toast open + }); }; const TOAST_KEY = "analytics"; diff --git a/src/toasts/UpdateToast.tsx b/src/toasts/UpdateToast.tsx index 0abc95c066..4ced8144c1 100644 --- a/src/toasts/UpdateToast.tsx +++ b/src/toasts/UpdateToast.tsx @@ -32,27 +32,27 @@ export const showToast = (version: string, newVersion: string, releaseNotes?: st let acceptLabel = _t("update|see_changes_button"); if (releaseNotes) { onAccept = () => { - Modal.createDialog(QuestionDialog, { + const { finished } = Modal.createDialog(QuestionDialog, { title: _t("update|release_notes_toast_title"), description:
{releaseNotes}
, button: _t("action|update"), - onFinished: (update) => { - if (update && PlatformPeg.get()) { - PlatformPeg.get()!.installUpdate(); - } - }, + }); + finished.then(([update]) => { + if (update && PlatformPeg.get()) { + PlatformPeg.get()!.installUpdate(); + } }); }; } else if (checkVersion(version) && checkVersion(newVersion)) { onAccept = () => { - Modal.createDialog(ChangelogDialog, { + const { finished } = Modal.createDialog(ChangelogDialog, { version, newVersion, - onFinished: (update) => { - if (update && PlatformPeg.get()) { - PlatformPeg.get()!.installUpdate(); - } - }, + }); + finished.then(([update]) => { + if (update && PlatformPeg.get()) { + PlatformPeg.get()!.installUpdate(); + } }); }; } else { diff --git a/src/utils/DialogOpener.ts b/src/utils/DialogOpener.ts index 8304d6c2aa..9eadf63adf 100644 --- a/src/utils/DialogOpener.ts +++ b/src/utils/DialogOpener.ts @@ -119,7 +119,7 @@ export class DialogOpener { break; case Action.OpenAddToExistingSpaceDialog: { const space = payload.space; - Modal.createDialog( + const { finished } = Modal.createDialog( AddExistingToSpaceDialog, { onCreateRoomClick: (ev: ButtonEvent) => { @@ -128,14 +128,14 @@ export class DialogOpener { }, onAddSubspaceClick: () => showAddExistingSubspace(space), space, - onFinished: (added: boolean) => { - if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { - defaultDispatcher.fire(Action.UpdateSpaceHierarchy); - } - }, }, "mx_AddExistingToSpaceDialog_wrapper", ); + finished.then(([added]) => { + if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { + defaultDispatcher.fire(Action.UpdateSpaceHierarchy); + } + }); break; } } diff --git a/src/utils/leave-behaviour.ts b/src/utils/leave-behaviour.ts index ec24903ad4..2ad6e7d4fe 100644 --- a/src/utils/leave-behaviour.ts +++ b/src/utils/leave-behaviour.ts @@ -171,20 +171,20 @@ export async function leaveRoomBehaviour( } export const leaveSpace = (space: Room): void => { - Modal.createDialog( + const { finished } = Modal.createDialog( LeaveSpaceDialog, { space, - onFinished: async (leave: boolean, rooms: Room[]): Promise => { - if (!leave) return; - await bulkSpaceBehaviour(space, rooms, (room) => leaveRoomBehaviour(space.client, room.roomId)); - - dis.dispatch({ - action: Action.AfterLeaveRoom, - room_id: space.roomId, - }); - }, }, "mx_LeaveSpaceDialog_wrapper", ); + finished.then(async ([leave, rooms]) => { + if (!leave) return; + await bulkSpaceBehaviour(space, rooms!, (room) => leaveRoomBehaviour(space.client, room.roomId)); + + dis.dispatch({ + action: Action.AfterLeaveRoom, + room_id: space.roomId, + }); + }); }; diff --git a/src/utils/space.tsx b/src/utils/space.tsx index 1a9b104c4b..230c783322 100644 --- a/src/utils/space.tsx +++ b/src/utils/space.tsx @@ -75,7 +75,7 @@ export const showCreateNewRoom = async (space: Room, type?: RoomType): Promise { }; export const showAddExistingSubspace = (space: Room): void => { - Modal.createDialog( + const { finished } = Modal.createDialog( AddExistingSubspaceDialog, { space, onCreateSubspaceClick: () => showCreateNewSubspace(space), - onFinished: (added: boolean) => { - if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { - defaultDispatcher.fire(Action.UpdateSpaceHierarchy); - } - }, }, "mx_AddExistingToSpaceDialog_wrapper", ); + finished.then(([added]) => { + if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { + defaultDispatcher.fire(Action.UpdateSpaceHierarchy); + } + }); }; export const showCreateNewSubspace = (space: Room): void => { - Modal.createDialog( + const { finished } = Modal.createDialog( CreateSubspaceDialog, { space, onAddExistingSpaceClick: () => showAddExistingSubspace(space), - onFinished: (added: boolean) => { - if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { - defaultDispatcher.fire(Action.UpdateSpaceHierarchy); - } - }, }, "mx_CreateSubspaceDialog_wrapper", ); + finished.then(([added]) => { + if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { + defaultDispatcher.fire(Action.UpdateSpaceHierarchy); + } + }); }; export const bulkSpaceBehaviour = async ( diff --git a/test/unit-tests/Modal-test.ts b/test/unit-tests/Modal-test.ts index d542fa3843..121c027b01 100644 --- a/test/unit-tests/Modal-test.ts +++ b/test/unit-tests/Modal-test.ts @@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details. import Modal from "../../src/Modal"; import QuestionDialog from "../../src/components/views/dialogs/QuestionDialog"; import defaultDispatcher from "../../src/dispatcher/dispatcher"; +import { flushPromises } from "../test-utils"; describe("Modal", () => { test("forceCloseAllModals should close all open modals", () => { @@ -23,7 +24,7 @@ describe("Modal", () => { expect(Modal.hasDialogs()).toBe(false); }); - test("open modals should be closed on logout", () => { + test("open modals should be closed on logout", async () => { const modal1OnFinished = jest.fn(); const modal2OnFinished = jest.fn(); @@ -31,18 +32,18 @@ describe("Modal", () => { title: "Test dialog 1", description: "This is a test dialog", button: "Word", - onFinished: modal1OnFinished, - }); + }).finished.then(modal1OnFinished); Modal.createDialog(QuestionDialog, { title: "Test dialog 2", description: "This is a test dialog", button: "Word", - onFinished: modal2OnFinished, - }); + }).finished.then(modal2OnFinished); defaultDispatcher.dispatch({ action: "logout" }, true); + await flushPromises(); + expect(modal1OnFinished).toHaveBeenCalled(); expect(modal2OnFinished).toHaveBeenCalled(); }); diff --git a/test/unit-tests/components/views/settings/devices/deleteDevices-test.tsx b/test/unit-tests/components/views/settings/devices/deleteDevices-test.tsx index 3b030c61c9..603d06d4a6 100644 --- a/test/unit-tests/components/views/settings/devices/deleteDevices-test.tsx +++ b/test/unit-tests/components/views/settings/devices/deleteDevices-test.tsx @@ -35,7 +35,7 @@ describe("deleteDevices()", () => { await deleteDevicesWithInteractiveAuth(mockClient, deviceIds, onFinished); expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith(deviceIds, undefined); - expect(onFinished).toHaveBeenCalledWith(true, undefined); + expect(onFinished).toHaveBeenCalledWith(true); // didnt open modal expect(modalSpy).not.toHaveBeenCalled(); diff --git a/test/unit-tests/components/views/settings/tabs/user/AccountUserSettingsTab-test.tsx b/test/unit-tests/components/views/settings/tabs/user/AccountUserSettingsTab-test.tsx index f6b6b2e850..531e66e00b 100644 --- a/test/unit-tests/components/views/settings/tabs/user/AccountUserSettingsTab-test.tsx +++ b/test/unit-tests/components/views/settings/tabs/user/AccountUserSettingsTab-test.tsx @@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details. import { fireEvent, render, screen, within } from "jest-matrix-react"; import React from "react"; import { type MatrixClient, ThreepidMedium } from "matrix-js-sdk/src/matrix"; +import { defer } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; import userEvent from "@testing-library/user-event"; import { type MockedObject } from "jest-mock"; @@ -153,7 +154,8 @@ describe("", () => { (settingName) => settingName === UIFeature.Deactivate, ); - const createDialogFn = jest.fn(); + const finishedDeferred = defer<[boolean]>(); + const createDialogFn = jest.fn().mockReturnValue({ finished: finishedDeferred.promise }); jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn); render(getComponent()); @@ -167,14 +169,16 @@ describe("", () => { (settingName) => settingName === UIFeature.Deactivate, ); - const createDialogFn = jest.fn(); + const finishedDeferred = defer<[boolean]>(); + const createDialogFn = jest.fn().mockReturnValue({ finished: finishedDeferred.promise }); jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn); render(getComponent()); await userEvent.click(screen.getByRole("button", { name: "Deactivate Account" })); - createDialogFn.mock.calls[0][1].onFinished(true); + finishedDeferred.resolve([true]); + await flushPromises(); expect(defaultProps.closeSettingsFn).toHaveBeenCalled(); }); @@ -183,14 +187,16 @@ describe("", () => { (settingName) => settingName === UIFeature.Deactivate, ); - const createDialogFn = jest.fn(); + const finishedDeferred = defer<[boolean]>(); + const createDialogFn = jest.fn().mockReturnValue({ finished: finishedDeferred.promise }); jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn); render(getComponent()); await userEvent.click(screen.getByRole("button", { name: "Deactivate Account" })); - createDialogFn.mock.calls[0][1].onFinished(false); + finishedDeferred.resolve([false]); + await flushPromises(); expect(defaultProps.closeSettingsFn).not.toHaveBeenCalled(); }); diff --git a/test/unit-tests/components/views/settings/tabs/user/EncryptionUserSettingsTab-test.tsx b/test/unit-tests/components/views/settings/tabs/user/EncryptionUserSettingsTab-test.tsx index 584a1b3c8c..018ec25ef3 100644 --- a/test/unit-tests/components/views/settings/tabs/user/EncryptionUserSettingsTab-test.tsx +++ b/test/unit-tests/components/views/settings/tabs/user/EncryptionUserSettingsTab-test.tsx @@ -62,7 +62,7 @@ describe("", () => { ); expect(asFragment()).toMatchSnapshot(); - const spy = jest.spyOn(Modal, "createDialog").mockReturnValue({} as any); + const spy = jest.spyOn(Modal, "createDialog").mockReturnValue({ finished: new Promise(() => {}) } as any); await user.click(screen.getByText("Verify this device")); expect(spy).toHaveBeenCalled(); }); diff --git a/test/unit-tests/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/unit-tests/components/views/settings/tabs/user/SessionManagerTab-test.tsx index eaae755382..2dcc6d1cd6 100644 --- a/test/unit-tests/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/unit-tests/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -631,9 +631,10 @@ describe("", () => { // click verify button from current session section fireEvent.click(getByTestId(`verification-status-button-${alicesMobileDevice.device_id}`)); - const { onFinished: modalOnFinished } = modalSpy.mock.calls[0][1] as any; - // simulate modal completing process - await modalOnFinished(); + // close the modal + const { close: closeModal } = modalSpy.mock.results[0].value; + closeModal(); + await flushPromises(); // cancelled in case it was a failure exit from modal expect(mockVerificationRequest.cancel).toHaveBeenCalled();