mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-09 01:30:57 +00:00
Compare commits
8 Commits
midhun/mod
...
rav/invite
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2db2a77d56 | ||
|
|
fc3a52cd4c | ||
|
|
b0a15d97f3 | ||
|
|
a646f3575f | ||
|
|
0541b7a961 | ||
|
|
58f1893997 | ||
|
|
a562b2928a | ||
|
|
f43fadd9b8 |
Binary file not shown.
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
@@ -142,6 +142,7 @@
|
||||
@import "./views/dialogs/_GenericFeatureFeedbackDialog.pcss";
|
||||
@import "./views/dialogs/_IncomingSasDialog.pcss";
|
||||
@import "./views/dialogs/_InviteDialog.pcss";
|
||||
@import "./views/dialogs/_InviteProgressBody.pcss";
|
||||
@import "./views/dialogs/_JoinRuleDropdown.pcss";
|
||||
@import "./views/dialogs/_LeaveSpaceDialog.pcss";
|
||||
@import "./views/dialogs/_LocationViewDialog.pcss";
|
||||
|
||||
@@ -63,17 +63,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
height: 25px;
|
||||
line-height: $font-25px;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_buttonAndSpinner {
|
||||
.mx_Spinner {
|
||||
/* Width and height are required to trick the layout engine. */
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-inline-start: 5px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_InviteDialog_section {
|
||||
@@ -218,6 +207,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.mx_InviteProgressBody {
|
||||
margin-top: var(--cpd-space-12x);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_InviteDialog_transfer {
|
||||
|
||||
16
res/css/views/dialogs/_InviteProgressBody.pcss
Normal file
16
res/css/views/dialogs/_InviteProgressBody.pcss
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_InviteProgressBody {
|
||||
text-align: center;
|
||||
font: var(--cpd-font-body-lg-regular);
|
||||
|
||||
h1 {
|
||||
color: var(--cpd-color-text-primary);
|
||||
font: var(--cpd-font-heading-sm-semibold);
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,6 @@ import Field from "../elements/Field";
|
||||
import TabbedView, { Tab, TabLocation } from "../../structures/TabbedView";
|
||||
import Dialpad from "../voip/DialPad";
|
||||
import QuestionDialog from "./QuestionDialog";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
|
||||
import LegacyCallHandler from "../../../LegacyCallHandler";
|
||||
@@ -65,6 +64,7 @@ import { UNKNOWN_PROFILE_ERRORS } from "../../../utils/MultiInviter";
|
||||
import AskInviteAnywayDialog, { type UnknownProfiles } from "./AskInviteAnywayDialog";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { type UserProfilesStore } from "../../../stores/UserProfilesStore";
|
||||
import InviteProgressBody from "./InviteProgressBody.tsx";
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
@@ -329,8 +329,14 @@ interface IInviteDialogState {
|
||||
dialPadValue: string;
|
||||
currentTabId: TabId;
|
||||
|
||||
// These two flags are used for the 'Go' button to communicate what is going on.
|
||||
/**
|
||||
* True if we are sending the invites.
|
||||
*
|
||||
* We will grey out the action button, hide the suggestions, and display a spinner.
|
||||
*/
|
||||
busy: boolean;
|
||||
|
||||
/** Error from the last attempt to send invites. */
|
||||
errorText?: string;
|
||||
}
|
||||
|
||||
@@ -617,7 +623,10 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await inviteMultipleToRoom(cli, this.props.roomId, targetIds);
|
||||
const result = await inviteMultipleToRoom(cli, this.props.roomId, targetIds, {
|
||||
// We show our own progress body, so don't pop up a separate dialog.
|
||||
inhibitProgressDialog: true,
|
||||
});
|
||||
if (!this.shouldAbortAfterInviteError(result, room)) {
|
||||
// handles setting error message too
|
||||
this.props.onFinished(true);
|
||||
@@ -1328,11 +1337,6 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||
* "CallTransfer" one.
|
||||
*/
|
||||
private renderMainTab(): JSX.Element {
|
||||
let spinner: JSX.Element | undefined;
|
||||
if (this.state.busy) {
|
||||
spinner = <Spinner w={20} h={20} />;
|
||||
}
|
||||
|
||||
let helpText;
|
||||
let buttonText;
|
||||
let goButtonFn: (() => Promise<void>) | null = null;
|
||||
@@ -1437,12 +1441,9 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||
<p className="mx_InviteDialog_helpText">{helpText}</p>
|
||||
<div className="mx_InviteDialog_addressBar">
|
||||
{this.renderEditor()}
|
||||
<div className="mx_InviteDialog_buttonAndSpinner">
|
||||
{goButton}
|
||||
{spinner}
|
||||
</div>
|
||||
{goButton}
|
||||
</div>
|
||||
{this.renderSuggestions()}
|
||||
{this.state.busy ? <InviteProgressBody /> : this.renderSuggestions()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
24
src/components/views/dialogs/InviteProgressBody.tsx
Normal file
24
src/components/views/dialogs/InviteProgressBody.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import InlineSpinner from "../elements/InlineSpinner";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
/** The common body of components that show the progress of sending room invites. */
|
||||
const InviteProgressBody: React.FC = () => {
|
||||
return (
|
||||
<div className="mx_InviteProgressBody">
|
||||
<InlineSpinner w={32} h={32} />
|
||||
<h1>{_t("invite|progress|preparing")}</h1>
|
||||
{_t("invite|progress|dont_close")}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InviteProgressBody;
|
||||
42
src/components/views/dialogs/InviteProgressDialog.tsx
Normal file
42
src/components/views/dialogs/InviteProgressDialog.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import Modal from "../../../Modal.tsx";
|
||||
import InviteProgressBody from "./InviteProgressBody.tsx";
|
||||
|
||||
interface Props {
|
||||
onFinished: () => void;
|
||||
}
|
||||
|
||||
/** A Modal dialog that pops up while room invites are being sent. */
|
||||
const InviteProgressDialog: React.FC<Props> = (props) => {
|
||||
return <InviteProgressBody />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the invite progress dialog.
|
||||
*
|
||||
* Returns a callback which will close the dialog again.
|
||||
*/
|
||||
export function openInviteProgressDialog(): () => void {
|
||||
const onBeforeClose = async (reason?: string): Promise<boolean> => {
|
||||
// Inhibit closing via background click
|
||||
return reason != "backgroundClick";
|
||||
};
|
||||
|
||||
const { close } = Modal.createDialog(
|
||||
InviteProgressDialog,
|
||||
/* props */ {},
|
||||
/* className */ undefined,
|
||||
/* isPriorityModal */ false,
|
||||
/* isStaticModal */ false,
|
||||
{ onBeforeClose },
|
||||
);
|
||||
return close;
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import Modal from "../../../Modal";
|
||||
import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog";
|
||||
import RoomUpgradeWarningDialog, { type IFinishedOpts } from "../dialogs/RoomUpgradeWarningDialog";
|
||||
import { upgradeRoom } from "../../../utils/RoomUpgrade";
|
||||
import { type RoomUpgradeProgress, upgradeRoom } from "../../../utils/RoomUpgrade";
|
||||
import { arrayHasDiff } from "../../../utils/arrays";
|
||||
import { useLocalEcho } from "../../../hooks/useLocalEcho";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
@@ -120,7 +120,7 @@ const JoinRuleSettings: React.FC<JoinRuleSettingsProps> = ({
|
||||
opts: IFinishedOpts,
|
||||
fn: (progressText: string, progress: number, total: number) => void,
|
||||
): Promise<void> => {
|
||||
const roomId = await upgradeRoom(room, targetVersion, opts.invite, true, true, true, (progress) => {
|
||||
const progressCallback = (progress: RoomUpgradeProgress): void => {
|
||||
const total = 2 + progress.updateSpacesTotal + progress.inviteUsersTotal;
|
||||
if (!progress.roomUpgraded) {
|
||||
fn(_t("room_settings|security|join_rule_upgrade_upgrading_room"), 0, total);
|
||||
@@ -151,7 +151,20 @@ const JoinRuleSettings: React.FC<JoinRuleSettingsProps> = ({
|
||||
total,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
const roomId = await upgradeRoom(
|
||||
room,
|
||||
targetVersion,
|
||||
opts.invite,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
progressCallback,
|
||||
|
||||
// We want to keep the RoomUpgradeDialog open during the upgrade, so don't replace it with the
|
||||
// invite progress dialog.
|
||||
/* inhibitInviteProgressDialog: */ true,
|
||||
);
|
||||
|
||||
closeSettingsFn?.();
|
||||
|
||||
|
||||
@@ -1366,6 +1366,10 @@
|
||||
"name_email_mxid_share_space": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.",
|
||||
"name_mxid_share_room": "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
|
||||
"name_mxid_share_space": "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.",
|
||||
"progress": {
|
||||
"dont_close": "Do not close the app until finished.",
|
||||
"preparing": "Preparing invitations..."
|
||||
},
|
||||
"recents_section": "Recent Conversations",
|
||||
"room_failed_partial": "We sent the others, but the below people couldn't be invited to <RoomName/>",
|
||||
"room_failed_partial_title": "Some invites couldn't be sent",
|
||||
|
||||
@@ -16,6 +16,7 @@ import Modal from "../Modal";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import AskInviteAnywayDialog from "../components/views/dialogs/AskInviteAnywayDialog";
|
||||
import ConfirmUserActionDialog from "../components/views/dialogs/ConfirmUserActionDialog";
|
||||
import { openInviteProgressDialog } from "../components/views/dialogs/InviteProgressDialog.tsx";
|
||||
|
||||
export enum InviteState {
|
||||
Invited = "invited",
|
||||
@@ -44,6 +45,12 @@ const USER_BANNED = "IO.ELEMENT.BANNED";
|
||||
export interface MultiInviterOptions {
|
||||
/** Optional callback, fired after each invite */
|
||||
progressCallback?: () => void;
|
||||
|
||||
/**
|
||||
* By default, we will pop up a "Preparing invitations..." dialog while the invites are being sent. Set this to
|
||||
* `true` to inhibit it (in which case, you probably want to implement another bit of feedback UI).
|
||||
*/
|
||||
inhibitProgressDialog?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,49 +95,59 @@ export default class MultiInviter {
|
||||
this.addresses.push(...addresses);
|
||||
this.reason = reason;
|
||||
|
||||
for (const addr of this.addresses) {
|
||||
if (getAddressType(addr) === null) {
|
||||
this.completionStates[addr] = InviteState.Error;
|
||||
this.errors[addr] = {
|
||||
errcode: "M_INVALID",
|
||||
errorText: _t("invite|invalid_address"),
|
||||
};
|
||||
}
|
||||
let closeDialog: (() => void) | undefined;
|
||||
if (!this.options.inhibitProgressDialog) {
|
||||
closeDialog = openInviteProgressDialog();
|
||||
}
|
||||
|
||||
for (const addr of this.addresses) {
|
||||
// don't try to invite it if it's an invalid address
|
||||
// (it will already be marked as an error though,
|
||||
// so no need to do so again)
|
||||
if (getAddressType(addr) === null) {
|
||||
continue;
|
||||
try {
|
||||
for (const addr of this.addresses) {
|
||||
if (getAddressType(addr) === null) {
|
||||
this.completionStates[addr] = InviteState.Error;
|
||||
this.errors[addr] = {
|
||||
errcode: "M_INVALID",
|
||||
errorText: _t("invite|invalid_address"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// don't re-invite (there's no way in the UI to do this, but
|
||||
// for sanity's sake)
|
||||
if (this.completionStates[addr] === InviteState.Invited) {
|
||||
continue;
|
||||
for (const addr of this.addresses) {
|
||||
// don't try to invite it if it's an invalid address
|
||||
// (it will already be marked as an error though,
|
||||
// so no need to do so again)
|
||||
if (getAddressType(addr) === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// don't re-invite (there's no way in the UI to do this, but
|
||||
// for sanity's sake)
|
||||
if (this.completionStates[addr] === InviteState.Invited) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.doInvite(addr, false);
|
||||
|
||||
if (this._fatal) {
|
||||
// `doInvite` suffered a fatal error. The error should have been recorded in `errors`; it's up
|
||||
// to the caller to report back to the user.
|
||||
return this.completionStates;
|
||||
}
|
||||
}
|
||||
|
||||
await this.doInvite(addr, false);
|
||||
if (Object.keys(this.errors).length > 0) {
|
||||
// There were problems inviting some people - see if we can invite them
|
||||
// without caring if they exist or not.
|
||||
const unknownProfileUsers = Object.keys(this.errors).filter((a) =>
|
||||
UNKNOWN_PROFILE_ERRORS.includes(this.errors[a].errcode),
|
||||
);
|
||||
|
||||
if (this._fatal) {
|
||||
// `doInvite` suffered a fatal error. The error should have been recorded in `errors`; it's up
|
||||
// to the caller to report back to the user.
|
||||
return this.completionStates;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(this.errors).length > 0) {
|
||||
// There were problems inviting some people - see if we can invite them
|
||||
// without caring if they exist or not.
|
||||
const unknownProfileUsers = Object.keys(this.errors).filter((a) =>
|
||||
UNKNOWN_PROFILE_ERRORS.includes(this.errors[a].errcode),
|
||||
);
|
||||
|
||||
if (unknownProfileUsers.length > 0) {
|
||||
await this.handleUnknownProfileUsers(unknownProfileUsers);
|
||||
if (unknownProfileUsers.length > 0) {
|
||||
await this.handleUnknownProfileUsers(unknownProfileUsers);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Remember to close the progress dialog, if we opened one.
|
||||
closeDialog?.();
|
||||
}
|
||||
|
||||
return this.completionStates;
|
||||
|
||||
@@ -16,8 +16,9 @@ import { _t } from "../languageHandler";
|
||||
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
|
||||
import SpaceStore from "../stores/spaces/SpaceStore";
|
||||
import Spinner from "../components/views/elements/Spinner";
|
||||
import type { MultiInviterOptions } from "./MultiInviter";
|
||||
|
||||
interface IProgress {
|
||||
export interface RoomUpgradeProgress {
|
||||
roomUpgraded: boolean;
|
||||
roomSynced?: boolean;
|
||||
inviteUsersProgress?: number;
|
||||
@@ -50,7 +51,8 @@ export async function upgradeRoom(
|
||||
handleError = true,
|
||||
updateSpaces = true,
|
||||
awaitRoom = false,
|
||||
progressCallback?: (progress: IProgress) => void,
|
||||
progressCallback?: (progress: RoomUpgradeProgress) => void,
|
||||
inhibitInviteProgressDialog = false,
|
||||
): Promise<string> {
|
||||
const cli = room.client;
|
||||
let spinnerModal: IHandle<any> | undefined;
|
||||
@@ -77,7 +79,7 @@ export async function upgradeRoom(
|
||||
) as Room[];
|
||||
}
|
||||
|
||||
const progress: IProgress = {
|
||||
const progress: RoomUpgradeProgress = {
|
||||
roomUpgraded: false,
|
||||
roomSynced: awaitRoom || inviteUsers ? false : undefined,
|
||||
inviteUsersProgress: inviteUsers ? 0 : undefined,
|
||||
@@ -112,9 +114,12 @@ export async function upgradeRoom(
|
||||
|
||||
if (toInvite.length > 0) {
|
||||
// Errors are handled internally to this function
|
||||
await inviteUsersToRoom(cli, newRoomId, toInvite, () => {
|
||||
progress.inviteUsersProgress!++;
|
||||
progressCallback?.(progress);
|
||||
await inviteUsersToRoom(cli, newRoomId, toInvite, {
|
||||
progressCallback: () => {
|
||||
progress.inviteUsersProgress!++;
|
||||
progressCallback?.(progress);
|
||||
},
|
||||
inhibitProgressDialog: inhibitInviteProgressDialog,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -150,9 +155,9 @@ async function inviteUsersToRoom(
|
||||
client: MatrixClient,
|
||||
roomId: string,
|
||||
userIds: string[],
|
||||
progressCallback?: () => void,
|
||||
inviteOptions: MultiInviterOptions,
|
||||
): Promise<void> {
|
||||
const result = await inviteMultipleToRoom(client, roomId, userIds, { progressCallback });
|
||||
const result = await inviteMultipleToRoom(client, roomId, userIds, inviteOptions);
|
||||
const room = client.getRoom(roomId)!;
|
||||
showAnyInviteErrors(result.states, room, result.inviter);
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ describe("InviteDialog", () => {
|
||||
supportsThreads: jest.fn().mockReturnValue(false),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(true),
|
||||
getClientWellKnown: jest.fn().mockResolvedValue({}),
|
||||
invite: jest.fn(),
|
||||
});
|
||||
SdkConfig.put({ validated_server_config: {} as ValidatedServerConfig } as IConfigOptions);
|
||||
DMRoomMap.makeShared(mockClient);
|
||||
@@ -406,6 +407,18 @@ describe("InviteDialog", () => {
|
||||
expect(tile).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("while the invite is in progress", () => {
|
||||
it("should show a spinner", async () => {
|
||||
mockClient.invite.mockReturnValue(new Promise(() => {}));
|
||||
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
|
||||
await enterIntoSearchField(bobId);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Invite" }));
|
||||
|
||||
await screen.findByText("Preparing invitations...");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when inviting a user with an unknown profile", () => {
|
||||
beforeEach(async () => {
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import InviteProgressBody from "../../../../../src/components/views/dialogs/InviteProgressBody.tsx";
|
||||
|
||||
describe("InviteProgressBody", () => {
|
||||
it("should match snapshot", () => {
|
||||
const { asFragment } = render(<InviteProgressBody />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`InviteProgressBody should match snapshot 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_InviteProgressBody"
|
||||
>
|
||||
<div
|
||||
class="mx_InlineSpinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_InlineSpinner_icon mx_Spinner_icon"
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
</div>
|
||||
<h1>
|
||||
Preparing invitations...
|
||||
</h1>
|
||||
Do not close the app until finished.
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -15,7 +15,7 @@ import Modal, { type ComponentType, type ComponentProps } from "../../../src/Mod
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import MultiInviter, { type CompletionStates } from "../../../src/utils/MultiInviter";
|
||||
import * as TestUtilsMatrix from "../../test-utils";
|
||||
import type AskInviteAnywayDialog from "../../../src/components/views/dialogs/AskInviteAnywayDialog";
|
||||
import AskInviteAnywayDialog from "../../../src/components/views/dialogs/AskInviteAnywayDialog";
|
||||
import ConfirmUserActionDialog from "../../../src/components/views/dialogs/ConfirmUserActionDialog";
|
||||
|
||||
const ROOMID = "!room:server";
|
||||
@@ -24,10 +24,14 @@ const MXID1 = "@user1:server";
|
||||
const MXID2 = "@user2:server";
|
||||
const MXID3 = "@user3:server";
|
||||
|
||||
const MXID_PROFILE_STATES: Record<string, Promise<any>> = {
|
||||
[MXID1]: Promise.resolve({}),
|
||||
[MXID2]: Promise.reject(new MatrixError({ errcode: "M_FORBIDDEN" })),
|
||||
[MXID3]: Promise.reject(new MatrixError({ errcode: "M_NOT_FOUND" })),
|
||||
const MXID_PROFILE_STATES: Record<string, () => {}> = {
|
||||
[MXID1]: () => ({}),
|
||||
[MXID2]: () => {
|
||||
throw new MatrixError({ errcode: "M_FORBIDDEN" });
|
||||
},
|
||||
[MXID3]: () => {
|
||||
throw new MatrixError({ errcode: "M_NOT_FOUND" });
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock("../../../src/Modal", () => ({
|
||||
@@ -51,11 +55,12 @@ const mockPromptBeforeInviteUnknownUsers = (value: boolean) => {
|
||||
};
|
||||
|
||||
const mockCreateTrackedDialog = (callbackName: "onInviteAnyways" | "onGiveUp") => {
|
||||
mocked(Modal.createDialog).mockImplementation(
|
||||
(Element: ComponentType, props?: ComponentProps<ComponentType>): any => {
|
||||
mocked(Modal.createDialog).mockImplementation((Element: ComponentType, props?: ComponentProps<ComponentType>) => {
|
||||
if (Element === AskInviteAnywayDialog) {
|
||||
(props as ComponentProps<typeof AskInviteAnywayDialog>)[callbackName]();
|
||||
},
|
||||
);
|
||||
}
|
||||
return { close: jest.fn(), finished: new Promise(() => {}) };
|
||||
});
|
||||
};
|
||||
|
||||
const expectAllInvitedResult = (result: CompletionStates) => {
|
||||
@@ -72,6 +77,7 @@ describe("MultiInviter", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
mocked(Modal.createDialog).mockReturnValue({ close: jest.fn(), finished: new Promise(() => {}) });
|
||||
|
||||
TestUtilsMatrix.stubClient();
|
||||
client = MatrixClientPeg.safeGet() as jest.Mocked<MatrixClient>;
|
||||
@@ -80,8 +86,10 @@ describe("MultiInviter", () => {
|
||||
client.invite.mockResolvedValue({});
|
||||
|
||||
client.getProfileInfo = jest.fn();
|
||||
client.getProfileInfo.mockImplementation((userId: string) => {
|
||||
return MXID_PROFILE_STATES[userId] || Promise.reject();
|
||||
client.getProfileInfo.mockImplementation(async (userId: string) => {
|
||||
const m = MXID_PROFILE_STATES[userId];
|
||||
if (m) return m();
|
||||
throw new Error();
|
||||
});
|
||||
client.unban = jest.fn();
|
||||
|
||||
@@ -89,6 +97,22 @@ describe("MultiInviter", () => {
|
||||
});
|
||||
|
||||
describe("invite", () => {
|
||||
it("should show a progress dialog while the invite happens", async () => {
|
||||
const mockModalHandle = { close: jest.fn(), finished: new Promise<[]>(() => {}) };
|
||||
mocked(Modal.createDialog).mockReturnValue(mockModalHandle);
|
||||
|
||||
const invitePromise = Promise.withResolvers<{}>();
|
||||
client.invite.mockReturnValue(invitePromise.promise);
|
||||
|
||||
const resultPromise = inviter.invite([MXID1]);
|
||||
expect(Modal.createDialog).toHaveBeenCalledTimes(1);
|
||||
expect(mockModalHandle.close).not.toHaveBeenCalled();
|
||||
|
||||
invitePromise.resolve({});
|
||||
await resultPromise;
|
||||
expect(mockModalHandle.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("with promptBeforeInviteUnknownUsers = false", () => {
|
||||
beforeEach(() => mockPromptBeforeInviteUnknownUsers(false));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user