Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What actually changed here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

heh, great question. Here's the diff:

image

Looks like a sub-pixel difference in some of the text.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
15 changes: 4 additions & 11 deletions res/css/views/dialogs/_InviteDialog.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
16 changes: 16 additions & 0 deletions res/css/views/dialogs/_InviteProgressBody.pcss
Original file line number Diff line number Diff line change
@@ -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);
}
}
27 changes: 14 additions & 13 deletions src/components/views/dialogs/InviteDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 */
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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>
);
}
Expand Down
24 changes: 24 additions & 0 deletions src/components/views/dialogs/InviteProgressBody.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 42 additions & 0 deletions src/components/views/dialogs/InviteProgressDialog.tsx
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to have a comment here explaining that this is only added because Modal.createDialog expects this component to have this prop.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it's not needed at all. I'll get rid of it.

}

/** A Modal dialog that pops up while room invites are being sent. */
const InviteProgressDialog: React.FC<Props> = (props) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const InviteProgressDialog: React.FC<Props> = (props) => {
const InviteProgressDialog: React.FC<Props> = (_) => {

To make it clear that this is intentionally left unused.

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;
}
19 changes: 16 additions & 3 deletions src/components/views/settings/JoinRuleSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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?.();

Expand Down
4 changes: 4 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
83 changes: 50 additions & 33 deletions src/utils/MultiInviter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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);
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;
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 (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;
Expand Down
Loading
Loading