Add progress bar to Community to Space migration tool
and invite-one-by-one to workaround Synapse ratelimits
This commit is contained in:
parent
5eaf0e7e25
commit
8ac77c498f
6 changed files with 78 additions and 15 deletions
|
@ -75,7 +75,7 @@ limitations under the License.
|
||||||
@mixin ProgressBarBorderRadius 8px;
|
@mixin ProgressBarBorderRadius 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AddExistingToSpace_progressText {
|
.mx_AddExistingToSpaceDialog_progressText {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
font-size: $font-15px;
|
font-size: $font-15px;
|
||||||
line-height: $font-24px;
|
line-height: $font-24px;
|
||||||
|
|
|
@ -74,6 +74,7 @@ limitations under the License.
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
line-height: $font-15px;
|
line-height: $font-15px;
|
||||||
color: $secondary-content;
|
color: $secondary-content;
|
||||||
|
margin-top: -13px; // match height of buttons to prevent height changing
|
||||||
|
|
||||||
.mx_ProgressBar {
|
.mx_ProgressBar {
|
||||||
height: 8px;
|
height: 8px;
|
||||||
|
|
|
@ -42,10 +42,15 @@ export interface IInviteResult {
|
||||||
*
|
*
|
||||||
* @param {string} roomId The ID of the room to invite to
|
* @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 {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
|
* @returns {Promise} Promise
|
||||||
*/
|
*/
|
||||||
export function inviteMultipleToRoom(roomId: string, addresses: string[]): Promise<IInviteResult> {
|
export function inviteMultipleToRoom(
|
||||||
const inviter = new MultiInviter(roomId);
|
roomId: string,
|
||||||
|
addresses: string[],
|
||||||
|
progressCallback?: () => void,
|
||||||
|
): Promise<IInviteResult> {
|
||||||
|
const inviter = new MultiInviter(roomId, progressCallback);
|
||||||
return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter }));
|
return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,8 +109,8 @@ export function isValid3pidInvite(event: MatrixEvent): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise<void> {
|
export function inviteUsersToRoom(roomId: string, userIds: string[], progressCallback?: () => void): Promise<void> {
|
||||||
return inviteMultipleToRoom(roomId, userIds).then((result) => {
|
return inviteMultipleToRoom(roomId, userIds, progressCallback).then((result) => {
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
showAnyInviteErrors(result.states, room, result.inviter);
|
showAnyInviteErrors(result.states, room, result.inviter);
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
|
|
|
@ -39,6 +39,8 @@ import dis from "../../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { UserTab } from "./UserSettingsDialog";
|
import { UserTab } from "./UserSettingsDialog";
|
||||||
import TagOrderActions from "../../../actions/TagOrderActions";
|
import TagOrderActions from "../../../actions/TagOrderActions";
|
||||||
|
import { inviteUsersToRoom } from "../../../RoomInvite";
|
||||||
|
import ProgressBar from "../elements/ProgressBar";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
matrixClient: MatrixClient;
|
matrixClient: MatrixClient;
|
||||||
|
@ -90,10 +92,22 @@ export interface IGroupSummary {
|
||||||
}
|
}
|
||||||
/* eslint-enable camelcase */
|
/* 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 CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, groupId, onFinished }) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string>(null);
|
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 [avatar, setAvatar] = useState<File>(null); // undefined means to remove avatar
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
|
@ -122,30 +136,34 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
||||||
if (busy) return;
|
if (busy) return;
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
setBusy(true);
|
setProgress(Progress.ValidatingInputs);
|
||||||
|
|
||||||
// require & validate the space name field
|
// require & validate the space name field
|
||||||
if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
|
if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
|
||||||
setBusy(false);
|
setProgress(0);
|
||||||
spaceNameField.current.focus();
|
spaceNameField.current.focus();
|
||||||
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// validate the space name alias field but do not require it
|
// validate the space name alias field but do not require it
|
||||||
if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
|
if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
|
||||||
setBusy(false);
|
setProgress(0);
|
||||||
spaceAliasField.current.focus();
|
spaceAliasField.current.focus();
|
||||||
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setProgress(Progress.FetchingData);
|
||||||
|
|
||||||
const [rooms, members, invitedMembers] = await Promise.all([
|
const [rooms, members, invitedMembers] = await Promise.all([
|
||||||
cli.getGroupRooms(groupId).then(parseRoomsResponse) as Promise<IGroupRoom[]>,
|
cli.getGroupRooms(groupId).then(parseRoomsResponse) as Promise<IGroupRoom[]>,
|
||||||
cli.getGroupUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
cli.getGroupUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||||
cli.getGroupInvitedUsers(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[]>();
|
const viaMap = new Map<string, string[]>();
|
||||||
for (const { roomId, canonicalAlias } of rooms) {
|
for (const { roomId, canonicalAlias } of rooms) {
|
||||||
const room = cli.getRoom(roomId);
|
const room = cli.getRoom(roomId);
|
||||||
|
@ -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 spaceAvatar = avatar !== undefined ? avatar : groupSummary.profile.avatar_url;
|
||||||
const roomId = await createSpace(name, joinRule === JoinRule.Public, alias, topic, spaceAvatar, {
|
const roomId = await createSpace(name, joinRule === JoinRule.Public, alias, topic, spaceAvatar, {
|
||||||
creation_content: {
|
creation_content: {
|
||||||
|
@ -179,11 +199,16 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
||||||
via: viaMap.get(roomId) || [],
|
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,
|
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
|
// eagerly remove it from the community panel
|
||||||
dis.dispatch(TagOrderActions.removeTag(cli, groupId));
|
dis.dispatch(TagOrderActions.removeTag(cli, groupId));
|
||||||
|
|
||||||
|
@ -250,7 +275,7 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
||||||
setError(e);
|
setError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
setBusy(false);
|
setProgress(Progress.NotStarted);
|
||||||
};
|
};
|
||||||
|
|
||||||
let footer;
|
let footer;
|
||||||
|
@ -267,13 +292,41 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
||||||
{ _t("Retry") }
|
{ _t("Retry") }
|
||||||
</AccessibleButton>
|
</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 {
|
} else {
|
||||||
footer = <>
|
footer = <>
|
||||||
<AccessibleButton kind="primary_outline" disabled={busy} onClick={() => onFinished()}>
|
<AccessibleButton kind="primary_outline" onClick={() => onFinished()}>
|
||||||
{ _t("Cancel") }
|
{ _t("Cancel") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton kind="primary" disabled={busy} onClick={onCreateSpaceClick}>
|
<AccessibleButton kind="primary" onClick={onCreateSpaceClick}>
|
||||||
{ busy ? _t("Creating...") : _t("Create Space") }
|
{ _t("Create Space") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.",
|
"<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.",
|
"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",
|
"Failed to migrate community": "Failed to migrate community",
|
||||||
|
"Fetching data...": "Fetching data...",
|
||||||
|
"Creating Space...": "Creating Space...",
|
||||||
"Create Space from community": "Create Space from community",
|
"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.",
|
"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.",
|
"All rooms will be added and all community members will be invited.": "All rooms will be added and all community members will be invited.",
|
||||||
|
|
|
@ -62,8 +62,9 @@ export default class MultiInviter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} targetId The ID of the room or group to invite to
|
* @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] === '+') {
|
if (targetId[0] === '+') {
|
||||||
this.roomId = null;
|
this.roomId = null;
|
||||||
this.groupId = targetId;
|
this.groupId = targetId;
|
||||||
|
@ -181,6 +182,7 @@ export default class MultiInviter {
|
||||||
delete this.errors[address];
|
delete this.errors[address];
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
|
this.progressCallback?.();
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
if (this.canceled) {
|
if (this.canceled) {
|
||||||
return;
|
return;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue