/* Copyright 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import React, { useEffect, useRef, useState } from "react"; import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from '../../../languageHandler'; import BaseDialog from "./BaseDialog"; import AccessibleButton from "../elements/AccessibleButton"; import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu"; import JoinRuleDropdown from "../elements/JoinRuleDropdown"; import Field from "../elements/Field"; import RoomAliasField from "../elements/RoomAliasField"; import { GroupMember } from "../right_panel/UserInfo"; import { parseMembersResponse, parseRoomsResponse } from "../../../stores/GroupStore"; import { calculateRoomVia, makeRoomPermalink } from "../../../utils/permalinks/Permalinks"; import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; import Spinner from "../elements/Spinner"; import { mediaFromMxc } from "../../../customisations/Media"; import SpaceStore from "../../../stores/spaces/SpaceStore"; import Modal from "../../../Modal"; import InfoDialog from "./InfoDialog"; 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"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; interface IProps { matrixClient: MatrixClient; groupId: string; onFinished(spaceId?: string): void; } export const CreateEventField = "io.element.migrated_from_community"; interface IGroupRoom { displayname: string; name?: string; roomId: string; canonicalAlias?: string; avatarUrl?: string; topic?: string; numJoinedMembers?: number; worldReadable?: boolean; guestCanJoin?: boolean; isPublic?: boolean; } /* eslint-disable camelcase */ export interface IGroupSummary { profile: { avatar_url?: string; is_openly_joinable?: boolean; is_public?: boolean; long_description: string; name: string; short_description: string; }; rooms_section: { rooms: unknown[]; categories: Record; total_room_count_estimate: number; }; user: { is_privileged: boolean; is_public: boolean; is_publicised: boolean; membership: string; }; users_section: { users: unknown[]; roles: Record; total_user_count_estimate: number; }; } /* 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 [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(""); const spaceNameField = useRef(); const [alias, setAlias] = useState("#" + groupId.substring(1, groupId.indexOf(":")) + ":" + cli.getDomain()); const spaceAliasField = useRef(); const [topic, setTopic] = useState(""); const [joinRule, setJoinRule] = useState(JoinRule.Public); const groupSummary = useAsyncMemo(() => cli.getGroupSummary(groupId), [groupId]); useEffect(() => { if (groupSummary) { setName(groupSummary.profile.name || ""); setTopic(groupSummary.profile.short_description || ""); setJoinRule(groupSummary.profile.is_openly_joinable ? JoinRule.Public : JoinRule.Invite); setLoading(false); } }, [groupSummary]); if (loading) { return ; } const onCreateSpaceClick = async (e) => { e.preventDefault(); if (busy) return; setError(null); setProgress(Progress.ValidatingInputs); // require & validate the space name field if (!(await spaceNameField.current.validate({ allowEmpty: 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 }))) { 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); if (room) { viaMap.set(roomId, calculateRoomVia(room)); } else if (canonicalAlias) { try { const { servers } = await cli.getRoomIdForAlias(canonicalAlias); viaMap.set(roomId, servers); } catch (e) { logger.warn("Failed to resolve alias during community migration", e); } } if (!viaMap.get(roomId)?.length) { // XXX: lets guess the via, this might end up being incorrect. const str = canonicalAlias || roomId; viaMap.set(roomId, [str.substring(1, str.indexOf(":"))]); } } 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: { [CreateEventField]: groupId, }, initial_state: rooms.map(({ roomId }) => ({ type: EventType.SpaceChild, state_key: roomId, content: { via: viaMap.get(roomId) || [], }, })), // 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, false, () => setProgress(p => p + 1)); // eagerly remove it from the community panel dis.dispatch(TagOrderActions.removeTag(cli, groupId)); // don't bother awaiting this, as we don't hugely care if it fails cli.setGroupProfile(groupId, { ...groupSummary.profile, long_description: `

` + _t("This community has been upgraded into a Space") + `


` + groupSummary.profile.long_description, } as IGroupSummary["profile"]).catch(e => { logger.warn("Failed to update community profile during migration", e); }); onFinished(roomId); const onSpaceClick = () => { dis.dispatch({ action: Action.ViewRoom, room_id: roomId, _trigger: undefined, // other }); }; const onPreferencesClick = () => { dis.dispatch({ action: Action.ViewUserSettings, initialTabId: UserTab.Preferences, }); }; let spacesDisabledCopy; if (!SpaceStore.spacesEnabled) { spacesDisabledCopy = _t("To view Spaces, hide communities in Preferences", {}, { a: sub => { sub }, }); } Modal.createDialog(InfoDialog, { title: _t("Space created"), description: <>

{ _t(" has been made and everyone who was a part of the community has " + "been invited to it.", {}, { SpaceName: () => { name } , }) }   { spacesDisabledCopy }

{ _t("To create a Space from another community, just pick the community in Preferences.") }

, button: _t("Preferences"), onFinished: (openPreferences: boolean) => { if (openPreferences) { onPreferencesClick(); } }, }, "mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog"); } catch (e) { logger.error(e); setError(e); } setProgress(Progress.NotStarted); }; let footer; if (error) { footer = <>
{ _t("Failed to migrate community") }
{ _t("Try again") }
{ _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()}> { _t("Cancel") } { _t("Create Space") } ; } return

{ _t("A link to the Space will be put in your community description.") }   { _t("All rooms will be added and all community members will be invited.") }

{ _t("Flair won't be available in Spaces for the foreseeable future.") }

{ _t("This description will be shown to people when they view your space") }

{ joinRule === JoinRule.Public ? _t("Open space for anyone, best for communities") : _t("Invite only, best for yourself or teams") }

{ joinRule !== JoinRule.Public &&
}
{ footer }
; }; export default CreateSpaceFromCommunityDialog;