diff --git a/res/css/_components.scss b/res/css/_components.scss index e1e6b607df..035caec36a 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -75,6 +75,7 @@ @import "./views/dialogs/_CreateCommunityPrototypeDialog.scss"; @import "./views/dialogs/_CreateGroupDialog.scss"; @import "./views/dialogs/_CreateRoomDialog.scss"; +@import "./views/dialogs/_CreateSpaceFromCommunityDialog.scss"; @import "./views/dialogs/_CreateSubspaceDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss"; diff --git a/res/css/structures/_GroupView.scss b/res/css/structures/_GroupView.scss index 60f9ebdd08..fb660f4194 100644 --- a/res/css/structures/_GroupView.scss +++ b/res/css/structures/_GroupView.scss @@ -368,6 +368,65 @@ limitations under the License. padding: 40px 20px; } +.mx_GroupView_spaceUpgradePrompt { + padding: 16px 50px; + background-color: $header-panel-bg-color; + border-radius: 8px; + max-width: 632px; + font-size: $font-15px; + line-height: $font-24px; + margin-top: 24px; + position: relative; + + > h2 { + font-size: inherit; + font-weight: $font-semi-bold; + } + + > p, h2 { + margin: 0; + } + + &::before { + content: ""; + position: absolute; + height: $font-24px; + width: 20px; + left: 18px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + background-color: $secondary-fg-color; + } + + .mx_AccessibleButton_kind_link { + padding: 0; + } + + .mx_GroupView_spaceUpgradePrompt_close { + width: 16px; + height: 16px; + border-radius: 8px; + background-color: $input-darker-bg-color; + position: absolute; + top: 16px; + right: 16px; + + &::before { + content: ""; + position: absolute; + width: inherit; + height: inherit; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 8px; + mask-image: url('$(res)/img/image-view/close.svg'); + background-color: $secondary-fg-color; + } + } +} + .mx_GroupView .mx_MemberInfo .mx_AutoHideScrollbar > :not(.mx_MemberInfo_avatar) { padding-left: 16px; padding-right: 16px; diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 58a4b426c2..945de01eba 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -180,6 +180,18 @@ $SpaceRoomViewInnerWidth: 428px; } } + .mx_SpaceRoomView_preview_migratedCommunity { + margin-bottom: 16px; + padding: 8px 12px; + border-radius: 8px; + border: 1px solid $input-border-color; + width: max-content; + + .mx_BaseAvatar { + margin-right: 4px; + } + } + .mx_SpaceRoomView_preview_inviter { display: flex; align-items: center; @@ -342,7 +354,7 @@ $SpaceRoomViewInnerWidth: 428px; .mx_SpaceFeedbackPrompt { padding: 7px; // 8px - 1px border - border: 1px solid $menu-border-color; + border: 1px solid rgba($primary-fg-color, .1); border-radius: 8px; width: max-content; margin: 0 0 -40px auto; // collapse its own height to not push other components down diff --git a/res/css/views/context_menus/_TagTileContextMenu.scss b/res/css/views/context_menus/_TagTileContextMenu.scss index d707f4ce7c..14f5ec817e 100644 --- a/res/css/views/context_menus/_TagTileContextMenu.scss +++ b/res/css/views/context_menus/_TagTileContextMenu.scss @@ -51,6 +51,10 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/hide.svg'); } +.mx_TagTileContextMenu_createSpace::before { + mask-image: url('$(res)/img/element-icons/message/fwd.svg'); +} + .mx_TagTileContextMenu_separator { margin-top: 0; margin-bottom: 0; diff --git a/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss b/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss new file mode 100644 index 0000000000..afa722e05e --- /dev/null +++ b/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss @@ -0,0 +1,187 @@ +/* +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. +*/ + +.mx_CreateSpaceFromCommunityDialog_wrapper { + .mx_Dialog { + display: flex; + flex-direction: column; + } +} + +.mx_CreateSpaceFromCommunityDialog { + width: 480px; + color: $primary-fg-color; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-height: 0; + + .mx_CreateSpaceFromCommunityDialog_content { + > p { + font-size: $font-15px; + line-height: $font-24px; + + &:first-of-type { + margin-top: 0; + } + + &.mx_CreateSpaceFromCommunityDialog_flairNotice { + font-size: $font-12px; + line-height: $font-15px; + } + } + + .mx_SpaceBasicSettings { + > p { + font-size: $font-12px; + line-height: $font-15px; + margin: 16px 0; + } + + .mx_Field_textarea { + margin-bottom: 0; + } + } + + .mx_JoinRuleDropdown .mx_Dropdown_menu { + width: auto !important; // override fixed width + } + + .mx_CreateSpaceFromCommunityDialog_nonPublicSpacer { + height: 63px; // balance the height of the missing room alias field to prevent modal bouncing + } + } + + .mx_CreateSpaceFromCommunityDialog_footer { + display: flex; + margin-top: 20px; + + > span { + flex-grow: 1; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + + .mx_ProgressBar { + height: 8px; + width: 100%; + + @mixin ProgressBarBorderRadius 8px; + } + + .mx_CreateSpaceFromCommunityDialog_progressText { + margin-top: 8px; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; + } + + > * { + vertical-align: middle; + } + } + + .mx_CreateSpaceFromCommunityDialog_error { + padding-left: 12px; + + > img { + align-self: center; + } + + .mx_CreateSpaceFromCommunityDialog_errorHeading { + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + color: $notice-primary-color; + } + + .mx_CreateSpaceFromCommunityDialog_errorCaption { + margin-top: 4px; + font-size: $font-12px; + line-height: $font-15px; + color: $primary-fg-color; + } + } + + .mx_AccessibleButton { + display: inline-block; + align-self: center; + } + + .mx_AccessibleButton_kind_primary { + padding: 8px 36px; + margin-left: 24px; + } + + .mx_AccessibleButton_kind_primary_outline { + margin-left: auto; + } + + .mx_CreateSpaceFromCommunityDialog_retryButton { + margin-left: 12px; + padding-left: 24px; + position: relative; + + &::before { + content: ''; + position: absolute; + background-color: $primary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/retry.svg'); + width: 18px; + height: 18px; + left: 0; + } + } + + .mx_AccessibleButton_kind_link { + padding: 0; + } + } +} + +.mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog { + .mx_InfoDialog { + max-width: 500px; + } + + .mx_AccessibleButton_kind_link { + padding: 0; + } + + .mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog_checkmark { + position: relative; + border-radius: 50%; + border: 3px solid $accent-color; + width: 68px; + height: 68px; + margin: 12px auto 32px; + + &::before { + width: inherit; + height: inherit; + content: ''; + position: absolute; + background-color: $accent-color; + mask-repeat: no-repeat; + mask-position: center; + mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); + mask-size: 48px; + } + } +} diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index cae81dcc97..50cd14c4da 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -38,6 +38,7 @@ limitations under the License. .mx_Field input, .mx_Field select, .mx_Field textarea { + font-family: inherit; font-weight: normal; font-size: $font-14px; border: none; diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 9f40372690..3290a998ab 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -50,15 +50,21 @@ limitations under the License. } .mx_SettingsTab_section { + $right-gutter: 80px; + margin-bottom: 24px; .mx_SettingsFlag { - margin-right: 80px; + margin-right: $right-gutter; margin-bottom: 10px; } + > p { + margin-right: $right-gutter; + } + &.mx_SettingsTab_subsectionText .mx_SettingsFlag { - margin-right: 0px !important; + margin-right: 0 !important; } } diff --git a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss index be0af9123b..4cdfa0b40f 100644 --- a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss @@ -22,4 +22,25 @@ limitations under the License. .mx_SettingsTab_section { margin-bottom: 30px; } + + .mx_PreferencesUserSettingsTab_CommunityMigrator { + margin-right: 200px; + + > div { + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + color: $primary-fg-color; + margin: 16px 0; + + .mx_BaseAvatar { + margin-right: 12px; + vertical-align: middle; + } + + .mx_AccessibleButton { + float: right; + } + } + } } diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss index 097b2b648e..41536bc8b1 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.scss +++ b/res/css/views/spaces/_SpaceCreateMenu.scss @@ -41,7 +41,6 @@ $spacePanelWidth: 71px; > p { font-size: $font-15px; color: $secondary-fg-color; - margin: 0; } .mx_SpaceFeedbackPrompt { @@ -51,13 +50,6 @@ $spacePanelWidth: 71px; } } - // XXX remove this when spaces leaves Beta - .mx_BetaCard_betaPill { - position: absolute; - top: 24px; - right: 24px; - } - .mx_SpaceCreateMenuType { @mixin SpacePillButton; } @@ -100,6 +92,11 @@ $spacePanelWidth: 71px; width: min-content; } + .mx_AccessibleButton_kind_link { + padding: 0; + font-size: inherit; + } + .mx_AccessibleButton_disabled { cursor: not-allowed; } diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 99fa94e62b..f4f1d50d63 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -41,6 +41,9 @@ import RightPanelStore from "../../stores/RightPanelStore"; import AutoHideScrollbar from "./AutoHideScrollbar"; import { mediaFromMxc } from "../../customisations/Media"; import { replaceableComponent } from "../../utils/replaceableComponent"; +import { createSpaceFromCommunity } from "../../utils/space"; +import { Action } from "../../dispatcher/actions"; +import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; const LONG_DESC_PLACEHOLDER = _td( `

HTML for your community's page

@@ -399,6 +402,8 @@ class FeaturedUser extends React.Component { const GROUP_JOINPOLICY_OPEN = "open"; const GROUP_JOINPOLICY_INVITE = "invite"; +const UPGRADE_NOTICE_LS_KEY = "mx_hide_community_upgrade_notice"; + @replaceableComponent("structures.GroupView") export default class GroupView extends React.Component { static propTypes = { @@ -422,6 +427,7 @@ export default class GroupView extends React.Component { publicityBusy: false, inviterProfile: null, showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup, + showUpgradeNotice: !localStorage.getItem(UPGRADE_NOTICE_LS_KEY), }; componentDidMount() { @@ -807,6 +813,22 @@ export default class GroupView extends React.Component { showGroupAddRoomDialog(this.props.groupId); }; + _dismissUpgradeNotice = () => { + localStorage.setItem(UPGRADE_NOTICE_LS_KEY, "true"); + this.setState({ showUpgradeNotice: false }); + } + + _onCreateSpaceClick = () => { + createSpaceFromCommunity(this._matrixClient, this.props.groupId); + }; + + _onAdminsLinkClick = () => { + dis.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.GroupMemberList, + }); + }; + _getGroupSection() { const groupSettingsSectionClasses = classnames({ "mx_GroupView_group": this.state.editing, @@ -843,10 +865,46 @@ export default class GroupView extends React.Component { }, ) } :
; + + let communitiesUpgradeNotice; + if (this.state.showUpgradeNotice) { + let text; + if (this.state.isUserPrivileged) { + text = _t("You can create a Space from this community here.", {}, { + a: sub => + { sub } + , + }); + } else { + text = _t("Ask the admins of this community to make it into a Space " + + "and keep a look out for the invite.", {}, { + a: sub => + { sub } + , + }); + } + + communitiesUpgradeNotice =
+

{ _t("Communities can now be made into Spaces") }

+

+ { _t("Spaces are a new way to make a community, with new features coming.") } +   + { text } +   + { _t("Communities won't receive further updates.") } +

+ +
; + } + return
{ header } { hostingSignup } { changeDelayWarning } + { communitiesUpgradeNotice } { this._getJoinableNode() } { this._getLongDescriptionNode() } { this._getRoomsNode() } diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 6f63ea090c..7887e9b744 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -74,6 +74,10 @@ import { BetaPill } from "../views/beta/BetaCard"; import { UserTab } from "../views/dialogs/UserSettingsDialog"; import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership"; import { SpaceFeedbackPrompt } from "../views/spaces/SpaceCreateMenu"; +import { CreateEventField, IGroupSummary } from "../views/dialogs/CreateSpaceFromCommunityDialog"; +import { useAsyncMemo } from "../../hooks/useAsyncMemo"; +import Spinner from "../views/elements/Spinner"; +import GroupAvatar from "../views/avatars/GroupAvatar"; interface IProps { space: Room; @@ -158,7 +162,33 @@ const onBetaClick = () => { }); }; -const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => { +// XXX: temporary community migration component +const GroupTile = ({ groupId }: { groupId: string }) => { + const cli = useContext(MatrixClientContext); + const groupSummary = useAsyncMemo(() => cli.getGroupSummary(groupId), [cli, groupId]); + + if (!groupSummary) return ; + + return <> + + { groupSummary.profile.name } + ; +}; + +interface ISpacePreviewProps { + space: Room; + onJoinButtonClicked(): void; + onRejectButtonClicked(): void; +} + +const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISpacePreviewProps) => { const cli = useContext(MatrixClientContext); const myMembership = useMyRoomMembership(space); @@ -270,8 +300,18 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
; } + let migratedCommunitySection: JSX.Element; + const createContent = space.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent(); + if (createContent[CreateEventField]) { + migratedCommunitySection =
+ { _t("Created from ", {}, { + Community: () => , + }) } +
; + } + return
- + { migratedCommunitySection } { inviterSection }

diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js index c40ff4207b..0c3c48a07f 100644 --- a/src/components/views/context_menus/TagTileContextMenu.js +++ b/src/components/views/context_menus/TagTileContextMenu.js @@ -24,6 +24,8 @@ import { MenuItem } from "../../structures/ContextMenu"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore"; +import { createSpaceFromCommunity } from "../../../utils/space"; +import GroupStore from "../../../stores/GroupStore"; @replaceableComponent("views.context_menus.TagTileContextMenu") export default class TagTileContextMenu extends React.Component { @@ -49,6 +51,11 @@ export default class TagTileContextMenu extends React.Component { this.props.onFinished(); }; + _onCreateSpaceClick = () => { + createSpaceFromCommunity(this.context, this.props.tag); + this.props.onFinished(); + }; + _onMoveUp = () => { dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index - 1)); this.props.onFinished(); @@ -77,6 +84,16 @@ export default class TagTileContextMenu extends React.Component { ); } + let createSpaceOption; + if (GroupStore.isUserPrivileged(this.props.tag)) { + createSpaceOption = <> +
+ + { _t("Create Space") } + + ; + } + return
{ _t('View Community') } @@ -88,6 +105,7 @@ export default class TagTileContextMenu extends React.Component { { _t("Unpin") } + { createSpaceOption }
; } } diff --git a/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx b/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx new file mode 100644 index 0000000000..4fb0994e23 --- /dev/null +++ b/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx @@ -0,0 +1,340 @@ +/* +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 { _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/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"; + +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 */ + +const CreateSpaceFromCommunityDialog: React.FC = ({ matrixClient: cli, groupId, onFinished }) => { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + + 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); + setBusy(true); + + // require & validate the space name field + if (!await spaceNameField.current.validate({ allowEmpty: false })) { + setBusy(false); + 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); + spaceAliasField.current.focus(); + spaceAliasField.current.validate({ allowEmpty: true, focused: true }); + return; + } + + try { + 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, + ]); + + 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) { + console.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(":"))]); + } + } + + 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) || [], + }, + })), + invite: [...members, ...invitedMembers].map(m => m.userId).filter(m => m !== cli.getUserId()), + }, { + andView: false, + }); + + // 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 => { + console.warn("Failed to update community profile during migration", e); + }); + + onFinished(roomId); + + const onSpaceClick = () => { + dis.dispatch({ + action: "view_room", + room_id: roomId, + }); + }; + + 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) { + console.error(e); + setError(e); + } + + setBusy(false); + }; + + let footer; + if (error) { + footer = <> + + + +
{ _t("Failed to migrate community") }
+
{ _t("Try again") }
+
+ + + { _t("Retry") } + + ; + } else { + footer = <> + onFinished()}> + { _t("Cancel") } + + + { busy ? _t("Creating...") : _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; + diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx index 0d71eb2de3..03927c7d62 100644 --- a/src/components/views/dialogs/CreateSubspaceDialog.tsx +++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx @@ -16,8 +16,7 @@ limitations under the License. import React, { useRef, useState } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; -import { JoinRule, Preset } from "matrix-js-sdk/src/@types/partials"; -import { RoomType } from "matrix-js-sdk/src/@types/event"; +import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { _t } from '../../../languageHandler'; import BaseDialog from "./BaseDialog"; @@ -27,8 +26,7 @@ import { BetaPill } from "../beta/BetaCard"; import Field from "../elements/Field"; import RoomAliasField from "../elements/RoomAliasField"; import SpaceStore from "../../../stores/SpaceStore"; -import { SpaceCreateForm } from "../spaces/SpaceCreateMenu"; -import createRoom from "../../../createRoom"; +import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu"; import { SubspaceSelector } from "./AddExistingToSpaceDialog"; import JoinRuleDropdown from "../elements/JoinRuleDropdown"; @@ -81,28 +79,7 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick } try { - await createRoom({ - createOpts: { - preset: joinRule === JoinRule.Public ? Preset.PublicChat : Preset.PrivateChat, - name, - power_level_content_override: { - // Only allow Admins to write to the timeline to prevent hidden sync spam - events_default: 100, - ...joinRule === JoinRule.Public ? { invite: 0 } : {}, - }, - room_alias_name: joinRule === JoinRule.Public && alias - ? alias.substr(1, alias.indexOf(":") - 1) - : undefined, - topic, - }, - avatar, - roomType: RoomType.Space, - parentSpace, - spinner: false, - encryption: false, - andView: true, - inlineErrors: true, - }); + await createSpace(name, joinRule === JoinRule.Public, alias, topic, avatar, {}, { parentSpace }); onFinished(true); } catch (e) { diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index 7608d7cb55..9613b27d17 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -114,7 +114,7 @@ export default class UserSettingsDialog extends React.Component UserTab.Preferences, _td("Preferences"), "mx_UserSettingsDialog_preferencesIcon", - , + , )); if (SettingsStore.getValue(UIFeature.Voip)) { diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index ba6bb26cbf..138f5bf9fe 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -851,7 +851,7 @@ const RoomAdminToolsContainer: React.FC = ({ return
; }; -interface GroupMember { +export interface GroupMember { userId: string; displayname?: string; // XXX: GroupMember objects are inconsistent :(( avatarUrl?: string; diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index ca85db967a..21c3ab24ec 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -15,7 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { useContext, useEffect, useState } from 'react'; +import { EventType } from 'matrix-js-sdk/src/@types/event'; + import { _t } from "../../../../../languageHandler"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; import SettingsStore from "../../../../../settings/SettingsStore"; @@ -27,6 +29,18 @@ import SettingsFlag from '../../../elements/SettingsFlag'; import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts"; import AccessibleButton from "../../../elements/AccessibleButton"; import SpaceStore from "../../../../../stores/SpaceStore"; +import GroupAvatar from "../../../avatars/GroupAvatar"; +import dis from "../../../../../dispatcher/dispatcher"; +import GroupActions from "../../../../../actions/GroupActions"; +import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; +import { useDispatcher } from "../../../../../hooks/useDispatcher"; +import { CreateEventField, IGroupSummary } from "../../../dialogs/CreateSpaceFromCommunityDialog"; +import { createSpaceFromCommunity } from "../../../../../utils/space"; +import Spinner from "../../../elements/Spinner"; + +interface IProps { + closeSettingsFn(success: boolean): void; +} interface IState { autoLaunch: boolean; @@ -42,8 +56,86 @@ interface IState { readMarkerOutOfViewThresholdMs: string; } +type Community = IGroupSummary & { + groupId: string; + spaceId?: string; +}; + +const CommunityMigrator = ({ onFinished }) => { + const cli = useContext(MatrixClientContext); + const [communities, setCommunities] = useState(null); + useEffect(() => { + dis.dispatch(GroupActions.fetchJoinedGroups(cli)); + }, [cli]); + useDispatcher(dis, async payload => { + if (payload.action === "GroupActions.fetchJoinedGroups.success") { + const communities: Community[] = []; + + const migratedSpaceMap = new Map(cli.getRooms().map(room => { + const createContent = room.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent(); + if (createContent?.[CreateEventField]) { + return [createContent[CreateEventField], room.roomId] as [string, string]; + } + }).filter(Boolean)); + + for (const groupId of payload.result.groups) { + const summary = await cli.getGroupSummary(groupId) as IGroupSummary; + if (summary.user.is_privileged) { + communities.push({ + ...summary, + groupId, + spaceId: migratedSpaceMap.get(groupId), + }); + } + } + + setCommunities(communities); + } + }); + + if (!communities) { + return ; + } + + return
+ { communities.map(community => ( +
+ + { community.profile.name } + { + if (community.spaceId) { + dis.dispatch({ + action: "view_room", + room_id: community.spaceId, + }); + onFinished(); + } else { + createSpaceFromCommunity(cli, community.groupId).then(([spaceId]) => { + if (spaceId) { + community.spaceId = spaceId; + setCommunities([...communities]); // force component re-render + } + }); + } + }} + > + { community.spaceId ? _t("Open Space") : _t("Create Space") } + +
+ )) } +
; +}; + @replaceableComponent("views.settings.tabs.user.PreferencesUserSettingsTab") -export default class PreferencesUserSettingsTab extends React.Component<{}, IState> { +export default class PreferencesUserSettingsTab extends React.Component { static ROOM_LIST_SETTINGS = [ 'breadcrumbs', ]; @@ -52,6 +144,10 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta "Spaces.allRoomsInHome", ]; + static COMMUNITIES_SETTINGS = [ + // TODO: part of delabsing move the toggle here - https://github.com/vector-im/element-web/issues/18088 + ]; + static KEYBINDINGS_SETTINGS = [ 'ctrlFForSearch', ]; @@ -242,6 +338,19 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta { this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS) }
} +
+ { _t("Communities") } +

{ _t("Communities have been archived to make way for Spaces but you can convert your " + + "communities into Spaces below. Converting will ensure your conversations get the latest " + + "features.") }

+
+ { _t("Show my Communities") } +

{ _t("If a community isn't shown you may not have permission to convert it.") }

+ +
+ { this.renderGroup(PreferencesUserSettingsTab.COMMUNITIES_SETTINGS) } +
+
{ _t("Keyboard shortcuts") } diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 406028dbc7..33e4a990ef 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -18,22 +18,59 @@ import React, { ComponentProps, RefObject, SyntheticEvent, useContext, useRef, u import classNames from "classnames"; import { RoomType } from "matrix-js-sdk/src/@types/event"; import FocusLock from "react-focus-lock"; +import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials"; +import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests"; import { _t } from "../../../languageHandler"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { ChevronFace, ContextMenu } from "../../structures/ContextMenu"; -import createRoom from "../../../createRoom"; +import createRoom, { IOpts as ICreateOpts } from "../../../createRoom"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import SpaceBasicSettings, { SpaceAvatar } from "./SpaceBasicSettings"; import AccessibleButton from "../elements/AccessibleButton"; import Field from "../elements/Field"; import withValidation from "../elements/Validation"; -import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials"; import RoomAliasField from "../elements/RoomAliasField"; import SdkConfig from "../../../SdkConfig"; import Modal from "../../../Modal"; import GenericFeatureFeedbackDialog from "../dialogs/GenericFeatureFeedbackDialog"; import SettingsStore from "../../../settings/SettingsStore"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import { Action } from "../../../dispatcher/actions"; +import { UserTab } from "../dialogs/UserSettingsDialog"; + +export const createSpace = async ( + name: string, + isPublic: boolean, + alias?: string, + topic?: string, + avatar?: string | File, + createOpts: Partial = {}, + otherOpts: Partial> = {}, +) => { + return createRoom({ + createOpts: { + name, + preset: isPublic ? Preset.PublicChat : Preset.PrivateChat, + power_level_content_override: { + // Only allow Admins to write to the timeline to prevent hidden sync spam + events_default: 100, + ...isPublic ? { invite: 0 } : {}, + }, + room_alias_name: isPublic && alias ? alias.substr(1, alias.indexOf(":") - 1) : undefined, + topic, + ...createOpts, + }, + avatar, + roomType: RoomType.Space, + historyVisibility: isPublic ? HistoryVisibility.WorldReadable : HistoryVisibility.Invited, + spinner: false, + encryption: false, + andView: true, + inlineErrors: true, + ...otherOpts, + }); +}; const SpaceCreateMenuType = ({ title, description, className, onClick }) => { return ( @@ -92,7 +129,7 @@ export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
; }; -type BProps = Pick, "setAvatar" | "name" | "setName" | "topic" | "setTopic">; +type BProps = Omit, "nameDisabled" | "topicDisabled" | "avatarDisabled">; interface ISpaceCreateFormProps extends BProps { busy: boolean; alias: string; @@ -106,6 +143,7 @@ interface ISpaceCreateFormProps extends BProps { export const SpaceCreateForm: React.FC = ({ busy, onSubmit, + avatarUrl, setAvatar, name, setName, @@ -122,7 +160,7 @@ export const SpaceCreateForm: React.FC = ({ const domain = cli.getDomain(); return
- + { } try { - await createRoom({ - createOpts: { - preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat, - name, - power_level_content_override: { - // Only allow Admins to write to the timeline to prevent hidden sync spam - events_default: 100, - ...visibility === Visibility.Public ? { invite: 0 } : {}, - }, - room_alias_name: visibility === Visibility.Public && alias - ? alias.substr(1, alias.indexOf(":") - 1) - : undefined, - topic, - }, - avatar, - roomType: RoomType.Space, - historyVisibility: visibility === Visibility.Public - ? HistoryVisibility.WorldReadable - : HistoryVisibility.Invited, - spinner: false, - encryption: false, - andView: true, - inlineErrors: true, - }); + await createSpace(name, visibility === Visibility.Public, alias, topic, avatar); onFinished(); } catch (e) { @@ -233,10 +248,23 @@ const SpaceCreateMenu = ({ onFinished }) => { let body; if (visibility === null) { + const onCreateSpaceFromCommunityClick = () => { + defaultDispatcher.dispatch({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Preferences, + }); + onFinished(); + }; + body =

{ _t("Create a space") }

-

{ _t("Spaces are a new way to group rooms and people. " + - "To join an existing space you'll need an invite.") }

+

+ { _t("Spaces are a new way to group rooms and people.") } +   + { _t("What kind of Space do you want to create?") } +   + { _t("You can change this later.") } +

{ onClick={() => setVisibility(Visibility.Private)} /> -

{ _t("You can change this later") }

+

+ { _t("You can also create a Space from a community.", {}, { + a: sub => + { sub } + , + }) } +
+ { _t("To join an existing space you'll need an invite.") } +

; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fa6d30cdd3..c9dbc00a78 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1029,12 +1029,14 @@ "e.g. my-space": "e.g. my-space", "Address": "Address", "Create a space": "Create a space", - "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.", + "What kind of Space do you want to create?": "What kind of Space do you want to create?", + "You can change this later.": "You can change this later.", "Public": "Public", "Open space for anyone, best for communities": "Open space for anyone, best for communities", "Private": "Private", "Invite only, best for yourself or teams": "Invite only, best for yourself or teams", - "You can change this later": "You can change this later", + "You can also create a Space from a community.": "You can also create a Space from a community.", + "To join an existing space you'll need an invite.": "To join an existing space you'll need an invite.", "Go back": "Go back", "Your public space": "Your public space", "Your private space": "Your private space", @@ -1337,12 +1339,18 @@ "If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.", "Room ID or address of ban list": "Room ID or address of ban list", "Subscribe": "Subscribe", + "Open Space": "Open Space", + "Create Space": "Create Space", "Start automatically after system login": "Start automatically after system login", "Warn before quitting": "Warn before quitting", "Always show the window menu bar": "Always show the window menu bar", "Show tray icon and minimize window to it on close": "Show tray icon and minimize window to it on close", "Preferences": "Preferences", "Room list": "Room list", + "Communities": "Communities", + "Communities have been archived to make way for Spaces but you can convert your communities into Spaces below. Converting will ensure your conversations get the latest features.": "Communities have been archived to make way for Spaces but you can convert your communities into Spaces below. Converting will ensure your conversations get the latest features.", + "Show my Communities": "Show my Communities", + "If a community isn't shown you may not have permission to convert it.": "If a community isn't shown you may not have permission to convert it.", "Keyboard shortcuts": "Keyboard shortcuts", "To view all keyboard shortcuts, click here.": "To view all keyboard shortcuts, click here.", "Displaying time": "Displaying time", @@ -2224,13 +2232,24 @@ "Visible to space members": "Visible to space members", "Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.", "Create Room": "Create Room", + "This community has been upgraded into a Space": "This community has been upgraded into a Space", + "To view Spaces, hide communities in Preferences": "To view Spaces, hide communities in Preferences", + "Space created": "Space created", + " 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", + "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.", + "Flair won't be available in Spaces for the foreseeable future.": "Flair won't be available in Spaces for the foreseeable future.", + "This description will be shown to people when they view your space": "This description will be shown to people when they view your space", + "Space visibility": "Space visibility", + "Private space (invite only)": "Private space (invite only)", + "Public space": "Public space", "Anyone in will be able to find and join.": "Anyone in will be able to find and join.", "Anyone will be able to find and join this space, not just members of .": "Anyone will be able to find and join this space, not just members of .", "Only people invited will be able to find and join this space.": "Only people invited will be able to find and join this space.", "Add a space to a space you manage.": "Add a space to a space you manage.", - "Space visibility": "Space visibility", - "Private space (invite only)": "Private space (invite only)", - "Public space": "Public space", "Want to add an existing space instead?": "Want to add an existing space instead?", "Adding...": "Adding...", "Sign out": "Sign out", @@ -2682,7 +2701,6 @@ "You must join the room to see its files": "You must join the room to see its files", "No files visible in this room": "No files visible in this room", "Attach files from chat or just drag and drop them anywhere in a room.": "Attach files from chat or just drag and drop them anywhere in a room.", - "Communities": "Communities", "Create community": "Create community", "

HTML for your community's page

\n

\n Use the long description to introduce new members to the community, or distribute\n some important links\n

\n

\n You can even add images with Matrix URLs \n

\n": "

HTML for your community's page

\n

\n Use the long description to introduce new members to the community, or distribute\n some important links\n

\n

\n You can even add images with Matrix URLs \n

\n", "Add rooms to the community summary": "Add rooms to the community summary", @@ -2710,6 +2728,11 @@ "Community Settings": "Community Settings", "Want more than a community? Get your own server": "Want more than a community? Get your own server", "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.", + "You can create a Space from this community here.": "You can create a Space from this community here.", + "Ask the admins of this community to make it into a Space and keep a look out for the invite.": "Ask the admins of this community to make it into a Space and keep a look out for the invite.", + "Communities can now be made into Spaces": "Communities can now be made into Spaces", + "Spaces are a new way to make a community, with new features coming.": "Spaces are a new way to make a community, with new features coming.", + "Communities won't receive further updates.": "Communities won't receive further updates.", "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.", "Featured Rooms:": "Featured Rooms:", "Featured Users:": "Featured Users:", @@ -2841,6 +2864,7 @@ "To view %(spaceName)s, turn on the Spaces beta": "To view %(spaceName)s, turn on the Spaces beta", "To join %(spaceName)s, turn on the Spaces beta": "To join %(spaceName)s, turn on the Spaces beta", "To view %(spaceName)s, you need an invite": "To view %(spaceName)s, you need an invite", + "Created from ": "Created from ", "Welcome to ": "Welcome to ", "Random": "Random", "Support": "Support", diff --git a/src/stores/GroupFilterOrderStore.js b/src/stores/GroupFilterOrderStore.js index e81d1b81f7..821fbefc4f 100644 --- a/src/stores/GroupFilterOrderStore.js +++ b/src/stores/GroupFilterOrderStore.js @@ -14,12 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ import { Store } from 'flux/utils'; +import { EventType } from "matrix-js-sdk/src/@types/event"; import dis from '../dispatcher/dispatcher'; import GroupStore from './GroupStore'; import Analytics from '../Analytics'; import * as RoomNotifs from "../RoomNotifs"; import { MatrixClientPeg } from '../MatrixClientPeg'; import SettingsStore from "../settings/SettingsStore"; +import { CreateEventField } from "../components/views/dialogs/CreateSpaceFromCommunityDialog"; const INITIAL_STATE = { orderedTags: null, @@ -235,8 +237,12 @@ class GroupFilterOrderStore extends Store { (t) => (t[0] !== '+' || groupIds.includes(t)) && !removedTags.has(t), ); + const cli = MatrixClientPeg.get(); + const migratedCommunities = new Set(cli.getRooms().map(r => { + return r.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent()[CreateEventField]; + }).filter(Boolean)); const groupIdsToAdd = groupIds.filter( - (groupId) => !tags.includes(groupId) && !removedTags.has(groupId), + (groupId) => !tags.includes(groupId) && !removedTags.has(groupId) && !migratedCommunities.has(groupId), ); return tagsToKeep.concat(groupIdsToAdd); diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index f1122cb945..63972b31fb 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -20,11 +20,11 @@ import FlairStore from './FlairStore'; import { MatrixClientPeg } from '../MatrixClientPeg'; import dis from '../dispatcher/dispatcher'; -function parseMembersResponse(response) { +export function parseMembersResponse(response) { return response.chunk.map((apiMember) => groupMemberFromApiObject(apiMember)); } -function parseRoomsResponse(response) { +export function parseRoomsResponse(response) { return response.chunk.map((apiRoom) => groupRoomFromApiObject(apiRoom)); } diff --git a/src/utils/space.tsx b/src/utils/space.tsx index fecb581e65..c1d8dbfbea 100644 --- a/src/utils/space.tsx +++ b/src/utils/space.tsx @@ -17,6 +17,7 @@ limitations under the License. import React from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import { EventType } from "matrix-js-sdk/src/@types/event"; +import { MatrixClient } from "matrix-js-sdk/src/client"; import { calculateRoomVia } from "./permalinks/Permalinks"; import Modal from "../Modal"; @@ -37,6 +38,7 @@ import { leaveRoomBehaviour } from "./membership"; import Spinner from "../components/views/elements/Spinner"; import dis from "../dispatcher/dispatcher"; import LeaveSpaceDialog from "../components/views/dialogs/LeaveSpaceDialog"; +import CreateSpaceFromCommunityDialog from "../components/views/dialogs/CreateSpaceFromCommunityDialog"; export const shouldShowSpaceSettings = (space: Room) => { const userId = space.client.getUserId(); @@ -173,3 +175,10 @@ export const leaveSpace = (space: Room) => { }, }, "mx_LeaveSpaceDialog_wrapper"); }; + +export const createSpaceFromCommunity = (cli: MatrixClient, groupId: string): Promise<[string?]> => { + return Modal.createTrackedDialog('Create Space', 'from community', CreateSpaceFromCommunityDialog, { + matrixClient: cli, + groupId, + }, "mx_CreateSpaceFromCommunityDialog_wrapper").finished as Promise<[string?]>; +};