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

 Conflicts:
	src/components/views/spaces/SpaceTreeLevel.tsx
	src/dispatcher/actions.ts
	src/i18n/strings/en_EN.json
This commit is contained in:
Michael Telatynski 2021-07-29 16:09:18 +01:00
commit 02dbdc5c0a
41 changed files with 1672 additions and 709 deletions

View file

@ -67,7 +67,6 @@
@import "./views/dialogs/_AddExistingToSpaceDialog.scss"; @import "./views/dialogs/_AddExistingToSpaceDialog.scss";
@import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_AddressPickerDialog.scss";
@import "./views/dialogs/_Analytics.scss"; @import "./views/dialogs/_Analytics.scss";
@import "./views/dialogs/_BetaFeedbackDialog.scss";
@import "./views/dialogs/_BugReportDialog.scss"; @import "./views/dialogs/_BugReportDialog.scss";
@import "./views/dialogs/_ChangelogDialog.scss"; @import "./views/dialogs/_ChangelogDialog.scss";
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss"; @import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
@ -76,16 +75,20 @@
@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";
@import "./views/dialogs/_FeedbackDialog.scss"; @import "./views/dialogs/_FeedbackDialog.scss";
@import "./views/dialogs/_ForwardDialog.scss"; @import "./views/dialogs/_ForwardDialog.scss";
@import "./views/dialogs/_GenericFeatureFeedbackDialog.scss";
@import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_GroupAddressPicker.scss";
@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/_LeaveSpaceDialog.scss";
@import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss"; @import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss";
@import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss";
@import "./views/dialogs/_ModalWidgetDialog.scss"; @import "./views/dialogs/_ModalWidgetDialog.scss";

View file

@ -61,6 +61,7 @@ limitations under the License.
.mx_AccessibleButton_kind_link { .mx_AccessibleButton_kind_link {
padding: 0; padding: 0;
font-size: inherit;
} }
.mx_SearchBox { .mx_SearchBox {

View file

@ -335,24 +335,17 @@ $SpaceRoomViewInnerWidth: 428px;
word-wrap: break-word; word-wrap: break-word;
} }
> hr {
border: none;
height: 1px;
background-color: $groupFilterPanel-bg-color;
}
.mx_SearchBox { .mx_SearchBox {
margin: 0 0 20px; margin: 0 0 20px;
flex: 0; flex: 0;
} }
.mx_SpaceFeedbackPrompt { .mx_SpaceFeedbackPrompt {
margin-bottom: 16px; padding: 7px; // 8px - 1px border
border: 1px solid $menu-border-color;
// hide the HR as we have our own border-radius: 8px;
& + hr { width: max-content;
display: none; margin: 0 0 -40px auto; // collapse its own height to not push other components down
}
} }
.mx_SpaceRoomDirectory_list { .mx_SpaceRoomDirectory_list {
@ -513,66 +506,3 @@ $SpaceRoomViewInnerWidth: 428px;
} }
} }
} }
.mx_SpaceFeedbackPrompt {
margin-top: 18px;
margin-bottom: 12px;
> hr {
border: none;
border-top: 1px solid $input-border-color;
margin-bottom: 12px;
}
> div {
display: flex;
flex-direction: row;
font-size: $font-15px;
line-height: $font-24px;
> span {
color: $secondary-fg-color;
position: relative;
padding-left: 32px;
font-size: inherit;
line-height: inherit;
margin-right: auto;
&::before {
content: '';
position: absolute;
left: 0;
top: 2px;
height: 20px;
width: 20px;
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 {
color: $accent-color;
position: relative;
padding: 0 0 0 24px;
margin-left: 8px;
font-size: inherit;
line-height: inherit;
&::before {
content: '';
position: absolute;
left: 0;
height: 16px;
width: 16px;
background-color: $accent-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/chat-bubbles.svg');
mask-position: center;
}
}
}
}

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

@ -50,64 +50,11 @@ limitations under the License.
line-height: $font-15px; line-height: $font-15px;
} }
.mx_AddExistingToSpace_entry { .mx_AccessibleButton_kind_link {
display: flex; font-size: $font-12px;
margin-top: 12px; line-height: $font-15px;
margin-top: 8px;
// we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling padding: 0;
.mx_DecoratedRoomAvatar {
margin-right: 12px;
}
.mx_AddExistingToSpace_entry_name {
font-size: $font-15px;
line-height: 30px;
flex-grow: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 12px;
}
.mx_Checkbox {
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;
} }
} }
@ -205,77 +152,106 @@ limitations under the License.
min-height: 0; min-height: 0;
height: 80vh; 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 { .mx_AddExistingToSpace {
display: contents; 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;
}
}
.mx_AddExistingToSpace_entry {
display: flex;
margin-top: 12px;
.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;
flex-grow: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 12px;
}
.mx_Checkbox {
align-items: center;
}
}

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

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_BetaFeedbackDialog { .mx_GenericFeatureFeedbackDialog {
.mx_BetaFeedbackDialog_subheading { .mx_GenericFeatureFeedbackDialog_subheading {
color: $primary-fg-color; color: $primary-fg-color;
font-size: $font-14px; font-size: $font-14px;
line-height: $font-20px; line-height: $font-20px;

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

@ -0,0 +1,96 @@
/*
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_LeaveSpaceDialog_wrapper {
.mx_Dialog {
display: flex;
flex-direction: column;
padding: 24px 32px;
}
}
.mx_LeaveSpaceDialog {
width: 440px;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
max-height: 520px;
.mx_Dialog_content {
flex-grow: 1;
margin: 0;
overflow-y: auto;
.mx_RadioButton + .mx_RadioButton {
margin-top: 16px;
}
.mx_SearchBox {
// To match the space around the title
margin: 0 0 15px 0;
flex-grow: 0;
border-radius: 8px;
}
.mx_LeaveSpaceDialog_noResults {
display: block;
margin-top: 24px;
}
.mx_LeaveSpaceDialog_section {
margin: 16px 0;
}
.mx_LeaveSpaceDialog_section_warning {
position: relative;
border-radius: 8px;
margin: 12px 0 0;
padding: 12px 8px 12px 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;
}
}
> p {
color: $primary-fg-color;
}
}
.mx_Dialog_buttons {
margin-top: 20px;
.mx_Dialog_primary {
background-color: $notice-primary-color !important; // override default colour
border-color: $notice-primary-color;
}
}
}

View file

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

View file

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

View file

@ -59,7 +59,6 @@ $hover-select-border: 4px;
font-size: $font-14px; font-size: $font-14px;
display: inline-block; /* anti-zalgo, with overflow hidden */ display: inline-block; /* anti-zalgo, with overflow hidden */
overflow: hidden; overflow: hidden;
cursor: pointer;
padding-bottom: 0px; padding-bottom: 0px;
padding-top: 0px; padding-top: 0px;
margin: 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 // 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 { .mx_EventTile_bubbleContainer {
display: grid; display: grid;
grid-template-columns: 1fr 100px; grid-template-columns: 1fr 100px;

View file

@ -43,6 +43,12 @@ $spacePanelWidth: 71px;
color: $secondary-fg-color; color: $secondary-fg-color;
margin: 0; margin: 0;
} }
.mx_SpaceFeedbackPrompt {
border-top: 1px solid $input-border-color;
padding-top: 12px;
margin-top: 16px;
}
} }
// XXX remove this when spaces leaves Beta // XXX remove this when spaces leaves Beta
@ -99,3 +105,25 @@ $spacePanelWidth: 71px;
} }
} }
} }
.mx_SpaceFeedbackPrompt {
font-size: $font-15px;
line-height: $font-24px;
> span {
color: $secondary-fg-color;
position: relative;
font-size: inherit;
line-height: inherit;
margin-right: auto;
}
.mx_AccessibleButton_kind_link {
color: $accent-color;
position: relative;
padding: 0;
margin-left: 8px;
font-size: inherit;
line-height: inherit;
}
}

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 { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces"; import { ISpaceSummaryRoom, ISpaceSummaryEvent } 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: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void; showRoom(room: ISpaceSummaryRoom, 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,
ISpaceSummaryRoom[], ISpaceSummaryRoom[],
Map<string, Map<string, ISpaceSummaryEvent>>?, Map<string, Map<string, ISpaceSummaryEvent>>?,
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 data = await cli.getSpaceSummary(space.roomId); const data = await space.client.getSpaceSummary(space.roomId);
const parentChildRelations = new EnhancedMap<string, Map<string, ISpaceSummaryEvent>>(); const parentChildRelations = new EnhancedMap<string, Map<string, ISpaceSummaryEvent>>();
const childParentRelations = new EnhancedMap<string, Set<string>>(); const childParentRelations = new EnhancedMap<string, Set<string>>();
@ -354,7 +362,6 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
space, space,
initialText = "", initialText = "",
showRoom, showRoom,
refreshToken,
additionalButtons, additionalButtons,
children, 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 [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,
@ -62,10 +72,8 @@ import IconizedContextMenu, {
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { BetaPill } from "../views/beta/BetaCard"; import { BetaPill } from "../views/beta/BetaCard";
import { UserTab } from "../views/dialogs/UserSettingsDialog"; import { UserTab } from "../views/dialogs/UserSettingsDialog";
import Modal from "../../Modal";
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
import SdkConfig from "../../SdkConfig";
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership"; import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
import { SpaceFeedbackPrompt } from "../views/spaces/SpaceCreateMenu";
interface IProps { interface IProps {
space: Room; space: Room;
@ -92,28 +100,6 @@ enum Phase {
PrivateExistingRooms, PrivateExistingRooms,
} }
// XXX: Temporary for the Spaces Beta only
export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
if (!SdkConfig.get().bug_report_endpoint_url) return null;
return <div className="mx_SpaceFeedbackPrompt">
<hr />
<div>
<span className="mx_SpaceFeedbackPrompt_text">{ _t("Spaces are a beta feature.") }</span>
<AccessibleButton
kind="link"
onClick={() => {
if (onClick) onClick();
Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, {
featureId: "feature_spaces",
});
}}>
{ _t("Feedback") }
</AccessibleButton>
</div>
</div>;
};
const RoomMemberCount = ({ room, children }) => { const RoomMemberCount = ({ room, children }) => {
const members = useRoomMembers(room); const members = useRoomMembers(room);
const count = members.length; const count = members.length;
@ -307,7 +293,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 +317,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 space")}
iconClassName="mx_RoomList_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
showCreateNewSubspace(space);
}}
>
<BetaPill />
</IconizedContextMenuOption>
</IconizedContextMenuOptionList> </IconizedContextMenuOptionList>
</IconizedContextMenu>; </IconizedContextMenu>;
} }
@ -389,11 +383,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;
@ -416,6 +408,7 @@ const SpaceLanding = ({ space }) => {
}; };
return <div className="mx_SpaceRoomView_landing"> return <div className="mx_SpaceRoomView_landing">
<SpaceFeedbackPrompt />
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} /> <RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
<div className="mx_SpaceRoomView_landing_name"> <div className="mx_SpaceRoomView_landing_name">
<RoomName room={space}> <RoomName room={space}>
@ -440,15 +433,8 @@ const SpaceLanding = ({ space }) => {
</div> </div>
) } ) }
</RoomTopic> </RoomTopic>
<SpaceFeedbackPrompt />
<hr />
<SpaceHierarchy <SpaceHierarchy space={space} showRoom={showRoom} additionalButtons={addRoomButton} />
space={space}
showRoom={showRoom}
refreshToken={refreshToken}
additionalButtons={addRoomButton}
/>
</div>; </div>;
}; };
@ -531,7 +517,6 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
value={buttonLabel} value={buttonLabel}
/> />
</div> </div>
<SpaceFeedbackPrompt />
</div>; </div>;
}; };
@ -550,11 +535,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 />
</div>; </div>;
}; };
@ -574,7 +560,6 @@ const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, createdRoom
{ createdRooms ? _t("Go to my first room") : _t("Go to my space") } { createdRooms ? _t("Go to my first room") : _t("Go to my space") }
</AccessibleButton> </AccessibleButton>
</div> </div>
<SpaceFeedbackPrompt />
</div>; </div>;
}; };
@ -603,9 +588,8 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
</AccessibleButton> </AccessibleButton>
<div className="mx_SpaceRoomView_betaWarning"> <div className="mx_SpaceRoomView_betaWarning">
<h3>{ _t("Teammates might not be able to view or join any private rooms you make.") }</h3> <h3>{ _t("Teammates might not be able to view or join any private rooms you make.") }</h3>
<p>{ _t("We're working on this as part of the beta, but just want to let you know.") }</p> <p>{ _t("We're working on this, but just want to let you know.") }</p>
</div> </div>
<SpaceFeedbackPrompt />
</div>; </div>;
}; };
@ -728,7 +712,6 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
value={buttonLabel} value={buttonLabel}
/> />
</div> </div>
<SpaceFeedbackPrompt />
</div>; </div>;
}; };

View file

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

View file

@ -27,6 +27,8 @@ import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import SettingsFlag from "../elements/SettingsFlag"; import SettingsFlag from "../elements/SettingsFlag";
// XXX: Keep this around for re-use in future Betas
interface IProps { interface IProps {
title?: string; title?: string;
featureId: string; featureId: string;

View file

@ -93,10 +93,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

@ -24,9 +24,11 @@ import {
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu"; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { import {
leaveSpace,
shouldShowSpaceSettings, shouldShowSpaceSettings,
showAddExistingRooms, showAddExistingRooms,
showCreateNewRoom, showCreateNewRoom,
showCreateNewSubspace,
showSpaceInvite, showSpaceInvite,
showSpaceSettings, showSpaceSettings,
} from "../../../utils/space"; } from "../../../utils/space";
@ -37,6 +39,7 @@ import RoomViewStore from "../../../stores/RoomViewStore";
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import { BetaPill } from "../beta/BetaCard";
interface IProps extends IContextMenuProps { interface IProps extends IContextMenuProps {
space: Room; space: Room;
@ -89,10 +92,7 @@ const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
defaultDispatcher.dispatch({ leaveSpace(space);
action: "leave_room",
room_id: space.roomId,
});
onFinished(); onFinished();
}; };
@ -125,6 +125,14 @@ const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
onFinished(); onFinished();
}; };
const onNewSubspaceClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showCreateNewSubspace(space);
onFinished();
};
newRoomSection = <IconizedContextMenuOptionList first> newRoomSection = <IconizedContextMenuOptionList first>
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus" iconClassName="mx_SpacePanel_iconPlus"
@ -136,6 +144,13 @@ const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
label={_t("Add existing room")} label={_t("Add existing room")}
onClick={onAddExistingRoomClick} onClick={onAddExistingRoomClick}
/> />
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus"
label={_t("Add space")}
onClick={onNewSubspaceClick}
>
<BetaPill />
</IconizedContextMenuOption>
</IconizedContextMenuOptionList>; </IconizedContextMenuOptionList>;
} }

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.
*/
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 { 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>
</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";
@ -35,19 +35,20 @@ import StyledCheckbox from "../elements/StyledCheckbox";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import ProgressBar from "../elements/ProgressBar"; import ProgressBar from "../elements/ProgressBar";
import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import QueryMatcher from "../../../autocomplete/QueryMatcher"; import QueryMatcher from "../../../autocomplete/QueryMatcher";
import TruncatedList from "../elements/TruncatedList"; 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 +66,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 +219,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 +229,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,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"> 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 ? (
<div className="mx_AddExistingToSpace_section"> roomsRenderer(rooms, selectedToAdd, onChange, truncateAt, overflowTile)
<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>
) : undefined } ) : undefined }
{ spaces.length > 0 ? ( { spaces.length > 0 && spacesRenderer ? (
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces"> spacesRenderer(spaces, selectedToAdd, onChange)
<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>
) : null } ) : null }
{ dms.length > 0 ? ( { dms.length > 0 && dmsRenderer ? (
<div className="mx_AddExistingToSpace_section"> dmsRenderer(dms, selectedToAdd, onChange)
<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>
) : null } ) : null }
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults"> { noResults ? <span className="mx_AddExistingToSpace_noResults">
{ _t("No results") } { _t("No results") }
</span> : undefined } </span> : undefined }
</AutoHideScrollbar> </AutoHideScrollbar>
@ -299,50 +285,126 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
</div>; </div>;
}; };
const AddExistingToSpaceDialog: React.FC<IProps> = ({ space, onCreateRoomClick, onFinished }) => { export const defaultRoomsRenderer: IAddExistingToSpaceProps["roomsRenderer"] = (
const [selectedSpace, setSelectedSpace] = useState(space); rooms, selectedToAdd, onChange, truncateAt, overflowTile,
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); ) => (
<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; export const defaultSpacesRenderer: IAddExistingToSpaceProps["spacesRenderer"] = (spaces, selectedToAdd, onChange) => (
if (existingSubspaces.length > 0) { <div className="mx_AddExistingToSpace_section">
const options = [space, ...existingSubspaces].map((space) => { { spaces.map(space => {
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", { return <Entry
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace, key={space.roomId}
}); room={space}
return <div key={space.roomId} className={classes}> checked={selectedToAdd.has(space)}
<RoomAvatar room={space} width={24} height={24} /> onChange={onChange ? (checked) => {
{ space.name || getDisplayAliasForRoom(space) || space.roomId } onChange(checked, space);
</div>; } : 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 <Dropdown
id="mx_SpaceSelectDropdown" id="mx_SpaceSelectDropdown"
className="mx_SpaceSelectDropdown"
onOptionChange={(key: string) => { 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")} 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> </Dropdown>
); );
} else { } else {
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace"> body = (
{ space.name || getDisplayAliasForRoom(space) || space.roomId } <div className="mx_SubspaceSelector_onlySpace">
</div>; { space.name || getDisplayAliasForRoom(space) || space.roomId }
</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,14 +416,35 @@ 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>
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
</BaseDialog>; </BaseDialog>;
}; };

View file

@ -14,22 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { useState } from "react"; import React from "react";
import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Field from "../elements/Field";
import SdkConfig from "../../../SdkConfig";
import { IDialogProps } from "./IDialogProps"; import { IDialogProps } from "./IDialogProps";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { submitFeedback } from "../../../rageshake/submit-rageshake";
import StyledCheckbox from "../elements/StyledCheckbox";
import Modal from "../../../Modal";
import InfoDialog from "./InfoDialog";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { UserTab } from "./UserSettingsDialog"; import { UserTab } from "./UserSettingsDialog";
import GenericFeatureFeedbackDialog from "./GenericFeatureFeedbackDialog";
// XXX: Keep this around for re-use in future Betas
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
featureId: string; featureId: string;
@ -38,77 +34,28 @@ interface IProps extends IDialogProps {
const BetaFeedbackDialog: React.FC<IProps> = ({ featureId, onFinished }) => { const BetaFeedbackDialog: React.FC<IProps> = ({ featureId, onFinished }) => {
const info = SettingsStore.getBetaInfo(featureId); const info = SettingsStore.getBetaInfo(featureId);
const [comment, setComment] = useState(""); return <GenericFeatureFeedbackDialog
const [canContact, setCanContact] = useState(false);
const sendFeedback = async (ok: boolean) => {
if (!ok) return onFinished(false);
const extraData = SettingsStore.getBetaInfo(featureId)?.extraSettings.reduce((o, k) => {
o[k] = SettingsStore.getValue(k);
return o;
}, {});
submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact, extraData);
onFinished(true);
Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, {
title: _t("Beta feedback"),
description: _t("Thank you for your feedback, we really appreciate it."),
button: _t("Done"),
hasCloseButton: false,
fixedWidth: false,
});
};
return (<QuestionDialog
className="mx_BetaFeedbackDialog"
hasCancelButton={true}
title={_t("%(featureName)s beta feedback", { featureName: info.title })} title={_t("%(featureName)s beta feedback", { featureName: info.title })}
description={<React.Fragment> subheading={_t(info.feedbackSubheading)}
<div className="mx_BetaFeedbackDialog_subheading"> onFinished={onFinished}
{ _t(info.feedbackSubheading) } rageshakeLabel={info.feedbackLabel}
&nbsp; rageshakeData={Object.fromEntries((SettingsStore.getBetaInfo(featureId)?.extraSettings || []).map(k => {
{ _t("Your platform and username will be noted to help us use your feedback as much as we can.") } return SettingsStore.getValue(k);
}))}
<AccessibleButton >
kind="link" <AccessibleButton
onClick={() => { kind="link"
onFinished(false); onClick={() => {
defaultDispatcher.dispatch({ onFinished(false);
action: Action.ViewUserSettings, defaultDispatcher.dispatch({
initialTabId: UserTab.Labs, action: Action.ViewUserSettings,
}); initialTabId: UserTab.Labs,
}} });
> }}
{ _t("To leave the beta, visit your settings.") } >
</AccessibleButton> { _t("To leave the beta, visit your settings.") }
</div> </AccessibleButton>
</GenericFeatureFeedbackDialog>;
<Field
id="feedbackComment"
label={_t("Feedback")}
type="text"
autoComplete="off"
value={comment}
element="textarea"
onChange={(ev) => {
setComment(ev.target.value);
}}
autoFocus={true}
/>
<StyledCheckbox
checked={canContact}
onClick={e => setCanContact((e.target as HTMLInputElement).checked)}
>
{ _t("You may contact me if you have any follow up questions") }
</StyledCheckbox>
</React.Fragment>}
button={_t("Send feedback")}
buttonDisabled={!comment}
onFinished={sendFeedback}
/>);
}; };
export default BetaFeedbackDialog; export default BetaFeedbackDialog;

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 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,101 @@
/*
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 QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler';
import Field from "../elements/Field";
import SdkConfig from "../../../SdkConfig";
import { IDialogProps } from "./IDialogProps";
import { submitFeedback } from "../../../rageshake/submit-rageshake";
import StyledCheckbox from "../elements/StyledCheckbox";
import Modal from "../../../Modal";
import InfoDialog from "./InfoDialog";
interface IProps extends IDialogProps {
title: string;
subheading: string;
rageshakeLabel: string;
rageshakeData?: Record<string, string>;
}
const GenericFeatureFeedbackDialog: React.FC<IProps> = ({
title,
subheading,
children,
rageshakeLabel,
rageshakeData = {},
onFinished,
}) => {
const [comment, setComment] = useState("");
const [canContact, setCanContact] = useState(false);
const sendFeedback = async (ok: boolean) => {
if (!ok) return onFinished(false);
submitFeedback(SdkConfig.get().bug_report_endpoint_url, rageshakeLabel, comment, canContact, rageshakeData);
onFinished(true);
Modal.createTrackedDialog("Feedback Sent", rageshakeLabel, InfoDialog, {
title,
description: _t("Thank you for your feedback, we really appreciate it."),
button: _t("Done"),
hasCloseButton: false,
fixedWidth: false,
});
};
return (<QuestionDialog
className="mx_GenericFeatureFeedbackDialog"
hasCancelButton={true}
title={title}
description={<React.Fragment>
<div className="mx_GenericFeatureFeedbackDialog_subheading">
{ subheading }
&nbsp;
{ _t("Your platform and username will be noted to help us use your feedback as much as we can.") }
{ children }
</div>
<Field
id="feedbackComment"
label={_t("Feedback")}
type="text"
autoComplete="off"
value={comment}
element="textarea"
onChange={(ev) => {
setComment(ev.target.value);
}}
autoFocus={true}
/>
<StyledCheckbox
checked={canContact}
onChange={e => setCanContact((e.target as HTMLInputElement).checked)}
>
{ _t("You may contact me if you have any follow up questions") }
</StyledCheckbox>
</React.Fragment>}
button={_t("Send feedback")}
buttonDisabled={!comment}
onFinished={sendFeedback}
/>);
};
export default GenericFeatureFeedbackDialog;

View file

@ -0,0 +1,197 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useEffect, useMemo, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { _t } from '../../../languageHandler';
import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "../dialogs/BaseDialog";
import SpaceStore from "../../../stores/SpaceStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { Entry } from "./AddExistingToSpaceDialog";
import SearchBox from "../../structures/SearchBox";
import QueryMatcher from "../../../autocomplete/QueryMatcher";
import StyledRadioGroup from "../elements/StyledRadioGroup";
enum RoomsToLeave {
All = "All",
Specific = "Specific",
None = "None",
}
const SpaceChildPicker = ({ filterPlaceholder, rooms, selected, onChange }) => {
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase().trim();
const filteredRooms = useMemo(() => {
if (!lcQuery) {
return rooms;
}
const matcher = new QueryMatcher<Room>(rooms, {
keys: ["name"],
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
shouldMatchWordsOnly: false,
});
return matcher.match(lcQuery);
}, [rooms, lcQuery]);
return <div className="mx_LeaveSpaceDialog_section">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={filterPlaceholder}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_LeaveSpaceDialog_content">
{ filteredRooms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selected.has(room)}
onChange={(checked) => {
onChange(checked, room);
}}
/>;
}) }
{ filteredRooms.length < 1 ? <span className="mx_LeaveSpaceDialog_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>
</div>;
};
const LeaveRoomsPicker = ({ space, spaceChildren, roomsToLeave, setRoomsToLeave }) => {
const selected = useMemo(() => new Set(roomsToLeave), [roomsToLeave]);
const [state, setState] = useState<string>(RoomsToLeave.All);
useEffect(() => {
if (state === RoomsToLeave.All) {
setRoomsToLeave(spaceChildren);
} else {
setRoomsToLeave([]);
}
}, [setRoomsToLeave, state, spaceChildren]);
return <div className="mx_LeaveSpaceDialog_section">
<StyledRadioGroup
name="roomsToLeave"
value={state}
onChange={setState}
definitions={[
{
value: RoomsToLeave.All,
label: _t("Leave all rooms and spaces"),
}, {
value: RoomsToLeave.None,
label: _t("Don't leave any"),
}, {
value: RoomsToLeave.Specific,
label: _t("Leave specific rooms and spaces"),
},
]}
/>
{ state === RoomsToLeave.Specific && (
<SpaceChildPicker
filterPlaceholder={_t("Search %(spaceName)s", { spaceName: space.name })}
rooms={spaceChildren}
selected={selected}
onChange={(selected: boolean, room: Room) => {
if (selected) {
setRoomsToLeave([room, ...roomsToLeave]);
} else {
setRoomsToLeave(roomsToLeave.filter(r => r !== room));
}
}}
/>
) }
</div>;
};
interface IProps {
space: Room;
onFinished(leave: boolean, rooms?: Room[]): void;
}
const isOnlyAdmin = (room: Room): boolean => {
return !room.getJoinedMembers().some(member => {
return member.userId !== room.client.credentials.userId && member.powerLevelNorm === 100;
});
};
const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
const spaceChildren = useMemo(() => SpaceStore.instance.getChildren(space.roomId), [space.roomId]);
const [roomsToLeave, setRoomsToLeave] = useState<Room[]>([]);
let rejoinWarning;
if (space.getJoinRule() !== JoinRule.Public) {
rejoinWarning = _t("You won't be able to rejoin unless you are re-invited.");
}
let onlyAdminWarning;
if (isOnlyAdmin(space)) {
onlyAdminWarning = _t("You're the only admin of this space. " +
"Leaving it will mean no one has control over it.");
} else {
const numChildrenOnlyAdminIn = roomsToLeave.filter(isOnlyAdmin).length;
if (numChildrenOnlyAdminIn > 0) {
onlyAdminWarning = _t("You're the only admin of some of the rooms or spaces you wish to leave. " +
"Leaving them will leave them without any admins.");
}
}
return <BaseDialog
title={_t("Leave %(spaceName)s", { spaceName: space.name })}
className="mx_LeaveSpaceDialog"
contentId="mx_LeaveSpaceDialog"
onFinished={() => onFinished(false)}
fixedWidth={false}
>
<div className="mx_Dialog_content" id="mx_LeaveSpaceDialog">
<p>
{ _t("Are you sure you want to leave <spaceName/>?", {}, {
spaceName: () => <b>{ space.name }</b>,
}) }
&nbsp;
{ rejoinWarning }
</p>
{ spaceChildren.length > 0 && <LeaveRoomsPicker
space={space}
spaceChildren={spaceChildren}
roomsToLeave={roomsToLeave}
setRoomsToLeave={setRoomsToLeave}
/> }
{ onlyAdminWarning && <div className="mx_LeaveSpaceDialog_section_warning">
{ onlyAdminWarning }
</div> }
</div>
<DialogButtons
primaryButton={_t("Leave space")}
onPrimaryButtonClick={() => onFinished(true, roomsToLeave)}
hasCancel={true}
onCancel={onFinished}
/>
</BaseDialog>;
};
export default LeaveSpaceDialog;

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

@ -192,7 +192,8 @@ class Pill extends React.Component {
}); });
} }
onUserPillClicked = () => { onUserPillClicked = (e) => {
e.preventDefault();
dis.dispatch({ dis.dispatch({
action: Action.ViewUser, action: Action.ViewUser,
member: this.state.member, member: this.state.member,

View file

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

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,18 +24,16 @@ 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 defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
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 { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
import { 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";
import SdkConfig from "../../../SdkConfig";
import Modal from "../../../Modal";
import GenericFeatureFeedbackDialog from "../dialogs/GenericFeatureFeedbackDialog";
import SettingsStore from "../../../settings/SettingsStore";
const SpaceCreateMenuType = ({ title, description, className, onClick }) => { const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
return ( return (
@ -66,8 +64,111 @@ const nameToAlias = (name: string, domain: string): string => {
return `#${localpart}:${domain}`; return `#${localpart}:${domain}`;
}; };
const SpaceCreateMenu = ({ onFinished }) => { // XXX: Temporary for the Spaces release only
export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
if (!SdkConfig.get().bug_report_endpoint_url) return null;
return <div className="mx_SpaceFeedbackPrompt">
<span className="mx_SpaceFeedbackPrompt_text">{ _t("Spaces are a new feature.") }</span>
<AccessibleButton
kind="link"
onClick={() => {
if (onClick) onClick();
Modal.createTrackedDialog("Spaces Feedback", "", GenericFeatureFeedbackDialog, {
title: _t("Spaces feedback"),
subheading: _t("Thank you for trying Spaces. " +
"Your feedback will help inform the next versions."),
rageshakeLabel: "spaces-feedback",
rageshakeData: Object.fromEntries([
"feature_spaces.all_rooms",
"feature_spaces.space_member_dms",
"feature_spaces.space_dm_badges",
].map(k => [k, SettingsStore.getValue(k)])),
});
}}
>
{ _t("Give feedback.") }
</AccessibleButton>
</div>;
};
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 +199,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 +256,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,49 +276,20 @@ 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") }
@ -252,13 +307,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
managed={false} managed={false}
> >
<FocusLock returnFocus={true}> <FocusLock returnFocus={true}>
<BetaPill onClick={() => {
onFinished();
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
});
}} />
{ body } { body }
</FocusLock> </FocusLock>
</ContextMenu>; </ContextMenu>;

View file

@ -21,12 +21,11 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
import SpaceBasicSettings from "./SpaceBasicSettings"; import SpaceBasicSettings from "./SpaceBasicSettings";
import { avatarUrlForRoom } from "../../../Avatar"; import { avatarUrlForRoom } from "../../../Avatar";
import { IDialogProps } from "../dialogs/IDialogProps"; import { IDialogProps } from "../dialogs/IDialogProps";
import { getTopic } from "../elements/RoomTopic"; import { getTopic } from "../elements/RoomTopic";
import { defaultDispatcher } from "../../../dispatcher/dispatcher"; import { leaveSpace } from "../../../utils/space";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
@ -96,8 +95,6 @@ const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProp
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> } { error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
<SpaceFeedbackPrompt />
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<SpaceBasicSettings <SpaceBasicSettings
avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")} avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")}
@ -128,10 +125,7 @@ const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProp
<AccessibleButton <AccessibleButton
kind="danger" kind="danger"
onClick={() => { onClick={() => {
defaultDispatcher.dispatch({ leaveSpace(space);
action: "leave_room",
room_id: space.roomId,
});
}} }}
> >
{ _t("Leave Space") } { _t("Leave Space") }

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

@ -194,6 +194,11 @@ export enum Action {
*/ */
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",
/** /**
* Fires when a monitored setting is updated, * Fires when a monitored setting is updated,
* see SettingsStore::monitorSetting for more details. * see SettingsStore::monitorSetting for more details.

View file

@ -1006,6 +1006,12 @@
"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",
"Spaces are a new feature.": "Spaces are a new feature.",
"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", "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",
@ -1018,8 +1024,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",
"Home": "Home", "Home": "Home",
@ -2110,17 +2114,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 space": "Create a new space",
"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",
@ -2134,15 +2141,8 @@
"Invite anyway and never warn me again": "Invite anyway and never warn me again", "Invite anyway and never warn me again": "Invite anyway and never warn me again",
"Invite anyway": "Invite anyway", "Invite anyway": "Invite anyway",
"Close dialog": "Close dialog", "Close dialog": "Close dialog",
"Beta feedback": "Beta feedback",
"Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.",
"Done": "Done",
"%(featureName)s beta feedback": "%(featureName)s beta feedback", "%(featureName)s beta feedback": "%(featureName)s beta feedback",
"Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.",
"To leave the beta, visit your settings.": "To leave the beta, visit your settings.", "To leave the beta, visit your settings.": "To leave the beta, visit your settings.",
"Feedback": "Feedback",
"You may contact me if you have any follow up questions": "You may contact me if you have any follow up questions",
"Send feedback": "Send feedback",
"Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.", "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.",
"Preparing to send logs": "Preparing to send logs", "Preparing to send logs": "Preparing to send logs",
"Logs sent": "Logs sent", "Logs sent": "Logs sent",
@ -2208,13 +2208,22 @@
"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.",
"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", "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.",
@ -2280,8 +2289,10 @@
"Comment": "Comment", "Comment": "Comment",
"There are two ways you can provide feedback and help us improve %(brand)s.": "There are two ways you can provide feedback and help us improve %(brand)s.", "There are two ways you can provide feedback and help us improve %(brand)s.": "There are two ways you can provide feedback and help us improve %(brand)s.",
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.", "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.",
"Feedback": "Feedback",
"Report a bug": "Report a bug", "Report a bug": "Report a bug",
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.", "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.",
"Send feedback": "Send feedback",
"You don't have permission to do this": "You don't have permission to do this", "You don't have permission to do this": "You don't have permission to do this",
"Sending": "Sending", "Sending": "Sending",
"Sent": "Sent", "Sent": "Sent",
@ -2289,6 +2300,10 @@
"Forward message": "Forward message", "Forward message": "Forward message",
"Message preview": "Message preview", "Message preview": "Message preview",
"Search for rooms or people": "Search for rooms or people", "Search for rooms or people": "Search for rooms or people",
"Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.",
"Done": "Done",
"Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.",
"You may contact me if you have any follow up questions": "You may contact me if you have any follow up questions",
"Confirm abort of host creation": "Confirm abort of host creation", "Confirm abort of host creation": "Confirm abort of host creation",
"Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.", "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.",
"Abort": "Abort", "Abort": "Abort",
@ -2360,6 +2375,16 @@
"Clear cache and resync": "Clear cache and resync", "Clear cache and resync": "Clear cache and resync",
"%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!", "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!",
"Updating %(brand)s": "Updating %(brand)s", "Updating %(brand)s": "Updating %(brand)s",
"Leave all rooms and spaces": "Leave all rooms and spaces",
"Don't leave any": "Don't leave any",
"Leave specific rooms and spaces": "Leave specific rooms and spaces",
"Search %(spaceName)s": "Search %(spaceName)s",
"You won't be able to rejoin unless you are re-invited.": "You won't be able to rejoin unless you are re-invited.",
"You're the only admin of this space. Leaving it will mean no one has control over it.": "You're the only admin of this space. Leaving it will mean no one has control over it.",
"You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.": "You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.",
"Leave %(spaceName)s": "Leave %(spaceName)s",
"Are you sure you want to leave <spaceName/>?": "Are you sure you want to leave <spaceName/>?",
"Leave space": "Leave space",
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.",
"Start using Key Backup": "Start using Key Backup", "Start using Key Backup": "Start using Key Backup",
"I don't want my encrypted messages": "I don't want my encrypted messages", "I don't want my encrypted messages": "I don't want my encrypted messages",
@ -2568,7 +2593,7 @@
"Source URL": "Source URL", "Source URL": "Source URL",
"Collapse reply thread": "Collapse reply thread", "Collapse reply thread": "Collapse reply thread",
"Report": "Report", "Report": "Report",
"Leave space": "Leave space", "Add space": "Add space",
"Manage & explore rooms": "Manage & explore rooms", "Manage & explore rooms": "Manage & explore rooms",
"Clear status": "Clear status", "Clear status": "Clear status",
"Update status": "Update status", "Update status": "Update status",
@ -2802,8 +2827,6 @@
"Search names and descriptions": "Search names and descriptions", "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>.", "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", "Create room": "Create room",
"Spaces are a beta feature.": "Spaces are a beta feature.",
"Public space": "Public space",
"Private space": "Private space", "Private space": "Private space",
"<inviter/> invites you": "<inviter/> invites you", "<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>", "To view %(spaceName)s, turn on the <a>Spaces beta</a>": "To view %(spaceName)s, turn on the <a>Spaces beta</a>",
@ -2818,6 +2841,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",
@ -2829,7 +2853,7 @@
"Me and my teammates": "Me and my teammates", "Me and my teammates": "Me and my teammates",
"A private space for you and your teammates": "A private space for you and your teammates", "A private space for you and your teammates": "A private space for you and your teammates",
"Teammates might not be able to view or join any private rooms you make.": "Teammates might not be able to view or join any private rooms you make.", "Teammates might not be able to view or join any private rooms you make.": "Teammates might not be able to view or join any private rooms you make.",
"We're working on this as part of the beta, but just want to let you know.": "We're working on this as part of the beta, but just want to let you know.", "We're working on this, but just want to let you know.": "We're working on this, but just want to let you know.",
"Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s", "Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s",
"Inviting...": "Inviting...", "Inviting...": "Inviting...",
"Invite your teammates": "Invite your teammates", "Invite your teammates": "Invite your teammates",

View file

@ -124,6 +124,7 @@ export interface ISetting {
// not use this for new settings. // not use this for new settings.
invertedSettingName?: string; invertedSettingName?: string;
// XXX: Keep this around for re-use in future Betas
betaInfo?: { betaInfo?: {
title: string; // _td title: string; // _td
caption: string; // _td caption: string; // _td

View file

@ -335,7 +335,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}, roomId); }, roomId);
} }
private getChildren(spaceId: string): Room[] { public getChildren(spaceId: string): Room[] {
const room = this.matrixClient?.getRoom(spaceId); const room = this.matrixClient?.getRoom(spaceId);
const childEvents = room?.currentState.getStateEvents(EventType.SpaceChild).filter(ev => ev.getContent()?.via); const childEvents = room?.currentState.getStateEvents(EventType.SpaceChild).filter(ev => ev.getContent()?.via);
return sortBy(childEvents, ev => { return sortBy(childEvents, ev => {

View file

@ -141,21 +141,3 @@ export function objectKeyChanges<O extends {}>(a: O, b: O): (keyof O)[] {
export function objectClone<O extends {}>(obj: O): O { export function objectClone<O extends {}>(obj: O): O {
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj));
} }
/**
* Converts a series of entries to an object.
* @param entries The entries to convert.
* @returns The converted object.
*/
// NOTE: Deprecated once we have Object.fromEntries() support.
// @ts-ignore - return type is complaining about non-string keys, but we know better
export function objectFromEntries<K, V>(entries: Iterable<[K, V]>): {[k: K]: V} {
const obj: {
// @ts-ignore - same as return type
[k: K]: V;} = {};
for (const e of entries) {
// @ts-ignore - same as return type
obj[e[0]] = e[1];
}
return obj;
}

View file

@ -28,6 +28,15 @@ 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";
import { leaveRoomBehaviour } from "./membership";
import Spinner from "../components/views/elements/Spinner";
import dis from "../dispatcher/dispatcher";
import LeaveSpaceDialog from "../components/views/dialogs/LeaveSpaceDialog";
export const shouldShowSpaceSettings = (space: Room) => { export const shouldShowSpaceSettings = (space: Room) => {
const userId = space.client.getUserId(); const userId = space.client.getUserId();
@ -54,21 +63,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 +99,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 +116,60 @@ 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",
);
};
export const leaveSpace = (space: Room) => {
Modal.createTrackedDialog("Leave Space", "", LeaveSpaceDialog, {
space,
onFinished: async (leave: boolean, rooms: Room[]) => {
if (!leave) return;
const modal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner");
try {
await Promise.all(rooms.map(r => leaveRoomBehaviour(r.roomId)));
await leaveRoomBehaviour(space.roomId);
} finally {
modal.close();
}
dis.dispatch({
action: "after_leave_room",
room_id: space.roomId,
});
},
}, "mx_LeaveSpaceDialog_wrapper");
};

View file

@ -29,7 +29,6 @@ import {
ArrayUtil, ArrayUtil,
GroupedArray, GroupedArray,
} from "../../src/utils/arrays"; } from "../../src/utils/arrays";
import { objectFromEntries } from "../../src/utils/objects";
function expectSample(i: number, input: number[], expected: number[], smooth = false) { function expectSample(i: number, input: number[], expected: number[], smooth = false) {
console.log(`Resample case index: ${i}`); // for debugging test failures console.log(`Resample case index: ${i}`); // for debugging test failures
@ -336,7 +335,7 @@ describe('arrays', () => {
expect(result).toBeDefined(); expect(result).toBeDefined();
expect(result.value).toBeDefined(); expect(result.value).toBeDefined();
const asObject = objectFromEntries(result.value.entries()); const asObject = Object.fromEntries(result.value.entries());
expect(asObject).toMatchObject(output); expect(asObject).toMatchObject(output);
}); });
}); });

View file

@ -18,7 +18,6 @@ import {
objectClone, objectClone,
objectDiff, objectDiff,
objectExcluding, objectExcluding,
objectFromEntries,
objectHasDiff, objectHasDiff,
objectKeyChanges, objectKeyChanges,
objectShallowClone, objectShallowClone,
@ -242,21 +241,4 @@ describe('objects', () => {
expect(result.test.third).not.toBe(a.test.third); expect(result.test.third).not.toBe(a.test.third);
}); });
}); });
describe('objectFromEntries', () => {
it('should create an object from an array of entries', () => {
const output = { a: 1, b: 2, c: 3 };
const result = objectFromEntries(Object.entries(output));
expect(result).toBeDefined();
expect(result).toMatchObject(output);
});
it('should maintain pointers in values', () => {
const output = { a: {}, b: 2, c: 3 };
const result = objectFromEntries(Object.entries(output));
expect(result).toBeDefined();
expect(result).toMatchObject(output);
expect(result['a']).toBe(output.a);
});
});
}); });