diff --git a/res/css/_components.scss b/res/css/_components.scss index 9c895490b3..742bb0e795 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -117,6 +117,7 @@ @import "./views/elements/_EditableItemList.scss"; @import "./views/elements/_ErrorBoundary.scss"; @import "./views/elements/_EventListSummary.scss"; +@import "./views/elements/_FacePile.scss"; @import "./views/elements/_Field.scss"; @import "./views/elements/_FormButton.scss"; @import "./views/elements/_ImageView.scss"; diff --git a/res/css/structures/_RoomSearch.scss b/res/css/structures/_RoomSearch.scss index c33a3c0ff9..7fdafab5a6 100644 --- a/res/css/structures/_RoomSearch.scss +++ b/res/css/structures/_RoomSearch.scss @@ -22,7 +22,7 @@ limitations under the License. // keep border thickness consistent to prevent movement border: 1px solid transparent; height: 28px; - padding: 2px; + padding: 1px; // Create a flexbox for the icons (easier to manage) display: flex; diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index 5cca4aca11..873fa967ab 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -330,10 +330,6 @@ $activeBorderColor: $secondary-fg-color; mask-image: url('$(res)/img/element-icons/leave.svg'); } - .mx_SpacePanel_iconHome::before { - mask-image: url('$(res)/img/element-icons/roomlist/home.svg'); - } - .mx_SpacePanel_iconMembers::before { mask-image: url('$(res)/img/element-icons/room/members.svg'); } diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss index b20554166a..dcceee6371 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -182,7 +182,7 @@ limitations under the License. .mx_SpaceRoomDirectory_roomTile { position: relative; - padding: 6px 16px; + padding: 8px 16px; border-radius: 8px; min-height: 56px; box-sizing: border-box; @@ -190,6 +190,7 @@ limitations under the License. display: grid; grid-template-columns: 20px auto max-content; grid-column-gap: 8px; + grid-row-gap: 6px; align-items: center; .mx_BaseAvatar { @@ -213,16 +214,28 @@ limitations under the License. .mx_InfoTooltip_icon { margin-right: 4px; + position: relative; + vertical-align: text-top; + + &::before { + position: absolute; + top: 0; + left: 0; + } } } } .mx_SpaceRoomDirectory_roomTile_info { - font-size: $font-12px; - line-height: $font-15px; - color: $tertiary-fg-color; + font-size: $font-14px; + line-height: $font-18px; + color: $secondary-fg-color; grid-row: 2; grid-column: 1/3; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; } .mx_SpaceRoomDirectory_actions { @@ -232,9 +245,9 @@ limitations under the License. grid-row: 1/3; .mx_AccessibleButton { - padding: 6px 18px; - - display: none; + padding: 8px 18px; + display: inline-block; + visibility: hidden; } .mx_Checkbox { @@ -248,7 +261,7 @@ limitations under the License. background-color: $groupFilterPanel-bg-color; .mx_AccessibleButton { - display: inline-block; + visibility: visible; } } } diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 3d3b5d1bb8..2e7cfb55d9 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -22,7 +22,7 @@ $SpaceRoomViewInnerWidth: 428px; width: 432px; box-sizing: border-box; border-radius: 8px; - border: 1px solid $space-button-outline-color; + border: 1px solid $input-border-color; font-size: $font-15px; margin: 20px 0; @@ -122,7 +122,6 @@ $SpaceRoomViewInnerWidth: 428px; max-width: 480px; box-sizing: border-box; box-shadow: 2px 15px 30px $dialog-shadow-color; - border: 1px solid $input-border-color; border-radius: 8px; .mx_SpaceRoomView_preview_inviter { @@ -154,53 +153,6 @@ $SpaceRoomViewInnerWidth: 428px; margin: 20px 0 !important; // override default margin from above } - .mx_SpaceRoomView_preview_info { - color: $tertiary-fg-color; - font-size: $font-15px; - line-height: $font-24px; - margin: 20px 0; - - .mx_SpaceRoomView_preview_info_public, - .mx_SpaceRoomView_preview_info_private { - padding-left: 20px; - position: relative; - - &::before { - position: absolute; - content: ""; - width: 20px; - height: 20px; - top: 0; - left: -2px; - mask-position: center; - mask-repeat: no-repeat; - background-color: $tertiary-fg-color; - } - } - - .mx_SpaceRoomView_preview_info_public::before { - mask-size: 12px; - mask-image: url("$(res)/img/globe.svg"); - } - - .mx_SpaceRoomView_preview_info_private::before { - mask-size: 14px; - mask-image: url("$(res)/img/element-icons/lock.svg"); - } - - .mx_AccessibleButton_kind_link { - color: inherit; - position: relative; - padding-left: 16px; - - &::before { - content: "·"; // visual separator - position: absolute; - left: 6px; - } - } - } - .mx_SpaceRoomView_preview_topic { font-size: $font-14px; line-height: $font-22px; @@ -254,36 +206,90 @@ $SpaceRoomViewInnerWidth: 428px; vertical-align: middle; } } + } - .mx_SpaceRoomView_landing_memberCount { + .mx_SpaceRoomView_landing_info { + display: flex; + align-items: center; + + .mx_SpaceRoomView_info { + display: inline-block; + margin: 0; + } + + .mx_FacePile { + display: inline-block; + margin-left: auto; + margin-right: 12px; + + .mx_FacePile_faces { + cursor: pointer; + + > span:hover { + .mx_BaseAvatar { + filter: brightness(0.8); + } + } + + > span:first-child { + position: relative; + + .mx_BaseAvatar { + filter: brightness(0.8); + } + + &::before { + content: ""; + z-index: 1; + position: absolute; + top: 0; + left: 0; + height: 30px; + width: 30px; + background: #ffffff; // white icon fill + mask-position: center; + mask-size: 24px; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); + } + } + } + } + + .mx_SpaceRoomView_landing_inviteButton { position: relative; - margin-left: 24px; - padding: 0 0 0 28px; - line-height: $font-24px; - vertical-align: text-bottom; + padding-left: 40px; + height: min-content; &::before { position: absolute; - content: ''; - width: 24px; - height: 24px; - top: 0; - left: 0; + content: ""; + left: 8px; + height: 16px; + width: 16px; + background: #ffffff; // white icon fill mask-position: center; + mask-size: 16px; mask-repeat: no-repeat; - mask-size: contain; - background-color: $accent-color; - mask-image: url('$(res)/img/element-icons/community-members.svg'); + mask-image: url('$(res)/img/element-icons/room/invite.svg'); } } } .mx_SpaceRoomView_landing_topic { font-size: $font-15px; + margin-top: 12px; + margin-bottom: 16px; + } + + > hr { + border: none; + height: 1px; + background-color: $groupFilterPanel-bg-color; } .mx_SpaceRoomView_landing_adminButtons { - margin-top: 32px; + margin-top: 24px; .mx_AccessibleButton { position: relative; @@ -292,9 +298,9 @@ $SpaceRoomViewInnerWidth: 428px; box-sizing: border-box; padding: 72px 16px 0; border-radius: 12px; - border: 1px solid $space-button-outline-color; + border: 1px solid $input-border-color; margin-right: 28px; - margin-bottom: 28px; + margin-bottom: 20px; font-size: $font-14px; display: inline-block; vertical-align: bottom; @@ -324,16 +330,6 @@ $SpaceRoomViewInnerWidth: 428px; background: #ffffff; // white icon fill } - &.mx_SpaceRoomView_landing_inviteButton { - &::before { - background-color: $accent-color; - } - - &::after { - mask-image: url('$(res)/img/element-icons/room/invite.svg'); - } - } - &.mx_SpaceRoomView_landing_addButton { &::before { background-color: #ac3ba8; @@ -366,12 +362,8 @@ $SpaceRoomViewInnerWidth: 428px; } } - .mx_SpaceRoomDirectory_list { - max-width: 600px; - - .mx_SpaceRoomDirectory_roomTile_actions { - display: none; - } + .mx_SearchBox { + margin: 0 0 20px; } } @@ -424,3 +416,50 @@ $SpaceRoomViewInnerWidth: 428px; } } } + +.mx_SpaceRoomView_info { + color: $secondary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + margin: 20px 0; + + .mx_SpaceRoomView_info_public, + .mx_SpaceRoomView_info_private { + padding-left: 20px; + position: relative; + + &::before { + position: absolute; + content: ""; + width: 20px; + height: 20px; + top: 0; + left: -2px; + mask-position: center; + mask-repeat: no-repeat; + background-color: $tertiary-fg-color; + } + } + + .mx_SpaceRoomView_info_public::before { + mask-size: 12px; + mask-image: url("$(res)/img/globe.svg"); + } + + .mx_SpaceRoomView_info_private::before { + mask-size: 14px; + mask-image: url("$(res)/img/element-icons/lock.svg"); + } + + .mx_AccessibleButton_kind_link { + color: inherit; + position: relative; + padding-left: 16px; + + &::before { + content: "·"; // visual separator + position: absolute; + left: 6px; + } + } +} diff --git a/res/css/views/elements/_FacePile.scss b/res/css/views/elements/_FacePile.scss new file mode 100644 index 0000000000..9a992f59d1 --- /dev/null +++ b/res/css/views/elements/_FacePile.scss @@ -0,0 +1,42 @@ +/* +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_FacePile { + .mx_FacePile_faces { + display: inline-flex; + flex-direction: row-reverse; + vertical-align: middle; + + > span + span { + margin-right: -8px; + } + + .mx_BaseAvatar_image { + border: 1px solid $primary-bg-color; + } + + .mx_BaseAvatar_initial { + margin: 1px; // to offset the border on the image + } + } + + > span { + margin-left: 12px; + font-size: $font-14px; + line-height: $font-24px; + color: $tertiary-fg-color; + } +} diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index 22440fa6db..8eda25d0c9 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -37,7 +37,7 @@ limitations under the License. .mx_RoomList_explorePrompt { margin: 4px 12px 4px; padding-top: 12px; - border-top: 1px solid $tertiary-fg-color; + border-top: 1px solid $input-border-color; font-size: $font-14px; div:first-child { diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 7a751ad9c1..cf1fd17e58 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -123,7 +123,6 @@ $roomsublist-divider-color: $primary-fg-color; $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%); $groupFilterPanel-divider-color: $roomlist-header-color; -$space-button-outline-color: rgba(141, 151, 165, 0.2); $roomtile-preview-color: $secondary-fg-color; $roomtile-default-badge-bg-color: #61708b; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 764b8f302a..ff58314bdd 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -120,7 +120,6 @@ $roomsublist-divider-color: $primary-fg-color; $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%); $groupFilterPanel-divider-color: $roomlist-header-color; -$space-button-outline-color: rgba(141, 151, 165, 0.2); $roomtile-preview-color: #9e9e9e; $roomtile-default-badge-bg-color: #61708b; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index d7ee496d80..8938efb792 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -187,7 +187,6 @@ $roomsublist-divider-color: $primary-fg-color; $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%); $groupFilterPanel-divider-color: $roomlist-header-color; -$space-button-outline-color: #E3E8F0; $voice-record-stop-border-color: #E3E8F0; $voice-record-stop-symbol-color: $warning-color; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 577204ef0c..0db291fc41 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -178,7 +178,6 @@ $roomsublist-divider-color: $primary-fg-color; $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%); $groupFilterPanel-divider-color: $roomlist-header-color; -$space-button-outline-color: #E3E8F0; $voice-record-stop-border-color: #E3E8F0; $voice-record-stop-symbol-color: $warning-color; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 59d281a0b6..532b1f4225 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -80,10 +80,10 @@ import DialPadModal from "../views/voip/DialPadModal"; import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast'; import { shouldUseLoginForWelcome } from "../../utils/pages"; import SpaceStore from "../../stores/SpaceStore"; -import SpaceRoomDirectory from "./SpaceRoomDirectory"; import {replaceableComponent} from "../../utils/replaceableComponent"; import RoomListStore from "../../stores/room-list/RoomListStore"; import {RoomUpdateCause} from "../../stores/room-list/models"; +import defaultDispatcher from "../../dispatcher/dispatcher"; /** constants for MatrixChat.state.view */ export enum Views { @@ -690,10 +690,10 @@ export default class MatrixChat extends React.PureComponent { } case Action.ViewRoomDirectory: { if (SpaceStore.instance.activeSpace) { - Modal.createTrackedDialog("Space room directory", "", SpaceRoomDirectory, { - space: SpaceStore.instance.activeSpace, - initialText: payload.initialText, - }, "mx_SpaceRoomDirectory_dialogWrapper", false, true); + defaultDispatcher.dispatch({ + action: "view_room", + room_id: SpaceStore.instance.activeSpace.roomId, + }); } else { const RoomDirectory = sdk.getComponent("structures.RoomDirectory"); Modal.createTrackedDialog('Room directory', '', RoomDirectory, { diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index fda09f9774..eb4c65ded8 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020, 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. @@ -26,6 +26,7 @@ import { Action } from "../../dispatcher/actions"; import RoomListStore from "../../stores/room-list/RoomListStore"; import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore"; interface IProps { isMinimized: boolean; @@ -53,6 +54,8 @@ export default class RoomSearch extends React.PureComponent { }; this.dispatcherRef = defaultDispatcher.register(this.onAction); + // clear filter when changing spaces, in future we may wish to maintain a filter per-space + SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.clearInput); } public componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { @@ -72,6 +75,7 @@ export default class RoomSearch extends React.PureComponent { public componentWillUnmount() { defaultDispatcher.unregister(this.dispatcherRef); + SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput); } private onAction = (payload: ActionPayload) => { diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 0dfb33379d..930cfa15a9 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -40,10 +40,11 @@ import InfoTooltip from "../views/elements/InfoTooltip"; import TextWithTooltip from "../views/elements/TextWithTooltip"; import {useStateToggle} from "../../hooks/useStateToggle"; -interface IProps { +interface IHierarchyProps { space: Room; initialText?: string; - onFinished(): void; + refreshToken?: any; + showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void; } /* eslint-disable camelcase */ @@ -111,7 +112,7 @@ const Tile: React.FC = ({ let button; if (myMembership === "join") { button = - { _t("Open") } + { _t("View") } ; } else if (onJoinClick) { button = @@ -251,7 +252,7 @@ export const HierarchyLevel = ({ }: IHierarchyLevelProps) => { const cli = MatrixClientPeg.get(); const space = cli.getRoom(spaceId); - const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()) + const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); const sortedChildren = sortBy([...(relations.get(spaceId)?.values() || [])], ev => ev.content.order || null); const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => { @@ -344,22 +345,20 @@ export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: a }, [space, refreshToken], []); }; -const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinished }) => { +export const SpaceHierarchy: React.FC = ({ + space, + initialText = "", + showRoom, + refreshToken, + children, +}) => { const cli = MatrixClientPeg.get(); const userId = cli.getUserId(); const [query, setQuery] = useState(initialText); - const onCreateRoomClick = () => { - dis.dispatch({ - action: 'view_create_room', - public: true, - }); - onFinished(); - }; - const [selected, setSelected] = useState(new Map>()); // Map> - const [rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space); + const [rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken); const roomsMap = useMemo(() => { if (!rooms) return null; @@ -394,21 +393,6 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis return roomsMap; }, [rooms, childParentMap, query]); - const title = - -
-

{ _t("Explore rooms") }

-
-
-
; - - const explanation = - _t("If you can't find the room you're looking for, ask for an invite or create a new room.", null, - {a: sub => { - return {sub}; - }}, - ); - const [error, setError] = useState(""); const [removing, setRemoving] = useState(false); const [saving, setSaving] = useState(false); @@ -503,6 +487,8 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis let results; if (roomsMap.size) { + const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); + results = <> = ({ space, initialText = "", onFinis relations={parentChildMap} parents={new Set()} selectedMap={selected} - onToggleClick={(parentId, childId) => { + onToggleClick={hasPermissions ? (parentId, childId) => { setError(""); if (!selected.has(parentId)) { setSelected(new Map(selected.set(parentId, new Set([childId])))); @@ -525,13 +511,12 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis parentSet.delete(childId); setSelected(new Map(selected.set(parentId, new Set(parentSet)))); - }} + } : undefined} onViewRoomClick={(roomId, autoJoin) => { showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin); - onFinished(); }} /> -
+ { children &&
} ; } else { results =
@@ -550,34 +535,78 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis
} { results } - - { _t("Create room") } - + { children } ; - } else { + } else if (!rooms) { content = ; + } else { + content =

{_t("Your server does not support showing space hierarchies.")}

; } // TODO loading state/error state + return <> + + + { content } + ; +}; + +interface IProps { + space: Room; + initialText?: string; + onFinished(): void; +} + +const SpaceRoomDirectory: React.FC = ({ space, onFinished, initialText }) => { + const onCreateRoomClick = () => { + dis.dispatch({ + action: 'view_create_room', + public: true, + }); + onFinished(); + }; + + const title = + +
+

{ _t("Explore rooms") }

+
+
+
; + return (
- { explanation } + { _t("If you can't find the room you're looking for, ask for an invite or create a new room.", + null, + {a: sub => { + return {sub}; + }}, + ) } - - - { content } + { + showRoom(room, viaServers, autoJoin); + onFinished(); + }} + initialText={initialText} + > + + { _t("Create room") } + +
); diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index cea59093ac..31358a3731 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {RefObject, useContext, useMemo, useRef, useState} from "react"; -import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; +import React, {RefObject, useContext, useRef, useState} from "react"; +import {EventType} from "matrix-js-sdk/src/@types/event"; import {Room} from "matrix-js-sdk/src/models/room"; import {EventSubscription} from "fbemitter"; @@ -46,11 +46,11 @@ import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanel import {useStateArray} from "../../hooks/useStateArray"; import SpacePublicShare from "../views/spaces/SpacePublicShare"; import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space"; -import {HierarchyLevel, ISpaceSummaryRoom, showRoom, useSpaceSummary} from "./SpaceRoomDirectory"; -import AutoHideScrollbar from "./AutoHideScrollbar"; +import {showRoom, SpaceHierarchy} from "./SpaceRoomDirectory"; import MemberAvatar from "../views/avatars/MemberAvatar"; import {useStateToggle} from "../../hooks/useStateToggle"; import SpaceStore from "../../stores/SpaceStore"; +import FacePile from "../views/elements/FacePile"; interface IProps { space: Room; @@ -92,6 +92,41 @@ const useMyRoomMembership = (room: Room) => { return membership; }; +const SpaceInfo = ({ space }) => { + const joinRule = space.getJoinRule(); + + let visibilitySection; + if (joinRule === "public") { + visibilitySection = + { _t("Public space") } + ; + } else { + visibilitySection = + { _t("Private space") } + ; + } + + return
+ { visibilitySection } + { joinRule === "public" && + {(count) => count > 0 ? ( + { + defaultDispatcher.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.RoomMemberList, + refireParams: { space }, + }); + }} + > + { _t("%(count)s members", { count }) } + + ) : null} + } +
+}; + const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => { const cli = useContext(MatrixClientContext); const myMembership = useMyRoomMembership(space); @@ -158,43 +193,13 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => joinButtons = ; } - let visibilitySection; - if (space.getJoinRule() === "public") { - visibilitySection = - { _t("Public space") } - ; - } else { - visibilitySection = - { _t("Private space") } - ; - } - return
{ inviterSection }

-
- { visibilitySection } - - {(count) => count > 0 ? ( - { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.RoomMemberList, - refireParams: { space }, - }); - }} - > - { _t("%(count)s members", { count }) } - - ) : null} - -
+ {(topic, ref) =>
@@ -202,6 +207,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
}
+ { space.getJoinRule() === "public" && }
{ joinButtons }
@@ -216,10 +222,14 @@ const SpaceLanding = ({ space }) => { let inviteButton; if (myMembership === "join" && space.canInvite(userId)) { inviteButton = ( - { - showRoomInviteDialog(space.roomId); - }}> - { _t("Invite people") } + { + showRoomInviteDialog(space.roomId); + }} + > + { _t("Invite") } ); } @@ -256,36 +266,13 @@ const SpaceLanding = ({ space }) => { ; } - const [rooms, relations, viaMap] = useSpaceSummary(cli, space, refreshToken); - const [roomsMap, numRooms] = useMemo(() => { - if (!rooms) return []; - const roomsMap = new Map(rooms.map(r => [r.room_id, r])); - const numRooms = rooms.filter(r => r.room_type !== RoomType.Space).length; - return [roomsMap, numRooms]; - }, [rooms]); - - let previewRooms; - if (roomsMap) { - previewRooms = -
-

{ myMembership === "join" ? _t("Rooms") : _t("Default Rooms")}

- { numRooms } -
- { - showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin); - }} - /> -
; - } else if (!rooms) { - previewRooms = ; - } else { - previewRooms =

{_t("Your server does not support showing space hierarchies.")}

; - } + const onMembersClick = () => { + defaultDispatcher.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.RoomMemberList, + refireParams: { space }, + }); + }; return
@@ -294,45 +281,26 @@ const SpaceLanding = ({ space }) => { {(name) => { const tags = { name: () =>

{ name }

- - {(count) => count > 0 ? ( - { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.RoomMemberList, - refireParams: { space }, - }); - }} - > - { _t("%(count)s members", { count }) } - - ) : null} -
}; - if (shouldShowSpaceSettings(cli, space)) { - if (space.getJoinRule() === "public") { - return _t("Your public space ", {}, tags) as JSX.Element; - } else { - return _t("Your private space ", {}, tags) as JSX.Element; - } - } return _t("Welcome to ", {}, tags) as JSX.Element; }}
+
+ + + { inviteButton } +
+
- { inviteButton } { addRoomButtons } { settingsButton }
- { previewRooms } +
; }; @@ -675,9 +643,13 @@ export default class SpaceRoomView extends React.PureComponent { case Phase.PublicCreateRooms: return this.setState({ phase: Phase.PublicShare })} />; case Phase.PublicShare: diff --git a/src/components/views/dialogs/SpaceSettingsDialog.tsx b/src/components/views/dialogs/SpaceSettingsDialog.tsx index b016e320eb..83f5d7141b 100644 --- a/src/components/views/dialogs/SpaceSettingsDialog.tsx +++ b/src/components/views/dialogs/SpaceSettingsDialog.tsx @@ -126,8 +126,8 @@ const SpaceSettingsDialog: React.FC = ({ matrixClient: cli, space, onFin
{ _t("Make this space private") } setJoinRule(checked ? "private" : "invite")} + checked={joinRule !== "public"} + onChange={checked => setJoinRule(checked ? "invite" : "public")} disabled={!canSetJoinRule} aria-label={_t("Make this space private")} /> diff --git a/src/components/views/elements/FacePile.tsx b/src/components/views/elements/FacePile.tsx new file mode 100644 index 0000000000..e223744352 --- /dev/null +++ b/src/components/views/elements/FacePile.tsx @@ -0,0 +1,66 @@ +/* +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, { HTMLAttributes } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { sortBy } from "lodash"; + +import MemberAvatar from "../avatars/MemberAvatar"; +import { _t } from "../../../languageHandler"; +import DMRoomMap from "../../../utils/DMRoomMap"; +import TextWithTooltip from "../elements/TextWithTooltip"; +import { useRoomMembers } from "../../../hooks/useRoomMembers"; + +const DEFAULT_NUM_FACES = 5; + +interface IProps extends HTMLAttributes { + room: Room; + onlyKnownUsers?: boolean; + numShown?: number; +} + +const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsForUserId(member.userId)?.length; + +const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }: IProps) => { + let members = useRoomMembers(room); + + // sort users with an explicit avatar first + const iteratees = [member => !!member.getMxcAvatarUrl()]; + if (onlyKnownUsers) { + members = members.filter(isKnownMember); + } else { + // sort known users first + iteratees.unshift(member => isKnownMember(member)); + } + if (members.length < 1) return null; + + const shownMembers = sortBy(members, iteratees).slice(0, numShown); + return
+
+ { shownMembers.map(member => { + return + + ; + }) } +
+ { onlyKnownUsers && + { _t("%(count)s people you know have already joined", { count: members.length }) } + } +
+}; + +export default FacePile; diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index c85b9d7868..3f6054304d 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020, 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. @@ -29,6 +29,7 @@ import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload"; import {Action} from "../../../dispatcher/actions"; import dis from "../../../dispatcher/dispatcher"; import SpaceStore from "../../../stores/SpaceStore"; +import {showSpaceInvite} from "../../../utils/space"; const NewRoomIntro = () => { const cli = useContext(MatrixClientContext); @@ -116,7 +117,7 @@ const NewRoomIntro = () => { className="mx_NewRoomIntro_inviteButton" kind="primary" onClick={() => { - dis.dispatch({ action: "view_invite", roomId }); + showSpaceInvite(parentSpace); }} > {_t("Invite to %(spaceName)s", { spaceName: parentSpace.name })} diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index e83b07f71b..963e94ebbb 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -50,14 +50,10 @@ import AccessibleButton from "../elements/AccessibleButton"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import CallHandler from "../../../CallHandler"; import SpaceStore, {SUGGESTED_ROOMS} from "../../../stores/SpaceStore"; -import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space"; +import {showAddExistingRooms, showCreateNewRoom, showSpaceInvite} from "../../../utils/space"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import RoomAvatar from "../avatars/RoomAvatar"; import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory"; -import { showRoomInviteDialog } from "../../../RoomInvite"; -import Modal from "../../../Modal"; -import SpacePublicShare from "../spaces/SpacePublicShare"; -import InfoDialog from "../dialogs/InfoDialog"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; @@ -431,21 +427,7 @@ export default class RoomList extends React.PureComponent { private onSpaceInviteClick = () => { const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search; - if (this.props.activeSpace.getJoinRule() === "public") { - const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, { - title: _t("Invite to %(spaceName)s", { spaceName: this.props.activeSpace.name }), - description: - { _t("Share your public space") } - modal.close()} /> - , - fixedWidth: false, - button: false, - className: "mx_SpacePanel_sharePublicSpace", - hasCloseButton: true, - }); - } else { - showRoomInviteDialog(this.props.activeSpace.roomId, initialText); - } + showSpaceInvite(this.props.activeSpace, initialText); }; private renderSuggestedRooms(): ReactComponentElement[] { diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 9ee6edc489..9c42b9c7c4 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -148,7 +148,7 @@ const SpaceCreateMenu = ({ onFinished }) => { - + { busy ? _t("Creating...") : _t("Create") } ; diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index ca6f90fa91..ca9e26cabe 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -34,21 +34,17 @@ import { shouldShowSpaceSettings, showAddExistingRooms, showCreateNewRoom, + showSpaceInvite, showSpaceSettings, } from "../../../utils/space"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import AccessibleButton, {ButtonEvent} from "../elements/AccessibleButton"; import defaultDispatcher from "../../../dispatcher/dispatcher"; -import Modal from "../../../Modal"; -import SpacePublicShare from "./SpacePublicShare"; import {Action} from "../../../dispatcher/actions"; import RoomViewStore from "../../../stores/RoomViewStore"; import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; -import {showRoomInviteDialog} from "../../../RoomInvite"; -import InfoDialog from "../dialogs/InfoDialog"; import {EventType} from "matrix-js-sdk/src/@types/event"; -import SpaceRoomDirectory from "../../structures/SpaceRoomDirectory"; interface IItemProps { space?: Room; @@ -115,36 +111,11 @@ export class SpaceItem extends React.PureComponent { this.setState({contextMenuPosition: null}); }; - private onHomeClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - defaultDispatcher.dispatch({ - action: "view_room", - room_id: this.props.space.roomId, - }); - this.setState({contextMenuPosition: null}); // also close the menu - }; - private onInviteClick = (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); - if (this.props.space.getJoinRule() === "public") { - const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, { - title: _t("Invite to %(spaceName)s", { spaceName: this.props.space.name }), - description: - { _t("Share your public space") } - modal.close()} /> - , - fixedWidth: false, - button: false, - className: "mx_SpacePanel_sharePublicSpace", - hasCloseButton: true, - }); - } else { - showRoomInviteDialog(this.props.space.roomId); - } + showSpaceInvite(this.props.space); this.setState({contextMenuPosition: null}); // also close the menu }; @@ -206,9 +177,10 @@ export class SpaceItem extends React.PureComponent { ev.preventDefault(); ev.stopPropagation(); - Modal.createTrackedDialog("Space room directory", "Space panel", SpaceRoomDirectory, { - space: this.props.space, - }, "mx_SpaceRoomDirectory_dialogWrapper", false, true); + defaultDispatcher.dispatch({ + action: "view_room", + room_id: this.props.space.roomId, + }); this.setState({contextMenuPosition: null}); // also close the menu }; @@ -249,6 +221,8 @@ export class SpaceItem extends React.PureComponent { ; } + const canAddRooms = this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId); + let newRoomSection; if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { newRoomSection = @@ -276,11 +250,6 @@ export class SpaceItem extends React.PureComponent {
{ inviteOption } - { { settingsOption } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2370741978..6ecc74d93d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -723,6 +723,8 @@ "Common names and surnames are easy to guess": "Common names and surnames are easy to guess", "Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess", "Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess", + "Invite to %(spaceName)s": "Invite to %(spaceName)s", + "Share your public space": "Share your public space", "Unknown App": "Unknown App", "Help us improve %(brand)s": "Help us improve %(brand)s", "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.", @@ -1012,14 +1014,12 @@ "Share invite link": "Share invite link", "Invite people": "Invite people", "Invite with email or username": "Invite with email or username", - "Invite to %(spaceName)s": "Invite to %(spaceName)s", - "Share your public space": "Share your public space", "Settings": "Settings", "Leave space": "Leave space", "Create new room": "Create new room", "Add existing room": "Add existing room", - "Space Home": "Space Home", "Members": "Members", + "Manage & explore rooms": "Manage & explore rooms", "Explore rooms": "Explore rooms", "Space options": "Space options", "Remove": "Remove", @@ -1910,6 +1910,8 @@ "Please create a new issue on GitHub so that we can investigate this bug.": "Please create a new issue on GitHub so that we can investigate this bug.", "collapse": "collapse", "expand": "expand", + "%(count)s people you know have already joined|other": "%(count)s people you know have already joined", + "%(count)s people you know have already joined|one": "%(count)s person you know has already joined", "You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)", "Uploaded on %(date)s by %(user)s": "Uploaded on %(date)s by %(user)s", "Rotate Left": "Rotate Left", @@ -2610,7 +2612,6 @@ "Drop file here to upload": "Drop file here to upload", "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.", - "Open": "Open", "You don't have permission": "You don't have permission", "%(count)s members|other": "%(count)s members", "%(count)s members|one": "%(count)s member", @@ -2618,7 +2619,6 @@ "%(count)s rooms|one": "%(count)s room", "This room is suggested as a good one to join": "This room is suggested as a good one to join", "Suggested": "Suggested", - "If you can't find the room you're looking for, ask for an invite or create a new room.": "If you can't find the room you're looking for, ask for an invite or create a new room.", "%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s rooms and %(numSpaces)s spaces", "%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s room and %(numSpaces)s spaces", "%(count)s rooms and 1 space|other": "%(count)s rooms and 1 space", @@ -2629,16 +2629,14 @@ "Mark as suggested": "Mark as suggested", "No results found": "No results found", "You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.", - "Create room": "Create room", + "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.", "Search names and description": "Search names and description", - " invites you": " invites you", + "If you can't find the room you're looking for, ask for an invite or create a new room.": "If you can't find the room you're looking for, ask for an invite or create a new room.", + "Create room": "Create room", "Public space": "Public space", "Private space": "Private space", + " invites you": " invites you", "Add existing rooms & spaces": "Add existing rooms & spaces", - "Default Rooms": "Default Rooms", - "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.", - "Your public space ": "Your public space ", - "Your private space ": "Your private space ", "Welcome to ": "Welcome to ", "Random": "Random", "Support": "Support", @@ -2660,8 +2658,9 @@ "Invite your teammates": "Invite your teammates", "Make sure the right people have access. You can invite more later.": "Make sure the right people have access. You can invite more later.", "Invite by username": "Invite by username", - "What are some things you want to discuss?": "What are some things you want to discuss?", - "Let's create a room for each of them. You can add more later too, including already existing ones.": "Let's create a room for each of them. You can add more later too, including already existing ones.", + "What are some things you want to discuss in %(spaceName)s?": "What are some things you want to discuss in %(spaceName)s?", + "Let's create a room for each of them.": "Let's create a room for each of them.", + "You can add more later too, including already existing ones.": "You can add more later too, including already existing ones.", "What projects are you working on?": "What projects are you working on?", "We'll create rooms for each of them. You can add more later too, including already existing ones.": "We'll create rooms for each of them. You can add more later too, including already existing ones.", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index bcf95a82be..dec8832792 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -122,7 +122,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const data = await this.fetchSuggestedRooms(space); if (this._activeSpace === space) { this._suggestedRooms = data.rooms.filter(roomInfo => { - return roomInfo.room_type !== RoomType.Space && !this.matrixClient.getRoom(roomInfo.room_id); + return roomInfo.room_type !== RoomType.Space + && this.matrixClient.getRoom(roomInfo.room_id)?.getMyMembership() !== "join"; }); this.emit(SUGGESTED_ROOMS, this._suggestedRooms); } @@ -294,6 +295,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } }; + private onSpaceMembersChange = (ev: MatrixEvent) => { + // skip this update if we do not have a DM with this user + if (DMRoomMap.shared().getDMRoomsForUserId(ev.getStateKey()).length < 1) return; + this.onRoomsUpdate(); + }; + private onRoomsUpdate = throttle(() => { // TODO resolve some updates as deltas const visibleRooms = this.matrixClient.getVisibleRooms(); @@ -374,10 +381,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.setActiveSpace(room); } - const numSuggestedRooms = this._suggestedRooms.length; - this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId); - if (numSuggestedRooms !== this._suggestedRooms.length) { - this.emit(SUGGESTED_ROOMS, this._suggestedRooms); + if (room.getMyMembership() === "join") { + const numSuggestedRooms = this._suggestedRooms.length; + this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId); + if (numSuggestedRooms !== this._suggestedRooms.length) { + this.emit(SUGGESTED_ROOMS, this._suggestedRooms); + } } }; @@ -385,18 +394,30 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const room = this.matrixClient.getRoom(ev.getRoomId()); if (!room) return; - if (ev.getType() === EventType.SpaceChild && room.isSpaceRoom()) { - this.onSpaceUpdate(); - this.emit(room.roomId); - } else if (ev.getType() === EventType.SpaceParent) { - // TODO rebuild the space parent and not the room - check permissions? - // TODO confirm this after implementing parenting behaviour - if (room.isSpaceRoom()) { - this.onSpaceUpdate(); - } else { - this.onRoomUpdate(room); - } - this.emit(room.roomId); + switch (ev.getType()) { + case EventType.SpaceChild: + if (room.isSpaceRoom()) { + this.onSpaceUpdate(); + this.emit(room.roomId); + } + break; + + case EventType.SpaceParent: + // TODO rebuild the space parent and not the room - check permissions? + // TODO confirm this after implementing parenting behaviour + if (room.isSpaceRoom()) { + this.onSpaceUpdate(); + } else { + this.onRoomUpdate(room); + } + this.emit(room.roomId); + break; + + case EventType.RoomMember: + if (room.isSpaceRoom()) { + this.onSpaceMembersChange(ev); + } + break; } }; diff --git a/src/utils/space.ts b/src/utils/space.tsx similarity index 74% rename from src/utils/space.ts rename to src/utils/space.tsx index bc31829f45..3f2b6f9bb4 100644 --- a/src/utils/space.ts +++ b/src/utils/space.tsx @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from "react"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixClient} from "matrix-js-sdk/src/client"; import {EventType} from "matrix-js-sdk/src/@types/event"; @@ -24,6 +25,10 @@ import SpaceSettingsDialog from "../components/views/dialogs/SpaceSettingsDialog import AddExistingToSpaceDialog from "../components/views/dialogs/AddExistingToSpaceDialog"; import CreateRoomDialog from "../components/views/dialogs/CreateRoomDialog"; import createRoom, {IOpts} from "../createRoom"; +import {_t} from "../languageHandler"; +import SpacePublicShare from "../components/views/spaces/SpacePublicShare"; +import InfoDialog from "../components/views/dialogs/InfoDialog"; +import { showRoomInviteDialog } from "../RoomInvite"; export const shouldShowSpaceSettings = (cli: MatrixClient, space: Room) => { const userId = cli.getUserId(); @@ -79,3 +84,21 @@ export const showCreateNewRoom = async (cli: MatrixClient, space: Room) => { await createRoom(opts); } }; + +export const showSpaceInvite = (space: Room, initialText = "") => { + if (space.getJoinRule() === "public") { + const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, { + title: _t("Invite to %(spaceName)s", { spaceName: space.name }), + description: + { _t("Share your public space") } + modal.close()} /> + , + fixedWidth: false, + button: false, + className: "mx_SpacePanel_sharePublicSpace", + hasCloseButton: true, + }); + } else { + showRoomInviteDialog(space.roomId, initialText); + } +};