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

 Conflicts:
	src/components/structures/SpaceRoomView.tsx
	src/components/views/spaces/SpaceCreateMenu.tsx
	src/i18n/strings/en_EN.json
This commit is contained in:
Michael Telatynski 2021-07-29 15:16:14 +01:00
commit efd8a2f9b0
25 changed files with 1104 additions and 419 deletions

View file

@ -75,6 +75,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";

View file

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

View file

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

View file

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

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

@ -43,8 +43,10 @@ limitations under the License.
margin-bottom: 7px;
mask-image: url('$(res)/img/feather-customised/minimise.svg');
}
}
&:hover .mx_ViewSourceEvent_toggle {
.mx_EventTile:hover {
.mx_ViewSourceEvent_toggle {
visibility: visible;
}
}

View file

@ -38,18 +38,22 @@ limitations under the License.
padding-top: 0;
}
&::before {
content: '';
position: absolute;
top: -1px;
bottom: -1px;
left: -60px;
right: -60px;
z-index: -1;
border-radius: 4px;
}
&:hover,
&.mx_EventTile_selected {
&::before {
content: '';
position: absolute;
top: -1px;
bottom: -1px;
left: -60px;
right: -60px;
z-index: -1;
background: $eventbubble-bg-hover;
border-radius: 4px;
}
.mx_EventTile_avatar {
@ -267,7 +271,7 @@ limitations under the License.
display: flex;
align-items: center;
justify-content: center;
justify-content: start;
padding: 5px 0;
.mx_EventTile_avatar {
@ -275,12 +279,22 @@ limitations under the License.
order: -1;
margin-right: 5px;
}
.mx_EventTile_e2eIcon {
margin-left: 9px;
}
.mx_EventTile_line > a {
right: auto;
top: -15px;
left: -68px;
}
}
.mx_EventListSummary[data-layout=bubble] {
--maxWidth: 80%;
--maxWidth: 70%;
margin-left: calc(var(--avatarSize) + var(--gutterSize));
margin-right: calc(var(--gutterSize) + var(--avatarSize));
margin-right: 94px;
.mx_EventListSummary_toggle {
float: none;
margin: 0;

View file

@ -59,7 +59,6 @@ $hover-select-border: 4px;
font-size: $font-14px;
display: inline-block; /* anti-zalgo, with overflow hidden */
overflow: hidden;
cursor: pointer;
padding-bottom: 0px;
padding-top: 0px;
margin: 0px;
@ -322,6 +321,10 @@ $hover-select-border: 4px;
// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter
}
.mx_SenderProfile {
cursor: pointer;
}
.mx_EventTile_bubbleContainer {
display: grid;
grid-template-columns: 1fr 100px;

View file

@ -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";
@ -44,11 +43,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 +316,25 @@ export const HierarchyLevel = ({
</React.Fragment>;
};
// mutate argument refreshToken to force a reload
export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [
export const useSpaceSummary = (space: Room): [
null,
ISpaceSummaryRoom[],
Map<string, Map<string, ISpaceSummaryEvent>>?,
Map<string, Set<string>>?,
Map<string, Set<string>>?,
] | [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<string, Map<string, ISpaceSummaryEvent>>();
const childParentRelations = new EnhancedMap<string, Set<string>>();
@ -354,7 +362,6 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
space,
initialText = "",
showRoom,
refreshToken,
additionalButtons,
children,
}) => {
@ -364,7 +371,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
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(() => {
if (!rooms) return null;

View file

@ -47,13 +47,23 @@ 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,
@ -283,7 +293,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
</div>;
};
const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
const SpaceLandingAddButton = ({ space }) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
let contextMenu;
@ -307,24 +317,32 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
closeMenu();
if (await showCreateNewRoom(space)) {
onNewRoomAdded();
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
}
}}
/>
<IconizedContextMenuOption
label={_t("Add existing room")}
iconClassName="mx_RoomList_iconHash"
onClick={async (e) => {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
const [added] = await showAddExistingRooms(space);
if (added) {
onNewRoomAdded();
}
showAddExistingRooms(space);
}}
/>
<IconizedContextMenuOption
label={_t("Add space")}
iconClassName="mx_RoomList_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
showCreateNewSubspace(space);
}}
>
<BetaPill />
</IconizedContextMenuOption>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
}
@ -365,11 +383,9 @@ const SpaceLanding = ({ space }) => {
const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
const [refreshToken, forceUpdate] = useStateToggle(false);
let addRoomButton;
if (canAddRooms) {
addRoomButton = <SpaceLandingAddButton space={space} onNewRoomAdded={forceUpdate} />;
addRoomButton = <SpaceLandingAddButton space={space} />;
}
let settingsButton;
@ -418,12 +434,7 @@ const SpaceLanding = ({ space }) => {
) }
</RoomTopic>
<SpaceHierarchy
space={space}
showRoom={showRoom}
refreshToken={refreshToken}
additionalButtons={addRoomButton}
/>
<SpaceHierarchy space={space} showRoom={showRoom} additionalButtons={addRoomButton} />
</div>;
};
@ -524,7 +535,11 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
{ _t("Skip for now") }
</AccessibleButton>
}
filterPlaceholder={_t("Search for rooms or spaces")}
onFinished={onFinished}
roomsRenderer={defaultRoomsRenderer}
spacesRenderer={defaultSpacesRenderer}
dmsRenderer={defaultDmsRenderer}
/>
</div>;
};

View file

@ -103,8 +103,8 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
}
private resetRecaptcha() {
if (this.captchaWidgetId !== null) {
global.grecaptcha.reset(this.captchaWidgetId);
if (this.captchaWidgetId) {
global?.grecaptcha?.reset(this.captchaWidgetId);
}
}

View file

@ -90,10 +90,11 @@ export const IconizedContextMenuCheckbox: React.FC<ICheckboxProps> = ({
</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}>
{ iconClassName && <span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} /> }
<span className="mx_IconizedContextMenu_label">{ label }</span>
{ children }
</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 space") }
</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 { 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";
@ -41,12 +41,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 <label className="mx_AddExistingToSpace_entry">
{ room?.isSpaceRoom()
? <RoomAvatar room={room} height={32} width={32} />
@ -64,14 +66,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<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> = ({
space,
footerPrompt,
emptySelectionButton,
filterPlaceholder,
roomsRenderer,
dmsRenderer,
spacesRenderer,
onFinished,
}) => {
const cli = useContext(MatrixClientContext);
@ -195,7 +219,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
</>;
}
const onChange = !busy && !error ? (checked, room) => {
const onChange = !busy && !error ? (checked: boolean, room: Room) => {
if (checked) {
selectedToAdd.add(room);
} else {
@ -205,7 +229,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
} : 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 (
<EntityTile
@ -221,73 +245,36 @@ 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">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={_t("Filter your rooms and spaces")}
placeholder={filterPlaceholder}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_AddExistingToSpace_content">
{ rooms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Rooms") }</h3>
<TruncatedList
truncateAt={truncateAt}
createOverflowElement={overflowTile}
getChildren={(start, end) => rooms.slice(start, end).map(room =>
<Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>,
)}
getChildCount={() => rooms.length}
/>
</div>
{ rooms.length > 0 && roomsRenderer ? (
roomsRenderer(rooms, selectedToAdd, onChange, truncateAt, overflowTile)
) : undefined }
{ spaces.length > 0 ? (
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
<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 => {
return <Entry
key={space.roomId}
room={space}
checked={selectedToAdd.has(space)}
onChange={onChange ? (checked) => {
onChange(checked, space);
} : null}
/>;
}) }
</div>
{ spaces.length > 0 && spacesRenderer ? (
spacesRenderer(spaces, selectedToAdd, onChange)
) : null }
{ dms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Direct Messages") }</h3>
{ dms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
{ dms.length > 0 && dmsRenderer ? (
dmsRenderer(dms, selectedToAdd, onChange)
) : null }
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
{ noResults ? <span className="mx_AddExistingToSpace_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>
@ -298,50 +285,126 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
</div>;
};
const AddExistingToSpaceDialog: React.FC<IProps> = ({ 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,
) => (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Rooms") }</h3>
<TruncatedList
truncateAt={truncateAt}
createOverflowElement={overflowTile}
getChildren={(start, end) => rooms.slice(start, end).map(room =>
<Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked: boolean) => {
onChange(checked, room);
} : null}
/>,
)}
getChildCount={() => rooms.length}
/>
</div>
);
let spaceOptionSection;
if (existingSubspaces.length > 0) {
const options = [space, ...existingSubspaces].map((space) => {
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
});
return <div key={space.roomId} className={classes}>
<RoomAvatar room={space} width={24} height={24} />
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
});
export const defaultSpacesRenderer: IAddExistingToSpaceProps["spacesRenderer"] = (spaces, selectedToAdd, onChange) => (
<div className="mx_AddExistingToSpace_section">
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selectedToAdd.has(space)}
onChange={onChange ? (checked) => {
onChange(checked, space);
} : null}
/>;
}) }
</div>
);
spaceOptionSection = (
export const defaultDmsRenderer: IAddExistingToSpaceProps["dmsRenderer"] = (dms, selectedToAdd, onChange) => (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Direct Messages") }</h3>
{ dms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked: boolean) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
);
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 = (
<Dropdown
id="mx_SpaceSelectDropdown"
className="mx_SpaceSelectDropdown"
onOptionChange={(key: string) => {
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 <div key={space.roomId} className={classes}>
<RoomAvatar room={space} width={24} height={24} />
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
}) }
</Dropdown>
);
} else {
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
body = (
<div className="mx_SubspaceSelector_onlySpace">
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>
);
}
const title = <React.Fragment>
<RoomAvatar room={selectedSpace} height={40} width={40} />
return <div className="mx_SubspaceSelector">
<RoomAvatar room={value} height={40} width={40} />
<div>
<h1>{ _t("Add existing rooms") }</h1>
{ spaceOptionSection }
<h1>{ title }</h1>
{ body }
</div>
</React.Fragment>;
</div>;
};
const AddExistingToSpaceDialog: React.FC<IProps> = ({ space, onCreateRoomClick, onAddSubspaceClick, onFinished }) => {
const [selectedSpace, setSelectedSpace] = useState(space);
return <BaseDialog
title={title}
title={(
<SubspaceSelector
title={_t("Add existing rooms")}
space={space}
value={selectedSpace}
onChange={setSelectedSpace}
/>
)}
className="mx_AddExistingToSpaceDialog"
contentId="mx_AddExistingToSpace"
onFinished={onFinished}
@ -353,10 +416,33 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ space, onCreateRoomClick,
onFinished={onFinished}
footerPrompt={<>
<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") }
</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>
</BaseDialog>;

View file

@ -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;
@ -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');
}
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 (
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished} title={title}>
<form onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
@ -356,16 +341,14 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
className="mx_CreateRoomDialog_topic"
/>
<Dropdown
id="mx_CreateRoomDialog_typeDropdown"
className="mx_CreateRoomDialog_typeDropdown"
onOptionChange={this.onJoinRuleChange}
menuWidth={448}
value={this.state.joinRule}
<JoinRuleDropdown
label={_t("Room visibility")}
>
{ options }
</Dropdown>
labelInvite={_t("Private room (invite only)")}
labelPublic={_t("Public room")}
labelRestricted={this.supportsRestricted ? _t("Visible to space members") : undefined}
value={this.state.joinRule}
onChange={this.onJoinRuleChange}
/>
{ publicPrivateLabel }
{ 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 space")}
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 space 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("Space visibility")}
labelInvite={_t("Private space (invite only)")}
labelPublic={_t("Public space")}
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

@ -15,18 +15,21 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixEvent } from 'matrix-js-sdk/src';
import classNames from 'classnames';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@replaceableComponent("views.messages.ViewSourceEvent")
export default class ViewSourceEvent extends React.PureComponent {
static propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
};
interface IProps {
mxEvent: MatrixEvent;
}
interface IState {
expanded: boolean;
}
@replaceableComponent("views.messages.ViewSourceEvent")
export default class ViewSourceEvent extends React.PureComponent<IProps, IState> {
constructor(props) {
super(props);
@ -35,7 +38,7 @@ export default class ViewSourceEvent extends React.PureComponent {
};
}
componentDidMount() {
public componentDidMount(): void {
const { mxEvent } = this.props;
const client = MatrixClientPeg.get();
@ -46,15 +49,15 @@ export default class ViewSourceEvent extends React.PureComponent {
}
}
onToggle = (ev) => {
private onToggle = (ev: React.MouseEvent) => {
ev.preventDefault();
const { expanded } = this.state;
this.setState({
expanded: !expanded,
});
}
};
render() {
public render(): React.ReactNode {
const { mxEvent } = this.props;
const { expanded } = this.state;

View file

@ -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,12 +24,11 @@ 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 Field from "../elements/Field";
import withValidation from "../elements/Validation";
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";
import SdkConfig from "../../../SdkConfig";
import Modal from "../../../Modal";
@ -93,8 +92,83 @@ export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
</div>;
};
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 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 [busy, setBusy] = useState<boolean>(false);
@ -125,42 +199,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,
@ -198,7 +256,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
<SpaceFeedbackPrompt onClick={onFinished} />
</React.Fragment>;
} else {
const domain = cli.getDomain();
body = <React.Fragment>
<AccessibleTooltipButton
className="mx_SpaceCreateMenu_back"
@ -219,49 +276,20 @@ const SpaceCreateMenu = ({ onFinished }) => {
}
</p>
<form className="mx_SpaceBasicSettings" onSubmit={onSpaceCreateClick}>
<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={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>
<SpaceCreateForm
busy={busy}
onSubmit={onSpaceCreateClick}
setAvatar={setAvatar}
name={name}
setName={setName}
nameFieldRef={spaceNameField}
topic={topic}
setTopic={setTopic}
alias={alias}
setAlias={setAlias}
showAliasField={visibility === Visibility.Public}
aliasFieldRef={spaceAliasField}
/>
<AccessibleButton kind="primary" onClick={onSpaceCreateClick} disabled={busy}>
{ busy ? _t("Creating...") : _t("Create") }

View file

@ -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<HTMLLIElement> {
space?: Room;
@ -234,6 +236,14 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
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<IItemProps, IItemState> {
label={_t("Add existing room")}
onClick={this.onAddExistingRoomClick}
/>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus"
label={_t("Add space")}
onClick={this.onNewSubspaceClick}
>
<BetaPill />
</IconizedContextMenuOption>
</IconizedContextMenuOptionList>;
}

View file

@ -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<string | null> {
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<string | null> {
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<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;
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.
*/
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

@ -1009,6 +1009,8 @@
"Spaces feedback": "Spaces feedback",
"Thank you for trying Spaces. Your feedback will help inform the next versions.": "Thank you for trying Spaces. Your feedback will help inform the next versions.",
"Give feedback.": "Give feedback.",
"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",
@ -1021,8 +1023,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",
@ -1060,6 +1060,7 @@
"Leave space": "Leave space",
"Create new room": "Create new room",
"Add existing room": "Add existing room",
"Add space": "Add space",
"Members": "Members",
"Manage & explore rooms": "Manage & explore rooms",
"Explore rooms": "Explore rooms",
@ -2114,17 +2115,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 space": "Create a new space",
"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,22 @@
"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",
"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.",
"Add a space to a space you manage.": "Add a space to a space you manage.",
"Space visibility": "Space visibility",
"Private space (invite only)": "Private space (invite only)",
"Public space": "Public space",
"Want to add an existing space instead?": "Want to add an existing space instead?",
"Adding...": "Adding...",
"Sign out": "Sign out",
"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.",
@ -2803,7 +2816,6 @@
"Search names and descriptions": "Search names and descriptions",
"If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.",
"Create room": "Create room",
"Public space": "Public space",
"Private space": "Private space",
"<inviter/> invites you": "<inviter/> invites you",
"To view %(spaceName)s, turn on the <a>Spaces beta</a>": "To view %(spaceName)s, turn on the <a>Spaces beta</a>",
@ -2818,6 +2830,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",

View file

@ -28,6 +28,11 @@ 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";
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();
@ -54,21 +59,26 @@ export const showSpaceSettings = (space: Room) => {
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
};
export const showAddExistingRooms = async (space: Room) => {
return Modal.createTrackedDialog(
export const showAddExistingRooms = (space: Room): void => {
Modal.createTrackedDialog(
"Space Landing",
"Add Existing",
AddExistingToSpaceDialog,
{
matrixClient: space.client,
onCreateRoomClick: showCreateNewRoom,
onCreateRoomClick: () => showCreateNewRoom(space),
onAddSubspaceClick: () => showAddExistingSubspace(space),
space,
onFinished: (added: boolean) => {
if (added && RoomViewStore.getRoomId() === space.roomId) {
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
}
},
},
"mx_AddExistingToSpaceDialog_wrapper",
).finished;
);
};
export const showCreateNewRoom = async (space: Room) => {
export const showCreateNewRoom = async (space: Room): Promise<boolean> => {
const modal = Modal.createTrackedDialog<[boolean, IOpts]>(
"Space Landing",
"Create Room",
@ -85,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 }),
@ -102,3 +112,39 @@ export const showSpaceInvite = (space: Room, 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",
);
};