From 8ac77c498f76f871648aad0a4c4589a5a61fc1ed Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 30 Sep 2021 13:43:59 +0100 Subject: [PATCH] Add progress bar to Community to Space migration tool and invite-one-by-one to workaround Synapse ratelimits --- .../dialogs/_AddExistingToSpaceDialog.scss | 2 +- .../_CreateSpaceFromCommunityDialog.scss | 1 + src/RoomInvite.tsx | 13 ++-- .../CreateSpaceFromCommunityDialog.tsx | 71 ++++++++++++++++--- src/i18n/strings/en_EN.json | 2 + src/utils/MultiInviter.ts | 4 +- 6 files changed, 78 insertions(+), 15 deletions(-) diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index 444b29c9bf8..8b19f506f54 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -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; diff --git a/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss b/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss index 6ff328f6abf..f1af24cc5fb 100644 --- a/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss +++ b/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss @@ -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; diff --git a/src/RoomInvite.tsx b/src/RoomInvite.tsx index 7d093f40929..5c9d96f5093 100644 --- a/src/RoomInvite.tsx +++ b/src/RoomInvite.tsx @@ -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 { - const inviter = new MultiInviter(roomId); +export function inviteMultipleToRoom( + roomId: string, + addresses: string[], + progressCallback?: () => void, +): Promise { + const inviter = new MultiInviter(roomId, progressCallback); return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter })); } @@ -104,8 +109,8 @@ export function isValid3pidInvite(event: MatrixEvent): boolean { return true; } -export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise { - return inviteMultipleToRoom(roomId, userIds).then((result) => { +export function inviteUsersToRoom(roomId: string, userIds: string[], progressCallback?: () => void): Promise { + return inviteMultipleToRoom(roomId, userIds, progressCallback).then((result) => { const room = MatrixClientPeg.get().getRoom(roomId); showAnyInviteErrors(result.states, room, result.inviter); }).catch((err) => { diff --git a/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx b/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx index e74082427f5..c7706c115cb 100644 --- a/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx +++ b/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx @@ -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; @@ -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 = ({ matrixClient: cli, groupId, onFinished }) => { const [loading, setLoading] = useState(true); const [error, setError] = useState(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(null); // undefined means to remove avatar const [name, setName] = useState(""); @@ -122,30 +136,34 @@ const CreateSpaceFromCommunityDialog: React.FC = ({ 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, cli.getGroupUsers(groupId).then(parseMembersResponse) as Promise, cli.getGroupInvitedUsers(groupId).then(parseMembersResponse) as Promise, ]); + setNumInvites(members.length + invitedMembers.length); + const viaMap = new Map(); for (const { roomId, canonicalAlias } of rooms) { const room = cli.getRoom(roomId); @@ -167,6 +185,8 @@ const CreateSpaceFromCommunityDialog: React.FC = ({ 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: { @@ -179,11 +199,16 @@ const CreateSpaceFromCommunityDialog: React.FC = ({ 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)); @@ -250,7 +275,7 @@ const CreateSpaceFromCommunityDialog: React.FC = ({ matrixClient: cli, g setError(e); } - setBusy(false); + setProgress(Progress.NotStarted); }; let footer; @@ -267,13 +292,41 @@ const CreateSpaceFromCommunityDialog: React.FC = ({ matrixClient: cli, g { _t("Retry") } ; + } 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 = + Progress.FetchingData ? progress : 0} + max={numInvites + Progress.InvitingUsers} + /> +
+ { description } +
+
; } else { footer = <> - onFinished()}> + onFinished()}> { _t("Cancel") } - - { busy ? _t("Creating...") : _t("Create Space") } + + { _t("Create Space") } ; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9f245b8dab4..8fc5fd0afd2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2280,6 +2280,8 @@ " has been made and everyone who was a part of the community has been invited to it.": " 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.", diff --git a/src/utils/MultiInviter.ts b/src/utils/MultiInviter.ts index 5b79a2ff93d..abf72c97ff5 100644 --- a/src/utils/MultiInviter.ts +++ b/src/utils/MultiInviter.ts @@ -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; @@ -181,6 +182,7 @@ export default class MultiInviter { delete this.errors[address]; resolve(); + this.progressCallback?.(); }).catch((err) => { if (this.canceled) { return;