From 5ad3261cb210c862a7d9aecd387bd37fa99f74fa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 11 Nov 2021 13:07:41 +0000 Subject: [PATCH] Implement more meta-spaces (#7077) --- res/css/_components.scss | 1 + res/css/structures/_SpacePanel.scss | 30 +- .../views/dialogs/_UserSettingsDialog.scss | 4 + .../tabs/user/_SidebarUserSettingsTab.scss | 87 ++++++ res/img/element-icons/settings/sidebar.svg | 7 + src/@types/global.d.ts | 2 +- src/Avatar.ts | 2 +- src/autocomplete/Autocompleter.ts | 2 +- src/autocomplete/RoomProvider.tsx | 2 +- src/components/structures/LeftPanel.tsx | 17 +- src/components/structures/LoggedInView.tsx | 2 +- src/components/structures/MatrixChat.tsx | 6 +- src/components/structures/RightPanel.tsx | 2 +- src/components/structures/RoomSearch.tsx | 3 +- src/components/structures/RoomView.tsx | 2 +- src/components/structures/SpaceHierarchy.tsx | 2 +- src/components/structures/SpaceRoomView.tsx | 2 +- src/components/structures/UserMenu.tsx | 10 +- .../dialogs/AddExistingToSpaceDialog.tsx | 2 +- .../dialogs/ConfirmSpaceUserActionDialog.tsx | 2 +- .../views/dialogs/CreateRoomDialog.tsx | 2 +- .../CreateSpaceFromCommunityDialog.tsx | 2 +- .../views/dialogs/CreateSubspaceDialog.tsx | 2 +- .../views/dialogs/ForwardDialog.tsx | 2 +- src/components/views/dialogs/InviteDialog.tsx | 2 +- .../views/dialogs/LeaveSpaceDialog.tsx | 2 +- .../ManageRestrictedJoinRuleDialog.tsx | 4 +- .../views/dialogs/UserSettingsDialog.tsx | 11 + src/components/views/right_panel/UserInfo.tsx | 2 +- src/components/views/rooms/MemberList.tsx | 2 +- src/components/views/rooms/NewRoomIntro.tsx | 8 +- src/components/views/rooms/RoomList.tsx | 55 ++-- .../views/rooms/RoomListNumResults.tsx | 2 +- .../views/rooms/ThirdPartyMemberInfo.tsx | 2 +- .../views/settings/JoinRuleSettings.tsx | 10 +- .../tabs/user/SidebarUserSettingsTab.tsx | 123 ++++++++ .../views/spaces/SpaceCreateMenu.tsx | 1 + src/components/views/spaces/SpacePanel.tsx | 115 +++++-- .../views/spaces/SpaceTreeLevel.tsx | 27 +- src/createRoom.ts | 2 +- src/i18n/strings/en_EN.json | 16 +- src/settings/Settings.tsx | 20 ++ src/stores/BreadcrumbsStore.ts | 2 +- src/stores/room-list/RoomListStore.ts | 2 +- src/stores/room-list/SpaceWatcher.ts | 30 +- src/stores/room-list/algorithms/Algorithm.ts | 2 +- .../room-list/filters/SpaceFilterCondition.ts | 15 +- .../room-list/filters/VisibilityProvider.ts | 2 +- src/stores/{ => spaces}/SpaceStore.ts | 291 +++++++++++------- .../{ => spaces}/SpaceTreeLevelLayoutStore.ts | 0 src/stores/spaces/index.ts | 40 +++ src/utils/RoomUpgrade.ts | 2 +- test/stores/SpaceStore-test.ts | 217 +++++++------ test/stores/enable-metaspaces-labs.ts | 17 + test/stores/room-list/SpaceWatcher-test.ts | 104 ++++++- 55 files changed, 970 insertions(+), 353 deletions(-) create mode 100644 res/css/views/settings/tabs/user/_SidebarUserSettingsTab.scss create mode 100644 res/img/element-icons/settings/sidebar.svg create mode 100644 src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx rename src/stores/{ => spaces}/SpaceStore.ts (76%) rename src/stores/{ => spaces}/SpaceTreeLevelLayoutStore.ts (100%) create mode 100644 src/stores/spaces/index.ts create mode 100644 test/stores/enable-metaspaces-labs.ts diff --git a/res/css/_components.scss b/res/css/_components.scss index d4c383b1fe..f59da633d7 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -276,6 +276,7 @@ @import "./views/settings/tabs/user/_NotificationUserSettingsTab.scss"; @import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss"; @import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss"; +@import "./views/settings/tabs/user/_SidebarUserSettingsTab.scss"; @import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss"; @import "./views/spaces/_SpaceBasicSettings.scss"; @import "./views/spaces/_SpaceChildrenPicker.scss"; diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index bf79426c9d..d822b9baf2 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -189,15 +189,35 @@ $activeBorderColor: $secondary-content; } } - &.mx_SpaceButton_home .mx_SpaceButton_icon { - background-color: #ffffff; + &.mx_SpaceButton_home, + &.mx_SpaceButton_favourites, + &.mx_SpaceButton_people, + &.mx_SpaceButton_orphans { + .mx_SpaceButton_icon { + background-color: #ffffff; - &::before { - background-color: #3f3d3d; - mask-image: url('$(res)/img/element-icons/home.svg'); + &::before { + background-color: #3f3d3d; + } } } + &.mx_SpaceButton_home .mx_SpaceButton_icon::before { + mask-image: url('$(res)/img/element-icons/home.svg'); + } + + &.mx_SpaceButton_favourites .mx_SpaceButton_icon::before { + mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg'); + } + + &.mx_SpaceButton_people .mx_SpaceButton_icon::before { + mask-image: url('$(res)/img/element-icons/room/members.svg'); + } + + &.mx_SpaceButton_orphans .mx_SpaceButton_icon::before { + mask-image: url('$(res)/img/element-icons/roomlist/hash-circle.svg'); + } + &.mx_SpaceButton_new .mx_SpaceButton_icon { background-color: $roomlist-button-bg-color; diff --git a/res/css/views/dialogs/_UserSettingsDialog.scss b/res/css/views/dialogs/_UserSettingsDialog.scss index bd472710ea..f7728eb69b 100644 --- a/res/css/views/dialogs/_UserSettingsDialog.scss +++ b/res/css/views/dialogs/_UserSettingsDialog.scss @@ -37,6 +37,10 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/settings/preference.svg'); } +.mx_UserSettingsDialog_sidebarIcon::before { + mask-image: url('$(res)/img/element-icons/settings/sidebar.svg'); +} + .mx_UserSettingsDialog_securityIcon::before { mask-image: url('$(res)/img/element-icons/security.svg'); } diff --git a/res/css/views/settings/tabs/user/_SidebarUserSettingsTab.scss b/res/css/views/settings/tabs/user/_SidebarUserSettingsTab.scss new file mode 100644 index 0000000000..91869f4e02 --- /dev/null +++ b/res/css/views/settings/tabs/user/_SidebarUserSettingsTab.scss @@ -0,0 +1,87 @@ +/* +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_SidebarUserSettingsTab { + .mx_SidebarUserSettingsTab_subheading { + font-size: $font-15px; + line-height: $font-24px; + color: $primary-content; + margin-bottom: 4px; + } + + .mx_Checkbox { + margin-top: 12px; + font-size: $font-15px; + line-height: $font-24px; + color: $secondary-content; + } + + .mx_SidebarUserSettingsTab_checkboxMicrocopy { + margin-bottom: 12px; + margin-left: 24px; + font-size: $font-15px; + line-height: $font-24px; + color: $secondary-content; + } + + .mx_SidebarUserSettingsTab_homeAllRoomsCheckbox { + margin-left: 24px; + + & + div { + margin-left: 48px; + } + } + + .mx_SidebarUserSettingsTab_homeCheckbox, + .mx_SidebarUserSettingsTab_favouritesCheckbox, + .mx_SidebarUserSettingsTab_peopleCheckbox, + .mx_SidebarUserSettingsTab_orphansCheckbox { + .mx_Checkbox_background + div { + padding-left: 20px; + position: relative; + + &::before { + background-color: $secondary-content; + content: ""; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + width: 16px; + height: 16px; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + } + } + } + + .mx_SidebarUserSettingsTab_homeCheckbox .mx_Checkbox_background + div::before { + mask-image: url('$(res)/img/element-icons/home.svg'); + } + + .mx_SidebarUserSettingsTab_favouritesCheckbox .mx_Checkbox_background + div::before { + mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg'); + } + + .mx_SidebarUserSettingsTab_peopleCheckbox .mx_Checkbox_background + div::before { + mask-image: url('$(res)/img/element-icons/room/members.svg'); + } + + .mx_SidebarUserSettingsTab_orphansCheckbox .mx_Checkbox_background + div::before { + mask-image: url('$(res)/img/element-icons/roomlist/hash-circle.svg'); + } +} diff --git a/res/img/element-icons/settings/sidebar.svg b/res/img/element-icons/settings/sidebar.svg new file mode 100644 index 0000000000..24dc9562bc --- /dev/null +++ b/res/img/element-icons/settings/sidebar.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index a9d8e9547f..43c30c94b8 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -41,7 +41,7 @@ import UserActivity from "../UserActivity"; import { ModalWidgetStore } from "../stores/ModalWidgetStore"; import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; import VoipUserMapper from "../VoipUserMapper"; -import { SpaceStoreClass } from "../stores/SpaceStore"; +import { SpaceStoreClass } from "../stores/spaces/SpaceStore"; import TypingStore from "../stores/TypingStore"; import { EventIndexPeg } from "../indexing/EventIndexPeg"; import { VoiceRecordingStore } from "../stores/VoiceRecordingStore"; diff --git a/src/Avatar.ts b/src/Avatar.ts index 93109a470e..310fec5f4c 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -22,7 +22,7 @@ import { split } from "lodash"; import DMRoomMap from './utils/DMRoomMap'; import { mediaFromMxc } from "./customisations/Media"; -import SpaceStore from "./stores/SpaceStore"; +import SpaceStore from "./stores/spaces/SpaceStore"; // Not to be used for BaseAvatar urls as that has similar default avatar fallback already export function avatarUrlForMember( diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index 4c9e82f290..555429e75f 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -27,7 +27,7 @@ import NotifProvider from './NotifProvider'; import { timeout } from "../utils/promise"; import AutocompleteProvider, { ICommand } from "./AutocompleteProvider"; import SpaceProvider from "./SpaceProvider"; -import SpaceStore from "../stores/SpaceStore"; +import SpaceStore from "../stores/spaces/SpaceStore"; export interface ISelectionRange { beginning?: boolean; // whether the selection is in the first block of the editor or not diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index 00bfe6be5c..ced0e7ad17 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -28,7 +28,7 @@ import { PillCompletion } from './Components'; import { makeRoomPermalink } from "../utils/permalinks/Permalinks"; import { ICompletion, ISelectionRange } from "./Autocompleter"; import RoomAvatar from '../components/views/avatars/RoomAvatar'; -import SpaceStore from "../stores/SpaceStore"; +import SpaceStore from "../stores/spaces/SpaceStore"; const ROOM_REGEX = /\B#\S*/g; diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index f12b4cbcf5..98a92f4624 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -17,7 +17,6 @@ limitations under the License. import * as React from "react"; import { createRef } from "react"; import classNames from "classnames"; -import { Room } from "matrix-js-sdk/src/models/room"; import dis from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; @@ -37,10 +36,12 @@ import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import RoomListNumResults from "../views/rooms/RoomListNumResults"; import LeftPanelWidget from "./LeftPanelWidget"; import { replaceableComponent } from "../../utils/replaceableComponent"; -import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore"; +import SpaceStore from "../../stores/spaces/SpaceStore"; +import { SpaceKey, UPDATE_SELECTED_SPACE } from "../../stores/spaces"; import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; import UIStore from "../../stores/UIStore"; import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex"; +import MatrixClientContext from "../../contexts/MatrixClientContext"; interface IProps { isMinimized: boolean; @@ -49,7 +50,7 @@ interface IProps { interface IState { showBreadcrumbs: boolean; - activeSpace?: Room; + activeSpace: SpaceKey; } @replaceableComponent("structures.LeftPanel") @@ -61,6 +62,9 @@ export default class LeftPanel extends React.Component { private focusedElement = null; private isDoingStickyHeaders = false; + static contextType = MatrixClientContext; + public context!: React.ContextType; + constructor(props: IProps) { super(props); @@ -98,7 +102,7 @@ export default class LeftPanel extends React.Component { } } - private updateActiveSpace = (activeSpace: Room) => { + private updateActiveSpace = (activeSpace: SpaceKey) => { this.setState({ activeSpace }); }; @@ -343,6 +347,7 @@ export default class LeftPanel extends React.Component { />; } + const space = this.state.activeSpace[0] === "!" ? this.context.getRoom(this.state.activeSpace) : null; return (
{ mx_LeftPanel_exploreButton_space: !!this.state.activeSpace, })} onClick={this.onExplore} - title={this.state.activeSpace - ? _t("Explore %(spaceName)s", { spaceName: this.state.activeSpace.name }) - : _t("Explore rooms")} + title={space ? _t("Explore %(spaceName)s", { spaceName: space.name }) : _t("Explore rooms")} />
); diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 566e14e633..88756a334b 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -64,7 +64,7 @@ import MyGroups from "./MyGroups"; import UserView from "./UserView"; import GroupView from "./GroupView"; import BackdropPanel from "./BackdropPanel"; -import SpaceStore from "../../stores/SpaceStore"; +import SpaceStore from "../../stores/spaces/SpaceStore"; import classNames from 'classnames'; import GroupFilterPanel from './GroupFilterPanel'; import CustomRoomTagPanel from './CustomRoomTagPanel'; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index eb60506589..c82bfd8bd6 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -78,7 +78,7 @@ import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore"; import DialPadModal from "../views/voip/DialPadModal"; import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast'; import { shouldUseLoginForWelcome } from "../../utils/pages"; -import SpaceStore from "../../stores/SpaceStore"; +import SpaceStore from "../../stores/spaces/SpaceStore"; import { replaceableComponent } from "../../utils/replaceableComponent"; import RoomListStore from "../../stores/room-list/RoomListStore"; import { RoomUpdateCause } from "../../stores/room-list/models"; @@ -712,10 +712,10 @@ export default class MatrixChat extends React.PureComponent { break; } case Action.ViewRoomDirectory: { - if (SpaceStore.instance.activeSpace) { + if (SpaceStore.instance.activeSpace[0] === "!") { defaultDispatcher.dispatch({ action: "view_room", - room_id: SpaceStore.instance.activeSpace.roomId, + room_id: SpaceStore.instance.activeSpace, }); } else { Modal.createTrackedDialog('Room directory', '', RoomDirectory, { diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 570865731f..440ef65ceb 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -50,7 +50,7 @@ import NotificationPanel from "./NotificationPanel"; import ResizeNotifier from "../../utils/ResizeNotifier"; import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; import { throttle } from 'lodash'; -import SpaceStore from "../../stores/SpaceStore"; +import SpaceStore from "../../stores/spaces/SpaceStore"; import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; import { E2EStatus } from '../../utils/ShieldUtils'; import { dispatchShowThreadsPanelEvent } from '../../dispatcher/dispatch-actions/threads'; diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 1a1cf46023..23862ec3c6 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -28,7 +28,8 @@ import RoomListStore from "../../stores/room-list/RoomListStore"; import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition"; import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; import { replaceableComponent } from "../../utils/replaceableComponent"; -import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/SpaceStore"; +import SpaceStore from "../../stores/spaces/SpaceStore"; +import { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/spaces"; interface IProps { isMinimized: boolean; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 03cca683ae..3cf87e1bea 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -88,7 +88,7 @@ import RoomStatusBar from "./RoomStatusBar"; import MessageComposer from '../views/rooms/MessageComposer'; import JumpToBottomButton from "../views/rooms/JumpToBottomButton"; import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar"; -import SpaceStore from "../../stores/SpaceStore"; +import SpaceStore from "../../stores/spaces/SpaceStore"; import { logger } from "matrix-js-sdk/src/logger"; import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index 698f24d659..a944ee7f67 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -49,7 +49,7 @@ import { mediaFromMxc } from "../../customisations/Media"; import InfoTooltip from "../views/elements/InfoTooltip"; import TextWithTooltip from "../views/elements/TextWithTooltip"; import { useStateToggle } from "../../hooks/useStateToggle"; -import { getChildOrder } from "../../stores/SpaceStore"; +import { getChildOrder } from "../../stores/spaces/SpaceStore"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { linkifyElement } from "../../HtmlUtils"; import { useDispatcher } from "../../hooks/useDispatcher"; diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 25128dd4f0..ddf8f9225b 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -57,7 +57,7 @@ import { } from "../../utils/space"; import SpaceHierarchy, { joinRoom, showRoom } from "./SpaceHierarchy"; import MemberAvatar from "../views/avatars/MemberAvatar"; -import SpaceStore from "../../stores/SpaceStore"; +import SpaceStore from "../../stores/spaces/SpaceStore"; import FacePile from "../views/elements/FacePile"; import { AddExistingToSpace, diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 4a533f1f8e..5ffaab7746 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -54,7 +54,8 @@ import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototyp import { UIFeature } from "../../settings/UIFeature"; import HostSignupAction from "./HostSignupAction"; import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes"; -import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore"; +import SpaceStore from "../../stores/spaces/SpaceStore"; +import { UPDATE_SELECTED_SPACE } from "../../stores/spaces"; import RoomName from "../views/elements/RoomName"; import { replaceableComponent } from "../../utils/replaceableComponent"; import InlineSpinner from "../views/elements/InlineSpinner"; @@ -90,6 +91,7 @@ export default class UserMenu extends React.Component { isDarkTheme: this.isUserOnDarkTheme(), isHighContrast: this.isUserOnHighContrastTheme(), pendingRoomJoin: new Set(), + selectedSpace: SpaceStore.instance.activeSpaceRoom, }; OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); @@ -162,8 +164,10 @@ export default class UserMenu extends React.Component { this.forceUpdate(); }; - private onSelectedSpaceUpdate = async (selectedSpace?: Room) => { - this.setState({ selectedSpace }); + private onSelectedSpaceUpdate = async () => { + this.setState({ + selectedSpace: SpaceStore.instance.activeSpaceRoom, + }); }; private onThemeChanged = () => { diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 337941ce5f..7e2c7be0c3 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -24,7 +24,7 @@ import { _t } from '../../../languageHandler'; import BaseDialog from "./BaseDialog"; import Dropdown from "../elements/Dropdown"; import SearchBox from "../../structures/SearchBox"; -import SpaceStore from "../../../stores/SpaceStore"; +import SpaceStore from "../../../stores/spaces/SpaceStore"; import RoomAvatar from "../avatars/RoomAvatar"; import { getDisplayAliasForRoom } from "../../../Rooms"; import AccessibleButton from "../elements/AccessibleButton"; diff --git a/src/components/views/dialogs/ConfirmSpaceUserActionDialog.tsx b/src/components/views/dialogs/ConfirmSpaceUserActionDialog.tsx index 1c5dd3fafa..2c3d570eae 100644 --- a/src/components/views/dialogs/ConfirmSpaceUserActionDialog.tsx +++ b/src/components/views/dialogs/ConfirmSpaceUserActionDialog.tsx @@ -17,7 +17,7 @@ limitations under the License. import React, { ComponentProps, useMemo, useState } from 'react'; import ConfirmUserActionDialog from "./ConfirmUserActionDialog"; -import SpaceStore from "../../../stores/SpaceStore"; +import SpaceStore from "../../../stores/spaces/SpaceStore"; import { Room } from "matrix-js-sdk/src/models/room"; import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker"; diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx index c61d638204..e4c2f4f873 100644 --- a/src/components/views/dialogs/CreateRoomDialog.tsx +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -32,7 +32,7 @@ import RoomAliasField from "../elements/RoomAliasField"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import DialogButtons from "../elements/DialogButtons"; import BaseDialog from "../dialogs/BaseDialog"; -import SpaceStore from "../../../stores/SpaceStore"; +import SpaceStore from "../../../stores/spaces/SpaceStore"; import JoinRuleDropdown from "../elements/JoinRuleDropdown"; interface IProps { diff --git a/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx b/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx index b19c8d6496..f7417ce7db 100644 --- a/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx +++ b/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx @@ -32,7 +32,7 @@ import { calculateRoomVia, makeRoomPermalink } from "../../../utils/permalinks/P import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; import Spinner from "../elements/Spinner"; import { mediaFromMxc } from "../../../customisations/Media"; -import SpaceStore from "../../../stores/SpaceStore"; +import SpaceStore from "../../../stores/spaces/SpaceStore"; import Modal from "../../../Modal"; import InfoDialog from "./InfoDialog"; import dis from "../../../dispatcher/dispatcher"; diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx index 44ffd2afdd..c128808a3e 100644 --- a/src/components/views/dialogs/CreateSubspaceDialog.tsx +++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx @@ -25,7 +25,7 @@ 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 SpaceStore from "../../../stores/spaces/SpaceStore"; import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu"; import { SubspaceSelector } from "./AddExistingToSpaceDialog"; import JoinRuleDropdown from "../elements/JoinRuleDropdown"; diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 7f08a3eb58..9314902104 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -43,7 +43,7 @@ import QueryMatcher from "../../../autocomplete/QueryMatcher"; import TruncatedList from "../elements/TruncatedList"; import EntityTile from "../rooms/EntityTile"; import BaseAvatar from "../avatars/BaseAvatar"; -import SpaceStore from "../../../stores/SpaceStore"; +import SpaceStore from "../../../stores/spaces/SpaceStore"; const AVATAR_SIZE = 30; diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index d37b8c7d6b..d22c891b4b 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -71,7 +71,7 @@ import QuestionDialog from "./QuestionDialog"; import Spinner from "../elements/Spinner"; import BaseDialog from "./BaseDialog"; import DialPadBackspaceButton from "../elements/DialPadBackspaceButton"; -import SpaceStore from "../../../stores/SpaceStore"; +import SpaceStore from "../../../stores/spaces/SpaceStore"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/components/views/dialogs/LeaveSpaceDialog.tsx b/src/components/views/dialogs/LeaveSpaceDialog.tsx index 3793deee63..74fec7eae2 100644 --- a/src/components/views/dialogs/LeaveSpaceDialog.tsx +++ b/src/components/views/dialogs/LeaveSpaceDialog.tsx @@ -21,7 +21,7 @@ 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 SpaceStore from "../../../stores/spaces/SpaceStore"; import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker"; interface IProps { diff --git a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx index dd5c549bbe..e4b01526fc 100644 --- a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx +++ b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx @@ -21,7 +21,7 @@ import { _t } from '../../../languageHandler'; import { IDialogProps } from "./IDialogProps"; import BaseDialog from "./BaseDialog"; import SearchBox from "../../structures/SearchBox"; -import SpaceStore from "../../../stores/SpaceStore"; +import SpaceStore from "../../../stores/spaces/SpaceStore"; import RoomAvatar from "../avatars/RoomAvatar"; import AccessibleButton from "../elements/AccessibleButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; @@ -75,7 +75,7 @@ const ManageRestrictedJoinRuleDialog: React.FC = ({ room, selected = [], const [spacesContainingRoom, otherEntries] = useMemo(() => { const spaces = cli.getVisibleRooms().filter(r => r.getMyMembership() === "join" && r.isSpaceRoom()); return [ - spaces.filter(r => SpaceStore.instance.getSpaceFilteredRoomIds(r).has(room.roomId)), + spaces.filter(r => SpaceStore.instance.getSpaceFilteredRoomIds(r.roomId).has(room.roomId)), selected.map(roomId => { const room = cli.getRoom(roomId); if (!room) { diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index a848bf2773..a2699e9982 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -34,6 +34,7 @@ import { UIFeature } from "../../../settings/UIFeature"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import BaseDialog from "./BaseDialog"; import { IDialogProps } from "./IDialogProps"; +import SidebarUserSettingsTab from "../settings/tabs/user/SidebarUserSettingsTab"; export enum UserTab { General = "USER_GENERAL_TAB", @@ -41,6 +42,7 @@ export enum UserTab { Flair = "USER_FLAIR_TAB", Notifications = "USER_NOTIFICATIONS_TAB", Preferences = "USER_PREFERENCES_TAB", + Sidebar = "USER_SIDEBAR_TAB", Voice = "USER_VOICE_TAB", Security = "USER_SECURITY_TAB", Labs = "USER_LABS_TAB", @@ -117,6 +119,15 @@ export default class UserSettingsDialog extends React.Component , )); + if (SettingsStore.getValue("feature_spaces_metaspaces")) { + tabs.push(new Tab( + UserTab.Sidebar, + _td("Sidebar"), + "mx_UserSettingsDialog_sidebarIcon", + , + )); + } + if (SettingsStore.getValue(UIFeature.Voip)) { tabs.push(new Tab( UserTab.Voice, diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 2a355964ff..cb3e9949a8 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -69,7 +69,7 @@ import RoomName from "../elements/RoomName"; import { mediaFromMxc } from "../../../customisations/Media"; import UIStore from "../../../stores/UIStore"; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; -import SpaceStore from "../../../stores/SpaceStore"; +import SpaceStore from "../../../stores/spaces/SpaceStore"; import ConfirmSpaceUserActionDialog from "../dialogs/ConfirmSpaceUserActionDialog"; import { bulkSpaceBehaviour } from "../../../utils/space"; diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index 80214ca890..224bb03ff6 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -43,7 +43,7 @@ import EntityTile from "./EntityTile"; import MemberTile from "./MemberTile"; import BaseAvatar from '../avatars/BaseAvatar'; import { throttle } from 'lodash'; -import SpaceStore from "../../../stores/SpaceStore"; +import SpaceStore from "../../../stores/spaces/SpaceStore"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; import { JoinRule } from "matrix-js-sdk/src/@types/partials"; diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 3d92a9cced..100b1ca435 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -31,7 +31,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher"; import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload"; import { Action } from "../../../dispatcher/actions"; -import SpaceStore from "../../../stores/SpaceStore"; +import SpaceStore from "../../../stores/spaces/SpaceStore"; import { showSpaceInvite } from "../../../utils/space"; import { privateShouldBeEncrypted } from "../../../createRoom"; import EventTileBubble from "../messages/EventTileBubble"; @@ -126,12 +126,12 @@ const NewRoomIntro = () => { }); } - let parentSpace; + let parentSpace: Room; if ( - SpaceStore.instance.activeSpace?.canInvite(cli.getUserId()) && + SpaceStore.instance.activeSpaceRoom?.canInvite(cli.getUserId()) && SpaceStore.instance.getSpaceFilteredRoomIds(SpaceStore.instance.activeSpace).has(room.roomId) ) { - parentSpace = SpaceStore.instance.activeSpace; + parentSpace = SpaceStore.instance.activeSpaceRoom; } let buttons; diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 704af3f009..1be7eb0747 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -21,7 +21,7 @@ import * as fbEmitter from "fbemitter"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { _t, _td } from "../../../languageHandler"; -import { RovingTabIndexProvider, IState as IRovingTabIndexState } from "../../../accessibility/RovingTabIndex"; +import { IState as IRovingTabIndexState, RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import ResizeNotifier from "../../../utils/ResizeNotifier"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; import RoomViewStore from "../../../stores/RoomViewStore"; @@ -44,7 +44,8 @@ import { objectShallowClone, objectWithOnly } from "../../../utils/objects"; import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu"; import AccessibleButton from "../elements/AccessibleButton"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; -import SpaceStore, { ISuggestedRoom, SUGGESTED_ROOMS } from "../../../stores/SpaceStore"; +import SpaceStore from "../../../stores/spaces/SpaceStore"; +import { ISuggestedRoom, MetaSpace, SpaceKey, UPDATE_SUGGESTED_ROOMS } from "../../../stores/spaces"; import { showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../../utils/space"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import RoomAvatar from "../avatars/RoomAvatar"; @@ -52,6 +53,7 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; import { JoinRule } from "matrix-js-sdk/src/@types/partials"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; interface IProps { onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void; @@ -61,7 +63,7 @@ interface IProps { onListCollapse?: (isExpanded: boolean) => void; resizeNotifier: ResizeNotifier; isMinimized: boolean; - activeSpace: Room; + activeSpace: SpaceKey; } interface IState { @@ -131,9 +133,10 @@ const TAG_AESTHETICS: ITagAestheticsMap = { defaultHidden: false, addRoomLabel: _td("Add room"), addRoomContextMenu: (onFinished: () => void) => { - if (SpaceStore.instance.activeSpace) { - const canAddRooms = SpaceStore.instance.activeSpace.currentState.maySendStateEvent(EventType.SpaceChild, - MatrixClientPeg.get().getUserId()); + if (SpaceStore.instance.activeSpaceRoom) { + const userId = MatrixClientPeg.get().getUserId(); + const space = SpaceStore.instance.activeSpaceRoom; + const canAddRooms = space.currentState.maySendStateEvent(EventType.SpaceChild, userId); return { @@ -146,7 +149,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = { e.preventDefault(); e.stopPropagation(); onFinished(); - showCreateNewRoom(SpaceStore.instance.activeSpace); + showCreateNewRoom(space); }} disabled={!canAddRooms} tooltip={canAddRooms ? undefined @@ -159,7 +162,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = { e.preventDefault(); e.stopPropagation(); onFinished(); - showAddExistingRooms(SpaceStore.instance.activeSpace); + showAddExistingRooms(space); }} disabled={!canAddRooms} tooltip={canAddRooms ? undefined @@ -251,6 +254,9 @@ export default class RoomList extends React.PureComponent { private roomStoreToken: fbEmitter.EventSubscription; private treeRef = createRef(); + static contextType = MatrixClientContext; + public context!: React.ContextType; + constructor(props: IProps) { super(props); @@ -264,14 +270,14 @@ export default class RoomList extends React.PureComponent { public componentDidMount(): void { this.dispatcherRef = defaultDispatcher.register(this.onAction); this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); - SpaceStore.instance.on(SUGGESTED_ROOMS, this.updateSuggestedRooms); + SpaceStore.instance.on(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists); this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists); this.updateLists(); // trigger the first update } public componentWillUnmount() { - SpaceStore.instance.off(SUGGESTED_ROOMS, this.updateSuggestedRooms); + SpaceStore.instance.off(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists); defaultDispatcher.unregister(this.dispatcherRef); if (this.customTagStoreRef) this.customTagStoreRef.remove(); @@ -379,7 +385,7 @@ export default class RoomList extends React.PureComponent { private onSpaceInviteClick = () => { const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search; - showSpaceInvite(this.props.activeSpace, initialText); + showSpaceInvite(this.context.getRoom(this.props.activeSpace), initialText); }; private renderSuggestedRooms(): ReactComponentElement[] { @@ -485,6 +491,15 @@ export default class RoomList extends React.PureComponent { : TAG_AESTHETICS[orderedTagId]; if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`); + let alwaysVisible = ALWAYS_VISIBLE_TAGS.includes(orderedTagId); + if ( + (this.props.activeSpace === MetaSpace.Favourites && orderedTagId !== DefaultTagID.Favourite) || + (this.props.activeSpace === MetaSpace.People && orderedTagId !== DefaultTagID.DM) || + (this.props.activeSpace === MetaSpace.Orphans && orderedTagId === DefaultTagID.DM) + ) { + alwaysVisible = false; + } + // The cost of mounting/unmounting this component offsets the cost // of keeping it in the DOM and hiding it when it is not required return { showSkeleton={showSkeleton} extraTiles={extraTiles} resizeNotifier={this.props.resizeNotifier} - alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)} + alwaysVisible={alwaysVisible} onListCollapse={this.props.onListCollapse} />; }); @@ -515,6 +530,7 @@ export default class RoomList extends React.PureComponent { public render() { const cli = MatrixClientPeg.get(); const userId = cli.getUserId(); + const activeSpace = this.props.activeSpace[0] === "!" ? cli.getRoom(this.props.activeSpace) : null; let explorePrompt: JSX.Element; if (!this.props.isMinimized) { @@ -533,17 +549,16 @@ export default class RoomList extends React.PureComponent { kind="link" onClick={this.onExplore} > - { this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") } + { activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") } ; } else if ( - this.props.activeSpace?.canInvite(userId) || - this.props.activeSpace?.getMyMembership() === "join" || - this.props.activeSpace?.getJoinRule() === JoinRule.Public + activeSpace?.canInvite(userId) || + activeSpace?.getMyMembership() === "join" || + activeSpace?.getJoinRule() === JoinRule.Public ) { - const spaceName = this.props.activeSpace.name; - const canInvite = this.props.activeSpace?.canInvite(userId) || - this.props.activeSpace?.getJoinRule() === JoinRule.Public; + const spaceName = activeSpace.name; + const canInvite = activeSpace?.canInvite(userId) || activeSpace?.getJoinRule() === JoinRule.Public; explorePrompt =
{ _t("Quick actions") }
{ canInvite && { > { _t("Invite people") } } - { this.props.activeSpace?.getMyMembership() === "join" && void; diff --git a/src/components/views/rooms/ThirdPartyMemberInfo.tsx b/src/components/views/rooms/ThirdPartyMemberInfo.tsx index c29c558655..ef1902fcf3 100644 --- a/src/components/views/rooms/ThirdPartyMemberInfo.tsx +++ b/src/components/views/rooms/ThirdPartyMemberInfo.tsx @@ -27,7 +27,7 @@ import RoomName from "../elements/RoomName"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import ErrorDialog from '../dialogs/ErrorDialog'; import AccessibleButton from '../elements/AccessibleButton'; -import SpaceStore from "../../../stores/SpaceStore"; +import SpaceStore from "../../../stores/spaces/SpaceStore"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/components/views/settings/JoinRuleSettings.tsx b/src/components/views/settings/JoinRuleSettings.tsx index e93bac7fa7..152578d499 100644 --- a/src/components/views/settings/JoinRuleSettings.tsx +++ b/src/components/views/settings/JoinRuleSettings.tsx @@ -23,7 +23,7 @@ import StyledRadioGroup, { IDefinition } from "../elements/StyledRadioGroup"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; import RoomAvatar from "../avatars/RoomAvatar"; -import SpaceStore from "../../../stores/SpaceStore"; +import SpaceStore from "../../../stores/spaces/SpaceStore"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import Modal from "../../../Modal"; import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog"; @@ -67,8 +67,8 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet const editRestrictedRoomIds = async (): Promise => { let selected = restrictedAllowRoomIds; - if (!selected?.length && SpaceStore.instance.activeSpace) { - selected = [SpaceStore.instance.activeSpace.roomId]; + if (!selected?.length && SpaceStore.instance.activeSpaceRoom) { + selected = [SpaceStore.instance.activeSpaceRoom.roomId]; } const matrixClient = MatrixClientPeg.get(); @@ -176,9 +176,9 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet { moreText && { moreText } }
; - } else if (SpaceStore.instance.activeSpace) { + } else if (SpaceStore.instance.activeSpaceRoom) { description = _t("Anyone in can find and join. You can select other spaces too.", {}, { - spaceName: () => { SpaceStore.instance.activeSpace.name }, + spaceName: () => { SpaceStore.instance.activeSpaceRoom.name }, }); } else { description = _t("Anyone in a space can find and join. You can select multiple spaces."); diff --git a/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx new file mode 100644 index 0000000000..8b1d0d8f85 --- /dev/null +++ b/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx @@ -0,0 +1,123 @@ +/* +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, { ChangeEvent } from 'react'; + +import { _t } from "../../../../../languageHandler"; +import SettingsStore from "../../../../../settings/SettingsStore"; +import { SettingLevel } from "../../../../../settings/SettingLevel"; +import StyledCheckbox from "../../../elements/StyledCheckbox"; +import { useSettingValue } from "../../../../../hooks/useSettings"; +import { MetaSpace } from "../../../../../stores/spaces"; + +const onMetaSpaceChangeFactory = (metaSpace: MetaSpace) => (e: ChangeEvent) => { + const currentValue = SettingsStore.getValue("Spaces.enabledMetaSpaces"); + SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.ACCOUNT, { + ...currentValue, + [metaSpace]: e.target.checked, + }); +}; + +const SidebarUserSettingsTab = () => { + const { + [MetaSpace.Home]: homeEnabled, + [MetaSpace.Favourites]: favouritesEnabled, + [MetaSpace.People]: peopleEnabled, + [MetaSpace.Orphans]: orphansEnabled, + } = useSettingValue>("Spaces.enabledMetaSpaces"); + const allRoomsInHome = useSettingValue("Spaces.allRoomsInHome"); + + return ( +
+
{ _t("Sidebar") }
+ +
+ { _t("Spaces") } +
{ _t("Spaces are ways to group rooms and people.") }
+ +
{ _t("Spaces to show") }
+
+ { _t("Along with the spaces you're in, you can use some pre-built ones too.") } +
+ + + { _t("Home") } + +
+ { _t("Home is useful for getting an overview of everything.") } +
+ + { + SettingsStore.setValue( + "Spaces.allRoomsInHome", + null, + SettingLevel.ACCOUNT, + e.target.checked, + ); + }} + className="mx_SidebarUserSettingsTab_homeAllRoomsCheckbox" + > + { _t("Show all rooms") } + +
+ { _t("Show all your rooms in Home, even if they're in a space.") } +
+ + + { _t("Favourites") } + +
+ { _t("Automatically group all your favourite rooms and people together in one place.") } +
+ + + { _t("People") } + +
+ { _t("Automatically group all your people together in one place.") } +
+ + + { _t("Rooms outside of a space") } + +
+ { _t("Automatically group all your rooms that aren't part of a space in one place.") } +
+
+
+ ); +}; + +export default SidebarUserSettingsTab; diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 5ec44e970b..c5e15ad855 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -119,6 +119,7 @@ export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => { rageshakeLabel: "spaces-feedback", rageshakeData: Object.fromEntries([ "Spaces.allRoomsInHome", + "Spaces.enabledMetaSpaces", ].map(k => [k, SettingsStore.getValue(k)])), }); }} diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index f61ebdf0f7..9c572c9fe5 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -34,13 +34,15 @@ import SpaceCreateMenu from "./SpaceCreateMenu"; import { SpaceButton, SpaceItem } from "./SpaceTreeLevel"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; -import SpaceStore, { - HOME_SPACE, +import SpaceStore from "../../../stores/spaces/SpaceStore"; +import { + MetaSpace, + SpaceKey, UPDATE_HOME_BEHAVIOUR, UPDATE_INVITED_SPACES, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES, -} from "../../../stores/SpaceStore"; +} from "../../../stores/spaces"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; @@ -53,17 +55,21 @@ import SettingsStore from "../../../settings/SettingsStore"; import { SettingLevel } from "../../../settings/SettingLevel"; import UIStore from "../../../stores/UIStore"; -const useSpaces = (): [Room[], Room[], Room | null] => { +const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => { const invites = useEventEmitterState(SpaceStore.instance, UPDATE_INVITED_SPACES, () => { return SpaceStore.instance.invitedSpaces; }); - const spaces = useEventEmitterState(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, () => { - return SpaceStore.instance.spacePanelSpaces; - }); - const activeSpace = useEventEmitterState(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => { + const [metaSpaces, actualSpaces] = useEventEmitterState<[MetaSpace[], Room[]]>( + SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, + () => [ + SpaceStore.instance.enabledMetaSpaces, + SpaceStore.instance.spacePanelSpaces, + ], + ); + const activeSpace = useEventEmitterState(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => { return SpaceStore.instance.activeSpace; }); - return [invites, spaces, activeSpace]; + return [invites, metaSpaces, actualSpaces, activeSpace]; }; interface IInnerSpacePanelProps { @@ -99,37 +105,76 @@ const HomeButtonContextMenu = ({ onFinished, ...props }: ComponentProps; }; -interface IHomeButtonProps { +interface IMetaSpaceButtonProps extends ComponentProps { selected: boolean; isPanelCollapsed: boolean; } -const HomeButton = ({ selected, isPanelCollapsed }: IHomeButtonProps) => { - const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => { - return SpaceStore.instance.allRoomsInHome; - }); +type MetaSpaceButtonProps = Pick; +const MetaSpaceButton = ({ selected, isPanelCollapsed, ...props }: IMetaSpaceButtonProps) => { return
  • - SpaceStore.instance.setActiveSpace(null)} - selected={selected} - label={allRoomsInHome ? _t("All rooms") : _t("Home")} - notificationState={allRoomsInHome - ? RoomNotificationStateStore.instance.globalState - : SpaceStore.instance.getNotificationState(HOME_SPACE)} - isNarrow={isPanelCollapsed} - ContextMenuComponent={HomeButtonContextMenu} - contextMenuTooltip={_t("Options")} - /> +
  • ; }; +const HomeButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) => { + const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => { + return SpaceStore.instance.allRoomsInHome; + }); + + return ; +}; + +const FavouritesButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) => { + return ; +}; + +const PeopleButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) => { + return ; +}; + +const OrphansButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) => { + return ; +}; + const CreateSpaceButton = ({ isPanelCollapsed, setPanelCollapsed, @@ -181,13 +226,25 @@ const CreateSpaceButton = ({ ; }; +const metaSpaceComponentMap: Record = { + [MetaSpace.Home]: HomeButton, + [MetaSpace.Favourites]: FavouritesButton, + [MetaSpace.People]: PeopleButton, + [MetaSpace.Orphans]: OrphansButton, +}; + // Optimisation based on https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md#recommended-droppable--performance-optimisation const InnerSpacePanel = React.memo(({ children, isPanelCollapsed, setPanelCollapsed }) => { - const [invites, spaces, activeSpace] = useSpaces(); + const [invites, metaSpaces, actualSpaces, activeSpace] = useSpaces(); const activeSpaces = activeSpace ? [activeSpace] : []; + const metaSpacesSection = metaSpaces.map(key => { + const Component = metaSpaceComponentMap[key]; + return ; + }); + return
    - + { metaSpacesSection } { invites.map(s => ( (({ children, isPanelCo onExpand={() => setPanelCollapsed(false)} /> )) } - { spaces.map((s, i) => ( + { actualSpaces.map((s, i) => ( { (provided, snapshot) => ( , "title"> { +interface IButtonProps extends Omit, "title" | "onClick"> { space?: Room; + spaceKey?: SpaceKey; className?: string; selected?: boolean; label: string; @@ -53,14 +54,14 @@ interface IButtonProps extends Omit>; - onClick(ev: MouseEvent): void; + onClick?(ev?: ButtonEvent): void; } export const SpaceButton: React.FC = ({ space, + spaceKey, className, selected, - onClick, label, contextMenuTooltip, notificationState, @@ -88,7 +89,7 @@ export const SpaceButton: React.FC = ({ notifBadge =
    SpaceStore.instance.setActiveRoomInSpace(space || null)} + onClick={() => SpaceStore.instance.setActiveRoomInSpace(spaceKey ?? space.roomId)} forceCount={false} notification={notificationState} aria-label={ariaLabel} @@ -116,7 +117,7 @@ export const SpaceButton: React.FC = ({ mx_SpaceButton_narrow: isNarrow, })} title={label} - onClick={onClick} + onClick={spaceKey ? () => SpaceStore.instance.setActiveSpace(spaceKey) : props.onClick} onContextMenu={openMenu} forceHide={!isNarrow || menuDisplayed} inputRef={handle} @@ -146,7 +147,7 @@ export const SpaceButton: React.FC = ({ interface IItemProps extends InputHTMLAttributes { space?: Room; - activeSpaces: Room[]; + activeSpaces: SpaceKey[]; isNested?: boolean; isPanelCollapsed?: boolean; onExpand?: Function; @@ -258,7 +259,7 @@ export class SpaceItem extends React.PureComponent { private onClick = (ev: React.MouseEvent) => { ev.preventDefault(); ev.stopPropagation(); - SpaceStore.instance.setActiveSpace(this.props.space); + SpaceStore.instance.setActiveSpace(this.props.space.roomId); }; render() { @@ -316,7 +317,7 @@ export class SpaceItem extends React.PureComponent { {...restDragHandleProps} space={space} className={isInvite ? "mx_SpaceButton_invite" : undefined} - selected={activeSpaces.includes(space)} + selected={activeSpaces.includes(space.roomId)} label={space.name} contextMenuTooltip={_t("Space options")} notificationState={notificationState} @@ -337,7 +338,7 @@ export class SpaceItem extends React.PureComponent { interface ITreeLevelProps { spaces: Room[]; - activeSpaces: Room[]; + activeSpaces: SpaceKey[]; isNested?: boolean; parents: Set; } diff --git a/src/createRoom.ts b/src/createRoom.ts index 6394cb6849..f4d796cb4b 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -40,7 +40,7 @@ import GroupStore from "./stores/GroupStore"; import CountlyAnalytics from "./CountlyAnalytics"; import { isJoinedOrNearlyJoined } from "./utils/membership"; import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler"; -import SpaceStore from "./stores/SpaceStore"; +import SpaceStore from "./stores/spaces/SpaceStore"; import { makeSpaceParentEvent } from "./utils/space"; import { Action } from "./dispatcher/actions"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a25ee37386..1fab6eda59 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -830,6 +830,7 @@ "Polls (under active development)": "Polls (under active development)", "Show info about bridges in room settings": "Show info about bridges in room settings", "New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)", + "Meta Spaces": "Meta Spaces", "Don't send read receipts": "Don't send read receipts", "Font size": "Font size", "Use custom size": "Use custom size", @@ -1064,6 +1065,9 @@ "Show all rooms": "Show all rooms", "All rooms": "All rooms", "Options": "Options", + "Favourites": "Favourites", + "People": "People", + "Other rooms": "Other rooms", "Spaces": "Spaces", "Expand space panel": "Expand space panel", "Collapse space panel": "Collapse space panel", @@ -1426,6 +1430,16 @@ "Learn more about how we use analytics.": "Learn more about how we use analytics.", "Where you're signed in": "Where you're signed in", "Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Manage your signed-in devices below. A device's name is visible to people you communicate with.", + "Sidebar": "Sidebar", + "Spaces are ways to group rooms and people.": "Spaces are ways to group rooms and people.", + "Spaces to show": "Spaces to show", + "Along with the spaces you're in, you can use some pre-built ones too.": "Along with the spaces you're in, you can use some pre-built ones too.", + "Home is useful for getting an overview of everything.": "Home is useful for getting an overview of everything.", + "Show all your rooms in Home, even if they're in a space.": "Show all your rooms in Home, even if they're in a space.", + "Automatically group all your favourite rooms and people together in one place.": "Automatically group all your favourite rooms and people together in one place.", + "Automatically group all your people together in one place.": "Automatically group all your people together in one place.", + "Rooms outside of a space": "Rooms outside of a space", + "Automatically group all your rooms that aren't part of a space in one place.": "Automatically group all your rooms that aren't part of a space in one place.", "Default Device": "Default Device", "No media permissions": "No media permissions", "You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam", @@ -1670,8 +1684,6 @@ "Show Widgets": "Show Widgets", "Search": "Search", "Invites": "Invites", - "Favourites": "Favourites", - "People": "People", "Start chat": "Start chat", "Rooms": "Rooms", "Add room": "Add room", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 8b25309120..c263317dc4 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -42,6 +42,7 @@ import ReducedMotionController from './controllers/ReducedMotionController'; import IncompatibleController from "./controllers/IncompatibleController"; import PseudonymousAnalyticsController from './controllers/PseudonymousAnalyticsController'; import NewLayoutSwitcherController from './controllers/NewLayoutSwitcherController'; +import { MetaSpace } from "../stores/spaces"; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = [ @@ -283,6 +284,16 @@ export const SETTINGS: {[setting: string]: ISetting} = { default: false, controller: new NewLayoutSwitcherController(), }, + "feature_spaces_metaspaces": { + isFeature: true, + supportedLevels: LEVELS_FEATURE, + displayName: _td("Meta Spaces"), + default: false, + controller: new OrderedMultiController([ + new IncompatibleController("showCommunitiesInsteadOfSpaces"), + new ReloadOnChangeController(), + ]), + }, "RoomList.backgroundImage": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, default: null, @@ -755,6 +766,15 @@ export const SETTINGS: {[setting: string]: ISetting} = { default: false, controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", null), }, + "Spaces.enabledMetaSpaces": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + default: { + [MetaSpace.Home]: true, + }, + controller: new IncompatibleController("feature_spaces_metaspaces", { + [MetaSpace.Home]: true, + }, false), + }, "showCommunitiesInsteadOfSpaces": { displayName: _td("Display Communities instead of Spaces"), description: _td("Temporarily show communities instead of Spaces for this session. " + diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 8a85ca354f..7c33901ae4 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -22,7 +22,7 @@ import defaultDispatcher from "../dispatcher/dispatcher"; import { arrayHasDiff } from "../utils/arrays"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { SettingLevel } from "../settings/SettingLevel"; -import SpaceStore from "./SpaceStore"; +import SpaceStore from "./spaces/SpaceStore"; import { Action } from "../dispatcher/actions"; import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload"; diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index c4b1f012b1..9fbfcb32e2 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -35,7 +35,7 @@ import { NameFilterCondition } from "./filters/NameFilterCondition"; import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore"; import { VisibilityProvider } from "./filters/VisibilityProvider"; import { SpaceWatcher } from "./SpaceWatcher"; -import SpaceStore from "../SpaceStore"; +import SpaceStore from "../spaces/SpaceStore"; import { Action } from "../../dispatcher/actions"; import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload"; diff --git a/src/stores/room-list/SpaceWatcher.ts b/src/stores/room-list/SpaceWatcher.ts index fe2eb1e881..e7d6e78206 100644 --- a/src/stores/room-list/SpaceWatcher.ts +++ b/src/stores/room-list/SpaceWatcher.ts @@ -14,11 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Room } from "matrix-js-sdk/src/models/room"; - import { RoomListStoreClass } from "./RoomListStore"; import { SpaceFilterCondition } from "./filters/SpaceFilterCondition"; -import SpaceStore, { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../SpaceStore"; +import SpaceStore from "../spaces/SpaceStore"; +import { MetaSpace, SpaceKey, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces"; /** * Watches for changes in spaces to manage the filter on the provided RoomListStore @@ -26,11 +25,11 @@ import SpaceStore, { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../Spa export class SpaceWatcher { private readonly filter = new SpaceFilterCondition(); // we track these separately to the SpaceStore as we need to observe transitions - private activeSpace: Room = SpaceStore.instance.activeSpace; + private activeSpace: SpaceKey = SpaceStore.instance.activeSpace; private allRoomsInHome: boolean = SpaceStore.instance.allRoomsInHome; constructor(private store: RoomListStoreClass) { - if (!this.allRoomsInHome || this.activeSpace) { + if (SpaceWatcher.needsFilter(this.activeSpace, this.allRoomsInHome)) { this.updateFilter(); store.addFilter(this.filter); } @@ -38,21 +37,26 @@ export class SpaceWatcher { SpaceStore.instance.on(UPDATE_HOME_BEHAVIOUR, this.onHomeBehaviourUpdated); } - private onSelectedSpaceUpdated = (activeSpace?: Room, allRoomsInHome = this.allRoomsInHome) => { + private static needsFilter(spaceKey: SpaceKey, allRoomsInHome: boolean): boolean { + return !(spaceKey === MetaSpace.Home && allRoomsInHome); + } + + private onSelectedSpaceUpdated = (activeSpace: SpaceKey, allRoomsInHome = this.allRoomsInHome) => { if (activeSpace === this.activeSpace && allRoomsInHome === this.allRoomsInHome) return; // nop - const oldActiveSpace = this.activeSpace; - const oldAllRoomsInHome = this.allRoomsInHome; + const neededFilter = SpaceWatcher.needsFilter(this.activeSpace, this.allRoomsInHome); + const needsFilter = SpaceWatcher.needsFilter(activeSpace, allRoomsInHome); + this.activeSpace = activeSpace; this.allRoomsInHome = allRoomsInHome; - if (activeSpace || !allRoomsInHome) { + if (needsFilter) { this.updateFilter(); } - if (oldAllRoomsInHome && !oldActiveSpace) { + if (!neededFilter && needsFilter) { this.store.addFilter(this.filter); - } else if (allRoomsInHome && !activeSpace) { + } else if (neededFilter && !needsFilter) { this.store.removeFilter(this.filter); } }; @@ -62,8 +66,8 @@ export class SpaceWatcher { }; private updateFilter = () => { - if (this.activeSpace) { - SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => { + if (this.activeSpace[0] === "!") { + SpaceStore.instance.traverseSpace(this.activeSpace, roomId => { this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded(); }); } diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 754e1c1d94..c812edee48 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -34,7 +34,7 @@ import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } f import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm"; import { getListAlgorithmInstance } from "./list-ordering"; import { VisibilityProvider } from "../filters/VisibilityProvider"; -import SpaceStore from "../../SpaceStore"; +import SpaceStore from "../../spaces/SpaceStore"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/stores/room-list/filters/SpaceFilterCondition.ts b/src/stores/room-list/filters/SpaceFilterCondition.ts index 0e6965d843..fd815bf86f 100644 --- a/src/stores/room-list/filters/SpaceFilterCondition.ts +++ b/src/stores/room-list/filters/SpaceFilterCondition.ts @@ -19,7 +19,8 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition"; import { IDestroyable } from "../../../utils/IDestroyable"; -import SpaceStore, { HOME_SPACE } from "../../SpaceStore"; +import SpaceStore from "../../spaces/SpaceStore"; +import { MetaSpace, SpaceKey } from "../../spaces"; import { setHasDiff } from "../../../utils/sets"; /** @@ -30,7 +31,7 @@ import { setHasDiff } from "../../../utils/sets"; */ export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable { private roomIds = new Set(); - private space: Room = null; + private space: SpaceKey = MetaSpace.Home; public get kind(): FilterKind { return FilterKind.Prefilter; @@ -55,15 +56,13 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi } }; - private getSpaceEventKey = (space: Room | null) => space ? space.roomId : HOME_SPACE; - - public updateSpace(space: Room) { - SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate); - SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate); + public updateSpace(space: SpaceKey) { + SpaceStore.instance.off(this.space, this.onStoreUpdate); + SpaceStore.instance.on(this.space = space, this.onStoreUpdate); this.onStoreUpdate(); // initial update from the change to the space } public destroy(): void { - SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate); + SpaceStore.instance.off(this.space, this.onStoreUpdate); } } diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts index f63b622053..18b68da301 100644 --- a/src/stores/room-list/filters/VisibilityProvider.ts +++ b/src/stores/room-list/filters/VisibilityProvider.ts @@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import CallHandler from "../../../CallHandler"; import { RoomListCustomisations } from "../../../customisations/RoomList"; import VoipUserMapper from "../../../VoipUserMapper"; -import SpaceStore from "../../SpaceStore"; +import SpaceStore from "../../spaces/SpaceStore"; export class VisibilityProvider { private static internalInstance: VisibilityProvider; diff --git a/src/stores/SpaceStore.ts b/src/stores/spaces/SpaceStore.ts similarity index 76% rename from src/stores/SpaceStore.ts rename to src/stores/spaces/SpaceStore.ts index ea5ff56aea..5cea148b78 100644 --- a/src/stores/SpaceStore.ts +++ b/src/stores/spaces/SpaceStore.ts @@ -18,56 +18,51 @@ import { ListIteratee, Many, sortBy, throttle } from "lodash"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces"; import { IRoomCapability } from "matrix-js-sdk/src/client"; - -import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; -import defaultDispatcher from "../dispatcher/dispatcher"; -import { ActionPayload } from "../dispatcher/payloads"; -import RoomListStore from "./room-list/RoomListStore"; -import SettingsStore from "../settings/SettingsStore"; -import DMRoomMap from "../utils/DMRoomMap"; -import { FetchRoomFn } from "./notifications/ListNotificationState"; -import { SpaceNotificationState } from "./notifications/SpaceNotificationState"; -import { RoomNotificationStateStore } from "./notifications/RoomNotificationStateStore"; -import { DefaultTagID } from "./room-list/models"; -import { EnhancedMap, mapDiff } from "../utils/maps"; -import { setHasDiff } from "../utils/sets"; -import RoomViewStore from "./RoomViewStore"; -import { Action } from "../dispatcher/actions"; -import { arrayHasDiff, arrayHasOrderChange } from "../utils/arrays"; -import { objectDiff } from "../utils/objects"; -import { reorderLexicographically } from "../utils/stringOrderField"; -import { TAG_ORDER } from "../components/views/rooms/RoomList"; -import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload"; - import { logger } from "matrix-js-sdk/src/logger"; -type SpaceKey = string | symbol; +import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; +import defaultDispatcher from "../../dispatcher/dispatcher"; +import { ActionPayload } from "../../dispatcher/payloads"; +import RoomListStore from "../room-list/RoomListStore"; +import SettingsStore from "../../settings/SettingsStore"; +import DMRoomMap from "../../utils/DMRoomMap"; +import { FetchRoomFn } from "../notifications/ListNotificationState"; +import { SpaceNotificationState } from "../notifications/SpaceNotificationState"; +import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore"; +import { DefaultTagID } from "../room-list/models"; +import { EnhancedMap, mapDiff } from "../../utils/maps"; +import { setHasDiff } from "../../utils/sets"; +import RoomViewStore from "../RoomViewStore"; +import { Action } from "../../dispatcher/actions"; +import { arrayHasDiff, arrayHasOrderChange } from "../../utils/arrays"; +import { objectDiff } from "../../utils/objects"; +import { reorderLexicographically } from "../../utils/stringOrderField"; +import { TAG_ORDER } from "../../components/views/rooms/RoomList"; +import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload"; +import { + ISuggestedRoom, + MetaSpace, + SpaceKey, + UPDATE_HOME_BEHAVIOUR, + UPDATE_INVITED_SPACES, + UPDATE_SELECTED_SPACE, + UPDATE_SUGGESTED_ROOMS, + UPDATE_TOP_LEVEL_SPACES, +} from "."; interface IState {} const ACTIVE_SPACE_LS_KEY = "mx_active_space"; -export const HOME_SPACE = Symbol("home-space"); -export const SUGGESTED_ROOMS = Symbol("suggested-rooms"); - -export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces"); -export const UPDATE_INVITED_SPACES = Symbol("invited-spaces"); -export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); -export const UPDATE_HOME_BEHAVIOUR = Symbol("home-behaviour"); -// Space Room ID/HOME_SPACE will be emitted when a Space's children change - -export interface ISuggestedRoom extends IHierarchyRoom { - viaServers: string[]; -} +const metaSpaceOrder: MetaSpace[] = [MetaSpace.Home, MetaSpace.Favourites, MetaSpace.People, MetaSpace.Orphans]; const MAX_SUGGESTED_ROOMS = 20; // This setting causes the page to reload and can be costly if read frequently, so read it here only const spacesEnabled = !SettingsStore.getValue("showCommunitiesInsteadOfSpaces"); -const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "HOME_SPACE"}`; +const getSpaceContextKey = (space: SpaceKey) => `mx_space_context_${space}`; const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms] return arr.reduce((result, room: Room) => { @@ -105,30 +100,41 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private notificationStateMap = new Map(); // Map from space key to Set of room IDs that should be shown as part of that space's filter private spaceFilteredRooms = new Map>(); - // The space currently selected in the Space Panel - if null then Home is selected - private _activeSpace?: Room = null; + // The space currently selected in the Space Panel + private _activeSpace?: SpaceKey = MetaSpace.Home; // set properly by onReady private _suggestedRooms: ISuggestedRoom[] = []; private _invitedSpaces = new Set(); private spaceOrderLocalEchoMap = new Map(); private _restrictedJoinRuleSupport?: IRoomCapability; private _allRoomsInHome: boolean = SettingsStore.getValue("Spaces.allRoomsInHome"); + private _enabledMetaSpaces: MetaSpace[] = []; // set by onReady constructor() { super(defaultDispatcher, {}); SettingsStore.monitorSetting("Spaces.allRoomsInHome", null); + SettingsStore.monitorSetting("Spaces.enabledMetaSpaces", null); } public get invitedSpaces(): Room[] { return Array.from(this._invitedSpaces); } + public get enabledMetaSpaces(): MetaSpace[] { + return this._enabledMetaSpaces; + } + public get spacePanelSpaces(): Room[] { return this.rootSpaces; } - public get activeSpace(): Room | null { - return this._activeSpace || null; + public get activeSpace(): SpaceKey { + return this._activeSpace; + } + + public get activeSpaceRoom(): Room | null { + if (this._activeSpace[0] !== "!") return null; + return this.matrixClient?.getRoom(this._activeSpace); } public get suggestedRooms(): ISuggestedRoom[] { @@ -139,12 +145,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return this._allRoomsInHome; } - public setActiveRoomInSpace(space: Room | null): void { - if (space && !space.isSpaceRoom()) return; + public setActiveRoomInSpace(space: SpaceKey): void { + if (space[0] === "!" && !this.matrixClient?.getRoom(space)?.isSpaceRoom()) return; if (space !== this.activeSpace) this.setActiveSpace(space); if (space) { - const roomId = this.getNotificationState(space.roomId).getFirstRoomWithNotifications(); + const roomId = this.getNotificationState(space).getFirstRoomWithNotifications(); defaultDispatcher.dispatch({ action: "view_room", room_id: roomId, @@ -184,12 +190,20 @@ export class SpaceStoreClass extends AsyncStoreWithClient { * @param contextSwitch whether to switch the user's context, * should not be done when the space switch is done implicitly due to another event like switching room. */ - public setActiveSpace(space: Room | null, contextSwitch = true) { - if (!this.matrixClient || space === this.activeSpace || (space && !space.isSpaceRoom())) return; + public setActiveSpace(space: SpaceKey, contextSwitch = true) { + if (!space || !this.matrixClient || space === this.activeSpace) return; + + let cliSpace: Room; + if (space[0] === "!") { + cliSpace = this.matrixClient.getRoom(space); + if (!cliSpace?.isSpaceRoom()) return; + } else if (!this.enabledMetaSpaces.includes(space as MetaSpace)) { + return; + } this._activeSpace = space; this.emit(UPDATE_SELECTED_SPACE, this.activeSpace); - this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []); + this.emit(UPDATE_SUGGESTED_ROOMS, this._suggestedRooms = []); if (contextSwitch) { // view last selected room from space @@ -198,7 +212,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // if the space being selected is an invite then always view that invite // else if the last viewed room in this space is joined then view that // else view space home or home depending on what is being clicked on - if (space?.getMyMembership() !== "invite" && + if (cliSpace?.getMyMembership() !== "invite" && this.matrixClient.getRoom(roomId)?.getMyMembership() === "join" && this.getSpaceFilteredRoomIds(space).has(roomId) ) { @@ -207,10 +221,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { room_id: roomId, context_switch: true, }); - } else if (space) { + } else if (cliSpace) { defaultDispatcher.dispatch({ action: "view_room", - room_id: space.roomId, + room_id: space, context_switch: true, }); } else { @@ -221,22 +235,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } // persist space selected - if (space) { - window.localStorage.setItem(ACTIVE_SPACE_LS_KEY, space.roomId); - } else { - window.localStorage.removeItem(ACTIVE_SPACE_LS_KEY); - } + window.localStorage.setItem(ACTIVE_SPACE_LS_KEY, space); - if (space) { - this.loadSuggestedRooms(space); + if (cliSpace) { + this.loadSuggestedRooms(cliSpace); } } private async loadSuggestedRooms(space: Room): Promise { const suggestedRooms = await this.fetchSuggestedRooms(space); - if (this._activeSpace === space) { + if (this._activeSpace === space.roomId) { this._suggestedRooms = suggestedRooms; - this.emit(SUGGESTED_ROOMS, this._suggestedRooms); + this.emit(UPDATE_SUGGESTED_ROOMS, this._suggestedRooms); } } @@ -337,11 +347,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return this.parentMap.get(roomId) || new Set(); } - public getSpaceFilteredRoomIds = (space: Room | null): Set => { - if (!space && this.allRoomsInHome) { + public getSpaceFilteredRoomIds = (space: SpaceKey): Set => { + if (space === MetaSpace.Home && this.allRoomsInHome) { return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId)); } - return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set(); + return this.spaceFilteredRooms.get(space) || new Set(); }; private rebuild = throttle(() => { @@ -420,12 +430,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.parentMap = backrefs; // if the currently selected space no longer exists, remove its selection - if (this._activeSpace && detachedNodes.has(this._activeSpace)) { - this.setActiveSpace(null, false); + if (this._activeSpace[0] === "!" && detachedNodes.has(this.matrixClient.getRoom(this._activeSpace))) { + this.goToFirstSpace(); } this.onRoomsUpdate(); // TODO only do this if a change has happened - this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces); + this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces, this.enabledMetaSpaces); // build initial state of invited spaces as we would have missed the emitted events about the room at launch this._invitedSpaces = new Set(this.sortRootSpaces(invitedSpaces)); @@ -440,19 +450,22 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (this.allRoomsInHome) return true; if (room.isSpaceRoom()) return false; return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space - || DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space - || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite); // show all favourites + || DMRoomMap.shared().getUserIdForRoomId(room.roomId); // put all DMs in the Home Space }; // Update a given room due to its tag changing (e.g DM-ness or Fav-ness) // This can only change whether it shows up in the HOME_SPACE or not private onRoomUpdate = (room: Room) => { - if (this.showInHomeSpace(room)) { - this.spaceFilteredRooms.get(HOME_SPACE)?.add(room.roomId); - this.emit(HOME_SPACE); - } else if (!this.orphanedRooms.has(room.roomId)) { - this.spaceFilteredRooms.get(HOME_SPACE)?.delete(room.roomId); - this.emit(HOME_SPACE); + const enabledMetaSpaces = new Set(this.enabledMetaSpaces); + // TODO more metaspace stuffs + if (enabledMetaSpaces.has(MetaSpace.Home)) { + if (this.showInHomeSpace(room)) { + this.spaceFilteredRooms.get(MetaSpace.Home)?.add(room.roomId); + this.emit(MetaSpace.Home); + } else if (!this.orphanedRooms.has(room.roomId)) { + this.spaceFilteredRooms.get(MetaSpace.Home)?.delete(room.roomId); + this.emit(MetaSpace.Home); + } } }; @@ -469,18 +482,41 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const oldFilteredRooms = this.spaceFilteredRooms; this.spaceFilteredRooms = new Map(); - if (!this.allRoomsInHome) { + const enabledMetaSpaces = new Set(this.enabledMetaSpaces); + // populate the Home metaspace if it is enabled and is not set to all rooms + if (enabledMetaSpaces.has(MetaSpace.Home) && !this.allRoomsInHome) { // put all room invites in the Home Space const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite"); - this.spaceFilteredRooms.set(HOME_SPACE, new Set(invites.map(room => room.roomId))); + this.spaceFilteredRooms.set(MetaSpace.Home, new Set(invites.map(r => r.roomId))); visibleRooms.forEach(room => { if (this.showInHomeSpace(room)) { - this.spaceFilteredRooms.get(HOME_SPACE).add(room.roomId); + this.spaceFilteredRooms.get(MetaSpace.Home).add(room.roomId); } }); } + // populate the Favourites metaspace if it is enabled + if (enabledMetaSpaces.has(MetaSpace.Favourites)) { + const favourites = visibleRooms.filter(r => r.tags[DefaultTagID.Favourite]); + this.spaceFilteredRooms.set(MetaSpace.Favourites, new Set(favourites.map(r => r.roomId))); + } + + // populate the People metaspace if it is enabled + if (enabledMetaSpaces.has(MetaSpace.People)) { + const people = visibleRooms.filter(r => DMRoomMap.shared().getUserIdForRoomId(r.roomId)); + this.spaceFilteredRooms.set(MetaSpace.People, new Set(people.map(r => r.roomId))); + } + + // populate the Orphans metaspace if it is enabled + if (enabledMetaSpaces.has(MetaSpace.Orphans)) { + const orphans = visibleRooms.filter(r => { + // filter out DMs and rooms with >0 parents + return !this.parentMap.get(r.roomId)?.size && !DMRoomMap.shared().getUserIdForRoomId(r.roomId); + }); + this.spaceFilteredRooms.set(MetaSpace.Orphans, new Set(orphans.map(r => r.roomId))); + } + const hiddenChildren = new EnhancedMap>(); visibleRooms.forEach(room => { if (room.getMyMembership() !== "join") return; @@ -540,15 +576,23 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.emit(k); }); + let dmBadgeSpace: MetaSpace; + // only show badges on dms on the most relevant space if such exists + if (enabledMetaSpaces.has(MetaSpace.People)) { + dmBadgeSpace = MetaSpace.People; + } else if (enabledMetaSpaces.has(MetaSpace.Home)) { + dmBadgeSpace = MetaSpace.Home; + } + this.spaceFilteredRooms.forEach((roomIds, s) => { - if (this.allRoomsInHome && s === HOME_SPACE) return; // we'll be using the global notification state, skip + if (this.allRoomsInHome && s === MetaSpace.Home) return; // we'll be using the global notification state, skip // Update NotificationStates this.getNotificationState(s).setRooms(visibleRooms.filter(room => { if (!roomIds.has(room.roomId) || room.isSpaceRoom()) return false; - if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { - return s === HOME_SPACE; + if (dmBadgeSpace && DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { + return s === dmBadgeSpace; } return true; @@ -575,7 +619,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } // don't trigger a context switch when we are switching a space to match the chosen room - this.setActiveSpace(parent || null, false); + this.setActiveSpace(parent?.roomId ?? MetaSpace.Home, false); // TODO }; private onRoom = (room: Room, newMembership?: string, oldMembership?: string) => { @@ -597,7 +641,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const numSuggestedRooms = this._suggestedRooms.length; this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId); if (numSuggestedRooms !== this._suggestedRooms.length) { - this.emit(SUGGESTED_ROOMS, this._suggestedRooms); + this.emit(UPDATE_SUGGESTED_ROOMS, this._suggestedRooms); } // if the room currently being viewed was just joined then switch to its related space @@ -622,10 +666,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (membership === "join" && room.roomId === RoomViewStore.getRoomId()) { // if the user was looking at the space and then joined: select that space - this.setActiveSpace(room, false); - } else if (membership === "leave" && room.roomId === this.activeSpace?.roomId) { + this.setActiveSpace(room.roomId, false); + } else if (membership === "leave" && room.roomId === this.activeSpace) { // user's active space has gone away, go back to home - this.setActiveSpace(null, true); + this.goToFirstSpace(true); } }; @@ -633,7 +677,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const rootSpaces = this.sortRootSpaces(this.rootSpaces); if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) { this.rootSpaces = rootSpaces; - this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces); + this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces, this.enabledMetaSpaces); } } @@ -648,7 +692,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.emit(room.roomId); } - if (room === this.activeSpace && // current space + if (room.roomId === this.activeSpace && // current space this.matrixClient.getRoom(ev.getStateKey())?.getMyMembership() !== "join" && // target not joined ev.getPrevContent().suggested !== ev.getContent().suggested // suggested flag changed ) { @@ -694,7 +738,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (order !== lastOrder) { this.notifyIfOrderChanged(); } - } else if (ev.getType() === EventType.Tag && !this.allRoomsInHome) { + } else if (ev.getType() === EventType.Tag) { // If the room was in favourites and now isn't or the opposite then update its position in the trees const oldTags = lastEv?.getContent()?.tags || {}; const newTags = ev.getContent()?.tags || {}; @@ -728,9 +772,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.parentMap = new EnhancedMap(); this.notificationStateMap = new Map(); this.spaceFilteredRooms = new Map(); - this._activeSpace = null; + this._activeSpace = MetaSpace.Home; // set properly by onReady this._suggestedRooms = []; this._invitedSpaces = new Set(); + this._enabledMetaSpaces = []; } protected async onNotReady() { @@ -760,16 +805,27 @@ export class SpaceStoreClass extends AsyncStoreWithClient { ?.["m.room_versions"]?.["org.matrix.msc3244.room_capabilities"]?.["restricted"]; }); + const enabledMetaSpaces = SettingsStore.getValue("Spaces.enabledMetaSpaces"); + this._enabledMetaSpaces = metaSpaceOrder.filter(k => enabledMetaSpaces[k]) as MetaSpace[]; + await this.onSpaceUpdate(); // trigger an initial update // restore selected state from last session if any and still valid const lastSpaceId = window.localStorage.getItem(ACTIVE_SPACE_LS_KEY); - if (lastSpaceId) { + if (lastSpaceId && ( + lastSpaceId[0] === "!" ? this.matrixClient.getRoom(lastSpaceId) : enabledMetaSpaces[lastSpaceId] + )) { // don't context switch here as it may break permalinks - this.setActiveSpace(this.matrixClient.getRoom(lastSpaceId), false); + this.setActiveSpace(lastSpaceId, false); + } else { + this.goToFirstSpace(); } } + private goToFirstSpace(contextSwitch = false) { + this.setActiveSpace(this.enabledMetaSpaces[0] ?? this.spacePanelSpaces[0]?.roomId, contextSwitch); + } + protected async onAction(payload: ActionPayload) { if (!spacesEnabled) return; switch (payload.action) { @@ -783,9 +839,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (room?.isSpaceRoom()) { // Don't context switch when navigating to the space room // as it will cause you to end up in the wrong room - this.setActiveSpace(room, false); + this.setActiveSpace(room.roomId, false); } else if ( - (!this.allRoomsInHome || this.activeSpace) && + (!this.allRoomsInHome || this.activeSpace[0] === "!") && !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId) ) { this.switchToRelatedSpace(roomId); @@ -799,31 +855,54 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } case "after_leave_room": - if (this._activeSpace && payload.room_id === this._activeSpace.roomId) { - this.setActiveSpace(null, false); + if (this._activeSpace[0] === "!" && payload.room_id === this._activeSpace) { + // User has left the current space, go to first space + this.goToFirstSpace(); } break; - case Action.SwitchSpace: - // 1 is Home, 2-9 are the spaces after Home - if (payload.num === 1) { - this.setActiveSpace(null); - } else if (payload.num > 0 && this.spacePanelSpaces.length > payload.num - 2) { - this.setActiveSpace(this.spacePanelSpaces[payload.num - 2]); + case Action.SwitchSpace: { + // Metaspaces start at 1, Spaces follow + if (payload.num < 1 || payload.num > 9) break; + const numMetaSpaces = this.enabledMetaSpaces.length; + if (payload.num <= numMetaSpaces) { + this.setActiveSpace(this.enabledMetaSpaces[payload.num - 1]); + } else if (this.spacePanelSpaces.length > payload.num - numMetaSpaces - 1) { + this.setActiveSpace(this.spacePanelSpaces[payload.num - numMetaSpaces - 1].roomId); } break; + } case Action.SettingUpdated: { const settingUpdatedPayload = payload as SettingUpdatedPayload; - if (settingUpdatedPayload.settingName === "Spaces.allRoomsInHome") { - const newValue = SettingsStore.getValue("Spaces.allRoomsInHome"); - if (this.allRoomsInHome !== newValue) { - this._allRoomsInHome = newValue; - this.emit(UPDATE_HOME_BEHAVIOUR, this.allRoomsInHome); - this.rebuild(); // rebuild everything + switch (settingUpdatedPayload.settingName) { + case "Spaces.allRoomsInHome": { + const newValue = SettingsStore.getValue("Spaces.allRoomsInHome"); + if (this.allRoomsInHome !== newValue) { + this._allRoomsInHome = newValue; + this.emit(UPDATE_HOME_BEHAVIOUR, this.allRoomsInHome); + this.rebuild(); // rebuild everything + } + break; + } + + case "Spaces.enabledMetaSpaces": { + const newValue = SettingsStore.getValue("Spaces.enabledMetaSpaces"); + const enabledMetaSpaces = metaSpaceOrder.filter(k => newValue[k]) as MetaSpace[]; + if (arrayHasDiff(this._enabledMetaSpaces, enabledMetaSpaces)) { + this._enabledMetaSpaces = enabledMetaSpaces; + // if a metaspace currently being viewed was remove, go to another one + if (this.activeSpace[0] !== "!" && + !enabledMetaSpaces.includes(this.activeSpace as MetaSpace) + ) { + this.goToFirstSpace(); + } + this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces, this.enabledMetaSpaces); + this.rebuild(); // rebuild everything + } + break; } } - break; } } } diff --git a/src/stores/SpaceTreeLevelLayoutStore.ts b/src/stores/spaces/SpaceTreeLevelLayoutStore.ts similarity index 100% rename from src/stores/SpaceTreeLevelLayoutStore.ts rename to src/stores/spaces/SpaceTreeLevelLayoutStore.ts diff --git a/src/stores/spaces/index.ts b/src/stores/spaces/index.ts new file mode 100644 index 0000000000..7816932b05 --- /dev/null +++ b/src/stores/spaces/index.ts @@ -0,0 +1,40 @@ +/* +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 { Room } from "matrix-js-sdk/src/models/room"; +import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces"; + +// The consts & types are moved out here to prevent cyclical imports + +export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces"); +export const UPDATE_INVITED_SPACES = Symbol("invited-spaces"); +export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); +export const UPDATE_HOME_BEHAVIOUR = Symbol("home-behaviour"); +export const UPDATE_SUGGESTED_ROOMS = Symbol("suggested-rooms"); +// Space Key will be emitted when a Space's children change + +export enum MetaSpace { + Home = "home-space", + Favourites = "favourites-space", + People = "people-space", + Orphans = "orphans-space", +} + +export type SpaceKey = MetaSpace | Room["roomId"]; + +export interface ISuggestedRoom extends IHierarchyRoom { + viaServers: string[]; +} diff --git a/src/utils/RoomUpgrade.ts b/src/utils/RoomUpgrade.ts index b9ea93d7fc..902c5d00ca 100644 --- a/src/utils/RoomUpgrade.ts +++ b/src/utils/RoomUpgrade.ts @@ -21,7 +21,7 @@ import { inviteUsersToRoom } from "../RoomInvite"; import Modal, { IHandle } from "../Modal"; import { _t } from "../languageHandler"; import ErrorDialog from "../components/views/dialogs/ErrorDialog"; -import SpaceStore from "../stores/SpaceStore"; +import SpaceStore from "../stores/spaces/SpaceStore"; import Spinner from "../components/views/elements/Spinner"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index ccbf0af402..d6d0566811 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -18,13 +18,16 @@ import { EventEmitter } from "events"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import "./enable-metaspaces-labs"; import "../skinned-sdk"; // Must be first for skinning to work -import SpaceStore, { +import SpaceStore from "../../src/stores/spaces/SpaceStore"; +import { + MetaSpace, UPDATE_HOME_BEHAVIOUR, UPDATE_INVITED_SPACES, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES, -} from "../../src/stores/SpaceStore"; +} from "../../src/stores/spaces"; import * as testUtils from "../utils/test-utils"; import { mkEvent, stubClient } from "../test-utils"; import DMRoomMap from "../../src/utils/DMRoomMap"; @@ -90,10 +93,18 @@ describe("SpaceStore", () => { await emitProm; }; - beforeEach(() => { + beforeEach(async () => { jest.runAllTimers(); // run async dispatch client.getVisibleRooms.mockReturnValue(rooms = []); + + await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, { + [MetaSpace.Home]: true, + [MetaSpace.Favourites]: true, + [MetaSpace.People]: true, + [MetaSpace.Orphans]: true, + }); }); + afterEach(async () => { await testUtils.resetAsyncStoreWithClient(store); }); @@ -377,69 +388,84 @@ describe("SpaceStore", () => { }); it("home space contains orphaned rooms", () => { - expect(store.getSpaceFilteredRoomIds(null).has(orphan1)).toBeTruthy(); - expect(store.getSpaceFilteredRoomIds(null).has(orphan2)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(orphan1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(orphan2)).toBeTruthy(); }); - it("home space contains favourites", () => { - expect(store.getSpaceFilteredRoomIds(null).has(fav1)).toBeTruthy(); - expect(store.getSpaceFilteredRoomIds(null).has(fav2)).toBeTruthy(); - expect(store.getSpaceFilteredRoomIds(null).has(fav3)).toBeTruthy(); + it("home space does not contain all favourites", () => { + expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(fav1)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(fav2)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(fav3)).toBeFalsy(); }); it("home space contains dm rooms", () => { - expect(store.getSpaceFilteredRoomIds(null).has(dm1)).toBeTruthy(); - expect(store.getSpaceFilteredRoomIds(null).has(dm2)).toBeTruthy(); - expect(store.getSpaceFilteredRoomIds(null).has(dm3)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(dm1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(dm2)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(dm3)).toBeTruthy(); }); it("home space contains invites", () => { - expect(store.getSpaceFilteredRoomIds(null).has(invite1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite1)).toBeTruthy(); }); it("home space contains invites even if they are also shown in a space", () => { - expect(store.getSpaceFilteredRoomIds(null).has(invite2)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite2)).toBeTruthy(); }); it("all rooms space does contain rooms/low priority even if they are also shown in a space", async () => { await setShowAllRooms(true); - expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(room1)).toBeTruthy(); + }); + + it("favourites space does contain favourites even if they are also shown in a space", async () => { + expect(store.getSpaceFilteredRoomIds(MetaSpace.Favourites).has(fav1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.Favourites).has(fav2)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.Favourites).has(fav3)).toBeTruthy(); + }); + + it("people space does contain people even if they are also shown in a space", async () => { + expect(store.getSpaceFilteredRoomIds(MetaSpace.People).has(dm1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.People).has(dm2)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.People).has(dm3)).toBeTruthy(); + }); + + it("orphans space does contain orphans even if they are also shown in all rooms", async () => { + await setShowAllRooms(true); + expect(store.getSpaceFilteredRoomIds(MetaSpace.Orphans).has(orphan1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.Orphans).has(orphan2)).toBeTruthy(); }); it("home space doesn't contain rooms/low priority if they are also shown in a space", async () => { await setShowAllRooms(false); - expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(room1)).toBeFalsy(); }); it("space contains child rooms", () => { - const space = client.getRoom(space1); - expect(store.getSpaceFilteredRoomIds(space).has(fav1)).toBeTruthy(); - expect(store.getSpaceFilteredRoomIds(space).has(room1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(space1).has(fav1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(space1).has(room1)).toBeTruthy(); }); it("space contains child favourites", () => { - const space = client.getRoom(space2); - expect(store.getSpaceFilteredRoomIds(space).has(fav1)).toBeTruthy(); - expect(store.getSpaceFilteredRoomIds(space).has(fav2)).toBeTruthy(); - expect(store.getSpaceFilteredRoomIds(space).has(fav3)).toBeTruthy(); - expect(store.getSpaceFilteredRoomIds(space).has(room1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(space2).has(fav1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(space2).has(fav2)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(space2).has(fav3)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(space2).has(room1)).toBeTruthy(); }); it("space contains child invites", () => { - const space = client.getRoom(space3); - expect(store.getSpaceFilteredRoomIds(space).has(invite2)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(space3).has(invite2)).toBeTruthy(); }); it("spaces contain dms which you have with members of that space", () => { - expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm1)).toBeTruthy(); - expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm1)).toBeFalsy(); - expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm1)).toBeFalsy(); - expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm2)).toBeFalsy(); - expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm2)).toBeTruthy(); - expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm2)).toBeFalsy(); - expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm3)).toBeFalsy(); - expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm3)).toBeFalsy(); - expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm3)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(space1).has(dm1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(space2).has(dm1)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(space3).has(dm1)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(space1).has(dm2)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(space2).has(dm2)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(space3).has(dm2)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(space1).has(dm3)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(space2).has(dm3)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(space3).has(dm3)).toBeFalsy(); }); it("dms are only added to Notification States for only the Home Space", () => { @@ -491,11 +517,11 @@ describe("SpaceStore", () => { }); it("honours m.space.parent if sender has permission in parent space", () => { - expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(room2)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(space2).has(room2)).toBeTruthy(); }); it("does not honour m.space.parent if sender does not have permission in parent space", () => { - expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(room3)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(space3).has(room3)).toBeFalsy(); }); }); }); @@ -586,8 +612,8 @@ describe("SpaceStore", () => { expect(store.invitedSpaces).toStrictEqual([]); expect(store.getChildSpaces(space1)).toStrictEqual([]); expect(store.getChildRooms(space1)).toStrictEqual([]); - expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(invite1)).toBeFalsy(); - expect(store.getSpaceFilteredRoomIds(null).has(invite1)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(space1).has(invite1)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite1)).toBeFalsy(); const invite = mkRoom(invite1); invite.getMyMembership.mockReturnValue("invite"); @@ -599,8 +625,8 @@ describe("SpaceStore", () => { expect(store.invitedSpaces).toStrictEqual([]); expect(store.getChildSpaces(space1)).toStrictEqual([]); expect(store.getChildRooms(space1)).toStrictEqual([invite]); - expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(invite1)).toBeTruthy(); - expect(store.getSpaceFilteredRoomIds(null).has(invite1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(space1).has(invite1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite1)).toBeTruthy(); }); }); @@ -614,49 +640,46 @@ describe("SpaceStore", () => { ]); mkSpace(space3).getMyMembership.mockReturnValue("invite"); await run(); - store.setActiveSpace(null); - expect(store.activeSpace).toBe(null); + store.setActiveSpace(MetaSpace.Home); + expect(store.activeSpace).toBe(MetaSpace.Home); }); afterEach(() => { fn.mockClear(); }); it("switch to home space", async () => { - store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(space1); fn.mockClear(); - store.setActiveSpace(null); - expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, null); - expect(store.activeSpace).toBe(null); + store.setActiveSpace(MetaSpace.Home); + expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, MetaSpace.Home); + expect(store.activeSpace).toBe(MetaSpace.Home); }); it("switch to invited space", async () => { - const space = client.getRoom(space3); - store.setActiveSpace(space); - expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space); - expect(store.activeSpace).toBe(space); + store.setActiveSpace(space3); + expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space3); + expect(store.activeSpace).toBe(space3); }); it("switch to top level space", async () => { - const space = client.getRoom(space1); - store.setActiveSpace(space); - expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space); - expect(store.activeSpace).toBe(space); + store.setActiveSpace(space1); + expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space1); + expect(store.activeSpace).toBe(space1); }); it("switch to subspace", async () => { - const space = client.getRoom(space2); - store.setActiveSpace(space); - expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space); - expect(store.activeSpace).toBe(space); + store.setActiveSpace(space2); + expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space2); + expect(store.activeSpace).toBe(space2); }); it("switch to unknown space is a nop", async () => { - expect(store.activeSpace).toBe(null); + expect(store.activeSpace).toBe(MetaSpace.Home); const space = client.getRoom(room1); // not a space - store.setActiveSpace(space); - expect(fn).not.toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space); - expect(store.activeSpace).toBe(null); + store.setActiveSpace(space.roomId); + expect(fn).not.toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space.roomId); + expect(store.activeSpace).toBe(MetaSpace.Home); }); }); @@ -678,6 +701,7 @@ describe("SpaceStore", () => { }); afterEach(() => { localStorage.clear(); + localStorage.setItem("mx_labs_feature_feature_spaces_metaspaces", "true"); defaultDispatcher.unregister(dispatcherRef); }); @@ -687,59 +711,59 @@ describe("SpaceStore", () => { }; it("last viewed room in target space is the current viewed and in both spaces", async () => { - store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(space1); viewRoom(room2); - store.setActiveSpace(client.getRoom(space2)); + store.setActiveSpace(space2); viewRoom(room2); - store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(space1); expect(getCurrentRoom()).toBe(room2); }); it("last viewed room in target space is in the current space", async () => { - store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(space1); viewRoom(room2); - store.setActiveSpace(client.getRoom(space2)); + store.setActiveSpace(space2); expect(getCurrentRoom()).toBe(space2); - store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(space1); expect(getCurrentRoom()).toBe(room2); }); it("last viewed room in target space is not in the current space", async () => { - store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(space1); viewRoom(room1); - store.setActiveSpace(client.getRoom(space2)); + store.setActiveSpace(space2); viewRoom(room2); - store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(space1); expect(getCurrentRoom()).toBe(room1); }); it("last viewed room is target space is not known", async () => { - store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(space1); viewRoom(room1); localStorage.setItem(`mx_space_context_${space2}`, orphan2); - store.setActiveSpace(client.getRoom(space2)); + store.setActiveSpace(space2); expect(getCurrentRoom()).toBe(space2); }); it("last viewed room is target space is no longer in that space", async () => { - store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(space1); viewRoom(room1); localStorage.setItem(`mx_space_context_${space2}`, room1); - store.setActiveSpace(client.getRoom(space2)); + store.setActiveSpace(space2); expect(getCurrentRoom()).toBe(space2); // Space home instead of room1 }); it("no last viewed room in target space", async () => { - store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(space1); viewRoom(room1); - store.setActiveSpace(client.getRoom(space2)); + store.setActiveSpace(space2); expect(getCurrentRoom()).toBe(space2); }); it("no last viewed room in home space", async () => { - store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(space1); viewRoom(room1); - store.setActiveSpace(null); + store.setActiveSpace(MetaSpace.Home); expect(getCurrentRoom()).toBeNull(); // Home }); }); @@ -767,38 +791,51 @@ describe("SpaceStore", () => { it("no switch required, room is in current space", async () => { viewRoom(room1); - store.setActiveSpace(client.getRoom(space1), false); + store.setActiveSpace(space1, false); viewRoom(room2); - expect(store.activeSpace).toBe(client.getRoom(space1)); + expect(store.activeSpace).toBe(space1); }); it("switch to canonical parent space for room", async () => { viewRoom(room1); - store.setActiveSpace(client.getRoom(space2), false); + store.setActiveSpace(space2, false); viewRoom(room2); - expect(store.activeSpace).toBe(client.getRoom(space2)); + expect(store.activeSpace).toBe(space2); }); it("switch to first containing space for room", async () => { viewRoom(room2); - store.setActiveSpace(client.getRoom(space2), false); + store.setActiveSpace(space2, false); viewRoom(room3); - expect(store.activeSpace).toBe(client.getRoom(space1)); + expect(store.activeSpace).toBe(space1); }); it("switch to home for orphaned room", async () => { viewRoom(room1); - store.setActiveSpace(client.getRoom(space1), false); + store.setActiveSpace(space1, false); viewRoom(orphan1); - expect(store.activeSpace).toBeNull(); + expect(store.activeSpace).toBe(MetaSpace.Home); + }); + + it("switch to first space when selected metaspace is disabled", async () => { + store.setActiveSpace(MetaSpace.People, false); + expect(store.activeSpace).toBe(MetaSpace.People); + await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, { + [MetaSpace.Home]: false, + [MetaSpace.Favourites]: true, + [MetaSpace.People]: false, + [MetaSpace.Orphans]: true, + }); + jest.runAllTimers(); + expect(store.activeSpace).toBe(MetaSpace.Favourites); }); it("when switching rooms in the all rooms home space don't switch to related space", async () => { await setShowAllRooms(true); viewRoom(room2); - store.setActiveSpace(null, false); + store.setActiveSpace(MetaSpace.Home, false); viewRoom(room1); - expect(store.activeSpace).toBeNull(); + expect(store.activeSpace).toBe(MetaSpace.Home); }); }); diff --git a/test/stores/enable-metaspaces-labs.ts b/test/stores/enable-metaspaces-labs.ts new file mode 100644 index 0000000000..f22132a0d6 --- /dev/null +++ b/test/stores/enable-metaspaces-labs.ts @@ -0,0 +1,17 @@ +/* +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. +*/ + +localStorage.setItem("mx_labs_feature_feature_spaces_metaspaces", "true"); diff --git a/test/stores/room-list/SpaceWatcher-test.ts b/test/stores/room-list/SpaceWatcher-test.ts index cb2394349a..42ffbe5333 100644 --- a/test/stores/room-list/SpaceWatcher-test.ts +++ b/test/stores/room-list/SpaceWatcher-test.ts @@ -14,17 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ +import "../enable-metaspaces-labs"; import "../../skinned-sdk"; // Must be first for skinning to work import { SpaceWatcher } from "../../../src/stores/room-list/SpaceWatcher"; import type { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore"; import SettingsStore from "../../../src/settings/SettingsStore"; -import SpaceStore, { UPDATE_HOME_BEHAVIOUR } from "../../../src/stores/SpaceStore"; +import SpaceStore from "../../../src/stores/spaces/SpaceStore"; +import { MetaSpace, UPDATE_HOME_BEHAVIOUR } from "../../../src/stores/spaces"; import { stubClient } from "../../test-utils"; import { SettingLevel } from "../../../src/settings/SettingLevel"; +import * as testUtils from "../../utils/test-utils"; import { setupAsyncStoreWithClient } from "../../utils/test-utils"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; -import * as testUtils from "../../utils/test-utils"; import { SpaceFilterCondition } from "../../../src/stores/room-list/filters/SpaceFilterCondition"; +import DMRoomMap from "../../../src/utils/DMRoomMap"; let filter: SpaceFilterCondition = null; @@ -33,8 +36,13 @@ const mockRoomListStore = { removeFilter: () => filter = null, } as unknown as RoomListStoreClass; -const space1Id = "!space1:server"; -const space2Id = "!space2:server"; +const getUserIdForRoomId = jest.fn(); +const getDMRoomsForUserId = jest.fn(); +// @ts-ignore +DMRoomMap.sharedInstance = { getUserIdForRoomId, getDMRoomsForUserId }; + +const space1 = "!space1:server"; +const space2 = "!space2:server"; describe("SpaceWatcher", () => { stubClient(); @@ -50,17 +58,21 @@ describe("SpaceWatcher", () => { await testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR); }; - let space1; - let space2; - beforeEach(async () => { filter = null; store.removeAllListeners(); - store.setActiveSpace(null); + store.setActiveSpace(MetaSpace.Home); client.getVisibleRooms.mockReturnValue(rooms = []); - space1 = mkSpace(space1Id); - space2 = mkSpace(space2Id); + mkSpace(space1); + mkSpace(space2); + + await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, { + [MetaSpace.Home]: true, + [MetaSpace.Favourites]: true, + [MetaSpace.People]: true, + [MetaSpace.Orphans]: true, + }); client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId)); await setupAsyncStoreWithClient(store, client); @@ -80,14 +92,14 @@ describe("SpaceWatcher", () => { expect(filter).toBeNull(); }); - it("sets space=null filter for all -> home transition", async () => { + it("sets space=Home filter for all -> home transition", async () => { await setShowAllRooms(true); new SpaceWatcher(mockRoomListStore); await setShowAllRooms(false); expect(filter).toBeInstanceOf(SpaceFilterCondition); - expect(filter["space"]).toBeNull(); + expect(filter["space"]).toBe(MetaSpace.Home); }); it("sets filter correctly for all -> space transition", async () => { @@ -126,7 +138,43 @@ describe("SpaceWatcher", () => { SpaceStore.instance.setActiveSpace(space1); expect(filter).toBeInstanceOf(SpaceFilterCondition); expect(filter["space"]).toBe(space1); - SpaceStore.instance.setActiveSpace(null); + SpaceStore.instance.setActiveSpace(MetaSpace.Home); + + expect(filter).toBeNull(); + }); + + it("removes filter for favourites -> all transition", async () => { + await setShowAllRooms(true); + new SpaceWatcher(mockRoomListStore); + + SpaceStore.instance.setActiveSpace(MetaSpace.Favourites); + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(MetaSpace.Favourites); + SpaceStore.instance.setActiveSpace(MetaSpace.Home); + + expect(filter).toBeNull(); + }); + + it("removes filter for people -> all transition", async () => { + await setShowAllRooms(true); + new SpaceWatcher(mockRoomListStore); + + SpaceStore.instance.setActiveSpace(MetaSpace.People); + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(MetaSpace.People); + SpaceStore.instance.setActiveSpace(MetaSpace.Home); + + expect(filter).toBeNull(); + }); + + it("removes filter for orphans -> all transition", async () => { + await setShowAllRooms(true); + new SpaceWatcher(mockRoomListStore); + + SpaceStore.instance.setActiveSpace(MetaSpace.Orphans); + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(MetaSpace.Orphans); + SpaceStore.instance.setActiveSpace(MetaSpace.Home); expect(filter).toBeNull(); }); @@ -138,10 +186,36 @@ describe("SpaceWatcher", () => { new SpaceWatcher(mockRoomListStore); expect(filter).toBeInstanceOf(SpaceFilterCondition); expect(filter["space"]).toBe(space1); - SpaceStore.instance.setActiveSpace(null); + SpaceStore.instance.setActiveSpace(MetaSpace.Home); expect(filter).toBeInstanceOf(SpaceFilterCondition); - expect(filter["space"]).toBe(null); + expect(filter["space"]).toBe(MetaSpace.Home); + }); + + it("updates filter correctly for space -> orphans transition", async () => { + await setShowAllRooms(false); + SpaceStore.instance.setActiveSpace(space1); + + new SpaceWatcher(mockRoomListStore); + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + SpaceStore.instance.setActiveSpace(MetaSpace.Orphans); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(MetaSpace.Orphans); + }); + + it("updates filter correctly for orphans -> people transition", async () => { + await setShowAllRooms(false); + SpaceStore.instance.setActiveSpace(MetaSpace.Orphans); + + new SpaceWatcher(mockRoomListStore); + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(MetaSpace.Orphans); + SpaceStore.instance.setActiveSpace(MetaSpace.People); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(MetaSpace.People); }); it("updates filter correctly for space -> space transition", async () => {