Merge branch 't3chguy/fix/18091' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/18089

 Conflicts:
	src/components/structures/SpaceRoomDirectory.tsx
This commit is contained in:
Michael Telatynski 2021-07-29 12:52:01 +01:00
commit 56cd0e1c4f
20 changed files with 1057 additions and 394 deletions

View file

@ -76,6 +76,7 @@
@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss"; @import "./views/dialogs/_CreateCommunityPrototypeDialog.scss";
@import "./views/dialogs/_CreateGroupDialog.scss"; @import "./views/dialogs/_CreateGroupDialog.scss";
@import "./views/dialogs/_CreateRoomDialog.scss"; @import "./views/dialogs/_CreateRoomDialog.scss";
@import "./views/dialogs/_CreateSubspaceDialog.scss";
@import "./views/dialogs/_DeactivateAccountDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss";
@import "./views/dialogs/_DevtoolsDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss";
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss"; @import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
@ -85,6 +86,7 @@
@import "./views/dialogs/_HostSignupDialog.scss"; @import "./views/dialogs/_HostSignupDialog.scss";
@import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_IncomingSasDialog.scss";
@import "./views/dialogs/_InviteDialog.scss"; @import "./views/dialogs/_InviteDialog.scss";
@import "./views/dialogs/_JoinRuleDropdown.scss";
@import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss";
@import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss"; @import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss";
@import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss";

View file

@ -99,6 +99,10 @@ limitations under the License.
.mx_IconizedContextMenu_icon + .mx_IconizedContextMenu_label { .mx_IconizedContextMenu_icon + .mx_IconizedContextMenu_label {
padding-left: 14px; padding-left: 14px;
} }
.mx_BetaCard_betaPill {
margin-left: 16px;
}
} }
} }

View file

@ -54,11 +54,16 @@ limitations under the License.
display: flex; display: flex;
margin-top: 12px; margin-top: 12px;
// we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling .mx_DecoratedRoomAvatar, // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling
.mx_DecoratedRoomAvatar { .mx_BaseAvatar.mx_RoomAvatar_isSpaceRoom {
margin-right: 12px; margin-right: 12px;
} }
img.mx_RoomAvatar_isSpaceRoom,
.mx_RoomAvatar_isSpaceRoom img {
border-radius: 8px;
}
.mx_AddExistingToSpace_entry_name { .mx_AddExistingToSpace_entry_name {
font-size: $font-15px; font-size: $font-15px;
line-height: 30px; line-height: 30px;
@ -73,41 +78,12 @@ limitations under the License.
align-items: center; 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;
.mx_AccessibleButton_kind_link {
font-size: $font-12px; font-size: $font-12px;
line-height: $font-15px; line-height: $font-15px;
color: $secondary-fg-color; margin-top: 8px;
padding: 0;
&::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;
} }
} }
@ -205,7 +181,12 @@ limitations under the License.
min-height: 0; min-height: 0;
height: 80vh; height: 80vh;
.mx_Dialog_title { .mx_AddExistingToSpace {
display: contents;
}
}
.mx_SubspaceSelector {
display: flex; display: flex;
.mx_BaseAvatar_image { .mx_BaseAvatar_image {
@ -227,12 +208,6 @@ limitations under the License.
line-height: $font-22px; line-height: $font-22px;
margin: 0; margin: 0;
} }
.mx_AddExistingToSpaceDialog_onlySpace {
color: $secondary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
}
} }
.mx_Dropdown_input { .mx_Dropdown_input {
@ -252,7 +227,7 @@ limitations under the License.
} }
.mx_Dropdown_menu { .mx_Dropdown_menu {
.mx_AddExistingToSpaceDialog_dropdownOptionActive { .mx_SubspaceSelector_dropdownOptionActive {
color: $accent-color; color: $accent-color;
padding-right: 32px; padding-right: 32px;
position: relative; position: relative;
@ -273,9 +248,10 @@ limitations under the License.
} }
} }
} }
}
.mx_AddExistingToSpace { .mx_SubspaceSelector_onlySpace {
display: contents; color: $secondary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
} }
} }

View file

@ -109,56 +109,4 @@ limitations under the License.
margin: 0 85px 0 0; margin: 0 85px 0 0;
font-size: $font-12px; 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;
}
}
} }

View file

@ -0,0 +1,81 @@
/*
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;
vertical-align: middle;
}
}
.mx_JoinRuleDropdown + p {
color: $muted-fg-color;
font-size: $font-12px;
}
}
.mx_CreateSubspaceDialog_footer {
display: flex;
margin-top: 20px;
.mx_CreateSubspaceDialog_footer_prompt {
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;
}
}
}

View file

@ -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;
}
}

View file

@ -16,7 +16,6 @@ limitations under the License.
import React, { ReactNode, useMemo, useState } from "react"; import React, { ReactNode, useMemo, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room"; 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 { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { IRoomChild, IRoomChildState } from "matrix-js-sdk/src/@types/spaces"; import { IRoomChild, IRoomChildState } from "matrix-js-sdk/src/@types/spaces";
import classNames from "classnames"; import classNames from "classnames";
@ -44,11 +43,13 @@ import { getChildOrder } from "../../stores/SpaceStore";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { linkifyElement } from "../../HtmlUtils"; import { linkifyElement } from "../../HtmlUtils";
import { getDisplayAliasForAliasSet } from "../../Rooms"; import { getDisplayAliasForAliasSet } from "../../Rooms";
import { useDispatcher } from "../../hooks/useDispatcher";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions";
interface IHierarchyProps { interface IHierarchyProps {
space: Room; space: Room;
initialText?: string; initialText?: string;
refreshToken?: any;
additionalButtons?: ReactNode; additionalButtons?: ReactNode;
showRoom(room: IRoomChild, viaServers?: string[], autoJoin?: boolean): void; showRoom(room: IRoomChild, viaServers?: string[], autoJoin?: boolean): void;
} }
@ -315,18 +316,25 @@ export const HierarchyLevel = ({
</React.Fragment>; </React.Fragment>;
}; };
// mutate argument refreshToken to force a reload export const useSpaceSummary = (space: Room): [
export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [
null, null,
IRoomChild[], IRoomChild[],
Map<string, Map<string, IRoomChildState>>?, Map<string, Map<string, IRoomChildState>>?,
Map<string, Set<string>>?, Map<string, Set<string>>?,
Map<string, Set<string>>?, Map<string, Set<string>>?,
] | [Error] => { ] | [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 // TODO pagination
return useAsyncMemo(async () => { return useAsyncMemo(async () => {
try { try {
const { rooms } = await cli.getRoomChildren(space.roomId); const { rooms } = await space.client.getRoomChildren(space.roomId);
const parentChildRelations = new EnhancedMap<string, Map<string, IRoomChildState>>(); const parentChildRelations = new EnhancedMap<string, Map<string, IRoomChildState>>();
const childParentRelations = new EnhancedMap<string, Set<string>>(); const childParentRelations = new EnhancedMap<string, Set<string>>();
@ -357,7 +365,6 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
space, space,
initialText = "", initialText = "",
showRoom, showRoom,
refreshToken,
additionalButtons, additionalButtons,
children, children,
}) => { }) => {
@ -367,7 +374,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>> const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken); const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(space);
const roomsMap = useMemo(() => { const roomsMap = useMemo(() => {
if (!rooms) return null; if (!rooms) return null;

View file

@ -47,13 +47,23 @@ import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPanelPhasePayload"; import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPanelPhasePayload";
import { useStateArray } from "../../hooks/useStateArray"; import { useStateArray } from "../../hooks/useStateArray";
import SpacePublicShare from "../views/spaces/SpacePublicShare"; 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 { showRoom, SpaceHierarchy } from "./SpaceRoomDirectory";
import MemberAvatar from "../views/avatars/MemberAvatar"; import MemberAvatar from "../views/avatars/MemberAvatar";
import { useStateToggle } from "../../hooks/useStateToggle";
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/SpaceStore";
import FacePile from "../views/elements/FacePile"; 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 { ChevronFace, ContextMenuButton, useContextMenu } from "./ContextMenu";
import IconizedContextMenu, { import IconizedContextMenu, {
IconizedContextMenuOption, IconizedContextMenuOption,
@ -307,7 +317,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
</div>; </div>;
}; };
const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => { const SpaceLandingAddButton = ({ space }) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
let contextMenu; let contextMenu;
@ -331,24 +341,32 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
closeMenu(); closeMenu();
if (await showCreateNewRoom(space)) { if (await showCreateNewRoom(space)) {
onNewRoomAdded(); defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
} }
}} }}
/> />
<IconizedContextMenuOption <IconizedContextMenuOption
label={_t("Add existing room")} label={_t("Add existing room")}
iconClassName="mx_RoomList_iconHash" iconClassName="mx_RoomList_iconHash"
onClick={async (e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
closeMenu(); closeMenu();
showAddExistingRooms(space);
const [added] = await showAddExistingRooms(space);
if (added) {
onNewRoomAdded();
}
}} }}
/> />
<IconizedContextMenuOption
label={_t("Add subspace")}
iconClassName="mx_RoomList_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
showCreateNewSubspace(space);
}}
>
<BetaPill />
</IconizedContextMenuOption>
</IconizedContextMenuOptionList> </IconizedContextMenuOptionList>
</IconizedContextMenu>; </IconizedContextMenu>;
} }
@ -389,11 +407,9 @@ const SpaceLanding = ({ space }) => {
const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId); const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
const [refreshToken, forceUpdate] = useStateToggle(false);
let addRoomButton; let addRoomButton;
if (canAddRooms) { if (canAddRooms) {
addRoomButton = <SpaceLandingAddButton space={space} onNewRoomAdded={forceUpdate} />; addRoomButton = <SpaceLandingAddButton space={space} />;
} }
let settingsButton; let settingsButton;
@ -443,12 +459,7 @@ const SpaceLanding = ({ space }) => {
<SpaceFeedbackPrompt /> <SpaceFeedbackPrompt />
<hr /> <hr />
<SpaceHierarchy <SpaceHierarchy space={space} showRoom={showRoom} additionalButtons={addRoomButton} />
space={space}
showRoom={showRoom}
refreshToken={refreshToken}
additionalButtons={addRoomButton}
/>
</div>; </div>;
}; };
@ -550,10 +561,12 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
{ _t("Skip for now") } { _t("Skip for now") }
</AccessibleButton> </AccessibleButton>
} }
filterPlaceholder={_t("Search for rooms or spaces")}
onFinished={onFinished} onFinished={onFinished}
roomsRenderer={defaultRoomsRenderer}
spacesRenderer={defaultSpacesRenderer}
dmsRenderer={defaultDmsRenderer}
/> />
<div className="mx_SpaceRoomView_buttons" />
<SpaceFeedbackPrompt /> <SpaceFeedbackPrompt />
</div>; </div>;
}; };

View file

@ -90,10 +90,11 @@ export const IconizedContextMenuCheckbox: React.FC<ICheckboxProps> = ({
</MenuItemCheckbox>; </MenuItemCheckbox>;
}; };
export const IconizedContextMenuOption: React.FC<IOptionProps> = ({ label, iconClassName, ...props }) => { export const IconizedContextMenuOption: React.FC<IOptionProps> = ({ label, iconClassName, children, ...props }) => {
return <MenuItem {...props} label={label}> return <MenuItem {...props} label={label}>
{ iconClassName && <span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} /> } { iconClassName && <span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} /> }
<span className="mx_IconizedContextMenu_label">{ label }</span> <span className="mx_IconizedContextMenu_label">{ label }</span>
{ children }
</MenuItem>; </MenuItem>;
}; };

View file

@ -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<IProps> = ({ space, onCreateSubspaceClick, onFinished }) => {
const [selectedSpace, setSelectedSpace] = useState(space);
return <BaseDialog
title={(
<SubspaceSelector
title={_t("Add existing space")}
space={space}
value={selectedSpace}
onChange={setSelectedSpace}
/>
)}
className="mx_AddExistingToSpaceDialog"
contentId="mx_AddExistingToSpace"
onFinished={onFinished}
fixedWidth={false}
>
<MatrixClientContext.Provider value={space.client}>
<AddExistingToSpace
space={space}
onFinished={onFinished}
footerPrompt={<>
<div>{ _t("Want to add a new space instead?") }</div>
<AccessibleButton onClick={onCreateSubspaceClick} kind="link">
{ _t("Create a new subspace") }
</AccessibleButton>
</>}
filterPlaceholder={_t("Search for spaces")}
spacesRenderer={defaultSpacesRenderer}
/>
</MatrixClientContext.Provider>
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
</BaseDialog>;
};
export default AddExistingSubspaceDialog;

View file

@ -18,9 +18,9 @@ import React, { ReactNode, useContext, useMemo, useState } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { sleep } from "matrix-js-sdk/src/utils"; import { sleep } from "matrix-js-sdk/src/utils";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import Dropdown from "../elements/Dropdown"; import Dropdown from "../elements/Dropdown";
import SearchBox from "../../structures/SearchBox"; import SearchBox from "../../structures/SearchBox";
@ -42,12 +42,14 @@ import TruncatedList from "../elements/TruncatedList";
import EntityTile from "../rooms/EntityTile"; import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar"; import BaseAvatar from "../avatars/BaseAvatar";
interface IProps extends IDialogProps { interface IProps {
space: Room; 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 <label className="mx_AddExistingToSpace_entry"> return <label className="mx_AddExistingToSpace_entry">
{ room?.isSpaceRoom() { room?.isSpaceRoom()
? <RoomAvatar room={room} height={32} width={32} /> ? <RoomAvatar room={room} height={32} width={32} />
@ -65,14 +67,36 @@ const Entry = ({ room, checked, onChange }) => {
interface IAddExistingToSpaceProps { interface IAddExistingToSpaceProps {
space: Room; space: Room;
footerPrompt?: ReactNode; footerPrompt?: ReactNode;
filterPlaceholder: string;
emptySelectionButton?: ReactNode; emptySelectionButton?: ReactNode;
onFinished(added: boolean): void; onFinished(added: boolean): void;
roomsRenderer?(
rooms: Room[],
selectedToAdd: Set<Room>,
onChange: undefined | ((checked: boolean, room: Room) => void),
truncateAt: number,
overflowTile: (overflowCount: number, totalCount: number) => JSX.Element,
): ReactNode;
spacesRenderer?(
spaces: Room[],
selectedToAdd: Set<Room>,
onChange?: (checked: boolean, room: Room) => void,
): ReactNode;
dmsRenderer?(
dms: Room[],
selectedToAdd: Set<Room>,
onChange?: (checked: boolean, room: Room) => void,
): ReactNode;
} }
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
space, space,
footerPrompt, footerPrompt,
emptySelectionButton, emptySelectionButton,
filterPlaceholder,
roomsRenderer,
dmsRenderer,
spacesRenderer,
onFinished, onFinished,
}) => { }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
@ -196,7 +220,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
</>; </>;
} }
const onChange = !busy && !error ? (checked, room) => { const onChange = !busy && !error ? (checked: boolean, room: Room) => {
if (checked) { if (checked) {
selectedToAdd.add(room); selectedToAdd.add(room);
} else { } else {
@ -206,7 +230,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
} : null; } : null;
const [truncateAt, setTruncateAt] = useState(20); 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 }); const text = _t("and %(count)s others...", { count: overflowCount });
return ( return (
<EntityTile <EntityTile
@ -222,16 +246,49 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
); );
} }
let noResults = true;
if ((roomsRenderer && rooms.length > 0) ||
(dmsRenderer && dms.length > 0) ||
(!roomsRenderer && !dmsRenderer && spacesRenderer && spaces.length > 0) // only count spaces when alone
) {
noResults = false;
}
return <div className="mx_AddExistingToSpace"> return <div className="mx_AddExistingToSpace">
<SearchBox <SearchBox
className="mx_textinput_icon mx_textinput_search" className="mx_textinput_icon mx_textinput_search"
placeholder={_t("Filter your rooms and spaces")} placeholder={filterPlaceholder}
onSearch={setQuery} onSearch={setQuery}
autoComplete={true} autoComplete={true}
autoFocus={true} autoFocus={true}
/> />
<AutoHideScrollbar className="mx_AddExistingToSpace_content"> <AutoHideScrollbar className="mx_AddExistingToSpace_content">
{ rooms.length > 0 ? ( { rooms.length > 0 && roomsRenderer ? (
roomsRenderer(rooms, selectedToAdd, onChange, truncateAt, overflowTile)
) : undefined }
{ spaces.length > 0 && spacesRenderer ? (
spacesRenderer(spaces, selectedToAdd, onChange)
) : null }
{ dms.length > 0 && dmsRenderer ? (
dmsRenderer(dms, selectedToAdd, onChange)
) : null }
{ noResults ? <span className="mx_AddExistingToSpace_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>
<div className="mx_AddExistingToSpace_footer">
{ footer }
</div>
</div>;
};
export const defaultRoomsRenderer: IAddExistingToSpaceProps["roomsRenderer"] = (
rooms, selectedToAdd, onChange, truncateAt, overflowTile,
) => (
<div className="mx_AddExistingToSpace_section"> <div className="mx_AddExistingToSpace_section">
<h3>{ _t("Rooms") }</h3> <h3>{ _t("Rooms") }</h3>
<TruncatedList <TruncatedList
@ -242,7 +299,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
key={room.roomId} key={room.roomId}
room={room} room={room}
checked={selectedToAdd.has(room)} checked={selectedToAdd.has(room)}
onChange={onChange ? (checked) => { onChange={onChange ? (checked: boolean) => {
onChange(checked, room); onChange(checked, room);
} : null} } : null}
/>, />,
@ -250,15 +307,10 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
getChildCount={() => rooms.length} getChildCount={() => rooms.length}
/> />
</div> </div>
) : undefined } );
{ spaces.length > 0 ? ( export const defaultSpacesRenderer: IAddExistingToSpaceProps["spacesRenderer"] = (spaces, selectedToAdd, onChange) => (
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces"> <div className="mx_AddExistingToSpace_section">
<h3>{ _t("Spaces") }</h3>
<div className="mx_AddExistingToSpace_section_experimental">
<div>{ _t("Feeling experimental?") }</div>
<div>{ _t("You can add existing spaces to a space.") }</div>
</div>
{ spaces.map(space => { { spaces.map(space => {
return <Entry return <Entry
key={space.roomId} key={space.roomId}
@ -270,9 +322,9 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
/>; />;
}) } }) }
</div> </div>
) : null } );
{ dms.length > 0 ? ( export const defaultDmsRenderer: IAddExistingToSpaceProps["dmsRenderer"] = (dms, selectedToAdd, onChange) => (
<div className="mx_AddExistingToSpace_section"> <div className="mx_AddExistingToSpace_section">
<h3>{ _t("Direct Messages") }</h3> <h3>{ _t("Direct Messages") }</h3>
{ dms.map(room => { { dms.map(room => {
@ -280,69 +332,80 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
key={room.roomId} key={room.roomId}
room={room} room={room}
checked={selectedToAdd.has(room)} checked={selectedToAdd.has(room)}
onChange={onChange ? (checked) => { onChange={onChange ? (checked: boolean) => {
onChange(checked, room); onChange(checked, room);
} : null} } : null}
/>; />;
}) } }) }
</div> </div>
) : null } );
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults"> interface ISubspaceSelectorProps {
{ _t("No results") } title: string;
</span> : undefined } space: Room;
</AutoHideScrollbar> value: Room;
onChange(space: Room): void;
}
<div className="mx_AddExistingToSpace_footer"> export const SubspaceSelector = ({ title, space, value, onChange }: ISubspaceSelectorProps) => {
{ footer } const options = useMemo(() => {
</div> return [space, ...SpaceStore.instance.getChildSpaces(space.roomId).filter(space => {
</div>; return space.currentState.maySendStateEvent(EventType.SpaceChild, space.client.credentials.userId);
}; })];
}, [space]);
const AddExistingToSpaceDialog: React.FC<IProps> = ({ space, onCreateRoomClick, onFinished }) => { let body;
const [selectedSpace, setSelectedSpace] = useState(space); if (options.length > 1) {
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); body = (
<Dropdown
let spaceOptionSection; id="mx_SpaceSelectDropdown"
if (existingSubspaces.length > 0) { className="mx_SpaceSelectDropdown"
const options = [space, ...existingSubspaces].map((space) => { onOptionChange={(key: string) => {
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", { onChange(options.find(space => space.roomId === key) || space);
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace, }}
value={value.roomId}
label={_t("Space selection")}
>
{ options.map((space) => {
const classes = classNames({
mx_SubspaceSelector_dropdownOptionActive: space === value,
}); });
return <div key={space.roomId} className={classes}> return <div key={space.roomId} className={classes}>
<RoomAvatar room={space} width={24} height={24} /> <RoomAvatar room={space} width={24} height={24} />
{ space.name || getDisplayAliasForRoom(space) || space.roomId } { space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>; </div>;
}); }) }
spaceOptionSection = (
<Dropdown
id="mx_SpaceSelectDropdown"
onOptionChange={(key: string) => {
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
}}
value={selectedSpace.roomId}
label={_t("Space selection")}
>
{ options }
</Dropdown> </Dropdown>
); );
} else { } else {
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace"> body = (
<div className="mx_SubspaceSelector_onlySpace">
{ space.name || getDisplayAliasForRoom(space) || space.roomId } { space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>; </div>
);
} }
const title = <React.Fragment> return <div className="mx_SubspaceSelector">
<RoomAvatar room={selectedSpace} height={40} width={40} /> <RoomAvatar room={value} height={40} width={40} />
<div> <div>
<h1>{ _t("Add existing rooms") }</h1> <h1>{ title }</h1>
{ spaceOptionSection } { body }
</div> </div>
</React.Fragment>; </div>;
};
const AddExistingToSpaceDialog: React.FC<IProps> = ({ space, onCreateRoomClick, onAddSubspaceClick, onFinished }) => {
const [selectedSpace, setSelectedSpace] = useState(space);
return <BaseDialog return <BaseDialog
title={title} title={(
<SubspaceSelector
title={_t("Add existing rooms")}
space={space}
value={selectedSpace}
onChange={setSelectedSpace}
/>
)}
className="mx_AddExistingToSpaceDialog" className="mx_AddExistingToSpaceDialog"
contentId="mx_AddExistingToSpace" contentId="mx_AddExistingToSpace"
onFinished={onFinished} onFinished={onFinished}
@ -354,10 +417,33 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ space, onCreateRoomClick,
onFinished={onFinished} onFinished={onFinished}
footerPrompt={<> footerPrompt={<>
<div>{ _t("Want to add a new room instead?") }</div> <div>{ _t("Want to add a new room instead?") }</div>
<AccessibleButton onClick={() => onCreateRoomClick(space)} kind="link"> <AccessibleButton
kind="link"
onClick={() => {
onCreateRoomClick();
onFinished();
}}
>
{ _t("Create a new room") } { _t("Create a new room") }
</AccessibleButton> </AccessibleButton>
</>} </>}
filterPlaceholder={_t("Search for rooms")}
roomsRenderer={defaultRoomsRenderer}
spacesRenderer={() => (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Spaces") }</h3>
<AccessibleButton
kind="link"
onClick={() => {
onAddSubspaceClick();
onFinished();
}}
>
{ _t("Adding spaces has moved.") }
</AccessibleButton>
</div>
)}
dmsRenderer={defaultDmsRenderer}
/> />
</MatrixClientContext.Provider> </MatrixClientContext.Provider>

View file

@ -32,8 +32,8 @@ import RoomAliasField from "../elements/RoomAliasField";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "../dialogs/BaseDialog"; import BaseDialog from "../dialogs/BaseDialog";
import Dropdown from "../elements/Dropdown";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/SpaceStore";
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
interface IProps { interface IProps {
defaultPublic?: boolean; defaultPublic?: boolean;
@ -322,21 +322,6 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room'); title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room');
} }
const options = [
<div key={JoinRule.Invite} className="mx_CreateRoomDialog_dropdown_invite">
{ _t("Private room (invite only)") }
</div>,
<div key={JoinRule.Public} className="mx_CreateRoomDialog_dropdown_public">
{ _t("Public room") }
</div>,
];
if (this.supportsRestricted) {
options.unshift(<div key={JoinRule.Restricted} className="mx_CreateRoomDialog_dropdown_restricted">
{ _t("Visible to space members") }
</div>);
}
return ( return (
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished} title={title}> <BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished} title={title}>
<form onSubmit={this.onOk} onKeyDown={this.onKeyDown}> <form onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
@ -356,16 +341,14 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
className="mx_CreateRoomDialog_topic" className="mx_CreateRoomDialog_topic"
/> />
<Dropdown <JoinRuleDropdown
id="mx_CreateRoomDialog_typeDropdown"
className="mx_CreateRoomDialog_typeDropdown"
onOptionChange={this.onJoinRuleChange}
menuWidth={448}
value={this.state.joinRule}
label={_t("Room visibility")} label={_t("Room visibility")}
> labelInvite={_t("Private room (invite only)")}
{ options } labelPublic={_t("Public room")}
</Dropdown> labelRestricted={this.supportsRestricted ? _t("Visible to space members") : undefined}
value={this.state.joinRule}
onChange={this.onJoinRuleChange}
/>
{ publicPrivateLabel } { publicPrivateLabel }
{ e2eeSection } { e2eeSection }

View file

@ -0,0 +1,210 @@
/*
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, { useRef, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { JoinRule, Preset } from "matrix-js-sdk/src/@types/partials";
import { RoomType } from "matrix-js-sdk/src/@types/event";
import { _t } from '../../../languageHandler';
import BaseDialog from "./BaseDialog";
import AccessibleButton from "../elements/AccessibleButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { BetaPill } from "../beta/BetaCard";
import Field from "../elements/Field";
import RoomAliasField from "../elements/RoomAliasField";
import SpaceStore from "../../../stores/SpaceStore";
import { SpaceCreateForm } from "../spaces/SpaceCreateMenu";
import createRoom from "../../../createRoom";
import { SubspaceSelector } from "./AddExistingToSpaceDialog";
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
interface IProps {
space: Room;
onAddExistingSpaceClick(): void;
onFinished(added?: boolean): void;
}
const CreateSubspaceDialog: React.FC<IProps> = ({ space, onAddExistingSpaceClick, onFinished }) => {
const [parentSpace, setParentSpace] = useState(space);
const [busy, setBusy] = useState<boolean>(false);
const [name, setName] = useState("");
const spaceNameField = useRef<Field>();
const [alias, setAlias] = useState("");
const spaceAliasField = useRef<RoomAliasField>();
const [avatar, setAvatar] = useState<File>(null);
const [topic, setTopic] = useState<string>("");
const supportsRestricted = !!SpaceStore.instance.restrictedJoinRuleSupport?.preferred;
const spaceJoinRule = space.getJoinRule();
let defaultJoinRule = JoinRule.Invite;
if (spaceJoinRule === JoinRule.Public) {
defaultJoinRule = JoinRule.Public;
} else if (supportsRestricted) {
defaultJoinRule = JoinRule.Restricted;
}
const [joinRule, setJoinRule] = useState<JoinRule>(defaultJoinRule);
const onCreateSubspaceClick = async (e) => {
e.preventDefault();
if (busy) return;
setBusy(true);
// require & validate the space name field
if (!await spaceNameField.current.validate({ allowEmpty: false })) {
spaceNameField.current.focus();
spaceNameField.current.validate({ allowEmpty: false, focused: true });
setBusy(false);
return;
}
// validate the space name alias field but do not require it
if (joinRule === JoinRule.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) {
spaceAliasField.current.focus();
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
setBusy(false);
return;
}
try {
await createRoom({
createOpts: {
preset: joinRule === JoinRule.Public ? Preset.PublicChat : Preset.PrivateChat,
name,
power_level_content_override: {
// Only allow Admins to write to the timeline to prevent hidden sync spam
events_default: 100,
...joinRule === JoinRule.Public ? { invite: 0 } : {},
},
room_alias_name: joinRule === JoinRule.Public && alias
? alias.substr(1, alias.indexOf(":") - 1)
: undefined,
topic,
},
avatar,
roomType: RoomType.Space,
parentSpace,
spinner: false,
encryption: false,
andView: true,
inlineErrors: true,
});
onFinished(true);
} catch (e) {
console.error(e);
}
};
let joinRuleMicrocopy: JSX.Element;
if (joinRule === JoinRule.Restricted) {
joinRuleMicrocopy = <p>
{ _t(
"Anyone in <SpaceName/> will be able to find and join.", {}, {
SpaceName: () => <b>{ parentSpace.name }</b>,
},
) }
</p>;
} else if (joinRule === JoinRule.Public) {
joinRuleMicrocopy = <p>
{ _t(
"Anyone will be able to find and join this space, not just members of <SpaceName/>.", {}, {
SpaceName: () => <b>{ parentSpace.name }</b>,
},
) }
</p>;
} else if (joinRule === JoinRule.Invite) {
joinRuleMicrocopy = <p>
{ _t("Only people invited will be able to find and join this space.") }
</p>;
}
return <BaseDialog
title={(
<SubspaceSelector
title={_t("Create a subspace")}
space={space}
value={parentSpace}
onChange={setParentSpace}
/>
)}
className="mx_CreateSubspaceDialog"
contentId="mx_CreateSubspaceDialog"
onFinished={onFinished}
fixedWidth={false}
>
<MatrixClientContext.Provider value={space.client}>
<div className="mx_CreateSubspaceDialog_content">
<div className="mx_CreateSubspaceDialog_betaNotice">
<BetaPill />
{ _t("Add a subspace to a space you manage.") }
</div>
<SpaceCreateForm
busy={busy}
onSubmit={onCreateSubspaceClick}
setAvatar={setAvatar}
name={name}
setName={setName}
nameFieldRef={spaceNameField}
topic={topic}
setTopic={setTopic}
alias={alias}
setAlias={setAlias}
showAliasField={joinRule === JoinRule.Public}
aliasFieldRef={spaceAliasField}
>
<JoinRuleDropdown
label={_t("Subspace visibility")}
labelInvite={_t("Private subspace (invite only)")}
labelPublic={_t("Public subspace")}
labelRestricted={supportsRestricted ? _t("Visible to space members") : undefined}
width={478}
value={joinRule}
onChange={setJoinRule}
/>
{ joinRuleMicrocopy }
</SpaceCreateForm>
</div>
<div className="mx_CreateSubspaceDialog_footer">
<div className="mx_CreateSubspaceDialog_footer_prompt">
<div>{ _t("Want to add an existing space instead?") }</div>
<AccessibleButton
kind="link"
onClick={() => {
onAddExistingSpaceClick();
onFinished();
}}
>
{ _t("Add existing space") }
</AccessibleButton>
</div>
<AccessibleButton kind="primary_outline" disabled={busy} onClick={() => onFinished(false)}>
{ _t("Cancel") }
</AccessibleButton>
<AccessibleButton kind="primary" disabled={busy} onClick={onCreateSubspaceClick}>
{ busy ? _t("Adding...") : _t("Add") }
</AccessibleButton>
</div>
</MatrixClientContext.Provider>
</BaseDialog>;
};
export default CreateSubspaceDialog;

View file

@ -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 = [
<div key={JoinRule.Invite} className="mx_JoinRuleDropdown_invite">
{ labelInvite }
</div>,
<div key={JoinRule.Public} className="mx_JoinRuleDropdown_public">
{ labelPublic }
</div>,
];
if (labelRestricted) {
options.unshift(<div key={JoinRule.Restricted} className="mx_JoinRuleDropdown_restricted">
{ labelRestricted }
</div>);
}
return <Dropdown
id="mx_JoinRuleDropdown"
className="mx_JoinRuleDropdown"
onOptionChange={onChange}
menuWidth={width}
value={value}
label={label}
>
{ options }
</Dropdown>;
};
export default JoinRuleDropdown;

View file

@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. 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 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 FocusLock from "react-focus-lock";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
@ -24,7 +24,7 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { ChevronFace, ContextMenu } from "../../structures/ContextMenu"; import { ChevronFace, ContextMenu } from "../../structures/ContextMenu";
import createRoom from "../../../createRoom"; import createRoom from "../../../createRoom";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { SpaceAvatar } from "./SpaceBasicSettings"; import SpaceBasicSettings, { SpaceAvatar } from "./SpaceBasicSettings";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { BetaPill } from "../beta/BetaCard"; import { BetaPill } from "../beta/BetaCard";
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
@ -33,8 +33,7 @@ import { UserTab } from "../dialogs/UserSettingsDialog";
import Field from "../elements/Field"; import Field from "../elements/Field";
import withValidation from "../elements/Validation"; import withValidation from "../elements/Validation";
import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView"; import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
import { Preset } from "matrix-js-sdk/src/@types/partials"; import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
import RoomAliasField from "../elements/RoomAliasField"; import RoomAliasField from "../elements/RoomAliasField";
const SpaceCreateMenuType = ({ title, description, className, onClick }) => { const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
@ -66,8 +65,83 @@ const nameToAlias = (name: string, domain: string): string => {
return `#${localpart}:${domain}`; return `#${localpart}:${domain}`;
}; };
const SpaceCreateMenu = ({ onFinished }) => { type BProps = Pick<ComponentProps<typeof SpaceBasicSettings>, "setAvatar" | "name" | "setName" | "topic" | "setTopic">;
interface ISpaceCreateFormProps extends BProps {
busy: boolean;
alias: string;
nameFieldRef: RefObject<Field>;
aliasFieldRef: RefObject<RoomAliasField>;
showAliasField?: boolean;
onSubmit(e: SyntheticEvent): void;
setAlias(alias: string): void;
}
export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
busy,
onSubmit,
setAvatar,
name,
setName,
nameFieldRef,
alias,
aliasFieldRef,
setAlias,
showAliasField,
topic,
setTopic,
children,
}) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const domain = cli.getDomain();
return <form className="mx_SpaceBasicSettings" onSubmit={onSubmit}>
<SpaceAvatar setAvatar={setAvatar} avatarDisabled={busy} />
<Field
name="spaceName"
label={_t("Name")}
autoFocus={true}
value={name}
onChange={ev => {
const newName = ev.target.value;
if (!alias || alias === nameToAlias(name, domain)) {
setAlias(nameToAlias(newName, domain));
}
setName(newName);
}}
ref={nameFieldRef}
onValidate={spaceNameValidator}
disabled={busy}
/>
{ showAliasField
? <RoomAliasField
ref={aliasFieldRef}
onChange={setAlias}
domain={domain}
value={alias}
placeholder={name ? nameToAlias(name, domain) : _t("e.g. my-space")}
label={_t("Address")}
disabled={busy}
/>
: null
}
<Field
name="spaceTopic"
element="textarea"
label={_t("Description")}
value={topic}
onChange={ev => setTopic(ev.target.value)}
rows={3}
disabled={busy}
/>
{ children }
</form>;
};
const SpaceCreateMenu = ({ onFinished }) => {
const [visibility, setVisibility] = useState<Visibility>(null); const [visibility, setVisibility] = useState<Visibility>(null);
const [busy, setBusy] = useState<boolean>(false); const [busy, setBusy] = useState<boolean>(false);
@ -98,42 +172,26 @@ const SpaceCreateMenu = ({ onFinished }) => {
return; 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 { try {
await createRoom({ await createRoom({
createOpts: { createOpts: {
preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat, preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat,
name, name,
creation_content: {
[RoomCreateTypeField]: RoomType.Space,
},
initial_state: initialState,
power_level_content_override: { power_level_content_override: {
// Only allow Admins to write to the timeline to prevent hidden sync spam // Only allow Admins to write to the timeline to prevent hidden sync spam
events_default: 100, events_default: 100,
...Visibility.Public ? { invite: 0 } : {}, ...visibility === Visibility.Public ? { invite: 0 } : {},
}, },
room_alias_name: visibility === Visibility.Public && alias room_alias_name: visibility === Visibility.Public && alias
? alias.substr(1, alias.indexOf(":") - 1) ? alias.substr(1, alias.indexOf(":") - 1)
: undefined, : undefined,
topic, topic,
}, },
avatar,
roomType: RoomType.Space,
historyVisibility: visibility === Visibility.Public
? HistoryVisibility.WorldReadable
: HistoryVisibility.Invited,
spinner: false, spinner: false,
encryption: false, encryption: false,
andView: true, andView: true,
@ -171,7 +229,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
<SpaceFeedbackPrompt onClick={onFinished} /> <SpaceFeedbackPrompt onClick={onFinished} />
</React.Fragment>; </React.Fragment>;
} else { } else {
const domain = cli.getDomain();
body = <React.Fragment> body = <React.Fragment>
<AccessibleTooltipButton <AccessibleTooltipButton
className="mx_SpaceCreateMenu_back" className="mx_SpaceCreateMenu_back"
@ -192,50 +249,21 @@ const SpaceCreateMenu = ({ onFinished }) => {
} }
</p> </p>
<form className="mx_SpaceBasicSettings" onSubmit={onSpaceCreateClick}> <SpaceCreateForm
<SpaceAvatar setAvatar={setAvatar} avatarDisabled={busy} /> busy={busy}
onSubmit={onSpaceCreateClick}
<Field setAvatar={setAvatar}
name="spaceName" name={name}
label={_t("Name")} setName={setName}
autoFocus={true} nameFieldRef={spaceNameField}
value={name} topic={topic}
onChange={ev => { setTopic={setTopic}
const newName = ev.target.value; alias={alias}
if (!alias || alias === nameToAlias(name, domain)) { setAlias={setAlias}
setAlias(nameToAlias(newName, domain)); showAliasField={visibility === Visibility.Public}
} aliasFieldRef={spaceAliasField}
setName(newName);
}}
ref={spaceNameField}
onValidate={spaceNameValidator}
disabled={busy}
/> />
{ visibility === Visibility.Public
? <RoomAliasField
ref={spaceAliasField}
onChange={setAlias}
domain={domain}
value={alias}
placeholder={name ? nameToAlias(name, domain) : _t("e.g. my-space")}
label={_t("Address")}
disabled={busy}
/>
: null
}
<Field
name="spaceTopic"
element="textarea"
label={_t("Description")}
value={topic}
onChange={ev => setTopic(ev.target.value)}
rows={3}
disabled={busy}
/>
</form>
<AccessibleButton kind="primary" onClick={onSpaceCreateClick} disabled={busy}> <AccessibleButton kind="primary" onClick={onSpaceCreateClick} disabled={busy}>
{ busy ? _t("Creating...") : _t("Create") } { busy ? _t("Creating...") : _t("Create") }
</AccessibleButton> </AccessibleButton>

View file

@ -34,6 +34,7 @@ import {
shouldShowSpaceSettings, shouldShowSpaceSettings,
showAddExistingRooms, showAddExistingRooms,
showCreateNewRoom, showCreateNewRoom,
showCreateNewSubspace,
showSpaceInvite, showSpaceInvite,
showSpaceSettings, showSpaceSettings,
} from "../../../utils/space"; } from "../../../utils/space";
@ -48,6 +49,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import { NotificationColor } from "../../../stores/notifications/NotificationColor"; import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager"; import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
import { BetaPill } from "../beta/BetaCard";
interface IItemProps extends InputHTMLAttributes<HTMLLIElement> { interface IItemProps extends InputHTMLAttributes<HTMLLIElement> {
space?: Room; space?: Room;
@ -234,6 +236,14 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
this.setState({ contextMenuPosition: null }); // also close the menu 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) => { private onMembersClick = (ev: ButtonEvent) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
@ -318,6 +328,13 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
label={_t("Add existing room")} label={_t("Add existing room")}
onClick={this.onAddExistingRoomClick} onClick={this.onAddExistingRoomClick}
/> />
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus"
label={_t("Add subspace")}
onClick={this.onNewSubspaceClick}
>
<BetaPill />
</IconizedContextMenuOption>
</IconizedContextMenuOptionList>; </IconizedContextMenuOptionList>;
} }

View file

@ -18,9 +18,15 @@ limitations under the License.
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; 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 { 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 { MatrixClientPeg } from './MatrixClientPeg';
import Modal from './Modal'; import Modal from './Modal';
@ -52,6 +58,9 @@ export interface IOpts {
inlineErrors?: boolean; inlineErrors?: boolean;
andView?: boolean; andView?: boolean;
associatedWithCommunity?: string; associatedWithCommunity?: string;
avatar?: File | string; // will upload if given file, else mxcUrl is needed
roomType?: RoomType | string;
historyVisibility?: HistoryVisibility;
parentSpace?: Room; parentSpace?: Room;
joinRule?: JoinRule; joinRule?: JoinRule;
} }
@ -112,6 +121,13 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
createOpts.is_direct = true; createOpts.is_direct = true;
} }
if (opts.roomType) {
createOpts.creation_content = {
...createOpts.creation_content,
[RoomCreateTypeField]: opts.roomType,
};
}
// By default, view the room after creating it // By default, view the room after creating it
if (opts.andView === undefined) { if (opts.andView === undefined) {
opts.andView = true; opts.andView = true;
@ -144,12 +160,11 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
if (opts.parentSpace) { if (opts.parentSpace) {
createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true)); createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true));
createOpts.initial_state.push({ if (!opts.historyVisibility) {
type: EventType.RoomHistoryVisibility, opts.historyVisibility = createOpts.preset === Preset.PublicChat
content: { ? HistoryVisibility.WorldReadable
"history_visibility": createOpts.preset === Preset.PublicChat ? "world_readable" : "invited", : HistoryVisibility.Invited;
}, }
});
if (opts.joinRule === JoinRule.Restricted) { if (opts.joinRule === JoinRule.Restricted) {
if (SpaceStore.instance.restrictedJoinRuleSupport?.preferred) { if (SpaceStore.instance.restrictedJoinRuleSupport?.preferred) {
@ -176,6 +191,27 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
}); });
} }
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; let modal;
if (opts.spinner) modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner'); if (opts.spinner) modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');

View file

@ -193,4 +193,9 @@ export enum Action {
* Switches space. Should be used with SwitchSpacePayload. * Switches space. Should be used with SwitchSpacePayload.
*/ */
SwitchSpace = "switch_space", SwitchSpace = "switch_space",
/**
* Signals to the visible space hierarchy that a change has occurred an that it should refresh.
*/
UpdateSpaceHierarchy = "update_space_hierarchy",
} }

View file

@ -1005,6 +1005,8 @@
"Name": "Name", "Name": "Name",
"Description": "Description", "Description": "Description",
"Please enter a name for the space": "Please enter a name for the space", "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", "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.", "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", "Public": "Public",
@ -1017,8 +1019,6 @@
"Your private space": "Your private space", "Your private space": "Your private space",
"Add some details to help people recognise it.": "Add some details to help people recognise it.", "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.", "You can change these anytime.": "You can change these anytime.",
"e.g. my-space": "e.g. my-space",
"Address": "Address",
"Creating...": "Creating...", "Creating...": "Creating...",
"Create": "Create", "Create": "Create",
"All rooms": "All rooms", "All rooms": "All rooms",
@ -1056,6 +1056,7 @@
"Leave space": "Leave space", "Leave space": "Leave space",
"Create new room": "Create new room", "Create new room": "Create new room",
"Add existing room": "Add existing room", "Add existing room": "Add existing room",
"Add subspace": "Add subspace",
"Members": "Members", "Members": "Members",
"Manage & explore rooms": "Manage & explore rooms", "Manage & explore rooms": "Manage & explore rooms",
"Explore rooms": "Explore rooms", "Explore rooms": "Explore rooms",
@ -2110,17 +2111,20 @@
"Add a new server...": "Add a new server...", "Add a new server...": "Add a new server...",
"%(networkName)s rooms": "%(networkName)s rooms", "%(networkName)s rooms": "%(networkName)s rooms",
"Matrix rooms": "Matrix 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", "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)|other": "Adding rooms... (%(progress)s out of %(count)s)",
"Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...", "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", "Direct Messages": "Direct Messages",
"Space selection": "Space selection", "Space selection": "Space selection",
"Add existing rooms": "Add existing rooms", "Add existing rooms": "Add existing rooms",
"Want to add a new room instead?": "Want to add a new room instead?", "Want to add a new room instead?": "Want to add a new room instead?",
"Create a new room": "Create a new room", "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 ID": "Matrix ID",
"Matrix Room ID": "Matrix Room ID", "Matrix Room ID": "Matrix Room ID",
"email address": "email address", "email address": "email address",
@ -2208,13 +2212,23 @@
"Create a room in %(communityName)s": "Create a room in %(communityName)s", "Create a room in %(communityName)s": "Create a room in %(communityName)s",
"Create a public room": "Create a public room", "Create a public room": "Create a public room",
"Create a private room": "Create a private 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)", "Private room (invite only)": "Private room (invite only)",
"Public room": "Public room", "Public room": "Public room",
"Visible to space members": "Visible to space members", "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.", "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 Room": "Create Room",
"Anyone in <SpaceName/> will be able to find and join.": "Anyone in <SpaceName/> will be able to find and join.",
"Anyone will be able to find and join this space, not just members of <SpaceName/>.": "Anyone will be able to find and join this space, not just members of <SpaceName/>.",
"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",
"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", "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", "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.", "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.",
@ -2816,6 +2830,7 @@
"Creating rooms...": "Creating rooms...", "Creating rooms...": "Creating rooms...",
"What do you want to organise?": "What do you want to organise?", "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.", "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", "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.", "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", "Go to my first room": "Go to my first room",

View file

@ -28,6 +28,11 @@ import { _t } from "../languageHandler";
import SpacePublicShare from "../components/views/spaces/SpacePublicShare"; import SpacePublicShare from "../components/views/spaces/SpacePublicShare";
import InfoDialog from "../components/views/dialogs/InfoDialog"; import InfoDialog from "../components/views/dialogs/InfoDialog";
import { showRoomInviteDialog } from "../RoomInvite"; 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) => { export const shouldShowSpaceSettings = (space: Room) => {
const userId = space.client.getUserId(); const userId = space.client.getUserId();
@ -54,21 +59,26 @@ export const showSpaceSettings = (space: Room) => {
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
}; };
export const showAddExistingRooms = async (space: Room) => { export const showAddExistingRooms = (space: Room): void => {
return Modal.createTrackedDialog( Modal.createTrackedDialog(
"Space Landing", "Space Landing",
"Add Existing", "Add Existing",
AddExistingToSpaceDialog, AddExistingToSpaceDialog,
{ {
matrixClient: space.client, onCreateRoomClick: () => showCreateNewRoom(space),
onCreateRoomClick: showCreateNewRoom, onAddSubspaceClick: () => showAddExistingSubspace(space),
space, space,
onFinished: (added: boolean) => {
if (added && RoomViewStore.getRoomId() === space.roomId) {
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
}
},
}, },
"mx_AddExistingToSpaceDialog_wrapper", "mx_AddExistingToSpaceDialog_wrapper",
).finished; );
}; };
export const showCreateNewRoom = async (space: Room) => { export const showCreateNewRoom = async (space: Room): Promise<boolean> => {
const modal = Modal.createTrackedDialog<[boolean, IOpts]>( const modal = Modal.createTrackedDialog<[boolean, IOpts]>(
"Space Landing", "Space Landing",
"Create Room", "Create Room",
@ -85,7 +95,7 @@ export const showCreateNewRoom = async (space: Room) => {
return shouldCreate; return shouldCreate;
}; };
export const showSpaceInvite = (space: Room, initialText = "") => { export const showSpaceInvite = (space: Room, initialText = ""): void => {
if (space.getJoinRule() === "public") { if (space.getJoinRule() === "public") {
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, { const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
title: _t("Invite to %(spaceName)s", { spaceName: space.name }), title: _t("Invite to %(spaceName)s", { spaceName: space.name }),
@ -102,3 +112,39 @@ export const showSpaceInvite = (space: Room, initialText = "") => {
showRoomInviteDialog(space.roomId, initialText); showRoomInviteDialog(space.roomId, initialText);
} }
}; };
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",
);
};
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",
);
};