From a6e5112be08eabf3e7c7b0d138e81178ad209edb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 4 Aug 2021 10:37:35 +0100 Subject: [PATCH 1/6] Offer a way to create a space based on existing community --- res/css/_components.scss | 1 + res/css/structures/_GroupView.scss | 55 +++ res/css/structures/_SpaceRoomView.scss | 14 +- .../context_menus/_TagTileContextMenu.scss | 4 + .../_CreateSpaceFromCommunityDialog.scss | 176 +++++++++ .../user/_PreferencesUserSettingsTab.scss | 20 ++ src/components/structures/GroupView.js | 26 ++ src/components/structures/SpaceRoomView.tsx | 44 ++- .../views/context_menus/TagTileContextMenu.js | 18 + .../CreateSpaceFromCommunityDialog.tsx | 338 ++++++++++++++++++ .../views/dialogs/CreateSubspaceDialog.tsx | 29 +- .../views/dialogs/UserSettingsDialog.tsx | 2 +- src/components/views/right_panel/UserInfo.tsx | 2 +- .../tabs/user/PreferencesUserSettingsTab.tsx | 114 +++++- .../views/spaces/SpaceCreateMenu.tsx | 68 ++-- src/i18n/strings/en_EN.json | 31 +- src/stores/GroupFilterOrderStore.js | 8 +- src/stores/GroupStore.js | 4 +- src/utils/space.tsx | 9 + 19 files changed, 895 insertions(+), 68 deletions(-) create mode 100644 res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss create mode 100644 src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 76551b51f8..dd1b0d220b 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..21137d8a12 100644 --- a/res/css/structures/_GroupView.scss +++ b/res/css/structures/_GroupView.scss @@ -368,6 +368,61 @@ 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 { + 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..059d38c77c --- /dev/null +++ b/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss @@ -0,0 +1,176 @@ +/* +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; + color: $secondary-fg-color; + + &.mx_CreateSpaceFromCommunityDialog_flairNotice { + font-size: $font-12px; + line-height: $font-15px; + } + } + + .mx_SpaceBasicSettings { + > p { + font-size: $font-12px; + line-height: $font-15px; + margin: 8px 0 16px; + } + } + + .mx_JoinRuleDropdown .mx_Dropdown_menu { + width: auto !important; // override fixed width + } + } + + .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/settings/tabs/user/_PreferencesUserSettingsTab.scss b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss index be0af9123b..efd2548d3d 100644 --- a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss @@ -22,4 +22,24 @@ 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; + + .mx_BaseAvatar { + margin-right: 12px; + vertical-align: middle; + } + + .mx_AccessibleButton { + float: right; + } + } + } } diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 99fa94e62b..26bb1b8ae7 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -399,6 +399,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 +424,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 +810,11 @@ export default class GroupView extends React.Component { showGroupAddRoomDialog(this.props.groupId); }; + _dismissUpgradeNotice = () => { + localStorage.setItem(UPGRADE_NOTICE_LS_KEY, "true"); + this.setState({ showUpgradeNotice: false }); + } + _getGroupSection() { const groupSettingsSectionClasses = classnames({ "mx_GroupView_group": this.state.editing, @@ -843,10 +851,28 @@ export default class GroupView extends React.Component { }, ) } :
; + + let communitiesUpgradeNotice; + if (this.state.showUpgradeNotice) { + communitiesUpgradeNotice =
+

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

+

+ { _t("Spaces are a new way to make a community, with new features coming.") } +   + { _t("Ask the admins of this community to make it into a Space " + + "and keep a look out for the invite.") } +   + { _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 4064b2f48e..4eb9b855f7 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..bef50b008f --- /dev/null +++ b/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx @@ -0,0 +1,338 @@ +/* +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("Spaces are the new version of communities - with new features coming.") } +   + { _t("All rooms will automatically be automatically added, a link to the Space will be " + + "added to your old community description 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") + }

+
+
+ +
+ { 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 c837e814c8..088c59d724 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 d3da8a7784..1dea0a7770 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', ]; @@ -241,6 +337,20 @@ 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.") }

+ { _t("Convert your Communities to Spaces") } +

{ _t("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..ef70deb672 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -18,23 +18,57 @@ 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"; +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 +126,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 +140,7 @@ interface ISpaceCreateFormProps extends BProps { export const SpaceCreateForm: React.FC = ({ busy, onSubmit, + avatarUrl, setAvatar, name, setName, @@ -122,7 +157,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) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3ad8daa85c..850d6d18ed 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1322,12 +1322,20 @@ "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.": "Communities have been archived to make way for Spaces but you can convert your communities into Spaces below.", + "Convert your Communities to Spaces": "Convert your Communities to Spaces", + "Converting will ensure your conversations get the latest features.": "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", @@ -2218,13 +2226,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", + "Spaces are the new version of communities - with new features coming.": "Spaces are the new version of communities - with new features coming.", + "All rooms will automatically be automatically added, a link to the Space will be added to your old community description and all community members will be invited.": "All rooms will automatically be automatically added, a link to the Space will be added to your old community description 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", @@ -2676,7 +2695,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", @@ -2704,6 +2722,10 @@ "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.", + "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.", + "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 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:", @@ -2835,6 +2857,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?]>; +}; From f7818e01692229db352b1eaabbe147e19b5cadff Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Aug 2021 12:05:14 +0100 Subject: [PATCH 2/6] iterate PR based on feedback --- res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss | 7 +++++-- res/css/views/elements/_Field.scss | 1 + .../views/dialogs/CreateSpaceFromCommunityDialog.tsx | 2 +- src/i18n/strings/en_EN.json | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss b/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss index 059d38c77c..b011fb104a 100644 --- a/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss +++ b/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss @@ -33,7 +33,10 @@ limitations under the License. > p { font-size: $font-15px; line-height: $font-24px; - color: $secondary-fg-color; + + &:first-of-type { + margin-top: 0; + } &.mx_CreateSpaceFromCommunityDialog_flairNotice { font-size: $font-12px; @@ -45,7 +48,7 @@ limitations under the License. > p { font-size: $font-12px; line-height: $font-15px; - margin: 8px 0 16px; + margin: 16px 0; } } 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/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx b/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx index bef50b008f..335be80a70 100644 --- a/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx +++ b/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx @@ -288,7 +288,7 @@ const CreateSpaceFromCommunityDialog: React.FC = ({ matrixClient: cli, g

{ _t("Spaces are the new version of communities - with new features coming.") }   - { _t("All rooms will automatically be automatically added, a link to the Space will be " + + { _t("All rooms will automatically be added, a link to the Space will be " + "added to your old community description and all community members will be invited.") }

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f3c5156e66..e0b458dd8e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2235,7 +2235,7 @@ "Failed to migrate community": "Failed to migrate community", "Create Space from community": "Create Space from community", "Spaces are the new version of communities - with new features coming.": "Spaces are the new version of communities - with new features coming.", - "All rooms will automatically be automatically added, a link to the Space will be added to your old community description and all community members will be invited.": "All rooms will automatically be automatically added, a link to the Space will be added to your old community description and all community members will be invited.", + "All rooms will automatically be added, a link to the Space will be added to your old community description and all community members will be invited.": "All rooms will automatically be added, a link to the Space will be added to your old community description 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", From 195b8714fc592fd2286bae36e825518a2cec55f8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Aug 2021 09:55:51 +0100 Subject: [PATCH 3/6] Iterate PR based on feedback --- res/css/views/settings/tabs/_SettingsTab.scss | 10 ++++++++-- .../tabs/user/_PreferencesUserSettingsTab.scss | 1 + .../settings/tabs/user/PreferencesUserSettingsTab.tsx | 5 ++--- src/i18n/strings/en_EN.json | 4 +--- 4 files changed, 12 insertions(+), 8 deletions(-) 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 efd2548d3d..4cdfa0b40f 100644 --- a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss @@ -31,6 +31,7 @@ limitations under the License. font-size: $font-15px; line-height: $font-18px; color: $primary-fg-color; + margin: 16px 0; .mx_BaseAvatar { margin-right: 12px; diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 860f42c1e8..21c3ab24ec 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -341,9 +341,8 @@ export default class PreferencesUserSettingsTab extends React.Component { _t("Communities") }

{ _t("Communities have been archived to make way for Spaces but you can convert your " + - "communities into Spaces below.") }

- { _t("Convert your Communities to Spaces") } -

{ _t("Converting will ensure your conversations get the latest features.") }

+ "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.") }

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 008cbabac3..a725cdaff7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1342,9 +1342,7 @@ "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.": "Communities have been archived to make way for Spaces but you can convert your communities into Spaces below.", - "Convert your Communities to Spaces": "Convert your Communities to Spaces", - "Converting will ensure your conversations get the latest features.": "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.": "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", From 77cf5bf61317eee672d59c6f4d6500a935e6e0b5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Aug 2021 11:30:00 +0100 Subject: [PATCH 4/6] Update space create menu copy and add link to community migrator --- res/css/views/spaces/_SpaceCreateMenu.scss | 13 ++++---- .../views/spaces/SpaceCreateMenu.tsx | 30 +++++++++++++++++-- src/i18n/strings/en_EN.json | 6 ++-- 3 files changed, 36 insertions(+), 13 deletions(-) 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/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index ef70deb672..9921194b39 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -35,6 +35,9 @@ 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, @@ -245,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 a725cdaff7..f41a0dccc2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1027,12 +1027,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", From 54d85a3864b8d8fedd7da859df8768c3f77f9c8f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Aug 2021 16:32:48 +0100 Subject: [PATCH 5/6] Iterate PR based on feedback --- res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss | 4 ++++ .../views/dialogs/CreateSpaceFromCommunityDialog.tsx | 5 +++-- src/components/views/spaces/SpaceCreateMenu.tsx | 4 ++-- src/i18n/strings/en_EN.json | 5 +++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss b/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss index b011fb104a..306fc77011 100644 --- a/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss +++ b/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss @@ -50,6 +50,10 @@ limitations under the License. line-height: $font-15px; margin: 16px 0; } + + .mx_Field_textarea { + margin-bottom: 0; + } } .mx_JoinRuleDropdown .mx_Dropdown_menu { diff --git a/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx b/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx index 335be80a70..c9e13d2b61 100644 --- a/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx +++ b/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx @@ -288,8 +288,9 @@ const CreateSpaceFromCommunityDialog: React.FC = ({ matrixClient: cli, g

{ _t("Spaces are the new version of communities - with new features coming.") }   - { _t("All rooms will automatically be added, a link to the Space will be " + - "added to your old community description and all community members will be invited.") } + { _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.") } diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 9921194b39..33e4a990ef 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -285,8 +285,8 @@ const SpaceCreateMenu = ({ onFinished }) => { { sub } , }) } -   - { _t("To join an existing space you'll need an invite") } +
+ { _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 f41a0dccc2..3b5a748f89 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1034,7 +1034,7 @@ "Private": "Private", "Invite only, best for yourself or teams": "Invite only, best for yourself or teams", "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", + "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", @@ -2238,7 +2238,8 @@ "Failed to migrate community": "Failed to migrate community", "Create Space from community": "Create Space from community", "Spaces are the new version of communities - with new features coming.": "Spaces are the new version of communities - with new features coming.", - "All rooms will automatically be added, a link to the Space will be added to your old community description and all community members will be invited.": "All rooms will automatically be added, a link to the Space will be added to your old community description and all community members will be invited.", + "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", From 9d3569a5778b708ecec146226de0fbfd74912527 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 12 Aug 2021 10:58:56 +0100 Subject: [PATCH 6/6] Iterate PR based on feedback --- res/css/structures/_GroupView.scss | 6 ++- .../_CreateSpaceFromCommunityDialog.scss | 4 ++ src/components/structures/GroupView.js | 38 +++++++++++++++++-- .../CreateSpaceFromCommunityDialog.tsx | 5 ++- src/i18n/strings/en_EN.json | 4 +- 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/res/css/structures/_GroupView.scss b/res/css/structures/_GroupView.scss index 21137d8a12..fb660f4194 100644 --- a/res/css/structures/_GroupView.scss +++ b/res/css/structures/_GroupView.scss @@ -400,7 +400,11 @@ limitations under the License. background-color: $secondary-fg-color; } - .mx_AccessibleButton { + .mx_AccessibleButton_kind_link { + padding: 0; + } + + .mx_GroupView_spaceUpgradePrompt_close { width: 16px; height: 16px; border-radius: 8px; diff --git a/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss b/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss index 306fc77011..afa722e05e 100644 --- a/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss +++ b/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss @@ -59,6 +59,10 @@ limitations under the License. .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 { diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 26bb1b8ae7..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

@@ -815,6 +818,17 @@ export default class GroupView extends React.Component { 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, @@ -854,17 +868,35 @@ 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.") }   - { _t("Ask the admins of this community to make it into a Space " + - "and keep a look out for the invite.") } + { text }   { _t("Communities won't receive further updates.") }

- +
; } diff --git a/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx b/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx index c9e13d2b61..4fb0994e23 100644 --- a/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx +++ b/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx @@ -286,8 +286,6 @@ const CreateSpaceFromCommunityDialog: React.FC = ({ matrixClient: cli, g >

- { _t("Spaces are the new version of communities - with new features coming.") } -   { _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.") } @@ -326,6 +324,9 @@ const CreateSpaceFromCommunityDialog: React.FC = ({ matrixClient: cli, g ? _t("Open space for anyone, best for communities") : _t("Invite only, best for yourself or teams") }

+ { joinRule !== JoinRule.Public && +
+ }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3b5a748f89..987005a1b3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2237,7 +2237,6 @@ "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", - "Spaces are the new version of communities - with new features coming.": "Spaces are the new version of communities - with new features coming.", "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.", @@ -2727,9 +2726,10 @@ "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.", - "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 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:",