From 010baabfe6263e7ce76e30f78548f0b951cd7ca0 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 23 Jul 2021 08:46:20 +0100
Subject: [PATCH 1/6] Improve subspaces and some utilities around room/space
creation
---
res/css/_components.scss | 2 +
.../context_menus/_IconizedContextMenu.scss | 4 +
.../dialogs/_AddExistingToSpaceDialog.scss | 188 ++++++------
res/css/views/dialogs/_CreateRoomDialog.scss | 52 ----
.../views/dialogs/_CreateSubspaceDialog.scss | 75 +++++
res/css/views/dialogs/_JoinRuleDropdown.scss | 67 +++++
src/components/structures/SpaceRoomView.tsx | 38 ++-
.../context_menus/IconizedContextMenu.tsx | 3 +-
.../dialogs/AddExistingSubspaceDialog.tsx | 70 +++++
.../dialogs/AddExistingToSpaceDialog.tsx | 268 ++++++++++++------
.../views/dialogs/CreateRoomDialog.tsx | 33 +--
.../views/dialogs/CreateSubspaceDialog.tsx | 183 ++++++++++++
.../views/elements/JoinRuleDropdown.tsx | 68 +++++
.../views/spaces/SpaceCreateMenu.tsx | 172 ++++++-----
.../views/spaces/SpaceTreeLevel.tsx | 17 ++
src/createRoom.ts | 52 +++-
src/i18n/strings/en_EN.json | 26 +-
src/utils/space.tsx | 36 ++-
18 files changed, 983 insertions(+), 371 deletions(-)
create mode 100644 res/css/views/dialogs/_CreateSubspaceDialog.scss
create mode 100644 res/css/views/dialogs/_JoinRuleDropdown.scss
create mode 100644 src/components/views/dialogs/AddExistingSubspaceDialog.tsx
create mode 100644 src/components/views/dialogs/CreateSubspaceDialog.tsx
create mode 100644 src/components/views/elements/JoinRuleDropdown.tsx
diff --git a/res/css/_components.scss b/res/css/_components.scss
index f9e3ab1160..574578ccce 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -76,6 +76,7 @@
@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss";
@import "./views/dialogs/_CreateGroupDialog.scss";
@import "./views/dialogs/_CreateRoomDialog.scss";
+@import "./views/dialogs/_CreateSubspaceDialog.scss";
@import "./views/dialogs/_DeactivateAccountDialog.scss";
@import "./views/dialogs/_DevtoolsDialog.scss";
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
@@ -85,6 +86,7 @@
@import "./views/dialogs/_HostSignupDialog.scss";
@import "./views/dialogs/_IncomingSasDialog.scss";
@import "./views/dialogs/_InviteDialog.scss";
+@import "./views/dialogs/_JoinRuleDropdown.scss";
@import "./views/dialogs/_KeyboardShortcutsDialog.scss";
@import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss";
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
diff --git a/res/css/views/context_menus/_IconizedContextMenu.scss b/res/css/views/context_menus/_IconizedContextMenu.scss
index 204435995f..2aa43283b5 100644
--- a/res/css/views/context_menus/_IconizedContextMenu.scss
+++ b/res/css/views/context_menus/_IconizedContextMenu.scss
@@ -99,6 +99,10 @@ limitations under the License.
.mx_IconizedContextMenu_icon + .mx_IconizedContextMenu_label {
padding-left: 14px;
}
+
+ .mx_BetaCard_betaPill {
+ margin-left: 16px;
+ }
}
}
diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss
index 2776c477fc..b299198349 100644
--- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss
+++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss
@@ -54,11 +54,16 @@ limitations under the License.
display: flex;
margin-top: 12px;
- // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling
- .mx_DecoratedRoomAvatar {
+ .mx_DecoratedRoomAvatar, // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling
+ .mx_BaseAvatar.mx_RoomAvatar_isSpaceRoom {
margin-right: 12px;
}
+ img.mx_RoomAvatar_isSpaceRoom,
+ .mx_RoomAvatar_isSpaceRoom img {
+ border-radius: 8px;
+ }
+
.mx_AddExistingToSpace_entry_name {
font-size: $font-15px;
line-height: 30px;
@@ -73,41 +78,12 @@ limitations under the License.
align-items: center;
}
}
- }
- .mx_AddExistingToSpace_section_spaces {
- .mx_BaseAvatar {
- margin-right: 12px;
- }
-
- .mx_BaseAvatar_image {
- border-radius: 8px;
- }
- }
-
- .mx_AddExistingToSpace_section_experimental {
- position: relative;
- border-radius: 8px;
- margin: 12px 0;
- padding: 8px 8px 8px 42px;
- background-color: $header-panel-bg-color;
-
- font-size: $font-12px;
- line-height: $font-15px;
- color: $secondary-fg-color;
-
- &::before {
- content: '';
- position: absolute;
- left: 10px;
- top: calc(50% - 8px); // vertical centering
- height: 16px;
- width: 16px;
- background-color: $secondary-fg-color;
- mask-repeat: no-repeat;
- mask-size: contain;
- mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
- mask-position: center;
+ .mx_AccessibleButton_kind_link {
+ font-size: $font-12px;
+ line-height: $font-15px;
+ margin-top: 8px;
+ padding: 0;
}
}
@@ -205,77 +181,77 @@ limitations under the License.
min-height: 0;
height: 80vh;
- .mx_Dialog_title {
- display: flex;
-
- .mx_BaseAvatar_image {
- border-radius: 8px;
- margin: 0;
- vertical-align: unset;
- }
-
- .mx_BaseAvatar {
- display: inline-flex;
- margin: auto 16px auto 5px;
- vertical-align: middle;
- }
-
- > div {
- > h1 {
- font-weight: $font-semi-bold;
- font-size: $font-18px;
- line-height: $font-22px;
- margin: 0;
- }
-
- .mx_AddExistingToSpaceDialog_onlySpace {
- color: $secondary-fg-color;
- font-size: $font-15px;
- line-height: $font-24px;
- }
- }
-
- .mx_Dropdown_input {
- border: none;
-
- > .mx_Dropdown_option {
- padding-left: 0;
- flex: unset;
- height: unset;
- color: $secondary-fg-color;
- font-size: $font-15px;
- line-height: $font-24px;
-
- .mx_BaseAvatar {
- display: none;
- }
- }
-
- .mx_Dropdown_menu {
- .mx_AddExistingToSpaceDialog_dropdownOptionActive {
- color: $accent-color;
- padding-right: 32px;
- position: relative;
-
- &::before {
- content: '';
- width: 20px;
- height: 20px;
- top: 8px;
- right: 0;
- position: absolute;
- mask-position: center;
- mask-size: contain;
- mask-repeat: no-repeat;
- background-color: $accent-color;
- mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg');
- }
- }
- }
- }
- }
-
.mx_AddExistingToSpace {
display: contents;
}
}
+
+.mx_SubspaceSelector {
+ display: flex;
+
+ .mx_BaseAvatar_image {
+ border-radius: 8px;
+ margin: 0;
+ vertical-align: unset;
+ }
+
+ .mx_BaseAvatar {
+ display: inline-flex;
+ margin: auto 16px auto 5px;
+ vertical-align: middle;
+ }
+
+ > div {
+ > h1 {
+ font-weight: $font-semi-bold;
+ font-size: $font-18px;
+ line-height: $font-22px;
+ margin: 0;
+ }
+ }
+
+ .mx_Dropdown_input {
+ border: none;
+
+ > .mx_Dropdown_option {
+ padding-left: 0;
+ flex: unset;
+ height: unset;
+ color: $secondary-fg-color;
+ font-size: $font-15px;
+ line-height: $font-24px;
+
+ .mx_BaseAvatar {
+ display: none;
+ }
+ }
+
+ .mx_Dropdown_menu {
+ .mx_SubspaceSelector_dropdownOptionActive {
+ color: $accent-color;
+ padding-right: 32px;
+ position: relative;
+
+ &::before {
+ content: '';
+ width: 20px;
+ height: 20px;
+ top: 8px;
+ right: 0;
+ position: absolute;
+ mask-position: center;
+ mask-size: contain;
+ mask-repeat: no-repeat;
+ background-color: $accent-color;
+ mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg');
+ }
+ }
+ }
+ }
+
+ .mx_SubspaceSelector_onlySpace {
+ color: $secondary-fg-color;
+ font-size: $font-15px;
+ line-height: $font-24px;
+ }
+}
diff --git a/res/css/views/dialogs/_CreateRoomDialog.scss b/res/css/views/dialogs/_CreateRoomDialog.scss
index 5321d8ff69..e7cfbf6050 100644
--- a/res/css/views/dialogs/_CreateRoomDialog.scss
+++ b/res/css/views/dialogs/_CreateRoomDialog.scss
@@ -109,56 +109,4 @@ limitations under the License.
margin: 0 85px 0 0;
font-size: $font-12px;
}
-
- .mx_Dropdown {
- margin-bottom: 8px;
- font-weight: normal;
- font-family: $font-family;
- font-size: $font-14px;
- color: $primary-fg-color;
-
- .mx_Dropdown_input {
- border: 1px solid $input-border-color;
- }
-
- .mx_Dropdown_option {
- font-size: $font-14px;
- line-height: $font-32px;
- height: 32px;
- min-height: 32px;
-
- > div {
- padding-left: 30px;
- position: relative;
-
- &::before {
- content: "";
- position: absolute;
- height: 16px;
- width: 16px;
- left: 6px;
- top: 8px;
- mask-repeat: no-repeat;
- mask-position: center;
- background-color: $secondary-fg-color;
- }
- }
- }
-
- .mx_CreateRoomDialog_dropdown_invite::before {
- mask-image: url('$(res)/img/element-icons/lock.svg');
- mask-size: contain;
- }
-
- .mx_CreateRoomDialog_dropdown_public::before {
- mask-image: url('$(res)/img/globe.svg');
- mask-size: 12px;
- }
-
- .mx_CreateRoomDialog_dropdown_restricted::before {
- mask-image: url('$(res)/img/element-icons/community-members.svg');
- mask-size: contain;
- }
- }
}
-
diff --git a/res/css/views/dialogs/_CreateSubspaceDialog.scss b/res/css/views/dialogs/_CreateSubspaceDialog.scss
new file mode 100644
index 0000000000..b898ba6d73
--- /dev/null
+++ b/res/css/views/dialogs/_CreateSubspaceDialog.scss
@@ -0,0 +1,75 @@
+/*
+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_CreateSubspaceDialog_wrapper {
+ .mx_Dialog {
+ display: flex;
+ flex-direction: column;
+ }
+}
+
+.mx_CreateSubspaceDialog {
+ width: 480px;
+ color: $primary-fg-color;
+ display: flex;
+ flex-direction: column;
+ flex-wrap: nowrap;
+ min-height: 0;
+
+ .mx_CreateSubspaceDialog_content {
+ flex-grow: 1;
+
+ .mx_CreateSubspaceDialog_betaNotice {
+ padding: 12px 16px;
+ border-radius: 8px;
+ background-color: $header-panel-bg-color;
+
+ .mx_BetaCard_betaPill {
+ margin-right: 8px;
+ }
+ }
+ }
+
+ .mx_CreateSubspaceDialog_footer {
+ display: flex;
+ margin-top: 20px;
+
+ > span {
+ flex-grow: 1;
+ font-size: $font-12px;
+ line-height: $font-15px;
+ color: $secondary-fg-color;
+
+ > * {
+ vertical-align: middle;
+ }
+ }
+
+ .mx_AccessibleButton {
+ display: inline-block;
+ align-self: center;
+ }
+
+ .mx_AccessibleButton_kind_primary {
+ margin-left: 16px;
+ padding: 8px 36px;
+ }
+
+ .mx_AccessibleButton_kind_link {
+ padding: 0;
+ }
+ }
+}
diff --git a/res/css/views/dialogs/_JoinRuleDropdown.scss b/res/css/views/dialogs/_JoinRuleDropdown.scss
new file mode 100644
index 0000000000..c48a79af3c
--- /dev/null
+++ b/res/css/views/dialogs/_JoinRuleDropdown.scss
@@ -0,0 +1,67 @@
+/*
+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_JoinRuleDropdown {
+ margin-bottom: 8px;
+ font-weight: normal;
+ font-family: $font-family;
+ font-size: $font-14px;
+ color: $primary-fg-color;
+
+ .mx_Dropdown_input {
+ border: 1px solid $input-border-color;
+ }
+
+ .mx_Dropdown_option {
+ font-size: $font-14px;
+ line-height: $font-32px;
+ height: 32px;
+ min-height: 32px;
+
+ > div {
+ padding-left: 30px;
+ position: relative;
+
+ &::before {
+ content: "";
+ position: absolute;
+ height: 16px;
+ width: 16px;
+ left: 6px;
+ top: 8px;
+ mask-repeat: no-repeat;
+ mask-position: center;
+ background-color: $secondary-fg-color;
+ }
+ }
+ }
+
+ .mx_JoinRuleDropdown_invite::before {
+ mask-image: url('$(res)/img/element-icons/lock.svg');
+ mask-size: contain;
+ }
+
+ .mx_JoinRuleDropdown_public::before {
+ mask-image: url('$(res)/img/globe.svg');
+ mask-size: 12px;
+ }
+
+ .mx_JoinRuleDropdown_restricted::before {
+ mask-image: url('$(res)/img/element-icons/community-members.svg');
+ mask-size: contain;
+ }
+}
+
diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx
index 36b4d8d549..ec647e8ea8 100644
--- a/src/components/structures/SpaceRoomView.tsx
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -47,13 +47,24 @@ import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPanelPhasePayload";
import { useStateArray } from "../../hooks/useStateArray";
import SpacePublicShare from "../views/spaces/SpacePublicShare";
-import { shouldShowSpaceSettings, showAddExistingRooms, showCreateNewRoom, showSpaceSettings } from "../../utils/space";
+import {
+ shouldShowSpaceSettings,
+ showAddExistingRooms,
+ showCreateNewRoom,
+ showCreateNewSubspace,
+ showSpaceSettings,
+} from "../../utils/space";
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";
-import { AddExistingToSpace } from "../views/dialogs/AddExistingToSpaceDialog";
+import {
+ AddExistingToSpace,
+ defaultDmsRenderer,
+ defaultRoomsRenderer,
+ defaultSpacesRenderer,
+} from "../views/dialogs/AddExistingToSpaceDialog";
import { ChevronFace, ContextMenuButton, useContextMenu } from "./ContextMenu";
import IconizedContextMenu, {
IconizedContextMenuOption,
@@ -347,6 +358,22 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
}
}}
/>
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ closeMenu();
+
+ const [added] = await showCreateNewSubspace(space);
+ if (added) {
+ onNewRoomAdded();
+ }
+ }}
+ >
+
+
;
}
@@ -548,12 +575,13 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
{ _t("Skip for now") }
}
+ filterPlaceholder={_t("Search for rooms or spaces")}
onFinished={onFinished}
+ roomsRenderer={defaultRoomsRenderer}
+ spacesRenderer={defaultSpacesRenderer}
+ dmsRenderer={defaultDmsRenderer}
/>
-
-
-
;
};
diff --git a/src/components/views/context_menus/IconizedContextMenu.tsx b/src/components/views/context_menus/IconizedContextMenu.tsx
index 1d822fd246..c9506d7c98 100644
--- a/src/components/views/context_menus/IconizedContextMenu.tsx
+++ b/src/components/views/context_menus/IconizedContextMenu.tsx
@@ -90,10 +90,11 @@ export const IconizedContextMenuCheckbox: React.FC = ({
;
};
-export const IconizedContextMenuOption: React.FC = ({ label, iconClassName, ...props }) => {
+export const IconizedContextMenuOption: React.FC = ({ label, iconClassName, children, ...props }) => {
return
{ iconClassName && }
{ label }
+ { children }
;
};
diff --git a/src/components/views/dialogs/AddExistingSubspaceDialog.tsx b/src/components/views/dialogs/AddExistingSubspaceDialog.tsx
new file mode 100644
index 0000000000..f0e5e9241e
--- /dev/null
+++ b/src/components/views/dialogs/AddExistingSubspaceDialog.tsx
@@ -0,0 +1,70 @@
+/*
+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, { useState } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+
+import { _t } from '../../../languageHandler';
+import BaseDialog from "./BaseDialog";
+import AccessibleButton from "../elements/AccessibleButton";
+import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
+import { AddExistingToSpace, defaultSpacesRenderer, SubspaceSelector } from "./AddExistingToSpaceDialog";
+
+interface IProps {
+ space: Room;
+ onCreateSubspaceClick(): void;
+ onFinished(added?: boolean): void;
+}
+
+const AddExistingSubspaceDialog: React.FC = ({ space, onCreateSubspaceClick, onFinished }) => {
+ const [selectedSpace, setSelectedSpace] = useState(space);
+
+ return
+ )}
+ className="mx_AddExistingToSpaceDialog"
+ contentId="mx_AddExistingToSpace"
+ onFinished={onFinished}
+ fixedWidth={false}
+ >
+
+
+ { _t("Want to add a new space instead?") }
+
+ { _t("Create a new subspace") }
+
+ >}
+ filterPlaceholder={_t("Search for spaces")}
+ spacesRenderer={defaultSpacesRenderer}
+ />
+
+
+ onFinished(false)} />
+ ;
+};
+
+export default AddExistingSubspaceDialog;
+
diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
index 01dc729a83..a0c2a09943 100644
--- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
+++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
@@ -18,9 +18,9 @@ import React, { ReactNode, useContext, useMemo, useState } from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import { sleep } from "matrix-js-sdk/src/utils";
+import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from '../../../languageHandler';
-import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import Dropdown from "../elements/Dropdown";
import SearchBox from "../../structures/SearchBox";
@@ -42,12 +42,14 @@ import TruncatedList from "../elements/TruncatedList";
import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar";
-interface IProps extends IDialogProps {
+interface IProps {
space: Room;
- onCreateRoomClick(space: Room): void;
+ onCreateRoomClick(): void;
+ onAddSubspaceClick(): void;
+ onFinished(added?: boolean): void;
}
-const Entry = ({ room, checked, onChange }) => {
+export const Entry = ({ room, checked, onChange }) => {
return
{ room?.isSpaceRoom()
?
@@ -65,14 +67,36 @@ const Entry = ({ room, checked, onChange }) => {
interface IAddExistingToSpaceProps {
space: Room;
footerPrompt?: ReactNode;
+ filterPlaceholder: string;
emptySelectionButton?: ReactNode;
onFinished(added: boolean): void;
+ roomsRenderer?(
+ rooms: Room[],
+ selectedToAdd: Set,
+ onChange: undefined | ((checked: boolean, room: Room) => void),
+ truncateAt: number,
+ overflowTile: (overflowCount: number, totalCount: number) => JSX.Element,
+ ): ReactNode;
+ spacesRenderer?(
+ spaces: Room[],
+ selectedToAdd: Set,
+ onChange?: (checked: boolean, room: Room) => void,
+ ): ReactNode;
+ dmsRenderer?(
+ dms: Room[],
+ selectedToAdd: Set,
+ onChange?: (checked: boolean, room: Room) => void,
+ ): ReactNode;
}
export const AddExistingToSpace: React.FC = ({
space,
footerPrompt,
emptySelectionButton,
+ filterPlaceholder,
+ roomsRenderer,
+ dmsRenderer,
+ spacesRenderer,
onFinished,
}) => {
const cli = useContext(MatrixClientContext);
@@ -196,7 +220,7 @@ export const AddExistingToSpace: React.FC = ({
>;
}
- const onChange = !busy && !error ? (checked, room) => {
+ const onChange = !busy && !error ? (checked: boolean, room: Room) => {
if (checked) {
selectedToAdd.add(room);
} else {
@@ -206,83 +230,52 @@ export const AddExistingToSpace: React.FC = ({
} : null;
const [truncateAt, setTruncateAt] = useState(20);
- function overflowTile(overflowCount, totalCount) {
+ function overflowTile(overflowCount: number, totalCount: number): JSX.Element {
const text = _t("and %(count)s others...", { count: overflowCount });
return (
-
- } name={text} presenceState="online" suppressOnHover={true}
- onClick={() => setTruncateAt(totalCount)} />
+
+ }
+ name={text}
+ presenceState="online"
+ suppressOnHover={true}
+ onClick={() => setTruncateAt(totalCount)}
+ />
);
}
+ let noResults = true;
+ if ((roomsRenderer && rooms.length > 0) ||
+ (dmsRenderer && dms.length > 0) ||
+ (!roomsRenderer && !dmsRenderer && spacesRenderer && dms.length > 0) // only count spaces when alone
+ ) {
+ noResults = false;
+ }
+
return
- { rooms.length > 0 ? (
-
-
{ _t("Rooms") }
- rooms.slice(start, end).map(room =>
- {
- onChange(checked, room);
- } : null}
- />,
- )}
- getChildCount={() => rooms.length}
- />
-
+ { rooms.length > 0 && roomsRenderer ? (
+ roomsRenderer(rooms, selectedToAdd, onChange, truncateAt, overflowTile)
) : undefined }
- { spaces.length > 0 ? (
-
-
{ _t("Spaces") }
-
-
{ _t("Feeling experimental?") }
-
{ _t("You can add existing spaces to a space.") }
-
- { spaces.map(space => {
- return
{
- onChange(checked, space);
- } : null}
- />;
- }) }
-
+ { spaces.length > 0 && spacesRenderer ? (
+ spacesRenderer(spaces, selectedToAdd, onChange)
) : null }
- { dms.length > 0 ? (
-
-
{ _t("Direct Messages") }
- { dms.map(room => {
- return {
- onChange(checked, room);
- } : null}
- />;
- }) }
-
+ { dms.length > 0 && dmsRenderer ? (
+ dmsRenderer(dms, selectedToAdd, onChange)
) : null }
- { spaces.length + rooms.length + dms.length < 1 ?
+ { noResults ?
{ _t("No results") }
: undefined }
@@ -293,50 +286,126 @@ export const AddExistingToSpace: React.FC
= ({
;
};
-const AddExistingToSpaceDialog: React.FC = ({ space, onCreateRoomClick, onFinished }) => {
- const [selectedSpace, setSelectedSpace] = useState(space);
- const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
+export const defaultRoomsRenderer: IAddExistingToSpaceProps["roomsRenderer"] = (
+ rooms, selectedToAdd, onChange, truncateAt, overflowTile,
+) => (
+
+
{ _t("Rooms") }
+ rooms.slice(start, end).map(room =>
+ {
+ onChange(checked, room);
+ } : null}
+ />,
+ )}
+ getChildCount={() => rooms.length}
+ />
+
+);
- let spaceOptionSection;
- if (existingSubspaces.length > 0) {
- const options = [space, ...existingSubspaces].map((space) => {
- const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
- mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
- });
- return
-
- { space.name || getDisplayAliasForRoom(space) || space.roomId }
-
;
- });
+export const defaultSpacesRenderer: IAddExistingToSpaceProps["spacesRenderer"] = (spaces, selectedToAdd, onChange) => (
+
+ { spaces.map(space => {
+ return {
+ onChange(checked, space);
+ } : null}
+ />;
+ }) }
+
+);
- spaceOptionSection = (
+export const defaultDmsRenderer: IAddExistingToSpaceProps["dmsRenderer"] = (dms, selectedToAdd, onChange) => (
+
+
{ _t("Direct Messages") }
+ { dms.map(room => {
+ return {
+ onChange(checked, room);
+ } : null}
+ />;
+ }) }
+
+);
+
+interface ISubspaceSelectorProps {
+ title: string;
+ space: Room;
+ value: Room;
+ onChange(space: Room): void;
+}
+
+export const SubspaceSelector = ({ title, space, value, onChange }: ISubspaceSelectorProps) => {
+ const options = useMemo(() => {
+ return [space, ...SpaceStore.instance.getChildSpaces(space.roomId).filter(space => {
+ return space.currentState.maySendStateEvent(EventType.SpaceChild, space.client.credentials.userId);
+ })];
+ }, [space]);
+
+ let body;
+ if (options.length > 1) {
+ body = (
{
- setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
+ onChange(options.find(space => space.roomId === key) || space);
}}
- value={selectedSpace.roomId}
+ value={value.roomId}
label={_t("Space selection")}
>
- { options }
+ { options.map((space) => {
+ const classes = classNames({
+ mx_SubspaceSelector_dropdownOptionActive: space === value,
+ });
+ return
+
+ { space.name || getDisplayAliasForRoom(space) || space.roomId }
+
;
+ }) }
);
} else {
- spaceOptionSection =
- { space.name || getDisplayAliasForRoom(space) || space.roomId }
-
;
+ body = (
+
+ { space.name || getDisplayAliasForRoom(space) || space.roomId }
+
+ );
}
- const title =
-
+ return
+
-
{ _t("Add existing rooms") }
- { spaceOptionSection }
+ { title }
+ { body }
- ;
+
;
+};
+
+const AddExistingToSpaceDialog: React.FC = ({ space, onCreateRoomClick, onAddSubspaceClick, onFinished }) => {
+ const [selectedSpace, setSelectedSpace] = useState(space);
return
+ )}
className="mx_AddExistingToSpaceDialog"
contentId="mx_AddExistingToSpace"
onFinished={onFinished}
@@ -348,10 +417,27 @@ const AddExistingToSpaceDialog: React.FC = ({ space, onCreateRoomClick,
onFinished={onFinished}
footerPrompt={<>
{ _t("Want to add a new room instead?") }
- onCreateRoomClick(space)} kind="link">
+ {
+ onCreateRoomClick();
+ onFinished();
+ }}>
{ _t("Create a new room") }
>}
+ filterPlaceholder={_t("Search for rooms")}
+ roomsRenderer={defaultRoomsRenderer}
+ spacesRenderer={() => (
+
+
{ _t("Spaces") }
+
{
+ onAddSubspaceClick();
+ onFinished();
+ }}>
+ { _t("Adding spaces has moved.") }
+
+
+ )}
+ dmsRenderer={defaultDmsRenderer}
/>
diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx
index b5d8421ae0..8198f78f05 100644
--- a/src/components/views/dialogs/CreateRoomDialog.tsx
+++ b/src/components/views/dialogs/CreateRoomDialog.tsx
@@ -32,8 +32,8 @@ import RoomAliasField from "../elements/RoomAliasField";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "../dialogs/BaseDialog";
-import Dropdown from "../elements/Dropdown";
import SpaceStore from "../../../stores/SpaceStore";
+import JoinRuleDropdown from "../elements/JoinRuleDropdown";
interface IProps {
defaultPublic?: boolean;
@@ -321,21 +321,6 @@ export default class CreateRoomDialog extends React.Component {
title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room');
}
- const options = [
-
- { _t("Private room (invite only)") }
-
,
-
- { _t("Public room") }
-
,
- ];
-
- if (this.supportsRestricted) {
- options.unshift(
- { _t("Visible to space members") }
-
);
- }
-
return (
;
+};
+
+export default CreateSubspaceDialog;
+
diff --git a/src/components/views/elements/JoinRuleDropdown.tsx b/src/components/views/elements/JoinRuleDropdown.tsx
new file mode 100644
index 0000000000..e2d9b6d872
--- /dev/null
+++ b/src/components/views/elements/JoinRuleDropdown.tsx
@@ -0,0 +1,68 @@
+/*
+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 from 'react';
+import { JoinRule } from 'matrix-js-sdk/src/@types/partials';
+
+import Dropdown from "./Dropdown";
+
+interface IProps {
+ value: JoinRule;
+ label: string;
+ width?: number;
+ labelInvite: string;
+ labelPublic: string;
+ labelRestricted?: string; // if omitted then this option will be hidden, e.g if unsupported
+ onChange(value: JoinRule): void;
+}
+
+const JoinRuleDropdown = ({
+ label,
+ labelInvite,
+ labelPublic,
+ labelRestricted,
+ value,
+ width = 448,
+ onChange,
+}: IProps) => {
+ const options = [
+
+ { labelInvite }
+
,
+
+ { labelPublic }
+
,
+ ];
+
+ if (labelRestricted) {
+ options.unshift(
+ { labelRestricted }
+
);
+ }
+
+ return
+ { options }
+ ;
+};
+
+export default JoinRuleDropdown;
diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx
index 5f16684fb8..16c8ec6fb5 100644
--- a/src/components/views/spaces/SpaceCreateMenu.tsx
+++ b/src/components/views/spaces/SpaceCreateMenu.tsx
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { useContext, useRef, useState } from "react";
+import React, { ComponentProps, RefObject, SyntheticEvent, useContext, useRef, useState } from "react";
import classNames from "classnames";
-import { EventType, RoomType, RoomCreateTypeField } from "matrix-js-sdk/src/@types/event";
+import { RoomType } from "matrix-js-sdk/src/@types/event";
import FocusLock from "react-focus-lock";
import { _t } from "../../../languageHandler";
@@ -24,7 +24,7 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { ChevronFace, ContextMenu } from "../../structures/ContextMenu";
import createRoom from "../../../createRoom";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
-import { SpaceAvatar } from "./SpaceBasicSettings";
+import SpaceBasicSettings, { SpaceAvatar } from "./SpaceBasicSettings";
import AccessibleButton from "../elements/AccessibleButton";
import { BetaPill } from "../beta/BetaCard";
import defaultDispatcher from "../../../dispatcher/dispatcher";
@@ -33,8 +33,7 @@ import { UserTab } from "../dialogs/UserSettingsDialog";
import Field from "../elements/Field";
import withValidation from "../elements/Validation";
import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
-import { Preset } from "matrix-js-sdk/src/@types/partials";
-import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
+import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
import RoomAliasField from "../elements/RoomAliasField";
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
@@ -66,8 +65,83 @@ const nameToAlias = (name: string, domain: string): string => {
return `#${localpart}:${domain}`;
};
-const SpaceCreateMenu = ({ onFinished }) => {
+type BProps = Pick, "setAvatar" | "name" | "setName" | "topic" | "setTopic">;
+interface ISpaceCreateFormProps extends BProps {
+ busy: boolean;
+ alias: string;
+ nameFieldRef: RefObject;
+ aliasFieldRef: RefObject;
+ showAliasField?: boolean;
+ onSubmit(e: SyntheticEvent): void;
+ setAlias(alias: string): void;
+}
+
+export const SpaceCreateForm: React.FC = ({
+ busy,
+ onSubmit,
+ setAvatar,
+ name,
+ setName,
+ nameFieldRef,
+ alias,
+ aliasFieldRef,
+ setAlias,
+ showAliasField,
+ topic,
+ setTopic,
+ children,
+}) => {
const cli = useContext(MatrixClientContext);
+ const domain = cli.getDomain();
+
+ return ;
+};
+
+const SpaceCreateMenu = ({ onFinished }) => {
const [visibility, setVisibility] = useState(null);
const [busy, setBusy] = useState(false);
@@ -98,42 +172,26 @@ const SpaceCreateMenu = ({ onFinished }) => {
return;
}
- const initialState: ICreateRoomStateEvent[] = [
- {
- type: EventType.RoomHistoryVisibility,
- content: {
- "history_visibility": visibility === Visibility.Public ? "world_readable" : "invited",
- },
- },
- ];
- if (avatar) {
- const url = await cli.uploadContent(avatar);
-
- initialState.push({
- type: EventType.RoomAvatar,
- content: { url },
- });
- }
-
try {
await createRoom({
createOpts: {
preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat,
name,
- creation_content: {
- [RoomCreateTypeField]: RoomType.Space,
- },
- initial_state: initialState,
power_level_content_override: {
// Only allow Admins to write to the timeline to prevent hidden sync spam
events_default: 100,
- ...Visibility.Public ? { invite: 0 } : {},
+ ...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,
@@ -171,7 +229,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
;
} else {
- const domain = cli.getDomain();
body =
{
}
-
+
{ busy ? _t("Creating...") : _t("Create") }
diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx
index 90584a5361..2312494641 100644
--- a/src/components/views/spaces/SpaceTreeLevel.tsx
+++ b/src/components/views/spaces/SpaceTreeLevel.tsx
@@ -34,6 +34,7 @@ import {
shouldShowSpaceSettings,
showAddExistingRooms,
showCreateNewRoom,
+ showCreateNewSubspace,
showSpaceInvite,
showSpaceSettings,
} from "../../../utils/space";
@@ -48,6 +49,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
+import { BetaPill } from "../beta/BetaCard";
interface IItemProps extends InputHTMLAttributes {
space?: Room;
@@ -234,6 +236,14 @@ export class SpaceItem extends React.PureComponent {
this.setState({ contextMenuPosition: null }); // also close the menu
};
+ private onNewSubspaceClick = (ev: ButtonEvent) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ showCreateNewSubspace(this.props.space);
+ this.setState({ contextMenuPosition: null }); // also close the menu
+ };
+
private onMembersClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
@@ -318,6 +328,13 @@ export class SpaceItem extends React.PureComponent {
label={_t("Add existing room")}
onClick={this.onAddExistingRoomClick}
/>
+
+
+
;
}
diff --git a/src/createRoom.ts b/src/createRoom.ts
index 613fe26c9e..a3b06fa8ba 100644
--- a/src/createRoom.ts
+++ b/src/createRoom.ts
@@ -18,9 +18,15 @@ limitations under the License.
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
-import { EventType } from "matrix-js-sdk/src/@types/event";
+import { EventType, RoomCreateTypeField, RoomType } from "matrix-js-sdk/src/@types/event";
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
-import { JoinRule, Preset, RestrictedAllowType, Visibility } from "matrix-js-sdk/src/@types/partials";
+import {
+ HistoryVisibility,
+ JoinRule,
+ Preset,
+ RestrictedAllowType,
+ Visibility,
+} from "matrix-js-sdk/src/@types/partials";
import { MatrixClientPeg } from './MatrixClientPeg';
import Modal from './Modal';
@@ -52,6 +58,9 @@ export interface IOpts {
inlineErrors?: boolean;
andView?: boolean;
associatedWithCommunity?: string;
+ avatar?: File | string; // will upload if given file, else mxcUrl is needed
+ roomType?: RoomType | string;
+ historyVisibility?: HistoryVisibility;
parentSpace?: Room;
joinRule?: JoinRule;
}
@@ -112,6 +121,13 @@ export default async function createRoom(opts: IOpts): Promise {
createOpts.is_direct = true;
}
+ if (opts.roomType) {
+ createOpts.creation_content = {
+ ...createOpts.creation_content,
+ [RoomCreateTypeField]: opts.roomType,
+ };
+ }
+
// By default, view the room after creating it
if (opts.andView === undefined) {
opts.andView = true;
@@ -144,12 +160,11 @@ export default async function createRoom(opts: IOpts): Promise {
if (opts.parentSpace) {
createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true));
- createOpts.initial_state.push({
- type: EventType.RoomHistoryVisibility,
- content: {
- "history_visibility": createOpts.preset === Preset.PublicChat ? "world_readable" : "invited",
- },
- });
+ if (!opts.historyVisibility) {
+ opts.historyVisibility = createOpts.preset === Preset.PublicChat
+ ? HistoryVisibility.WorldReadable
+ : HistoryVisibility.Invited;
+ }
if (opts.joinRule === JoinRule.Restricted) {
if (SpaceStore.instance.restrictedJoinRuleSupport?.preferred) {
@@ -176,6 +191,27 @@ export default async function createRoom(opts: IOpts): Promise {
});
}
+ if (opts.avatar) {
+ let url = opts.avatar;
+ if (opts.avatar instanceof File) {
+ url = await client.uploadContent(opts.avatar);
+ }
+
+ createOpts.initial_state.push({
+ type: EventType.RoomAvatar,
+ content: { url },
+ });
+ }
+
+ if (opts.historyVisibility) {
+ createOpts.initial_state.push({
+ type: EventType.RoomHistoryVisibility,
+ content: {
+ "history_visibility": opts.historyVisibility,
+ },
+ });
+ }
+
let modal;
if (opts.spinner) modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index b36910b41b..f2acde22cb 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1008,6 +1008,8 @@
"Name": "Name",
"Description": "Description",
"Please enter a name for the space": "Please enter a name for the space",
+ "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.",
"Public": "Public",
@@ -1020,8 +1022,6 @@
"Your private space": "Your private space",
"Add some details to help people recognise it.": "Add some details to help people recognise it.",
"You can change these anytime.": "You can change these anytime.",
- "e.g. my-space": "e.g. my-space",
- "Address": "Address",
"Creating...": "Creating...",
"Create": "Create",
"All rooms": "All rooms",
@@ -1059,6 +1059,7 @@
"Leave space": "Leave space",
"Create new room": "Create new room",
"Add existing room": "Add existing room",
+ "Add subspace": "Add subspace",
"Members": "Members",
"Manage & explore rooms": "Manage & explore rooms",
"Explore rooms": "Explore rooms",
@@ -2108,17 +2109,20 @@
"Add a new server...": "Add a new server...",
"%(networkName)s rooms": "%(networkName)s rooms",
"Matrix rooms": "Matrix rooms",
+ "Add existing space": "Add existing space",
+ "Want to add a new space instead?": "Want to add a new space instead?",
+ "Create a new subspace": "Create a new subspace",
+ "Search for spaces": "Search for spaces",
"Not all selected were added": "Not all selected were added",
"Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)",
"Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...",
- "Filter your rooms and spaces": "Filter your rooms and spaces",
- "Feeling experimental?": "Feeling experimental?",
- "You can add existing spaces to a space.": "You can add existing spaces to a space.",
"Direct Messages": "Direct Messages",
"Space selection": "Space selection",
"Add existing rooms": "Add existing rooms",
"Want to add a new room instead?": "Want to add a new room instead?",
"Create a new room": "Create a new room",
+ "Search for rooms": "Search for rooms",
+ "Adding spaces has moved.": "Adding spaces has moved.",
"Matrix ID": "Matrix ID",
"Matrix Room ID": "Matrix Room ID",
"email address": "email address",
@@ -2205,13 +2209,20 @@
"Create a room in %(communityName)s": "Create a room in %(communityName)s",
"Create a public room": "Create a public room",
"Create a private room": "Create a private room",
+ "Topic (optional)": "Topic (optional)",
+ "Room visibility": "Room visibility",
"Private room (invite only)": "Private room (invite only)",
"Public room": "Public room",
"Visible to space members": "Visible to space members",
- "Topic (optional)": "Topic (optional)",
- "Room visibility": "Room visibility",
"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",
+ "Create a subspace": "Create a subspace",
+ "Add a subspace to a space you manage.": "Add a subspace to a space you manage.",
+ "Subspace visibility": "Subspace visibility",
+ "Private subspace (invite only)": "Private subspace (invite only)",
+ "Public subspace": "Public subspace",
+ "Want to add an existing space instead?": "Want to add an existing space instead?",
+ "Adding...": "Adding...",
"Sign out": "Sign out",
"To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this",
"You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.",
@@ -2813,6 +2824,7 @@
"Creating rooms...": "Creating rooms...",
"What do you want to organise?": "What do you want to organise?",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.",
+ "Search for rooms or spaces": "Search for rooms or spaces",
"Share %(name)s": "Share %(name)s",
"It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.",
"Go to my first room": "Go to my first room",
diff --git a/src/utils/space.tsx b/src/utils/space.tsx
index c238a83bc2..1e8a6fbeaf 100644
--- a/src/utils/space.tsx
+++ b/src/utils/space.tsx
@@ -28,6 +28,8 @@ import { _t } from "../languageHandler";
import SpacePublicShare from "../components/views/spaces/SpacePublicShare";
import InfoDialog from "../components/views/dialogs/InfoDialog";
import { showRoomInviteDialog } from "../RoomInvite";
+import CreateSubspaceDialog from "../components/views/dialogs/CreateSubspaceDialog";
+import AddExistingSubspaceDialog from "../components/views/dialogs/AddExistingSubspaceDialog";
export const shouldShowSpaceSettings = (space: Room) => {
const userId = space.client.getUserId();
@@ -54,18 +56,18 @@ export const showSpaceSettings = (space: Room) => {
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
};
-export const showAddExistingRooms = async (space: Room) => {
+export const showAddExistingRooms = async (space: Room): Promise<[boolean]> => {
return Modal.createTrackedDialog(
"Space Landing",
"Add Existing",
AddExistingToSpaceDialog,
{
- matrixClient: space.client,
- onCreateRoomClick: showCreateNewRoom,
+ onCreateRoomClick: () => showCreateNewRoom(space),
+ onAddSubspaceClick: () => showAddExistingSubspace(space),
space,
},
"mx_AddExistingToSpaceDialog_wrapper",
- ).finished;
+ ).finished as Promise<[boolean]>;
};
export const showCreateNewRoom = async (space: Room) => {
@@ -102,3 +104,29 @@ export const showSpaceInvite = (space: Room, initialText = "") => {
showRoomInviteDialog(space.roomId, initialText);
}
};
+
+export const showAddExistingSubspace = async (space: Room): Promise<[boolean]> => {
+ return Modal.createTrackedDialog(
+ "Space Landing",
+ "Create Subspace",
+ AddExistingSubspaceDialog,
+ {
+ space,
+ onCreateSubspaceClick: () => showCreateNewSubspace(space),
+ },
+ "mx_AddExistingToSpaceDialog_wrapper",
+ ).finished as Promise<[boolean]>;
+};
+
+export const showCreateNewSubspace = async (space: Room): Promise<[boolean]> => {
+ return Modal.createTrackedDialog(
+ "Space Landing",
+ "Create Subspace",
+ CreateSubspaceDialog,
+ {
+ space,
+ onAddExistingSpaceClick: () => showAddExistingSubspace(space),
+ },
+ "mx_CreateSubspaceDialog_wrapper",
+ ).finished as Promise<[boolean]>;
+};
From b89434fcbc52b49fe2e557b7c8798c01ed6e0123 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 23 Jul 2021 16:03:15 +0100
Subject: [PATCH 2/6] avoid abusing spans because you're too lazy to give
things a class
---
res/css/views/dialogs/_CreateSubspaceDialog.scss | 2 +-
src/components/views/dialogs/CreateSubspaceDialog.tsx | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/res/css/views/dialogs/_CreateSubspaceDialog.scss b/res/css/views/dialogs/_CreateSubspaceDialog.scss
index b898ba6d73..c780da4b9e 100644
--- a/res/css/views/dialogs/_CreateSubspaceDialog.scss
+++ b/res/css/views/dialogs/_CreateSubspaceDialog.scss
@@ -47,7 +47,7 @@ limitations under the License.
display: flex;
margin-top: 20px;
- > span {
+ .mx_CreateSubspaceDialog_footer_prompt {
flex-grow: 1;
font-size: $font-12px;
line-height: $font-15px;
diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx
index 55fa0ee881..9f89490f5f 100644
--- a/src/components/views/dialogs/CreateSubspaceDialog.tsx
+++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx
@@ -158,7 +158,7 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick
-
+
{ _t("Want to add an existing space instead?") }
{
onAddExistingSpaceClick();
@@ -166,7 +166,7 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick
}}>
{ _t("Add existing space") }
-
+
onFinished(false)}>
{ _t("Cancel") }
From 737aa1c55d1dac22288f6e612f0a337afed719e1 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 26 Jul 2021 12:41:52 +0100
Subject: [PATCH 3/6] delint
---
.../dialogs/AddExistingToSpaceDialog.tsx | 22 ++++++++++++-------
.../views/dialogs/CreateSubspaceDialog.tsx | 11 ++++++----
2 files changed, 21 insertions(+), 12 deletions(-)
diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
index a0c2a09943..7194f3d7e2 100644
--- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
+++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
@@ -417,10 +417,13 @@ const AddExistingToSpaceDialog: React.FC = ({ space, onCreateRoomClick,
onFinished={onFinished}
footerPrompt={<>
{ _t("Want to add a new room instead?") }
- {
- onCreateRoomClick();
- onFinished();
- }}>
+ {
+ onCreateRoomClick();
+ onFinished();
+ }}
+ >
{ _t("Create a new room") }
>}
@@ -429,10 +432,13 @@ const AddExistingToSpaceDialog: React.FC = ({ space, onCreateRoomClick,
spacesRenderer={() => (
{ _t("Spaces") }
-
{
- onAddSubspaceClick();
- onFinished();
- }}>
+ {
+ onAddSubspaceClick();
+ onFinished();
+ }}
+ >
{ _t("Adding spaces has moved.") }
diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx
index 9f89490f5f..12fd6a3232 100644
--- a/src/components/views/dialogs/CreateSubspaceDialog.tsx
+++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx
@@ -160,10 +160,13 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick
{ _t("Want to add an existing space instead?") }
-
{
- onAddExistingSpaceClick();
- onFinished();
- }}>
+ {
+ onAddExistingSpaceClick();
+ onFinished();
+ }}
+ >
{ _t("Add existing space") }
From ed950875e7d3cf8f38cf8b787f4376c08d34a173 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 28 Jul 2021 19:08:59 +0100
Subject: [PATCH 4/6] Fix space hierarchy not updating when user mutates it
---
.../structures/SpaceRoomDirectory.tsx | 20 +++++++---
src/components/structures/SpaceRoomView.tsx | 32 ++++-----------
.../dialogs/AddExistingToSpaceDialog.tsx | 2 +-
src/dispatcher/actions.ts | 5 +++
src/utils/space.tsx | 40 ++++++++++++++-----
5 files changed, 57 insertions(+), 42 deletions(-)
diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx
index 038c1df514..610ac5e103 100644
--- a/src/components/structures/SpaceRoomDirectory.tsx
+++ b/src/components/structures/SpaceRoomDirectory.tsx
@@ -44,11 +44,13 @@ import { getChildOrder } from "../../stores/SpaceStore";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { linkifyElement } from "../../HtmlUtils";
import { getDisplayAliasForAliasSet } from "../../Rooms";
+import { useDispatcher } from "../../hooks/useDispatcher";
+import defaultDispatcher from "../../dispatcher/dispatcher";
+import { Action } from "../../dispatcher/actions";
interface IHierarchyProps {
space: Room;
initialText?: string;
- refreshToken?: any;
additionalButtons?: ReactNode;
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
}
@@ -315,18 +317,25 @@ export const HierarchyLevel = ({
;
};
-// mutate argument refreshToken to force a reload
-export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [
+export const useSpaceSummary = (space: Room): [
null,
ISpaceSummaryRoom[],
Map
>?,
Map>?,
Map>?,
] | [Error] => {
+ // crude temporary refresh token approach until we have pagination and rework the data flow here
+ const [refreshToken, setRefreshToken] = useState(0);
+ useDispatcher(defaultDispatcher, (payload => {
+ if (payload.action === Action.UpdateSpaceHierarchy) {
+ setRefreshToken(t => t + 1);
+ }
+ }));
+
// TODO pagination
return useAsyncMemo(async () => {
try {
- const data = await cli.getSpaceSummary(space.roomId);
+ const data = await space.client.getSpaceSummary(space.roomId);
const parentChildRelations = new EnhancedMap>();
const childParentRelations = new EnhancedMap>();
@@ -354,7 +363,6 @@ export const SpaceHierarchy: React.FC = ({
space,
initialText = "",
showRoom,
- refreshToken,
additionalButtons,
children,
}) => {
@@ -364,7 +372,7 @@ export const SpaceHierarchy: React.FC = ({
const [selected, setSelected] = useState(new Map>()); // Map>
- const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken);
+ const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(space);
const roomsMap = useMemo(() => {
if (!rooms) return null;
diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx
index cf5cad1651..5829578cd2 100644
--- a/src/components/structures/SpaceRoomView.tsx
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -56,7 +56,6 @@ import {
} from "../../utils/space";
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";
import {
@@ -318,7 +317,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
;
};
-const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
+const SpaceLandingAddButton = ({ space }) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
let contextMenu;
@@ -342,36 +341,28 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
closeMenu();
if (await showCreateNewRoom(space)) {
- onNewRoomAdded();
+ defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
}
}}
/>
{
+ onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
-
- const [added] = await showAddExistingRooms(space);
- if (added) {
- onNewRoomAdded();
- }
+ showAddExistingRooms(space);
}}
/>
{
+ onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
-
- const [added] = await showCreateNewSubspace(space);
- if (added) {
- onNewRoomAdded();
- }
+ showCreateNewSubspace(space);
}}
>
@@ -416,11 +407,9 @@ const SpaceLanding = ({ space }) => {
const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
- const [refreshToken, forceUpdate] = useStateToggle(false);
-
let addRoomButton;
if (canAddRooms) {
- addRoomButton = ;
+ addRoomButton = ;
}
let settingsButton;
@@ -470,12 +459,7 @@ const SpaceLanding = ({ space }) => {
-
+
;
};
diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
index 7194f3d7e2..9b64af0c80 100644
--- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
+++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
@@ -249,7 +249,7 @@ export const AddExistingToSpace: React.FC = ({
let noResults = true;
if ((roomsRenderer && rooms.length > 0) ||
(dmsRenderer && dms.length > 0) ||
- (!roomsRenderer && !dmsRenderer && spacesRenderer && dms.length > 0) // only count spaces when alone
+ (!roomsRenderer && !dmsRenderer && spacesRenderer && spaces.length > 0) // only count spaces when alone
) {
noResults = false;
}
diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts
index 5732428201..06cbbba46c 100644
--- a/src/dispatcher/actions.ts
+++ b/src/dispatcher/actions.ts
@@ -193,4 +193,9 @@ export enum Action {
* Switches space. Should be used with SwitchSpacePayload.
*/
SwitchSpace = "switch_space",
+
+ /**
+ * Signals to the visible space hierarchy that a change has occurred an that it should refresh.
+ */
+ UpdateSpaceHierarchy = "update_space_hierarchy",
}
diff --git a/src/utils/space.tsx b/src/utils/space.tsx
index 1e8a6fbeaf..e705b4eee4 100644
--- a/src/utils/space.tsx
+++ b/src/utils/space.tsx
@@ -30,6 +30,9 @@ import InfoDialog from "../components/views/dialogs/InfoDialog";
import { showRoomInviteDialog } from "../RoomInvite";
import CreateSubspaceDialog from "../components/views/dialogs/CreateSubspaceDialog";
import AddExistingSubspaceDialog from "../components/views/dialogs/AddExistingSubspaceDialog";
+import defaultDispatcher from "../dispatcher/dispatcher";
+import RoomViewStore from "../stores/RoomViewStore";
+import { Action } from "../dispatcher/actions";
export const shouldShowSpaceSettings = (space: Room) => {
const userId = space.client.getUserId();
@@ -56,8 +59,8 @@ export const showSpaceSettings = (space: Room) => {
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
};
-export const showAddExistingRooms = async (space: Room): Promise<[boolean]> => {
- return Modal.createTrackedDialog(
+export const showAddExistingRooms = (space: Room): void => {
+ Modal.createTrackedDialog(
"Space Landing",
"Add Existing",
AddExistingToSpaceDialog,
@@ -65,12 +68,17 @@ export const showAddExistingRooms = async (space: Room): Promise<[boolean]> => {
onCreateRoomClick: () => showCreateNewRoom(space),
onAddSubspaceClick: () => showAddExistingSubspace(space),
space,
+ onFinished: (added: boolean) => {
+ if (added && RoomViewStore.getRoomId() === space.roomId) {
+ defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
+ }
+ },
},
"mx_AddExistingToSpaceDialog_wrapper",
- ).finished as Promise<[boolean]>;
+ );
};
-export const showCreateNewRoom = async (space: Room) => {
+export const showCreateNewRoom = async (space: Room): Promise => {
const modal = Modal.createTrackedDialog<[boolean, IOpts]>(
"Space Landing",
"Create Room",
@@ -87,7 +95,7 @@ export const showCreateNewRoom = async (space: Room) => {
return shouldCreate;
};
-export const showSpaceInvite = (space: Room, initialText = "") => {
+export const showSpaceInvite = (space: Room, initialText = ""): void => {
if (space.getJoinRule() === "public") {
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
title: _t("Invite to %(spaceName)s", { spaceName: space.name }),
@@ -105,28 +113,38 @@ export const showSpaceInvite = (space: Room, initialText = "") => {
}
};
-export const showAddExistingSubspace = async (space: Room): Promise<[boolean]> => {
- return Modal.createTrackedDialog(
+export const showAddExistingSubspace = (space: Room): void => {
+ Modal.createTrackedDialog(
"Space Landing",
"Create Subspace",
AddExistingSubspaceDialog,
{
space,
onCreateSubspaceClick: () => showCreateNewSubspace(space),
+ onFinished: (added: boolean) => {
+ if (added && RoomViewStore.getRoomId() === space.roomId) {
+ defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
+ }
+ },
},
"mx_AddExistingToSpaceDialog_wrapper",
- ).finished as Promise<[boolean]>;
+ );
};
-export const showCreateNewSubspace = async (space: Room): Promise<[boolean]> => {
- return Modal.createTrackedDialog(
+export const showCreateNewSubspace = (space: Room): void => {
+ Modal.createTrackedDialog(
"Space Landing",
"Create Subspace",
CreateSubspaceDialog,
{
space,
onAddExistingSpaceClick: () => showAddExistingSubspace(space),
+ onFinished: (added: boolean) => {
+ if (added && RoomViewStore.getRoomId() === space.roomId) {
+ defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
+ }
+ },
},
"mx_CreateSubspaceDialog_wrapper",
- ).finished as Promise<[boolean]>;
+ );
};
From 40493634ae98d517b3416ab0020b883fb2948b3e Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 28 Jul 2021 19:23:33 +0100
Subject: [PATCH 5/6] Iterate PR
---
.../views/dialogs/_CreateSubspaceDialog.scss | 6 +++++
.../views/dialogs/CreateSubspaceDialog.tsx | 24 +++++++++++++++++++
src/i18n/strings/en_EN.json | 3 +++
3 files changed, 33 insertions(+)
diff --git a/res/css/views/dialogs/_CreateSubspaceDialog.scss b/res/css/views/dialogs/_CreateSubspaceDialog.scss
index c780da4b9e..1ec4731ae6 100644
--- a/res/css/views/dialogs/_CreateSubspaceDialog.scss
+++ b/res/css/views/dialogs/_CreateSubspaceDialog.scss
@@ -39,8 +39,14 @@ limitations under the License.
.mx_BetaCard_betaPill {
margin-right: 8px;
+ vertical-align: middle;
}
}
+
+ .mx_JoinRuleDropdown + p {
+ color: $muted-fg-color;
+ font-size: $font-12px;
+ }
}
.mx_CreateSubspaceDialog_footer {
diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx
index 12fd6a3232..6a81bb04df 100644
--- a/src/components/views/dialogs/CreateSubspaceDialog.tsx
+++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx
@@ -110,6 +110,29 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick
}
};
+ let joinRuleMicrocopy: JSX.Element;
+ if (joinRule === JoinRule.Restricted) {
+ joinRuleMicrocopy =
+ { _t(
+ "Anyone in will be able to find and join.", {}, {
+ SpaceName: () => { parentSpace.name } ,
+ },
+ ) }
+
;
+ } else if (joinRule === JoinRule.Public) {
+ joinRuleMicrocopy =
+ { _t(
+ "Anyone will be able to find and join this space, not just members of .", {}, {
+ SpaceName: () => { parentSpace.name } ,
+ },
+ ) }
+
;
+ } else if (joinRule === JoinRule.Invite) {
+ joinRuleMicrocopy =
+ { _t("Only people invited will be able to find and join this space.") }
+
;
+ }
+
return = ({ space, onAddExistingSpaceClick
value={joinRule}
onChange={setJoinRule}
/>
+ { joinRuleMicrocopy }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index e6b96a132d..ef4537bddf 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2219,6 +2219,9 @@
"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",
+ "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.",
"Create a subspace": "Create a subspace",
"Add a subspace to a space you manage.": "Add a subspace to a space you manage.",
"Subspace visibility": "Subspace visibility",
From 67b8c0a8113752a13eb251a9f3443c370d10ba50 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 28 Jul 2021 19:33:54 +0100
Subject: [PATCH 6/6] remove unused import
---
src/components/structures/SpaceRoomDirectory.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx
index 610ac5e103..d8cc9593f0 100644
--- a/src/components/structures/SpaceRoomDirectory.tsx
+++ b/src/components/structures/SpaceRoomDirectory.tsx
@@ -16,7 +16,6 @@ limitations under the License.
import React, { ReactNode, useMemo, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
-import { MatrixClient } from "matrix-js-sdk/src/client";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces";
import classNames from "classnames";