Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add progress bar to Community to Space migration tool #6887

Merged
merged 1 commit into from
Oct 6, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion res/css/views/dialogs/_AddExistingToSpaceDialog.scss
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ limitations under the License.
@mixin ProgressBarBorderRadius 8px;
}

.mx_AddExistingToSpace_progressText {
.mx_AddExistingToSpaceDialog_progressText {
margin-top: 8px;
font-size: $font-15px;
line-height: $font-24px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ limitations under the License.
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-content;
margin-top: -13px; // match height of buttons to prevent height changing

.mx_ProgressBar {
height: 8px;
Expand Down
13 changes: 9 additions & 4 deletions src/RoomInvite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,15 @@ export interface IInviteResult {
*
* @param {string} roomId The ID of the room to invite to
* @param {string[]} addresses Array of strings of addresses to invite. May be matrix IDs or 3pids.
* @param {function} progressCallback optional callback, fired after each invite.
* @returns {Promise} Promise
*/
export function inviteMultipleToRoom(roomId: string, addresses: string[]): Promise<IInviteResult> {
const inviter = new MultiInviter(roomId);
export function inviteMultipleToRoom(
roomId: string,
addresses: string[],
progressCallback?: () => void,
): Promise<IInviteResult> {
const inviter = new MultiInviter(roomId, progressCallback);
return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter }));
}

Expand Down Expand Up @@ -104,8 +109,8 @@ export function isValid3pidInvite(event: MatrixEvent): boolean {
return true;
}

export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise<void> {
return inviteMultipleToRoom(roomId, userIds).then((result) => {
export function inviteUsersToRoom(roomId: string, userIds: string[], progressCallback?: () => void): Promise<void> {
return inviteMultipleToRoom(roomId, userIds, progressCallback).then((result) => {
const room = MatrixClientPeg.get().getRoom(roomId);
showAnyInviteErrors(result.states, room, result.inviter);
}).catch((err) => {
Expand Down
71 changes: 62 additions & 9 deletions src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "./UserSettingsDialog";
import TagOrderActions from "../../../actions/TagOrderActions";
import { inviteUsersToRoom } from "../../../RoomInvite";
import ProgressBar from "../elements/ProgressBar";

interface IProps {
matrixClient: MatrixClient;
Expand Down Expand Up @@ -90,10 +92,22 @@ export interface IGroupSummary {
}
/* eslint-enable camelcase */

enum Progress {
NotStarted,
ValidatingInputs,
FetchingData,
CreatingSpace,
InvitingUsers,
// anything beyond here is inviting user n - 4
}

const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, groupId, onFinished }) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>(null);
const [busy, setBusy] = useState(false);

const [progress, setProgress] = useState(Progress.NotStarted);
const [numInvites, setNumInvites] = useState(0);
const busy = progress > 0;

const [avatar, setAvatar] = useState<File>(null); // undefined means to remove avatar
const [name, setName] = useState("");
Expand Down Expand Up @@ -122,30 +136,34 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
if (busy) return;

setError(null);
setBusy(true);
setProgress(Progress.ValidatingInputs);

// require & validate the space name field
if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
setBusy(false);
setProgress(0);
spaceNameField.current.focus();
spaceNameField.current.validate({ allowEmpty: false, focused: true });
return;
}
// validate the space name alias field but do not require it
if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
setBusy(false);
setProgress(0);
spaceAliasField.current.focus();
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
return;
}

try {
setProgress(Progress.FetchingData);

const [rooms, members, invitedMembers] = await Promise.all([
cli.getGroupRooms(groupId).then(parseRoomsResponse) as Promise<IGroupRoom[]>,
cli.getGroupUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
cli.getGroupInvitedUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
]);

setNumInvites(members.length + invitedMembers.length);

const viaMap = new Map<string, string[]>();
for (const { roomId, canonicalAlias } of rooms) {
const room = cli.getRoom(roomId);
Expand All @@ -167,6 +185,8 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
}
}

setProgress(Progress.CreatingSpace);

const spaceAvatar = avatar !== undefined ? avatar : groupSummary.profile.avatar_url;
const roomId = await createSpace(name, joinRule === JoinRule.Public, alias, topic, spaceAvatar, {
creation_content: {
Expand All @@ -179,11 +199,16 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
via: viaMap.get(roomId) || [],
},
})),
invite: [...members, ...invitedMembers].map(m => m.userId).filter(m => m !== cli.getUserId()),
// we do not specify the inviters here because Synapse applies a limit and this may cause it to trip
}, {
andView: false,
});

setProgress(Progress.InvitingUsers);

const userIds = [...members, ...invitedMembers].map(m => m.userId).filter(m => m !== cli.getUserId());
await inviteUsersToRoom(roomId, userIds, () => setProgress(p => p + 1));

// eagerly remove it from the community panel
dis.dispatch(TagOrderActions.removeTag(cli, groupId));

Expand Down Expand Up @@ -250,7 +275,7 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
setError(e);
}

setBusy(false);
setProgress(Progress.NotStarted);
};

let footer;
Expand All @@ -267,13 +292,41 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
{ _t("Retry") }
</AccessibleButton>
</>;
} else if (busy) {
let description: string;
switch (progress) {
case Progress.ValidatingInputs:
case Progress.FetchingData:
description = _t("Fetching data...");
break;
case Progress.CreatingSpace:
description = _t("Creating Space...");
break;
case Progress.InvitingUsers:
default:
description = _t("Adding rooms... (%(progress)s out of %(count)s)", {
count: numInvites,
progress,
});
break;
}

footer = <span>
<ProgressBar
value={progress > Progress.FetchingData ? progress : 0}
max={numInvites + Progress.InvitingUsers}
/>
<div className="mx_CreateSpaceFromCommunityDialog_progressText">
{ description }
</div>
</span>;
} else {
footer = <>
<AccessibleButton kind="primary_outline" disabled={busy} onClick={() => onFinished()}>
<AccessibleButton kind="primary_outline" onClick={() => onFinished()}>
{ _t("Cancel") }
</AccessibleButton>
<AccessibleButton kind="primary" disabled={busy} onClick={onCreateSpaceClick}>
{ busy ? _t("Creating...") : _t("Create Space") }
<AccessibleButton kind="primary" onClick={onCreateSpaceClick}>
{ _t("Create Space") }
</AccessibleButton>
</>;
}
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2280,6 +2280,8 @@
"<SpaceName/> has been made and everyone who was a part of the community has been invited to it.": "<SpaceName/> has been made and everyone who was a part of the community has been invited to it.",
"To create a Space from another community, just pick the community in Preferences.": "To create a Space from another community, just pick the community in Preferences.",
"Failed to migrate community": "Failed to migrate community",
"Fetching data...": "Fetching data...",
"Creating Space...": "Creating Space...",
"Create Space from community": "Create Space from community",
"A link to the Space will be put in your community description.": "A link to the Space will be put in your community description.",
"All rooms will be added and all community members will be invited.": "All rooms will be added and all community members will be invited.",
Expand Down
4 changes: 3 additions & 1 deletion src/utils/MultiInviter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ export default class MultiInviter {

/**
* @param {string} targetId The ID of the room or group to invite to
* @param {function} progressCallback optional callback, fired after each invite.
*/
constructor(targetId: string) {
constructor(targetId: string, private readonly progressCallback?: () => void) {
if (targetId[0] === '+') {
this.roomId = null;
this.groupId = targetId;
Expand Down Expand Up @@ -181,6 +182,7 @@ export default class MultiInviter {
delete this.errors[address];

resolve();
this.progressCallback?.();
}).catch((err) => {
if (this.canceled) {
return;
Expand Down