Modal: remove support for onFinished callback (#29852)

* Fix up type for `finished` result of Modal

The `finished` promise can be called with an empty array, for example if the
dialog is closed by a background click. This was not correctly represented in
the typing. Fix that, and add some documentation while we're at it.

* Type fixes to onFinished callbacks from Modal

These can all be called with zero arguments, despite what the type annotations
may say, so mark them accordingly.

* Remove uses of Modal `onFinished` property

... because it is confusing.

Instead, use the `finished` promise returned by `createDialog`.

* Modal: remove support for now-unused `onFinished` prop

* StopGapWidgetDriver: use `await` instead of promise chaining

* Fix up unit tests
This commit is contained in:
Richard van der Hoff 2025-04-30 16:56:21 +01:00 committed by GitHub
parent ce1055f5fe
commit f25fbdebc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 345 additions and 315 deletions

View File

@ -679,7 +679,7 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
private showICEFallbackPrompt(): void { private showICEFallbackPrompt(): void {
const cli = MatrixClientPeg.safeGet(); const cli = MatrixClientPeg.safeGet();
Modal.createDialog( const { finished } = Modal.createDialog(
QuestionDialog, QuestionDialog,
{ {
title: _t("voip|misconfigured_server"), title: _t("voip|misconfigured_server"),
@ -703,13 +703,14 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
server: new URL(FALLBACK_ICE_SERVER).pathname, server: new URL(FALLBACK_ICE_SERVER).pathname,
}), }),
cancelButton: _t("action|ok"), cancelButton: _t("action|ok"),
onFinished: (allow) => {
SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow);
},
}, },
undefined, undefined,
true, true,
); );
finished.then(([allow]) => {
SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow);
});
} }
private showMediaCaptureError(call: MatrixCall): void { private showMediaCaptureError(call: MatrixCall): void {

View File

@ -469,12 +469,15 @@ type TryAgainFunction = () => void;
* @param tryAgain OPTIONAL function to call on try again button from error dialog * @param tryAgain OPTIONAL function to call on try again button from error dialog
*/ */
function onFailedDelegatedAuthLogin(description: string | ReactNode, tryAgain?: TryAgainFunction): void { function onFailedDelegatedAuthLogin(description: string | ReactNode, tryAgain?: TryAgainFunction): void {
Modal.createDialog(ErrorDialog, { const { finished } = Modal.createDialog(ErrorDialog, {
title: _t("auth|oidc|error_title"), title: _t("auth|oidc|error_title"),
description, description,
button: _t("action|try_again"), 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 // 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?.();
}); });
} }

View File

@ -30,12 +30,25 @@ export type ComponentType =
}> }>
| React.ComponentType<any>; | React.ComponentType<any>;
// 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<C extends ComponentType> = Parameters<React.ComponentProps<C>["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<C extends ComponentType> = Defaultize< export type ComponentProps<C extends ComponentType> = Defaultize<
Omit<React.ComponentProps<C>, "onFinished">, Omit<React.ComponentProps<C>, "onFinished">,
C["defaultProps"] C["defaultProps"]
> & >;
Partial<Pick<React.ComponentProps<C>, "onFinished">>;
export interface IModal<C extends ComponentType> { export interface IModal<C extends ComponentType> {
elem: React.ReactNode; elem: React.ReactNode;
@ -43,15 +56,44 @@ export interface IModal<C extends ComponentType> {
beforeClosePromise?: Promise<boolean>; beforeClosePromise?: Promise<boolean>;
closeReason?: ModalCloseReason; closeReason?: ModalCloseReason;
onBeforeClose?(reason?: ModalCloseReason): Promise<boolean>; onBeforeClose?(reason?: ModalCloseReason): Promise<boolean>;
onFinished: ComponentProps<C>["onFinished"];
close(...args: Parameters<ComponentProps<C>["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<C> | []): void;
hidden?: boolean; hidden?: boolean;
deferred?: IDeferred<Parameters<ComponentProps<C>["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<OnFinishedParams<C> | []>;
} }
/** The result of {@link Modal.createDialog}.
*
* @typeParam C - The type of the React component which forms the body of the dialog.
*/
export interface IHandle<C extends ComponentType> { export interface IHandle<C extends ComponentType> {
finished: Promise<Parameters<ComponentProps<C>["onFinished"]>>; /**
close(...args: Parameters<ComponentProps<C>["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<OnFinishedParams<C> | []>;
/**
* A function which, if called, will close the dialog.
*
* @param args - Arguments to return to {@link finished}.
*/
close(...args: OnFinishedParams<C>): void;
} }
interface IOptions<C extends ComponentType> { interface IOptions<C extends ComponentType> {
@ -164,7 +206,6 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
const modals = filterBoolean([...this.modals, this.staticModal, this.priorityModal]); const modals = filterBoolean([...this.modals, this.staticModal, this.priorityModal]);
for (const modal of modals) { for (const modal of modals) {
modal.deferred?.resolve([]); modal.deferred?.resolve([]);
if (modal.onFinished) modal.onFinished.apply(null);
this.emitClosed(); this.emitClosed();
} }
@ -188,7 +229,6 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
onFinishedProm: IHandle<C>["finished"]; onFinishedProm: IHandle<C>["finished"];
} { } {
const modal = { const modal = {
onFinished: props?.onFinished,
onBeforeClose: options?.onBeforeClose, onBeforeClose: options?.onBeforeClose,
className, className,
@ -196,8 +236,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
elem: null, elem: null,
} as IModal<C>; } as IModal<C>;
// never call this from onFinished() otherwise it will loop const [closeDialog, onFinishedProm] = this.getCloseFn<C>(modal);
const [closeDialog, onFinishedProm] = this.getCloseFn<C>(modal, props);
// don't attempt to reuse the same AsyncWrapper for different dialogs, // don't attempt to reuse the same AsyncWrapper for different dialogs,
// otherwise we'll get confused. // otherwise we'll get confused.
@ -214,13 +253,10 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
return { modal, closeDialog, onFinishedProm }; return { modal, closeDialog, onFinishedProm };
} }
private getCloseFn<C extends ComponentType>( private getCloseFn<C extends ComponentType>(modal: IModal<C>): [IHandle<C>["close"], IHandle<C>["finished"]] {
modal: IModal<C>, modal.deferred = defer<OnFinishedParams<C> | []>();
props?: ComponentProps<C>,
): [IHandle<C>["close"], IHandle<C>["finished"]] {
modal.deferred = defer<Parameters<ComponentProps<C>["onFinished"]>>();
return [ return [
async (...args: Parameters<ComponentProps<C>["onFinished"]>): Promise<void> => { async (...args: OnFinishedParams<C>): Promise<void> => {
if (modal.beforeClosePromise) { if (modal.beforeClosePromise) {
await modal.beforeClosePromise; await modal.beforeClosePromise;
} else if (modal.onBeforeClose) { } else if (modal.onBeforeClose) {
@ -232,7 +268,6 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
} }
} }
modal.deferred?.resolve(args); modal.deferred?.resolve(args);
if (props?.onFinished) props.onFinished.apply(null, args);
const i = this.modals.indexOf(modal); const i = this.modals.indexOf(modal);
if (i >= 0) { if (i >= 0) {
this.modals.splice(i, 1); this.modals.splice(i, 1);
@ -280,7 +315,8 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
* using React.lazy to async load the component. * using React.lazy to async load the component.
* e.g. `lazy(() => import('./MyComponent'))` * e.g. `lazy(() => 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 * @param className CSS class to apply to the modal wrapper
* *
@ -295,7 +331,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
* static at a time. * static at a time.
* @param options? extra options for the dialog * @param options? extra options for the dialog
* @param options.onBeforeClose a callback to decide whether to close the dialog * @param options.onBeforeClose a callback to decide whether to close the dialog
* @returns Object with 'close' parameter being a function that will close the dialog * @returns {@link IHandle} object.
*/ */
public createDialog<C extends ComponentType>( public createDialog<C extends ComponentType>(
component: C, component: C,

View File

@ -62,14 +62,14 @@ export async function startAnyRegistrationFlow(
</button>, </button>,
] ]
: [], : [],
onFinished: (proceed) => { });
if (proceed) { modal.finished.then(([proceed]) => {
dis.dispatch({ action: "start_login", screenAfterLogin: options.screen_after }); if (proceed) {
} else if (options.go_home_on_cancel) { dis.dispatch({ action: "start_login", screenAfterLogin: options.screen_after });
dis.dispatch({ action: Action.ViewHomePage }); } else if (options.go_home_on_cancel) {
} else if (options.go_welcome_on_cancel) { dis.dispatch({ action: Action.ViewHomePage });
dis.dispatch({ action: "view_welcome_page" }); } else if (options.go_welcome_on_cancel) {
} dis.dispatch({ action: "view_welcome_page" });
}, }
}); });
} }

View File

@ -49,15 +49,8 @@ export default function NewRecoveryMethodDialog({ onFinished }: NewRecoveryMetho
if (isKeyBackupEnabled) { if (isKeyBackupEnabled) {
onFinished(); onFinished();
} else { } else {
Modal.createDialog( const { finished } = Modal.createDialog(RestoreKeyBackupDialog, {}, undefined, false, true);
RestoreKeyBackupDialog, finished.then(onFinished);
{
onFinished,
},
undefined,
false,
true,
);
} }
} }

View File

@ -1229,7 +1229,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const warnings = this.leaveRoomWarnings(roomId); const warnings = this.leaveRoomWarnings(roomId);
const isSpace = roomToLeave?.isSpaceRoom(); const isSpace = roomToLeave?.isSpaceRoom();
Modal.createDialog(QuestionDialog, { const { finished } = Modal.createDialog(QuestionDialog, {
title: isSpace ? _t("space|leave_dialog_action") : _t("action|leave_room"), title: isSpace ? _t("space|leave_dialog_action") : _t("action|leave_room"),
description: ( description: (
<span> <span>
@ -1245,16 +1245,17 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
), ),
button: _t("action|leave"), button: _t("action|leave"),
danger: warnings.length > 0, danger: warnings.length > 0,
onFinished: async (shouldLeave) => { });
if (shouldLeave) {
await leaveRoomBehaviour(cli, roomId);
dis.dispatch<AfterLeaveRoomPayload>({ finished.then(async ([shouldLeave]) => {
action: Action.AfterLeaveRoom, if (shouldLeave) {
room_id: roomId, await leaveRoomBehaviour(cli, roomId);
});
} dis.dispatch<AfterLeaveRoomPayload>({
}, action: Action.AfterLeaveRoom,
room_id: roomId,
});
}
}); });
} }
@ -1558,7 +1559,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}); });
}); });
cli.on(HttpApiEvent.NoConsent, function (message, consentUri) { cli.on(HttpApiEvent.NoConsent, function (message, consentUri) {
Modal.createDialog( const { finished } = Modal.createDialog(
QuestionDialog, QuestionDialog,
{ {
title: _t("terms|tac_title"), title: _t("terms|tac_title"),
@ -1569,16 +1570,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
), ),
button: _t("terms|tac_button"), button: _t("terms|tac_button"),
cancelButton: _t("action|dismiss"), cancelButton: _t("action|dismiss"),
onFinished: (confirmed) => {
if (confirmed) {
const wnd = window.open(consentUri, "_blank")!;
wnd.opener = null;
}
},
}, },
undefined, undefined,
true, true,
); );
finished.then(([confirmed]) => {
if (confirmed) {
const wnd = window.open(consentUri, "_blank")!;
wnd.opener = null;
}
});
}); });
DecryptionFailureTracker.instance DecryptionFailureTracker.instance

View File

@ -1753,7 +1753,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
} }
if (reportRoom !== false) { 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)); actions.push(this.context.client.leave(this.state.room.roomId));

View File

@ -1505,11 +1505,13 @@ class TimelinePanel extends React.Component<IProps, IState> {
description = _t("timeline|load_error|unable_to_find"); description = _t("timeline|load_error|unable_to_find");
} }
Modal.createDialog(ErrorDialog, { const { finished } = Modal.createDialog(ErrorDialog, {
title: _t("timeline|load_error|title"), title: _t("timeline|load_error|title"),
description, description,
onFinished,
}); });
if (onFinished) {
finished.then(onFinished);
}
}; };
// if we already have the event in question, TimelineWindow.load // if we already have the event in question, TimelineWindow.load

View File

@ -90,14 +90,15 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
// We need to call onFinished now to close this dialog, and // We need to call onFinished now to close this dialog, and
// again later to signal that the verification is complete. // again later to signal that the verification is complete.
this.props.onFinished(); this.props.onFinished();
Modal.createDialog(VerificationRequestDialog, { const { finished: verificationFinished } = Modal.createDialog(VerificationRequestDialog, {
verificationRequestPromise: requestPromise, verificationRequestPromise: requestPromise,
member: cli.getUser(userId) ?? undefined, member: cli.getUser(userId) ?? undefined,
onFinished: async (): Promise<void> => { });
const request = await requestPromise;
request.cancel(); verificationFinished.then(async () => {
this.props.onFinished(); const request = await requestPromise;
}, request.cancel();
this.props.onFinished();
}); });
}; };

View File

@ -89,13 +89,12 @@ export default class SoftLogout extends React.Component<IProps, IState> {
} }
private onClearAll = (): void => { private onClearAll = (): void => {
Modal.createDialog(ConfirmWipeDeviceDialog, { const { finished } = Modal.createDialog(ConfirmWipeDeviceDialog);
onFinished: (wipeData) => { finished.then(([wipeData]) => {
if (!wipeData) return; if (!wipeData) return;
logger.log("Clearing data from soft-logged-out session"); logger.log("Clearing data from soft-logged-out session");
Lifecycle.logout(this.context.oidcClientStore); Lifecycle.logout(this.context.oidcClientStore);
},
}); });
}; };

View File

@ -127,19 +127,11 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
if (this.state.email === "") { if (this.state.email === "") {
if (this.showEmail()) { if (this.showEmail()) {
Modal.createDialog(RegistrationEmailPromptDialog, { const { finished } = Modal.createDialog(RegistrationEmailPromptDialog);
onFinished: async (confirmed: boolean, email?: string): Promise<void> => { finished.then(async ([confirmed, email]) => {
if (confirmed && email !== undefined) { if (confirmed && email !== undefined) {
this.setState( this.setState({ email }, () => this.doSubmit(ev));
{ }
email,
},
() => {
this.doSubmit(ev);
},
);
}
},
}); });
} else { } else {
// user can't set an e-mail so don't prompt them to // user can't set an e-mail so don't prompt them to

View File

@ -25,7 +25,6 @@ export const DeveloperToolsOption: React.FC<Props> = ({ onFinished, roomId }) =>
Modal.createDialog( Modal.createDialog(
DevtoolsDialog, DevtoolsDialog,
{ {
onFinished: () => {},
roomId: roomId, roomId: roomId,
}, },
"mx_DevtoolsDialog_wrapper", "mx_DevtoolsDialog_wrapper",

View File

@ -187,14 +187,15 @@ export const WidgetContextMenu: React.FC<IProps> = ({
onDeleteClick(); onDeleteClick();
} else if (roomId) { } else if (roomId) {
// Show delete confirmation dialog // Show delete confirmation dialog
Modal.createDialog(QuestionDialog, { const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("widget|context_menu|delete"), title: _t("widget|context_menu|delete"),
description: _t("widget|context_menu|delete_warning"), description: _t("widget|context_menu|delete_warning"),
button: _t("widget|context_menu|delete"), 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);
}); });
} }

View File

@ -11,7 +11,7 @@ import React from "react";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import Modal, { type ComponentProps } from "../../../Modal"; import Modal, { type ComponentProps, type IHandle } from "../../../Modal";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import { getPolicyUrl } from "../../../toasts/AnalyticsToast"; import { getPolicyUrl } from "../../../toasts/AnalyticsToast";
import ExternalLink from "../elements/ExternalLink"; import ExternalLink from "../elements/ExternalLink";
@ -91,10 +91,10 @@ export const AnalyticsLearnMoreDialog: React.FC<IProps> = ({
export const showDialog = ( export const showDialog = (
props: Omit<ComponentProps<typeof AnalyticsLearnMoreDialog>, "cookiePolicyUrl" | "analyticsOwner">, props: Omit<ComponentProps<typeof AnalyticsLearnMoreDialog>, "cookiePolicyUrl" | "analyticsOwner">,
): void => { ): IHandle<typeof AnalyticsLearnMoreDialog> => {
const privacyPolicyUrl = getPolicyUrl(); const privacyPolicyUrl = getPolicyUrl();
const analyticsOwner = SdkConfig.get("analytics_owner") ?? SdkConfig.get("brand"); const analyticsOwner = SdkConfig.get("analytics_owner") ?? SdkConfig.get("brand");
Modal.createDialog( return Modal.createDialog(
AnalyticsLearnMoreDialog, AnalyticsLearnMoreDialog,
{ {
privacyPolicyUrl, privacyPolicyUrl,

View File

@ -58,37 +58,32 @@ export function createRedactEventDialog({
const roomId = mxEvent.getRoomId(); const roomId = mxEvent.getRoomId();
if (!roomId) throw new Error(`cannot redact event ${mxEvent.getId()} without room ID`); if (!roomId) throw new Error(`cannot redact event ${mxEvent.getId()} without room ID`);
Modal.createDialog( const { finished } = Modal.createDialog(ConfirmRedactDialog, { event: mxEvent }, "mx_Dialog_confirmredact");
ConfirmRedactDialog,
{
event: mxEvent,
onFinished: async (proceed, reason): Promise<void> => {
if (!proceed) return;
const cli = MatrixClientPeg.safeGet(); finished.then(async ([proceed, reason]) => {
const withRelTypes: Pick<IRedactOpts, "with_rel_types"> = {}; if (!proceed) return;
try { const cli = MatrixClientPeg.safeGet();
onCloseDialog?.(); const withRelTypes: Pick<IRedactOpts, "with_rel_types"> = {};
await cli.redactEvent(roomId, eventId, undefined, {
...(reason ? { reason } : {}), try {
...withRelTypes, onCloseDialog?.();
}); await cli.redactEvent(roomId, eventId, undefined, {
} catch (e: any) { ...(reason ? { reason } : {}),
const code = e.errcode || e.statusCode; ...withRelTypes,
// 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 } catch (e: any) {
// detached queue and we show the room status bar to allow retry const code = e.errcode || e.statusCode;
if (typeof code !== "undefined") { // only show the dialog if failing for something other than a network error
// display error message stating you couldn't delete this. // (e.g. no errcode or statusCode) as in that case the redactions end up in the
Modal.createDialog(ErrorDialog, { // detached queue and we show the room status bar to allow retry
title: _t("common|error"), if (typeof code !== "undefined") {
description: _t("redact|error", { code }), // display error message stating you couldn't delete this.
}); Modal.createDialog(ErrorDialog, {
} title: _t("common|error"),
} description: _t("redact|error", { code }),
}, });
}, }
"mx_Dialog_confirmredact", }
); });
} }

View File

@ -31,13 +31,13 @@ export default class SessionRestoreErrorDialog extends React.Component<IProps> {
}; };
private onClearStorageClick = (): void => { private onClearStorageClick = (): void => {
Modal.createDialog(QuestionDialog, { const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("action|sign_out"), title: _t("action|sign_out"),
description: <div>{_t("error|session_restore|clear_storage_description")}</div>, description: <div>{_t("error|session_restore|clear_storage_description")}</div>,
button: _t("action|sign_out"), button: _t("action|sign_out"),
danger: true, danger: true,
onFinished: this.props.onFinished,
}); });
finished.then(([ok]) => this.props.onFinished(ok));
}; };
private onRefreshClick = (): void => { private onRefreshClick = (): void => {

View File

@ -66,12 +66,12 @@ export default class SetEmailDialog extends React.Component<IProps, IState> {
this.addThreepid = new AddThreepid(MatrixClientPeg.safeGet()); this.addThreepid = new AddThreepid(MatrixClientPeg.safeGet());
this.addThreepid.addEmailAddress(emailAddress).then( this.addThreepid.addEmailAddress(emailAddress).then(
() => { () => {
Modal.createDialog(QuestionDialog, { const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("auth|set_email|verification_pending_title"), title: _t("auth|set_email|verification_pending_title"),
description: _t("auth|set_email|verification_pending_description"), description: _t("auth|set_email|verification_pending_description"),
button: _t("action|continue"), button: _t("action|continue"),
onFinished: this.onEmailDialogFinished,
}); });
finished.then(([ok]) => this.onEmailDialogFinished(ok));
}, },
(err) => { (err) => {
this.setState({ emailBusy: false }); this.setState({ emailBusy: false });
@ -89,7 +89,7 @@ export default class SetEmailDialog extends React.Component<IProps, IState> {
this.props.onFinished(false); this.props.onFinished(false);
}; };
private onEmailDialogFinished = (ok: boolean): void => { private onEmailDialogFinished = (ok?: boolean): void => {
if (ok) { if (ok) {
this.verifyEmailAddress(); this.verifyEmailAddress();
} else { } else {
@ -115,12 +115,12 @@ export default class SetEmailDialog extends React.Component<IProps, IState> {
_t("settings|general|error_email_verification") + _t("settings|general|error_email_verification") +
" " + " " +
_t("auth|set_email|verification_pending_description"); _t("auth|set_email|verification_pending_description");
Modal.createDialog(QuestionDialog, { const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("auth|set_email|verification_pending_title"), title: _t("auth|set_email|verification_pending_title"),
description: message, description: message,
button: _t("action|continue"), button: _t("action|continue"),
onFinished: this.onEmailDialogFinished,
}); });
finished.then(([ok]) => this.onEmailDialogFinished(ok));
} else { } else {
logger.error("Unable to verify email address: " + err); logger.error("Unable to verify email address: " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {

View File

@ -222,10 +222,10 @@ export const NetworkDropdown: React.FC<IProps> = ({ protocols, config, setConfig
const [ok, newServer] = await finished; const [ok, newServer] = await finished;
if (!ok) return; if (!ok) return;
if (!allServers.includes(newServer)) { if (!allServers.includes(newServer!)) {
setUserDefinedServers([...userDefinedServers, newServer]); setUserDefinedServers([...userDefinedServers, newServer!]);
setConfig({ setConfig({
roomServer: newServer, roomServer: newServer!,
}); });
} }
}} }}

View File

@ -167,18 +167,18 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
.then(() => this.props.onFinished(true)) .then(() => this.props.onFinished(true))
.catch((e) => { .catch((e) => {
console.error("Failed to post poll:", e); console.error("Failed to post poll:", e);
Modal.createDialog(QuestionDialog, { const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("poll|failed_send_poll_title"), title: _t("poll|failed_send_poll_title"),
description: _t("poll|failed_send_poll_description"), description: _t("poll|failed_send_poll_description"),
button: _t("action|try_again"), button: _t("action|try_again"),
cancelButton: _t("action|cancel"), cancelButton: _t("action|cancel"),
onFinished: (tryAgain: boolean) => { });
if (!tryAgain) { finished.then(([tryAgain]) => {
this.cancel(); if (!tryAgain) {
} else { this.cancel();
this.setState({ busy: false, canSubmit: true }); } else {
} this.setState({ busy: false, canSubmit: true });
}, }
}); });
}); });
} }

View File

@ -28,9 +28,10 @@ interface IProps {
const showPickerDialog = ( const showPickerDialog = (
title: string | undefined, title: string | undefined,
serverConfig: ValidatedServerConfig, serverConfig: ValidatedServerConfig,
onFinished: (config: ValidatedServerConfig) => void, onFinished: (config?: ValidatedServerConfig) => void,
): void => { ): void => {
Modal.createDialog(ServerPickerDialog, { title, serverConfig, onFinished }); const { finished } = Modal.createDialog(ServerPickerDialog, { title, serverConfig });
finished.then(([config]) => onFinished(config));
}; };
const onHelpClick = (): void => { const onHelpClick = (): void => {

View File

@ -235,7 +235,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
scalarClient?.connect().then(() => { scalarClient?.connect().then(() => {
const completeUrl = scalarClient.getStarterLink(starterLink); const completeUrl = scalarClient.getStarterLink(starterLink);
const integrationsUrl = integrationManager!.uiUrl; const integrationsUrl = integrationManager!.uiUrl;
Modal.createDialog(QuestionDialog, { const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("timeline|scalar_starter_link|dialog_title"), title: _t("timeline|scalar_starter_link|dialog_title"),
description: ( description: (
<div> <div>
@ -243,18 +243,19 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
</div> </div>
), ),
button: _t("action|continue"), button: _t("action|continue"),
onFinished(confirmed) { });
if (!confirmed) {
return; finished.then(([confirmed]) => {
} if (!confirmed) {
const width = window.screen.width > 1024 ? 1024 : window.screen.width; return;
const height = window.screen.height > 800 ? 800 : window.screen.height; }
const left = (window.screen.width - width) / 2; const width = window.screen.width > 1024 ? 1024 : window.screen.width;
const top = (window.screen.height - height) / 2; const height = window.screen.height > 800 ? 800 : window.screen.height;
const features = `height=${height}, width=${width}, top=${top}, left=${left},`; const left = (window.screen.width - width) / 2;
const wnd = window.open(completeUrl, "_blank", features)!; const top = (window.screen.height - height) / 2;
wnd.opener = null; const features = `height=${height}, width=${width}, top=${top}, left=${left},`;
}, const wnd = window.open(completeUrl, "_blank", features)!;
wnd.opener = null;
}); });
}); });
}; };

View File

@ -119,15 +119,14 @@ export default class EventIndexPanel extends React.Component<EmptyObject, IState
}; };
private confirmEventStoreReset = (): void => { private confirmEventStoreReset = (): void => {
const { close } = Modal.createDialog(SeshatResetDialog, { const { finished, close } = Modal.createDialog(SeshatResetDialog);
onFinished: async (success): Promise<void> => { finished.then(async ([success]) => {
if (success) { if (success) {
await SettingsStore.setValue("enableEventIndexing", null, SettingLevel.DEVICE, false); await SettingsStore.setValue("enableEventIndexing", null, SettingLevel.DEVICE, false);
await EventIndexPeg.deleteEventIndex(); await EventIndexPeg.deleteEventIndex();
await this.onEnable(); await this.onEnable();
close(); close();
} }
},
}); });
}; };

View File

@ -11,7 +11,6 @@ import { type AuthDict, type IAuthData } from "matrix-js-sdk/src/interactive-aut
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import Modal from "../../../../Modal"; import Modal from "../../../../Modal";
import { type InteractiveAuthCallback } from "../../../structures/InteractiveAuth";
import { SSOAuthEntry } from "../../auth/InteractiveAuthEntryComponents"; import { SSOAuthEntry } from "../../auth/InteractiveAuthEntryComponents";
import InteractiveAuthDialog from "../../dialogs/InteractiveAuthDialog"; import InteractiveAuthDialog from "../../dialogs/InteractiveAuthDialog";
@ -24,7 +23,7 @@ const makeDeleteRequest =
export const deleteDevicesWithInteractiveAuth = async ( export const deleteDevicesWithInteractiveAuth = async (
matrixClient: MatrixClient, matrixClient: MatrixClient,
deviceIds: string[], deviceIds: string[],
onFinished: InteractiveAuthCallback<void>, onFinished: (success?: boolean) => Promise<void>,
): Promise<void> => { ): Promise<void> => {
if (!deviceIds.length) { if (!deviceIds.length) {
return; return;
@ -32,7 +31,7 @@ export const deleteDevicesWithInteractiveAuth = async (
try { try {
await makeDeleteRequest(matrixClient, deviceIds)(null); await makeDeleteRequest(matrixClient, deviceIds)(null);
// no interactive auth needed // no interactive auth needed
await onFinished(true, undefined); await onFinished(true);
} catch (error) { } catch (error) {
if (!(error instanceof MatrixError) || error.httpStatus !== 401 || !error.data?.flows) { if (!(error instanceof MatrixError) || error.httpStatus !== 401 || !error.data?.flows) {
// doesn't look like an interactive-auth failure // doesn't look like an interactive-auth failure
@ -62,16 +61,16 @@ export const deleteDevicesWithInteractiveAuth = async (
continueKind: "danger", continueKind: "danger",
}, },
}; };
Modal.createDialog(InteractiveAuthDialog, { const { finished } = Modal.createDialog(InteractiveAuthDialog, {
title: _t("common|authentication"), title: _t("common|authentication"),
matrixClient: matrixClient, matrixClient: matrixClient,
authData: error.data as IAuthData, authData: error.data as IAuthData,
onFinished,
makeRequest: makeDeleteRequest(matrixClient, deviceIds), makeRequest: makeDeleteRequest(matrixClient, deviceIds),
aestheticsForStagePhases: { aestheticsForStagePhases: {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
}, },
}); });
finished.then(([success]) => onFinished(success));
} }
}; };

View File

@ -155,7 +155,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
if (!confirm) return; if (!confirm) return;
} }
Modal.createDialog(QuestionDialog, { const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("room_settings|security|enable_encryption_confirm_title"), title: _t("room_settings|security|enable_encryption_confirm_title"),
description: _t( description: _t(
"room_settings|security|enable_encryption_confirm_description", "room_settings|security|enable_encryption_confirm_description",
@ -164,23 +164,23 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
a: (sub) => <ExternalLink href={SdkConfig.get("help_encryption_url")}>{sub}</ExternalLink>, a: (sub) => <ExternalLink href={SdkConfig.get("help_encryption_url")}>{sub}</ExternalLink>,
}, },
), ),
onFinished: (confirm) => { });
if (!confirm) { finished.then(([confirm]) => {
this.setState({ encrypted: false }); if (!confirm) {
return; this.setState({ encrypted: false });
} return;
}
const beforeEncrypted = this.state.encrypted; const beforeEncrypted = this.state.encrypted;
this.setState({ encrypted: true }); this.setState({ encrypted: true });
this.context this.context
.sendStateEvent(this.props.room.roomId, EventType.RoomEncryption, { .sendStateEvent(this.props.room.roomId, EventType.RoomEncryption, {
algorithm: MEGOLM_ENCRYPTION_ALGORITHM, algorithm: MEGOLM_ENCRYPTION_ALGORITHM,
}) })
.catch((e) => { .catch((e) => {
logger.error(e); logger.error(e);
this.setState({ encrypted: beforeEncrypted }); this.setState({ encrypted: beforeEncrypted });
}); });
},
}); });
}; };
@ -213,9 +213,9 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const [shouldCreate, opts] = await modal.finished; const [shouldCreate, opts] = await modal.finished;
if (shouldCreate) { if (shouldCreate) {
await createRoom(this.context, opts); await createRoom(this.context, opts!);
} }
return shouldCreate; return shouldCreate ?? false;
}; };
private onHistoryRadioToggle = (history: HistoryVisibility): void => { private onHistoryRadioToggle = (history: HistoryVisibility): void => {

View File

@ -165,10 +165,9 @@ const AccountUserSettingsTab: React.FC<IProps> = ({ closeSettingsFn }) => {
}, []); }, []);
const onDeactivateClicked = useCallback((): void => { const onDeactivateClicked = useCallback((): void => {
Modal.createDialog(DeactivateAccountDialog, { const { finished } = Modal.createDialog(DeactivateAccountDialog);
onFinished: (success) => { finished.then(([success]) => {
if (success) closeSettingsFn(); if (success) closeSettingsFn();
},
}); });
}, [closeSettingsFn]); }, [closeSettingsFn]);

View File

@ -247,7 +247,10 @@ function SetUpEncryptionPanel({ onFinish }: SetUpEncryptionPanelProps): JSX.Elem
<Button <Button
size="sm" size="sm"
Icon={ComputerIcon} Icon={ComputerIcon}
onClick={() => Modal.createDialog(SetupEncryptionDialog, { onFinished: onFinish })} onClick={() => {
const { finished } = Modal.createDialog(SetupEncryptionDialog);
finished.then(onFinish);
}}
> >
{_t("settings|encryption|device_not_verified_button")} {_t("settings|encryption|device_not_verified_button")}
</Button> </Button>

View File

@ -100,7 +100,7 @@ const useSignOut = (
} else { } else {
const deferredSuccess = defer<boolean>(); const deferredSuccess = defer<boolean>();
await deleteDevicesWithInteractiveAuth(matrixClient, deviceIds, async (success) => { await deleteDevicesWithInteractiveAuth(matrixClient, deviceIds, async (success) => {
deferredSuccess.resolve(success); deferredSuccess.resolve(!!success);
}); });
success = await deferredSuccess.promise; success = await deferredSuccess.promise;
} }
@ -203,7 +203,8 @@ const SessionManagerTab: React.FC<{
const shouldShowOtherSessions = otherSessionsCount > 0; const shouldShowOtherSessions = otherSessionsCount > 0;
const onVerifyCurrentDevice = (): void => { const onVerifyCurrentDevice = (): void => {
Modal.createDialog(SetupEncryptionDialog, { onFinished: refreshDevices }); const { finished } = Modal.createDialog(SetupEncryptionDialog);
finished.then(refreshDevices);
}; };
const onTriggerDeviceVerification = useCallback( const onTriggerDeviceVerification = useCallback(
@ -212,14 +213,14 @@ const SessionManagerTab: React.FC<{
return; return;
} }
const verificationRequestPromise = requestDeviceVerification(deviceId); const verificationRequestPromise = requestDeviceVerification(deviceId);
Modal.createDialog(VerificationRequestDialog, { const { finished } = Modal.createDialog(VerificationRequestDialog, {
verificationRequestPromise, verificationRequestPromise,
member: currentUserMember, member: currentUserMember,
onFinished: async (): Promise<void> => { });
const request = await verificationRequestPromise; finished.then(async () => {
request.cancel(); const request = await verificationRequestPromise;
await refreshDevices(); request.cancel();
}, await refreshDevices();
}); });
}, },
[requestDeviceVerification, refreshDevices, currentUserMember], [requestDeviceVerification, refreshDevices, currentUserMember],

View File

@ -124,18 +124,16 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
request.roomId, request.roomId,
); );
} else { } else {
Modal.createDialog( const { finished } = Modal.createDialog(
VerificationRequestDialog, VerificationRequestDialog,
{ {
verificationRequest: request, verificationRequest: request,
onFinished: () => {
request.cancel();
},
}, },
undefined, undefined,
/* priority = */ false, /* priority = */ false,
/* static = */ true, /* static = */ true,
); );
finished.then(() => request.cancel());
} }
await request.accept(); await request.accept();
} catch (err) { } catch (err) {

View File

@ -49,16 +49,14 @@ export const singleMxcUpload = async (cli: MatrixClient): Promise<string | null>
const file = (ev as HTMLInputEvent).target.files?.[0]; const file = (ev as HTMLInputEvent).target.files?.[0];
if (!file) return; if (!file) return;
Modal.createDialog(UploadConfirmDialog, { const { finished } = Modal.createDialog(UploadConfirmDialog, { file });
file, finished.then(async ([shouldContinue]) => {
onFinished: async (shouldContinue): Promise<void> => { if (shouldContinue) {
if (shouldContinue) { const { content_uri: uri } = await cli.uploadContent(file);
const { content_uri: uri } = await cli.uploadContent(file); resolve(uri);
resolve(uri); } else {
} else { resolve(null);
resolve(null); }
}
},
}); });
}; };

View File

@ -61,18 +61,18 @@ export class ModalWidgetStore extends AsyncStoreWithClient<IState> {
widgetDefinition: { ...requestData }, widgetDefinition: { ...requestData },
widgetRoomId, widgetRoomId,
sourceWidgetId: sourceWidget.id, 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, undefined,
/* priority = */ false, /* priority = */ false,
/* static = */ true, /* 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 = ( public closeModalWidget = (

View File

@ -557,19 +557,17 @@ export class StopGapWidgetDriver extends WidgetDriver {
observer.update({ state: OpenIDRequestState.PendingUserConfirmation }); observer.update({ state: OpenIDRequestState.PendingUserConfirmation });
Modal.createDialog(WidgetOpenIDPermissionsDialog, { const { finished } = Modal.createDialog(WidgetOpenIDPermissionsDialog, {
widget: this.forWidget, widget: this.forWidget,
widgetKind: this.forWidgetKind, widgetKind: this.forWidgetKind,
inRoomId: this.inRoomId, inRoomId: this.inRoomId,
onFinished: async (confirm): Promise<void> => {
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<void> { public async navigate(uri: string): Promise<void> {

View File

@ -34,34 +34,36 @@ const onReject = (): void => {
}; };
const onLearnMoreNoOptIn = (): void => { const onLearnMoreNoOptIn = (): void => {
showAnalyticsLearnMoreDialog({ const { finished } = 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
},
primaryButton: _t("action|enable"), 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 => { const onLearnMorePreviouslyOptedIn = (): void => {
showAnalyticsLearnMoreDialog({ const { finished } = 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
},
primaryButton: _t("analytics|accept_button"), primaryButton: _t("analytics|accept_button"),
cancelButton: _t("action|stop"), 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"; const TOAST_KEY = "analytics";

View File

@ -32,27 +32,27 @@ export const showToast = (version: string, newVersion: string, releaseNotes?: st
let acceptLabel = _t("update|see_changes_button"); let acceptLabel = _t("update|see_changes_button");
if (releaseNotes) { if (releaseNotes) {
onAccept = () => { onAccept = () => {
Modal.createDialog(QuestionDialog, { const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("update|release_notes_toast_title"), title: _t("update|release_notes_toast_title"),
description: <pre>{releaseNotes}</pre>, description: <pre>{releaseNotes}</pre>,
button: _t("action|update"), button: _t("action|update"),
onFinished: (update) => { });
if (update && PlatformPeg.get()) { finished.then(([update]) => {
PlatformPeg.get()!.installUpdate(); if (update && PlatformPeg.get()) {
} PlatformPeg.get()!.installUpdate();
}, }
}); });
}; };
} else if (checkVersion(version) && checkVersion(newVersion)) { } else if (checkVersion(version) && checkVersion(newVersion)) {
onAccept = () => { onAccept = () => {
Modal.createDialog(ChangelogDialog, { const { finished } = Modal.createDialog(ChangelogDialog, {
version, version,
newVersion, newVersion,
onFinished: (update) => { });
if (update && PlatformPeg.get()) { finished.then(([update]) => {
PlatformPeg.get()!.installUpdate(); if (update && PlatformPeg.get()) {
} PlatformPeg.get()!.installUpdate();
}, }
}); });
}; };
} else { } else {

View File

@ -119,7 +119,7 @@ export class DialogOpener {
break; break;
case Action.OpenAddToExistingSpaceDialog: { case Action.OpenAddToExistingSpaceDialog: {
const space = payload.space; const space = payload.space;
Modal.createDialog( const { finished } = Modal.createDialog(
AddExistingToSpaceDialog, AddExistingToSpaceDialog,
{ {
onCreateRoomClick: (ev: ButtonEvent) => { onCreateRoomClick: (ev: ButtonEvent) => {
@ -128,14 +128,14 @@ export class DialogOpener {
}, },
onAddSubspaceClick: () => showAddExistingSubspace(space), onAddSubspaceClick: () => showAddExistingSubspace(space),
space, space,
onFinished: (added: boolean) => {
if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) {
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
}
},
}, },
"mx_AddExistingToSpaceDialog_wrapper", "mx_AddExistingToSpaceDialog_wrapper",
); );
finished.then(([added]) => {
if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) {
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
}
});
break; break;
} }
} }

View File

@ -171,20 +171,20 @@ export async function leaveRoomBehaviour(
} }
export const leaveSpace = (space: Room): void => { export const leaveSpace = (space: Room): void => {
Modal.createDialog( const { finished } = Modal.createDialog(
LeaveSpaceDialog, LeaveSpaceDialog,
{ {
space, space,
onFinished: async (leave: boolean, rooms: Room[]): Promise<void> => {
if (!leave) return;
await bulkSpaceBehaviour(space, rooms, (room) => leaveRoomBehaviour(space.client, room.roomId));
dis.dispatch<AfterLeaveRoomPayload>({
action: Action.AfterLeaveRoom,
room_id: space.roomId,
});
},
}, },
"mx_LeaveSpaceDialog_wrapper", "mx_LeaveSpaceDialog_wrapper",
); );
finished.then(async ([leave, rooms]) => {
if (!leave) return;
await bulkSpaceBehaviour(space, rooms!, (room) => leaveRoomBehaviour(space.client, room.roomId));
dis.dispatch<AfterLeaveRoomPayload>({
action: Action.AfterLeaveRoom,
room_id: space.roomId,
});
});
}; };

View File

@ -75,7 +75,7 @@ export const showCreateNewRoom = async (space: Room, type?: RoomType): Promise<b
}); });
const [shouldCreate, opts] = await modal.finished; const [shouldCreate, opts] = await modal.finished;
if (shouldCreate) { if (shouldCreate) {
await createRoom(space.client, opts); await createRoom(space.client, opts!);
} }
return !!shouldCreate; return !!shouldCreate;
}; };
@ -106,35 +106,35 @@ export const showSpaceInvite = (space: Room, initialText = ""): void => {
}; };
export const showAddExistingSubspace = (space: Room): void => { export const showAddExistingSubspace = (space: Room): void => {
Modal.createDialog( const { finished } = Modal.createDialog(
AddExistingSubspaceDialog, AddExistingSubspaceDialog,
{ {
space, space,
onCreateSubspaceClick: () => showCreateNewSubspace(space), onCreateSubspaceClick: () => showCreateNewSubspace(space),
onFinished: (added: boolean) => {
if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) {
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
}
},
}, },
"mx_AddExistingToSpaceDialog_wrapper", "mx_AddExistingToSpaceDialog_wrapper",
); );
finished.then(([added]) => {
if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) {
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
}
});
}; };
export const showCreateNewSubspace = (space: Room): void => { export const showCreateNewSubspace = (space: Room): void => {
Modal.createDialog( const { finished } = Modal.createDialog(
CreateSubspaceDialog, CreateSubspaceDialog,
{ {
space, space,
onAddExistingSpaceClick: () => showAddExistingSubspace(space), onAddExistingSpaceClick: () => showAddExistingSubspace(space),
onFinished: (added: boolean) => {
if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) {
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
}
},
}, },
"mx_CreateSubspaceDialog_wrapper", "mx_CreateSubspaceDialog_wrapper",
); );
finished.then(([added]) => {
if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) {
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
}
});
}; };
export const bulkSpaceBehaviour = async ( export const bulkSpaceBehaviour = async (

View File

@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import Modal from "../../src/Modal"; import Modal from "../../src/Modal";
import QuestionDialog from "../../src/components/views/dialogs/QuestionDialog"; import QuestionDialog from "../../src/components/views/dialogs/QuestionDialog";
import defaultDispatcher from "../../src/dispatcher/dispatcher"; import defaultDispatcher from "../../src/dispatcher/dispatcher";
import { flushPromises } from "../test-utils";
describe("Modal", () => { describe("Modal", () => {
test("forceCloseAllModals should close all open modals", () => { test("forceCloseAllModals should close all open modals", () => {
@ -23,7 +24,7 @@ describe("Modal", () => {
expect(Modal.hasDialogs()).toBe(false); 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 modal1OnFinished = jest.fn();
const modal2OnFinished = jest.fn(); const modal2OnFinished = jest.fn();
@ -31,18 +32,18 @@ describe("Modal", () => {
title: "Test dialog 1", title: "Test dialog 1",
description: "This is a test dialog", description: "This is a test dialog",
button: "Word", button: "Word",
onFinished: modal1OnFinished, }).finished.then(modal1OnFinished);
});
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Test dialog 2", title: "Test dialog 2",
description: "This is a test dialog", description: "This is a test dialog",
button: "Word", button: "Word",
onFinished: modal2OnFinished, }).finished.then(modal2OnFinished);
});
defaultDispatcher.dispatch({ action: "logout" }, true); defaultDispatcher.dispatch({ action: "logout" }, true);
await flushPromises();
expect(modal1OnFinished).toHaveBeenCalled(); expect(modal1OnFinished).toHaveBeenCalled();
expect(modal2OnFinished).toHaveBeenCalled(); expect(modal2OnFinished).toHaveBeenCalled();
}); });

View File

@ -35,7 +35,7 @@ describe("deleteDevices()", () => {
await deleteDevicesWithInteractiveAuth(mockClient, deviceIds, onFinished); await deleteDevicesWithInteractiveAuth(mockClient, deviceIds, onFinished);
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith(deviceIds, undefined); expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith(deviceIds, undefined);
expect(onFinished).toHaveBeenCalledWith(true, undefined); expect(onFinished).toHaveBeenCalledWith(true);
// didnt open modal // didnt open modal
expect(modalSpy).not.toHaveBeenCalled(); expect(modalSpy).not.toHaveBeenCalled();

View File

@ -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 { fireEvent, render, screen, within } from "jest-matrix-react";
import React from "react"; import React from "react";
import { type MatrixClient, ThreepidMedium } from "matrix-js-sdk/src/matrix"; 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 { logger } from "matrix-js-sdk/src/logger";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { type MockedObject } from "jest-mock"; import { type MockedObject } from "jest-mock";
@ -153,7 +154,8 @@ describe("<AccountUserSettingsTab />", () => {
(settingName) => settingName === UIFeature.Deactivate, (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); jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn);
render(getComponent()); render(getComponent());
@ -167,14 +169,16 @@ describe("<AccountUserSettingsTab />", () => {
(settingName) => settingName === UIFeature.Deactivate, (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); jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn);
render(getComponent()); render(getComponent());
await userEvent.click(screen.getByRole("button", { name: "Deactivate Account" })); 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(); expect(defaultProps.closeSettingsFn).toHaveBeenCalled();
}); });
@ -183,14 +187,16 @@ describe("<AccountUserSettingsTab />", () => {
(settingName) => settingName === UIFeature.Deactivate, (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); jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn);
render(getComponent()); render(getComponent());
await userEvent.click(screen.getByRole("button", { name: "Deactivate Account" })); 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(); expect(defaultProps.closeSettingsFn).not.toHaveBeenCalled();
}); });

View File

@ -62,7 +62,7 @@ describe("<EncryptionUserSettingsTab />", () => {
); );
expect(asFragment()).toMatchSnapshot(); 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")); await user.click(screen.getByText("Verify this device"));
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
}); });

View File

@ -631,9 +631,10 @@ describe("<SessionManagerTab />", () => {
// click verify button from current session section // click verify button from current session section
fireEvent.click(getByTestId(`verification-status-button-${alicesMobileDevice.device_id}`)); fireEvent.click(getByTestId(`verification-status-button-${alicesMobileDevice.device_id}`));
const { onFinished: modalOnFinished } = modalSpy.mock.calls[0][1] as any; // close the modal
// simulate modal completing process const { close: closeModal } = modalSpy.mock.results[0].value;
await modalOnFinished(); closeModal();
await flushPromises();
// cancelled in case it was a failure exit from modal // cancelled in case it was a failure exit from modal
expect(mockVerificationRequest.cancel).toHaveBeenCalled(); expect(mockVerificationRequest.cancel).toHaveBeenCalled();