mirror of https://github.com/vector-im/riot-web
				
				
				
			Add progress bar to Community to Space migration tool
and invite-one-by-one to workaround Synapse ratelimitspull/21833/head
							parent
							
								
									5eaf0e7e25
								
							
						
					
					
						commit
						8ac77c498f
					
				| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 }));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<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("");
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			@ -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: {
 | 
			
		||||
| 
						 | 
				
			
			@ -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));
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -250,7 +275,7 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
 | 
			
		|||
            setError(e);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setBusy(false);
 | 
			
		||||
        setProgress(Progress.NotStarted);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let footer;
 | 
			
		||||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
        </>;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue