Implement more meta-spaces (#7077)

This commit is contained in:
Michael Telatynski 2021-11-11 13:07:41 +00:00 committed by GitHub
parent dadac386fe
commit 5ad3261cb2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 970 additions and 353 deletions

View file

@ -276,6 +276,7 @@
@import "./views/settings/tabs/user/_NotificationUserSettingsTab.scss"; @import "./views/settings/tabs/user/_NotificationUserSettingsTab.scss";
@import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss"; @import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss";
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss"; @import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
@import "./views/settings/tabs/user/_SidebarUserSettingsTab.scss";
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss"; @import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
@import "./views/spaces/_SpaceBasicSettings.scss"; @import "./views/spaces/_SpaceBasicSettings.scss";
@import "./views/spaces/_SpaceChildrenPicker.scss"; @import "./views/spaces/_SpaceChildrenPicker.scss";

View file

@ -189,15 +189,35 @@ $activeBorderColor: $secondary-content;
} }
} }
&.mx_SpaceButton_home .mx_SpaceButton_icon { &.mx_SpaceButton_home,
background-color: #ffffff; &.mx_SpaceButton_favourites,
&.mx_SpaceButton_people,
&.mx_SpaceButton_orphans {
.mx_SpaceButton_icon {
background-color: #ffffff;
&::before { &::before {
background-color: #3f3d3d; background-color: #3f3d3d;
mask-image: url('$(res)/img/element-icons/home.svg'); }
} }
} }
&.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 { &.mx_SpaceButton_new .mx_SpaceButton_icon {
background-color: $roomlist-button-bg-color; background-color: $roomlist-button-bg-color;

View file

@ -37,6 +37,10 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/settings/preference.svg'); 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 { .mx_UserSettingsDialog_securityIcon::before {
mask-image: url('$(res)/img/element-icons/security.svg'); mask-image: url('$(res)/img/element-icons/security.svg');
} }

View file

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

View file

@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="path-1-inside-1_152:29307" fill="white">
<rect x="1" y="1" width="14" height="14" rx="1"/>
</mask>
<rect x="1" y="1" width="14" height="14" rx="1" stroke="#737D8C" stroke-width="4" mask="url(#path-1-inside-1_152:29307)"/>
<line x1="7" y1="1" x2="7" y2="14" stroke="#737D8C" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 406 B

View file

@ -41,7 +41,7 @@ import UserActivity from "../UserActivity";
import { ModalWidgetStore } from "../stores/ModalWidgetStore"; import { ModalWidgetStore } from "../stores/ModalWidgetStore";
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import VoipUserMapper from "../VoipUserMapper"; import VoipUserMapper from "../VoipUserMapper";
import { SpaceStoreClass } from "../stores/SpaceStore"; import { SpaceStoreClass } from "../stores/spaces/SpaceStore";
import TypingStore from "../stores/TypingStore"; import TypingStore from "../stores/TypingStore";
import { EventIndexPeg } from "../indexing/EventIndexPeg"; import { EventIndexPeg } from "../indexing/EventIndexPeg";
import { VoiceRecordingStore } from "../stores/VoiceRecordingStore"; import { VoiceRecordingStore } from "../stores/VoiceRecordingStore";

View file

@ -22,7 +22,7 @@ import { split } from "lodash";
import DMRoomMap from './utils/DMRoomMap'; import DMRoomMap from './utils/DMRoomMap';
import { mediaFromMxc } from "./customisations/Media"; 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 // Not to be used for BaseAvatar urls as that has similar default avatar fallback already
export function avatarUrlForMember( export function avatarUrlForMember(

View file

@ -27,7 +27,7 @@ import NotifProvider from './NotifProvider';
import { timeout } from "../utils/promise"; import { timeout } from "../utils/promise";
import AutocompleteProvider, { ICommand } from "./AutocompleteProvider"; import AutocompleteProvider, { ICommand } from "./AutocompleteProvider";
import SpaceProvider from "./SpaceProvider"; import SpaceProvider from "./SpaceProvider";
import SpaceStore from "../stores/SpaceStore"; import SpaceStore from "../stores/spaces/SpaceStore";
export interface ISelectionRange { export interface ISelectionRange {
beginning?: boolean; // whether the selection is in the first block of the editor or not beginning?: boolean; // whether the selection is in the first block of the editor or not

View file

@ -28,7 +28,7 @@ import { PillCompletion } from './Components';
import { makeRoomPermalink } from "../utils/permalinks/Permalinks"; import { makeRoomPermalink } from "../utils/permalinks/Permalinks";
import { ICompletion, ISelectionRange } from "./Autocompleter"; import { ICompletion, ISelectionRange } from "./Autocompleter";
import RoomAvatar from '../components/views/avatars/RoomAvatar'; import RoomAvatar from '../components/views/avatars/RoomAvatar';
import SpaceStore from "../stores/SpaceStore"; import SpaceStore from "../stores/spaces/SpaceStore";
const ROOM_REGEX = /\B#\S*/g; const ROOM_REGEX = /\B#\S*/g;

View file

@ -17,7 +17,6 @@ limitations under the License.
import * as React from "react"; import * as React from "react";
import { createRef } from "react"; import { createRef } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
@ -37,10 +36,12 @@ import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import RoomListNumResults from "../views/rooms/RoomListNumResults"; import RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget"; import LeftPanelWidget from "./LeftPanelWidget";
import { replaceableComponent } from "../../utils/replaceableComponent"; 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 { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
import UIStore from "../../stores/UIStore"; import UIStore from "../../stores/UIStore";
import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex"; import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
import MatrixClientContext from "../../contexts/MatrixClientContext";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
@ -49,7 +50,7 @@ interface IProps {
interface IState { interface IState {
showBreadcrumbs: boolean; showBreadcrumbs: boolean;
activeSpace?: Room; activeSpace: SpaceKey;
} }
@replaceableComponent("structures.LeftPanel") @replaceableComponent("structures.LeftPanel")
@ -61,6 +62,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
private focusedElement = null; private focusedElement = null;
private isDoingStickyHeaders = false; private isDoingStickyHeaders = false;
static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
@ -98,7 +102,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
} }
} }
private updateActiveSpace = (activeSpace: Room) => { private updateActiveSpace = (activeSpace: SpaceKey) => {
this.setState({ activeSpace }); this.setState({ activeSpace });
}; };
@ -343,6 +347,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
/>; />;
} }
const space = this.state.activeSpace[0] === "!" ? this.context.getRoom(this.state.activeSpace) : null;
return ( return (
<div <div
className="mx_LeftPanel_filterContainer" className="mx_LeftPanel_filterContainer"
@ -363,9 +368,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
mx_LeftPanel_exploreButton_space: !!this.state.activeSpace, mx_LeftPanel_exploreButton_space: !!this.state.activeSpace,
})} })}
onClick={this.onExplore} onClick={this.onExplore}
title={this.state.activeSpace title={space ? _t("Explore %(spaceName)s", { spaceName: space.name }) : _t("Explore rooms")}
? _t("Explore %(spaceName)s", { spaceName: this.state.activeSpace.name })
: _t("Explore rooms")}
/> />
</div> </div>
); );

View file

@ -64,7 +64,7 @@ import MyGroups from "./MyGroups";
import UserView from "./UserView"; import UserView from "./UserView";
import GroupView from "./GroupView"; import GroupView from "./GroupView";
import BackdropPanel from "./BackdropPanel"; import BackdropPanel from "./BackdropPanel";
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/spaces/SpaceStore";
import classNames from 'classnames'; import classNames from 'classnames';
import GroupFilterPanel from './GroupFilterPanel'; import GroupFilterPanel from './GroupFilterPanel';
import CustomRoomTagPanel from './CustomRoomTagPanel'; import CustomRoomTagPanel from './CustomRoomTagPanel';

View file

@ -78,7 +78,7 @@ import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
import DialPadModal from "../views/voip/DialPadModal"; import DialPadModal from "../views/voip/DialPadModal";
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast'; import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
import { shouldUseLoginForWelcome } from "../../utils/pages"; import { shouldUseLoginForWelcome } from "../../utils/pages";
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/spaces/SpaceStore";
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import RoomListStore from "../../stores/room-list/RoomListStore"; import RoomListStore from "../../stores/room-list/RoomListStore";
import { RoomUpdateCause } from "../../stores/room-list/models"; import { RoomUpdateCause } from "../../stores/room-list/models";
@ -712,10 +712,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
break; break;
} }
case Action.ViewRoomDirectory: { case Action.ViewRoomDirectory: {
if (SpaceStore.instance.activeSpace) { if (SpaceStore.instance.activeSpace[0] === "!") {
defaultDispatcher.dispatch({ defaultDispatcher.dispatch({
action: "view_room", action: "view_room",
room_id: SpaceStore.instance.activeSpace.roomId, room_id: SpaceStore.instance.activeSpace,
}); });
} else { } else {
Modal.createTrackedDialog('Room directory', '', RoomDirectory, { Modal.createTrackedDialog('Room directory', '', RoomDirectory, {

View file

@ -50,7 +50,7 @@ import NotificationPanel from "./NotificationPanel";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/spaces/SpaceStore";
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
import { E2EStatus } from '../../utils/ShieldUtils'; import { E2EStatus } from '../../utils/ShieldUtils';
import { dispatchShowThreadsPanelEvent } from '../../dispatcher/dispatch-actions/threads'; import { dispatchShowThreadsPanelEvent } from '../../dispatcher/dispatch-actions/threads';

View file

@ -28,7 +28,8 @@ import RoomListStore from "../../stores/room-list/RoomListStore";
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition"; import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
import { replaceableComponent } from "../../utils/replaceableComponent"; 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 { interface IProps {
isMinimized: boolean; isMinimized: boolean;

View file

@ -88,7 +88,7 @@ import RoomStatusBar from "./RoomStatusBar";
import MessageComposer from '../views/rooms/MessageComposer'; import MessageComposer from '../views/rooms/MessageComposer';
import JumpToBottomButton from "../views/rooms/JumpToBottomButton"; import JumpToBottomButton from "../views/rooms/JumpToBottomButton";
import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar"; 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 { logger } from "matrix-js-sdk/src/logger";
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';

View file

@ -49,7 +49,7 @@ import { mediaFromMxc } from "../../customisations/Media";
import InfoTooltip from "../views/elements/InfoTooltip"; import InfoTooltip from "../views/elements/InfoTooltip";
import TextWithTooltip from "../views/elements/TextWithTooltip"; import TextWithTooltip from "../views/elements/TextWithTooltip";
import { useStateToggle } from "../../hooks/useStateToggle"; import { useStateToggle } from "../../hooks/useStateToggle";
import { getChildOrder } from "../../stores/SpaceStore"; import { getChildOrder } from "../../stores/spaces/SpaceStore";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { linkifyElement } from "../../HtmlUtils"; import { linkifyElement } from "../../HtmlUtils";
import { useDispatcher } from "../../hooks/useDispatcher"; import { useDispatcher } from "../../hooks/useDispatcher";

View file

@ -57,7 +57,7 @@ import {
} from "../../utils/space"; } from "../../utils/space";
import SpaceHierarchy, { joinRoom, showRoom } from "./SpaceHierarchy"; import SpaceHierarchy, { joinRoom, showRoom } from "./SpaceHierarchy";
import MemberAvatar from "../views/avatars/MemberAvatar"; import MemberAvatar from "../views/avatars/MemberAvatar";
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/spaces/SpaceStore";
import FacePile from "../views/elements/FacePile"; import FacePile from "../views/elements/FacePile";
import { import {
AddExistingToSpace, AddExistingToSpace,

View file

@ -54,7 +54,8 @@ import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototyp
import { UIFeature } from "../../settings/UIFeature"; import { UIFeature } from "../../settings/UIFeature";
import HostSignupAction from "./HostSignupAction"; import HostSignupAction from "./HostSignupAction";
import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes"; 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 RoomName from "../views/elements/RoomName";
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import InlineSpinner from "../views/elements/InlineSpinner"; import InlineSpinner from "../views/elements/InlineSpinner";
@ -90,6 +91,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
isDarkTheme: this.isUserOnDarkTheme(), isDarkTheme: this.isUserOnDarkTheme(),
isHighContrast: this.isUserOnHighContrastTheme(), isHighContrast: this.isUserOnHighContrastTheme(),
pendingRoomJoin: new Set<string>(), pendingRoomJoin: new Set<string>(),
selectedSpace: SpaceStore.instance.activeSpaceRoom,
}; };
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
@ -162,8 +164,10 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.forceUpdate(); this.forceUpdate();
}; };
private onSelectedSpaceUpdate = async (selectedSpace?: Room) => { private onSelectedSpaceUpdate = async () => {
this.setState({ selectedSpace }); this.setState({
selectedSpace: SpaceStore.instance.activeSpaceRoom,
});
}; };
private onThemeChanged = () => { private onThemeChanged = () => {

View file

@ -24,7 +24,7 @@ import { _t } from '../../../languageHandler';
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import Dropdown from "../elements/Dropdown"; import Dropdown from "../elements/Dropdown";
import SearchBox from "../../structures/SearchBox"; import SearchBox from "../../structures/SearchBox";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
import RoomAvatar from "../avatars/RoomAvatar"; import RoomAvatar from "../avatars/RoomAvatar";
import { getDisplayAliasForRoom } from "../../../Rooms"; import { getDisplayAliasForRoom } from "../../../Rooms";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";

View file

@ -17,7 +17,7 @@ limitations under the License.
import React, { ComponentProps, useMemo, useState } from 'react'; import React, { ComponentProps, useMemo, useState } from 'react';
import ConfirmUserActionDialog from "./ConfirmUserActionDialog"; 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 { Room } from "matrix-js-sdk/src/models/room";
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker"; import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";

View file

@ -32,7 +32,7 @@ import RoomAliasField from "../elements/RoomAliasField";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "../dialogs/BaseDialog"; import BaseDialog from "../dialogs/BaseDialog";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
import JoinRuleDropdown from "../elements/JoinRuleDropdown"; import JoinRuleDropdown from "../elements/JoinRuleDropdown";
interface IProps { interface IProps {

View file

@ -32,7 +32,7 @@ import { calculateRoomVia, makeRoomPermalink } from "../../../utils/permalinks/P
import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
import Spinner from "../elements/Spinner"; import Spinner from "../elements/Spinner";
import { mediaFromMxc } from "../../../customisations/Media"; import { mediaFromMxc } from "../../../customisations/Media";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import InfoDialog from "./InfoDialog"; import InfoDialog from "./InfoDialog";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";

View file

@ -25,7 +25,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { BetaPill } from "../beta/BetaCard"; import { BetaPill } from "../beta/BetaCard";
import Field from "../elements/Field"; import Field from "../elements/Field";
import RoomAliasField from "../elements/RoomAliasField"; import RoomAliasField from "../elements/RoomAliasField";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu"; import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu";
import { SubspaceSelector } from "./AddExistingToSpaceDialog"; import { SubspaceSelector } from "./AddExistingToSpaceDialog";
import JoinRuleDropdown from "../elements/JoinRuleDropdown"; import JoinRuleDropdown from "../elements/JoinRuleDropdown";

View file

@ -43,7 +43,7 @@ import QueryMatcher from "../../../autocomplete/QueryMatcher";
import TruncatedList from "../elements/TruncatedList"; import TruncatedList from "../elements/TruncatedList";
import EntityTile from "../rooms/EntityTile"; import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar"; import BaseAvatar from "../avatars/BaseAvatar";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
const AVATAR_SIZE = 30; const AVATAR_SIZE = 30;

View file

@ -71,7 +71,7 @@ import QuestionDialog from "./QuestionDialog";
import Spinner from "../elements/Spinner"; import Spinner from "../elements/Spinner";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton"; import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";

View file

@ -21,7 +21,7 @@ import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "../dialogs/BaseDialog"; import BaseDialog from "../dialogs/BaseDialog";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker"; import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";
interface IProps { interface IProps {

View file

@ -21,7 +21,7 @@ import { _t } from '../../../languageHandler';
import { IDialogProps } from "./IDialogProps"; import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import SearchBox from "../../structures/SearchBox"; import SearchBox from "../../structures/SearchBox";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
import RoomAvatar from "../avatars/RoomAvatar"; import RoomAvatar from "../avatars/RoomAvatar";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
@ -75,7 +75,7 @@ const ManageRestrictedJoinRuleDialog: React.FC<IProps> = ({ room, selected = [],
const [spacesContainingRoom, otherEntries] = useMemo(() => { const [spacesContainingRoom, otherEntries] = useMemo(() => {
const spaces = cli.getVisibleRooms().filter(r => r.getMyMembership() === "join" && r.isSpaceRoom()); const spaces = cli.getVisibleRooms().filter(r => r.getMyMembership() === "join" && r.isSpaceRoom());
return [ 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 => { selected.map(roomId => {
const room = cli.getRoom(roomId); const room = cli.getRoom(roomId);
if (!room) { if (!room) {

View file

@ -34,6 +34,7 @@ import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import { IDialogProps } from "./IDialogProps"; import { IDialogProps } from "./IDialogProps";
import SidebarUserSettingsTab from "../settings/tabs/user/SidebarUserSettingsTab";
export enum UserTab { export enum UserTab {
General = "USER_GENERAL_TAB", General = "USER_GENERAL_TAB",
@ -41,6 +42,7 @@ export enum UserTab {
Flair = "USER_FLAIR_TAB", Flair = "USER_FLAIR_TAB",
Notifications = "USER_NOTIFICATIONS_TAB", Notifications = "USER_NOTIFICATIONS_TAB",
Preferences = "USER_PREFERENCES_TAB", Preferences = "USER_PREFERENCES_TAB",
Sidebar = "USER_SIDEBAR_TAB",
Voice = "USER_VOICE_TAB", Voice = "USER_VOICE_TAB",
Security = "USER_SECURITY_TAB", Security = "USER_SECURITY_TAB",
Labs = "USER_LABS_TAB", Labs = "USER_LABS_TAB",
@ -117,6 +119,15 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
<PreferencesUserSettingsTab closeSettingsFn={this.props.onFinished} />, <PreferencesUserSettingsTab closeSettingsFn={this.props.onFinished} />,
)); ));
if (SettingsStore.getValue("feature_spaces_metaspaces")) {
tabs.push(new Tab(
UserTab.Sidebar,
_td("Sidebar"),
"mx_UserSettingsDialog_sidebarIcon",
<SidebarUserSettingsTab />,
));
}
if (SettingsStore.getValue(UIFeature.Voip)) { if (SettingsStore.getValue(UIFeature.Voip)) {
tabs.push(new Tab( tabs.push(new Tab(
UserTab.Voice, UserTab.Voice,

View file

@ -69,7 +69,7 @@ import RoomName from "../elements/RoomName";
import { mediaFromMxc } from "../../../customisations/Media"; import { mediaFromMxc } from "../../../customisations/Media";
import UIStore from "../../../stores/UIStore"; import UIStore from "../../../stores/UIStore";
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
import ConfirmSpaceUserActionDialog from "../dialogs/ConfirmSpaceUserActionDialog"; import ConfirmSpaceUserActionDialog from "../dialogs/ConfirmSpaceUserActionDialog";
import { bulkSpaceBehaviour } from "../../../utils/space"; import { bulkSpaceBehaviour } from "../../../utils/space";

View file

@ -43,7 +43,7 @@ import EntityTile from "./EntityTile";
import MemberTile from "./MemberTile"; import MemberTile from "./MemberTile";
import BaseAvatar from '../avatars/BaseAvatar'; import BaseAvatar from '../avatars/BaseAvatar';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature"; import { UIComponent } from "../../../settings/UIFeature";
import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { JoinRule } from "matrix-js-sdk/src/@types/partials";

View file

@ -31,7 +31,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload"; import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
import { showSpaceInvite } from "../../../utils/space"; import { showSpaceInvite } from "../../../utils/space";
import { privateShouldBeEncrypted } from "../../../createRoom"; import { privateShouldBeEncrypted } from "../../../createRoom";
import EventTileBubble from "../messages/EventTileBubble"; import EventTileBubble from "../messages/EventTileBubble";
@ -126,12 +126,12 @@ const NewRoomIntro = () => {
}); });
} }
let parentSpace; let parentSpace: Room;
if ( if (
SpaceStore.instance.activeSpace?.canInvite(cli.getUserId()) && SpaceStore.instance.activeSpaceRoom?.canInvite(cli.getUserId()) &&
SpaceStore.instance.getSpaceFilteredRoomIds(SpaceStore.instance.activeSpace).has(room.roomId) SpaceStore.instance.getSpaceFilteredRoomIds(SpaceStore.instance.activeSpace).has(room.roomId)
) { ) {
parentSpace = SpaceStore.instance.activeSpace; parentSpace = SpaceStore.instance.activeSpaceRoom;
} }
let buttons; let buttons;

View file

@ -21,7 +21,7 @@ import * as fbEmitter from "fbemitter";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t, _td } from "../../../languageHandler"; 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 ResizeNotifier from "../../../utils/ResizeNotifier";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import RoomViewStore from "../../../stores/RoomViewStore"; import RoomViewStore from "../../../stores/RoomViewStore";
@ -44,7 +44,8 @@ import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu"; import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; 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 { showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../../utils/space";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import RoomAvatar from "../avatars/RoomAvatar"; import RoomAvatar from "../avatars/RoomAvatar";
@ -52,6 +53,7 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature"; import { UIComponent } from "../../../settings/UIFeature";
import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps { interface IProps {
onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void; onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void;
@ -61,7 +63,7 @@ interface IProps {
onListCollapse?: (isExpanded: boolean) => void; onListCollapse?: (isExpanded: boolean) => void;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
isMinimized: boolean; isMinimized: boolean;
activeSpace: Room; activeSpace: SpaceKey;
} }
interface IState { interface IState {
@ -131,9 +133,10 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
defaultHidden: false, defaultHidden: false,
addRoomLabel: _td("Add room"), addRoomLabel: _td("Add room"),
addRoomContextMenu: (onFinished: () => void) => { addRoomContextMenu: (onFinished: () => void) => {
if (SpaceStore.instance.activeSpace) { if (SpaceStore.instance.activeSpaceRoom) {
const canAddRooms = SpaceStore.instance.activeSpace.currentState.maySendStateEvent(EventType.SpaceChild, const userId = MatrixClientPeg.get().getUserId();
MatrixClientPeg.get().getUserId()); const space = SpaceStore.instance.activeSpaceRoom;
const canAddRooms = space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
return <IconizedContextMenuOptionList first> return <IconizedContextMenuOptionList first>
{ {
@ -146,7 +149,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
onFinished(); onFinished();
showCreateNewRoom(SpaceStore.instance.activeSpace); showCreateNewRoom(space);
}} }}
disabled={!canAddRooms} disabled={!canAddRooms}
tooltip={canAddRooms ? undefined tooltip={canAddRooms ? undefined
@ -159,7 +162,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
onFinished(); onFinished();
showAddExistingRooms(SpaceStore.instance.activeSpace); showAddExistingRooms(space);
}} }}
disabled={!canAddRooms} disabled={!canAddRooms}
tooltip={canAddRooms ? undefined tooltip={canAddRooms ? undefined
@ -251,6 +254,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
private roomStoreToken: fbEmitter.EventSubscription; private roomStoreToken: fbEmitter.EventSubscription;
private treeRef = createRef<HTMLDivElement>(); private treeRef = createRef<HTMLDivElement>();
static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
@ -264,14 +270,14 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
public componentDidMount(): void { public componentDidMount(): void {
this.dispatcherRef = defaultDispatcher.register(this.onAction); this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); 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); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists); this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
this.updateLists(); // trigger the first update this.updateLists(); // trigger the first update
} }
public componentWillUnmount() { 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); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
defaultDispatcher.unregister(this.dispatcherRef); defaultDispatcher.unregister(this.dispatcherRef);
if (this.customTagStoreRef) this.customTagStoreRef.remove(); if (this.customTagStoreRef) this.customTagStoreRef.remove();
@ -379,7 +385,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
private onSpaceInviteClick = () => { private onSpaceInviteClick = () => {
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search; const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
showSpaceInvite(this.props.activeSpace, initialText); showSpaceInvite(this.context.getRoom(this.props.activeSpace), initialText);
}; };
private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] { private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {
@ -485,6 +491,15 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
: TAG_AESTHETICS[orderedTagId]; : TAG_AESTHETICS[orderedTagId];
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`); 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 // The cost of mounting/unmounting this component offsets the cost
// of keeping it in the DOM and hiding it when it is not required // of keeping it in the DOM and hiding it when it is not required
return <RoomSublist return <RoomSublist
@ -500,7 +515,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
showSkeleton={showSkeleton} showSkeleton={showSkeleton}
extraTiles={extraTiles} extraTiles={extraTiles}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)} alwaysVisible={alwaysVisible}
onListCollapse={this.props.onListCollapse} onListCollapse={this.props.onListCollapse}
/>; />;
}); });
@ -515,6 +530,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
public render() { public render() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const userId = cli.getUserId(); const userId = cli.getUserId();
const activeSpace = this.props.activeSpace[0] === "!" ? cli.getRoom(this.props.activeSpace) : null;
let explorePrompt: JSX.Element; let explorePrompt: JSX.Element;
if (!this.props.isMinimized) { if (!this.props.isMinimized) {
@ -533,17 +549,16 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
kind="link" kind="link"
onClick={this.onExplore} onClick={this.onExplore}
> >
{ this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") } { activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
</AccessibleButton> </AccessibleButton>
</div>; </div>;
} else if ( } else if (
this.props.activeSpace?.canInvite(userId) || activeSpace?.canInvite(userId) ||
this.props.activeSpace?.getMyMembership() === "join" || activeSpace?.getMyMembership() === "join" ||
this.props.activeSpace?.getJoinRule() === JoinRule.Public activeSpace?.getJoinRule() === JoinRule.Public
) { ) {
const spaceName = this.props.activeSpace.name; const spaceName = activeSpace.name;
const canInvite = this.props.activeSpace?.canInvite(userId) || const canInvite = activeSpace?.canInvite(userId) || activeSpace?.getJoinRule() === JoinRule.Public;
this.props.activeSpace?.getJoinRule() === JoinRule.Public;
explorePrompt = <div className="mx_RoomList_explorePrompt"> explorePrompt = <div className="mx_RoomList_explorePrompt">
<div>{ _t("Quick actions") }</div> <div>{ _t("Quick actions") }</div>
{ canInvite && <AccessibleTooltipButton { canInvite && <AccessibleTooltipButton
@ -553,7 +568,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
> >
{ _t("Invite people") } { _t("Invite people") }
</AccessibleTooltipButton> } </AccessibleTooltipButton> }
{ this.props.activeSpace?.getMyMembership() === "join" && <AccessibleTooltipButton { activeSpace?.getMyMembership() === "join" && <AccessibleTooltipButton
className="mx_RoomList_explorePrompt_spaceExplore" className="mx_RoomList_explorePrompt_spaceExplore"
onClick={this.onExplore} onClick={this.onExplore}
title={_t("Explore %(spaceName)s", { spaceName })} title={_t("Explore %(spaceName)s", { spaceName })}

View file

@ -19,7 +19,7 @@ import React, { useEffect, useState } from "react";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import { useEventEmitter } from "../../../hooks/useEventEmitter"; import { useEventEmitter } from "../../../hooks/useEventEmitter";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
interface IProps { interface IProps {
onVisibilityChange?: () => void; onVisibilityChange?: () => void;

View file

@ -27,7 +27,7 @@ import RoomName from "../elements/RoomName";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import ErrorDialog from '../dialogs/ErrorDialog'; import ErrorDialog from '../dialogs/ErrorDialog';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";

View file

@ -23,7 +23,7 @@ import StyledRadioGroup, { IDefinition } from "../elements/StyledRadioGroup";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import RoomAvatar from "../avatars/RoomAvatar"; import RoomAvatar from "../avatars/RoomAvatar";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog"; import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog";
@ -67,8 +67,8 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
const editRestrictedRoomIds = async (): Promise<string[] | undefined> => { const editRestrictedRoomIds = async (): Promise<string[] | undefined> => {
let selected = restrictedAllowRoomIds; let selected = restrictedAllowRoomIds;
if (!selected?.length && SpaceStore.instance.activeSpace) { if (!selected?.length && SpaceStore.instance.activeSpaceRoom) {
selected = [SpaceStore.instance.activeSpace.roomId]; selected = [SpaceStore.instance.activeSpaceRoom.roomId];
} }
const matrixClient = MatrixClientPeg.get(); const matrixClient = MatrixClientPeg.get();
@ -176,9 +176,9 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
{ moreText && <span>{ moreText }</span> } { moreText && <span>{ moreText }</span> }
</div> </div>
</div>; </div>;
} else if (SpaceStore.instance.activeSpace) { } else if (SpaceStore.instance.activeSpaceRoom) {
description = _t("Anyone in <spaceName/> can find and join. You can select other spaces too.", {}, { description = _t("Anyone in <spaceName/> can find and join. You can select other spaces too.", {}, {
spaceName: () => <b>{ SpaceStore.instance.activeSpace.name }</b>, spaceName: () => <b>{ SpaceStore.instance.activeSpaceRoom.name }</b>,
}); });
} else { } else {
description = _t("Anyone in a space can find and join. You can select multiple spaces."); description = _t("Anyone in a space can find and join. You can select multiple spaces.");

View file

@ -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<HTMLInputElement>) => {
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<Record<MetaSpace, boolean>>("Spaces.enabledMetaSpaces");
const allRoomsInHome = useSettingValue<boolean>("Spaces.allRoomsInHome");
return (
<div className="mx_SettingsTab mx_SidebarUserSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Sidebar") }</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Spaces") }</span>
<div className="mx_SettingsTab_subsectionText">{ _t("Spaces are ways to group rooms and people.") }</div>
<div className="mx_SidebarUserSettingsTab_subheading">{ _t("Spaces to show") }</div>
<div className="mx_SettingsTab_subsectionText">
{ _t("Along with the spaces you're in, you can use some pre-built ones too.") }
</div>
<StyledCheckbox
checked={!!homeEnabled}
onChange={onMetaSpaceChangeFactory(MetaSpace.Home)}
className="mx_SidebarUserSettingsTab_homeCheckbox"
>
{ _t("Home") }
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Home is useful for getting an overview of everything.") }
</div>
<StyledCheckbox
checked={allRoomsInHome}
disabled={!homeEnabled}
onChange={e => {
SettingsStore.setValue(
"Spaces.allRoomsInHome",
null,
SettingLevel.ACCOUNT,
e.target.checked,
);
}}
className="mx_SidebarUserSettingsTab_homeAllRoomsCheckbox"
>
{ _t("Show all rooms") }
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Show all your rooms in Home, even if they're in a space.") }
</div>
<StyledCheckbox
checked={!!favouritesEnabled}
onChange={onMetaSpaceChangeFactory(MetaSpace.Favourites)}
className="mx_SidebarUserSettingsTab_favouritesCheckbox"
>
{ _t("Favourites") }
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Automatically group all your favourite rooms and people together in one place.") }
</div>
<StyledCheckbox
checked={!!peopleEnabled}
onChange={onMetaSpaceChangeFactory(MetaSpace.People)}
className="mx_SidebarUserSettingsTab_peopleCheckbox"
>
{ _t("People") }
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Automatically group all your people together in one place.") }
</div>
<StyledCheckbox
checked={!!orphansEnabled}
onChange={onMetaSpaceChangeFactory(MetaSpace.Orphans)}
className="mx_SidebarUserSettingsTab_orphansCheckbox"
>
{ _t("Rooms outside of a space") }
</StyledCheckbox>
<div className="mx_SidebarUserSettingsTab_checkboxMicrocopy">
{ _t("Automatically group all your rooms that aren't part of a space in one place.") }
</div>
</div>
</div>
);
};
export default SidebarUserSettingsTab;

View file

@ -119,6 +119,7 @@ export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
rageshakeLabel: "spaces-feedback", rageshakeLabel: "spaces-feedback",
rageshakeData: Object.fromEntries([ rageshakeData: Object.fromEntries([
"Spaces.allRoomsInHome", "Spaces.allRoomsInHome",
"Spaces.enabledMetaSpaces",
].map(k => [k, SettingsStore.getValue(k)])), ].map(k => [k, SettingsStore.getValue(k)])),
}); });
}} }}

View file

@ -34,13 +34,15 @@ import SpaceCreateMenu from "./SpaceCreateMenu";
import { SpaceButton, SpaceItem } from "./SpaceTreeLevel"; import { SpaceButton, SpaceItem } from "./SpaceTreeLevel";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { useEventEmitterState } from "../../../hooks/useEventEmitter"; import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import SpaceStore, { import SpaceStore from "../../../stores/spaces/SpaceStore";
HOME_SPACE, import {
MetaSpace,
SpaceKey,
UPDATE_HOME_BEHAVIOUR, UPDATE_HOME_BEHAVIOUR,
UPDATE_INVITED_SPACES, UPDATE_INVITED_SPACES,
UPDATE_SELECTED_SPACE, UPDATE_SELECTED_SPACE,
UPDATE_TOP_LEVEL_SPACES, UPDATE_TOP_LEVEL_SPACES,
} from "../../../stores/SpaceStore"; } from "../../../stores/spaces";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
@ -53,17 +55,21 @@ import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel"; import { SettingLevel } from "../../../settings/SettingLevel";
import UIStore from "../../../stores/UIStore"; import UIStore from "../../../stores/UIStore";
const useSpaces = (): [Room[], Room[], Room | null] => { const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => {
const invites = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_INVITED_SPACES, () => { const invites = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_INVITED_SPACES, () => {
return SpaceStore.instance.invitedSpaces; return SpaceStore.instance.invitedSpaces;
}); });
const spaces = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, () => { const [metaSpaces, actualSpaces] = useEventEmitterState<[MetaSpace[], Room[]]>(
return SpaceStore.instance.spacePanelSpaces; SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES,
}); () => [
const activeSpace = useEventEmitterState<Room>(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => { SpaceStore.instance.enabledMetaSpaces,
SpaceStore.instance.spacePanelSpaces,
],
);
const activeSpace = useEventEmitterState<SpaceKey>(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
return SpaceStore.instance.activeSpace; return SpaceStore.instance.activeSpace;
}); });
return [invites, spaces, activeSpace]; return [invites, metaSpaces, actualSpaces, activeSpace];
}; };
interface IInnerSpacePanelProps { interface IInnerSpacePanelProps {
@ -99,37 +105,76 @@ const HomeButtonContextMenu = ({ onFinished, ...props }: ComponentProps<typeof S
</IconizedContextMenu>; </IconizedContextMenu>;
}; };
interface IHomeButtonProps { interface IMetaSpaceButtonProps extends ComponentProps<typeof SpaceButton> {
selected: boolean; selected: boolean;
isPanelCollapsed: boolean; isPanelCollapsed: boolean;
} }
const HomeButton = ({ selected, isPanelCollapsed }: IHomeButtonProps) => { type MetaSpaceButtonProps = Pick<IMetaSpaceButtonProps, "selected" | "isPanelCollapsed">;
const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
return SpaceStore.instance.allRoomsInHome;
});
const MetaSpaceButton = ({ selected, isPanelCollapsed, ...props }: IMetaSpaceButtonProps) => {
return <li return <li
className={classNames("mx_SpaceItem", { className={classNames("mx_SpaceItem", {
"collapsed": isPanelCollapsed, "collapsed": isPanelCollapsed,
})} })}
role="treeitem" role="treeitem"
> >
<SpaceButton <SpaceButton {...props} selected={selected} isNarrow={isPanelCollapsed} />
className="mx_SpaceButton_home"
onClick={() => 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")}
/>
</li>; </li>;
}; };
const HomeButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) => {
const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
return SpaceStore.instance.allRoomsInHome;
});
return <MetaSpaceButton
spaceKey={MetaSpace.Home}
className="mx_SpaceButton_home"
selected={selected}
isPanelCollapsed={isPanelCollapsed}
label={allRoomsInHome ? _t("All rooms") : _t("Home")}
notificationState={allRoomsInHome
? RoomNotificationStateStore.instance.globalState
: SpaceStore.instance.getNotificationState(MetaSpace.Home)}
ContextMenuComponent={HomeButtonContextMenu}
contextMenuTooltip={_t("Options")}
/>;
};
const FavouritesButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) => {
return <MetaSpaceButton
spaceKey={MetaSpace.Favourites}
className="mx_SpaceButton_favourites"
selected={selected}
isPanelCollapsed={isPanelCollapsed}
label={_t("Favourites")}
notificationState={SpaceStore.instance.getNotificationState(MetaSpace.Favourites)}
/>;
};
const PeopleButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) => {
return <MetaSpaceButton
spaceKey={MetaSpace.People}
className="mx_SpaceButton_people"
selected={selected}
isPanelCollapsed={isPanelCollapsed}
label={_t("People")}
notificationState={SpaceStore.instance.getNotificationState(MetaSpace.People)}
/>;
};
const OrphansButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) => {
return <MetaSpaceButton
spaceKey={MetaSpace.Orphans}
className="mx_SpaceButton_orphans"
selected={selected}
isPanelCollapsed={isPanelCollapsed}
label={_t("Other rooms")}
notificationState={SpaceStore.instance.getNotificationState(MetaSpace.Orphans)}
/>;
};
const CreateSpaceButton = ({ const CreateSpaceButton = ({
isPanelCollapsed, isPanelCollapsed,
setPanelCollapsed, setPanelCollapsed,
@ -181,13 +226,25 @@ const CreateSpaceButton = ({
</li>; </li>;
}; };
const metaSpaceComponentMap: Record<MetaSpace, typeof HomeButton> = {
[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 // Optimisation based on https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md#recommended-droppable--performance-optimisation
const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCollapsed, setPanelCollapsed }) => { const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCollapsed, setPanelCollapsed }) => {
const [invites, spaces, activeSpace] = useSpaces(); const [invites, metaSpaces, actualSpaces, activeSpace] = useSpaces();
const activeSpaces = activeSpace ? [activeSpace] : []; const activeSpaces = activeSpace ? [activeSpace] : [];
const metaSpacesSection = metaSpaces.map(key => {
const Component = metaSpaceComponentMap[key];
return <Component key={key} selected={activeSpace === key} isPanelCollapsed={isPanelCollapsed} />;
});
return <div className="mx_SpaceTreeLevel"> return <div className="mx_SpaceTreeLevel">
<HomeButton selected={!activeSpace} isPanelCollapsed={isPanelCollapsed} /> { metaSpacesSection }
{ invites.map(s => ( { invites.map(s => (
<SpaceItem <SpaceItem
key={s.roomId} key={s.roomId}
@ -197,7 +254,7 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCo
onExpand={() => setPanelCollapsed(false)} onExpand={() => setPanelCollapsed(false)}
/> />
)) } )) }
{ spaces.map((s, i) => ( { actualSpaces.map((s, i) => (
<Draggable key={s.roomId} draggableId={s.roomId} index={i}> <Draggable key={s.roomId} draggableId={s.roomId} index={i}>
{ (provided, snapshot) => ( { (provided, snapshot) => (
<SpaceItem <SpaceItem

View file

@ -16,7 +16,6 @@ limitations under the License.
import React, { import React, {
createRef, createRef,
MouseEvent,
InputHTMLAttributes, InputHTMLAttributes,
LegacyRef, LegacyRef,
ComponentProps, ComponentProps,
@ -26,14 +25,15 @@ import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import RoomAvatar from "../avatars/RoomAvatar"; import RoomAvatar from "../avatars/RoomAvatar";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore"; import { SpaceKey } from "../../../stores/spaces";
import SpaceTreeLevelLayoutStore from "../../../stores/spaces/SpaceTreeLevelLayoutStore";
import NotificationBadge from "../rooms/NotificationBadge"; import NotificationBadge from "../rooms/NotificationBadge";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import { toRightOf, useContextMenu } from "../../structures/ContextMenu"; import { toRightOf, useContextMenu } from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import { NotificationColor } from "../../../stores/notifications/NotificationColor"; import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager"; import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
@ -43,8 +43,9 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; import { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
interface IButtonProps extends Omit<ComponentProps<typeof AccessibleTooltipButton>, "title"> { interface IButtonProps extends Omit<ComponentProps<typeof AccessibleTooltipButton>, "title" | "onClick"> {
space?: Room; space?: Room;
spaceKey?: SpaceKey;
className?: string; className?: string;
selected?: boolean; selected?: boolean;
label: string; label: string;
@ -53,14 +54,14 @@ interface IButtonProps extends Omit<ComponentProps<typeof AccessibleTooltipButto
isNarrow?: boolean; isNarrow?: boolean;
avatarSize?: number; avatarSize?: number;
ContextMenuComponent?: ComponentType<ComponentProps<typeof SpaceContextMenu>>; ContextMenuComponent?: ComponentType<ComponentProps<typeof SpaceContextMenu>>;
onClick(ev: MouseEvent): void; onClick?(ev?: ButtonEvent): void;
} }
export const SpaceButton: React.FC<IButtonProps> = ({ export const SpaceButton: React.FC<IButtonProps> = ({
space, space,
spaceKey,
className, className,
selected, selected,
onClick,
label, label,
contextMenuTooltip, contextMenuTooltip,
notificationState, notificationState,
@ -88,7 +89,7 @@ export const SpaceButton: React.FC<IButtonProps> = ({
notifBadge = <div className="mx_SpacePanel_badgeContainer"> notifBadge = <div className="mx_SpacePanel_badgeContainer">
<NotificationBadge <NotificationBadge
onClick={() => SpaceStore.instance.setActiveRoomInSpace(space || null)} onClick={() => SpaceStore.instance.setActiveRoomInSpace(spaceKey ?? space.roomId)}
forceCount={false} forceCount={false}
notification={notificationState} notification={notificationState}
aria-label={ariaLabel} aria-label={ariaLabel}
@ -116,7 +117,7 @@ export const SpaceButton: React.FC<IButtonProps> = ({
mx_SpaceButton_narrow: isNarrow, mx_SpaceButton_narrow: isNarrow,
})} })}
title={label} title={label}
onClick={onClick} onClick={spaceKey ? () => SpaceStore.instance.setActiveSpace(spaceKey) : props.onClick}
onContextMenu={openMenu} onContextMenu={openMenu}
forceHide={!isNarrow || menuDisplayed} forceHide={!isNarrow || menuDisplayed}
inputRef={handle} inputRef={handle}
@ -146,7 +147,7 @@ export const SpaceButton: React.FC<IButtonProps> = ({
interface IItemProps extends InputHTMLAttributes<HTMLLIElement> { interface IItemProps extends InputHTMLAttributes<HTMLLIElement> {
space?: Room; space?: Room;
activeSpaces: Room[]; activeSpaces: SpaceKey[];
isNested?: boolean; isNested?: boolean;
isPanelCollapsed?: boolean; isPanelCollapsed?: boolean;
onExpand?: Function; onExpand?: Function;
@ -258,7 +259,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
private onClick = (ev: React.MouseEvent) => { private onClick = (ev: React.MouseEvent) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
SpaceStore.instance.setActiveSpace(this.props.space); SpaceStore.instance.setActiveSpace(this.props.space.roomId);
}; };
render() { render() {
@ -316,7 +317,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
{...restDragHandleProps} {...restDragHandleProps}
space={space} space={space}
className={isInvite ? "mx_SpaceButton_invite" : undefined} className={isInvite ? "mx_SpaceButton_invite" : undefined}
selected={activeSpaces.includes(space)} selected={activeSpaces.includes(space.roomId)}
label={space.name} label={space.name}
contextMenuTooltip={_t("Space options")} contextMenuTooltip={_t("Space options")}
notificationState={notificationState} notificationState={notificationState}
@ -337,7 +338,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
interface ITreeLevelProps { interface ITreeLevelProps {
spaces: Room[]; spaces: Room[];
activeSpaces: Room[]; activeSpaces: SpaceKey[];
isNested?: boolean; isNested?: boolean;
parents: Set<string>; parents: Set<string>;
} }

View file

@ -40,7 +40,7 @@ import GroupStore from "./stores/GroupStore";
import CountlyAnalytics from "./CountlyAnalytics"; import CountlyAnalytics from "./CountlyAnalytics";
import { isJoinedOrNearlyJoined } from "./utils/membership"; import { isJoinedOrNearlyJoined } from "./utils/membership";
import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler"; import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
import SpaceStore from "./stores/SpaceStore"; import SpaceStore from "./stores/spaces/SpaceStore";
import { makeSpaceParentEvent } from "./utils/space"; import { makeSpaceParentEvent } from "./utils/space";
import { Action } from "./dispatcher/actions"; import { Action } from "./dispatcher/actions";
import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import ErrorDialog from "./components/views/dialogs/ErrorDialog";

View file

@ -830,6 +830,7 @@
"Polls (under active development)": "Polls (under active development)", "Polls (under active development)": "Polls (under active development)",
"Show info about bridges in room settings": "Show info about bridges in room settings", "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)", "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", "Don't send read receipts": "Don't send read receipts",
"Font size": "Font size", "Font size": "Font size",
"Use custom size": "Use custom size", "Use custom size": "Use custom size",
@ -1064,6 +1065,9 @@
"Show all rooms": "Show all rooms", "Show all rooms": "Show all rooms",
"All rooms": "All rooms", "All rooms": "All rooms",
"Options": "Options", "Options": "Options",
"Favourites": "Favourites",
"People": "People",
"Other rooms": "Other rooms",
"Spaces": "Spaces", "Spaces": "Spaces",
"Expand space panel": "Expand space panel", "Expand space panel": "Expand space panel",
"Collapse space panel": "Collapse 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.", "Learn more about how we use analytics.": "Learn more about how we use analytics.",
"Where you're signed in": "Where you're signed in", "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.", "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", "Default Device": "Default Device",
"No media permissions": "No media permissions", "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", "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", "Show Widgets": "Show Widgets",
"Search": "Search", "Search": "Search",
"Invites": "Invites", "Invites": "Invites",
"Favourites": "Favourites",
"People": "People",
"Start chat": "Start chat", "Start chat": "Start chat",
"Rooms": "Rooms", "Rooms": "Rooms",
"Add room": "Add room", "Add room": "Add room",

View file

@ -42,6 +42,7 @@ import ReducedMotionController from './controllers/ReducedMotionController';
import IncompatibleController from "./controllers/IncompatibleController"; import IncompatibleController from "./controllers/IncompatibleController";
import PseudonymousAnalyticsController from './controllers/PseudonymousAnalyticsController'; import PseudonymousAnalyticsController from './controllers/PseudonymousAnalyticsController';
import NewLayoutSwitcherController from './controllers/NewLayoutSwitcherController'; 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 // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
const LEVELS_ROOM_SETTINGS = [ const LEVELS_ROOM_SETTINGS = [
@ -283,6 +284,16 @@ export const SETTINGS: {[setting: string]: ISetting} = {
default: false, default: false,
controller: new NewLayoutSwitcherController(), 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": { "RoomList.backgroundImage": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS, supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: null, default: null,
@ -755,6 +766,15 @@ export const SETTINGS: {[setting: string]: ISetting} = {
default: false, default: false,
controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", null), 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": { "showCommunitiesInsteadOfSpaces": {
displayName: _td("Display Communities instead of Spaces"), displayName: _td("Display Communities instead of Spaces"),
description: _td("Temporarily show communities instead of Spaces for this session. " + description: _td("Temporarily show communities instead of Spaces for this session. " +

View file

@ -22,7 +22,7 @@ import defaultDispatcher from "../dispatcher/dispatcher";
import { arrayHasDiff } from "../utils/arrays"; import { arrayHasDiff } from "../utils/arrays";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { SettingLevel } from "../settings/SettingLevel"; import { SettingLevel } from "../settings/SettingLevel";
import SpaceStore from "./SpaceStore"; import SpaceStore from "./spaces/SpaceStore";
import { Action } from "../dispatcher/actions"; import { Action } from "../dispatcher/actions";
import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload"; import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";

View file

@ -35,7 +35,7 @@ import { NameFilterCondition } from "./filters/NameFilterCondition";
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
import { VisibilityProvider } from "./filters/VisibilityProvider"; import { VisibilityProvider } from "./filters/VisibilityProvider";
import { SpaceWatcher } from "./SpaceWatcher"; import { SpaceWatcher } from "./SpaceWatcher";
import SpaceStore from "../SpaceStore"; import SpaceStore from "../spaces/SpaceStore";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload"; import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";

View file

@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomListStoreClass } from "./RoomListStore"; import { RoomListStoreClass } from "./RoomListStore";
import { SpaceFilterCondition } from "./filters/SpaceFilterCondition"; 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 * 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 { export class SpaceWatcher {
private readonly filter = new SpaceFilterCondition(); private readonly filter = new SpaceFilterCondition();
// we track these separately to the SpaceStore as we need to observe transitions // 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; private allRoomsInHome: boolean = SpaceStore.instance.allRoomsInHome;
constructor(private store: RoomListStoreClass) { constructor(private store: RoomListStoreClass) {
if (!this.allRoomsInHome || this.activeSpace) { if (SpaceWatcher.needsFilter(this.activeSpace, this.allRoomsInHome)) {
this.updateFilter(); this.updateFilter();
store.addFilter(this.filter); store.addFilter(this.filter);
} }
@ -38,21 +37,26 @@ export class SpaceWatcher {
SpaceStore.instance.on(UPDATE_HOME_BEHAVIOUR, this.onHomeBehaviourUpdated); 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 if (activeSpace === this.activeSpace && allRoomsInHome === this.allRoomsInHome) return; // nop
const oldActiveSpace = this.activeSpace; const neededFilter = SpaceWatcher.needsFilter(this.activeSpace, this.allRoomsInHome);
const oldAllRoomsInHome = this.allRoomsInHome; const needsFilter = SpaceWatcher.needsFilter(activeSpace, allRoomsInHome);
this.activeSpace = activeSpace; this.activeSpace = activeSpace;
this.allRoomsInHome = allRoomsInHome; this.allRoomsInHome = allRoomsInHome;
if (activeSpace || !allRoomsInHome) { if (needsFilter) {
this.updateFilter(); this.updateFilter();
} }
if (oldAllRoomsInHome && !oldActiveSpace) { if (!neededFilter && needsFilter) {
this.store.addFilter(this.filter); this.store.addFilter(this.filter);
} else if (allRoomsInHome && !activeSpace) { } else if (neededFilter && !needsFilter) {
this.store.removeFilter(this.filter); this.store.removeFilter(this.filter);
} }
}; };
@ -62,8 +66,8 @@ export class SpaceWatcher {
}; };
private updateFilter = () => { private updateFilter = () => {
if (this.activeSpace) { if (this.activeSpace[0] === "!") {
SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => { SpaceStore.instance.traverseSpace(this.activeSpace, roomId => {
this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded(); this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
}); });
} }

View file

@ -34,7 +34,7 @@ import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } f
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm"; import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
import { getListAlgorithmInstance } from "./list-ordering"; import { getListAlgorithmInstance } from "./list-ordering";
import { VisibilityProvider } from "../filters/VisibilityProvider"; import { VisibilityProvider } from "../filters/VisibilityProvider";
import SpaceStore from "../../SpaceStore"; import SpaceStore from "../../spaces/SpaceStore";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";

View file

@ -19,7 +19,8 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition"; import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
import { IDestroyable } from "../../../utils/IDestroyable"; 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"; import { setHasDiff } from "../../../utils/sets";
/** /**
@ -30,7 +31,7 @@ import { setHasDiff } from "../../../utils/sets";
*/ */
export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable { export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable {
private roomIds = new Set<string>(); private roomIds = new Set<string>();
private space: Room = null; private space: SpaceKey = MetaSpace.Home;
public get kind(): FilterKind { public get kind(): FilterKind {
return FilterKind.Prefilter; 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: SpaceKey) {
SpaceStore.instance.off(this.space, this.onStoreUpdate);
public updateSpace(space: Room) { SpaceStore.instance.on(this.space = space, this.onStoreUpdate);
SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate);
this.onStoreUpdate(); // initial update from the change to the space this.onStoreUpdate(); // initial update from the change to the space
} }
public destroy(): void { public destroy(): void {
SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate); SpaceStore.instance.off(this.space, this.onStoreUpdate);
} }
} }

View file

@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import CallHandler from "../../../CallHandler"; import CallHandler from "../../../CallHandler";
import { RoomListCustomisations } from "../../../customisations/RoomList"; import { RoomListCustomisations } from "../../../customisations/RoomList";
import VoipUserMapper from "../../../VoipUserMapper"; import VoipUserMapper from "../../../VoipUserMapper";
import SpaceStore from "../../SpaceStore"; import SpaceStore from "../../spaces/SpaceStore";
export class VisibilityProvider { export class VisibilityProvider {
private static internalInstance: VisibilityProvider; private static internalInstance: VisibilityProvider;

View file

@ -18,56 +18,51 @@ import { ListIteratee, Many, sortBy, throttle } from "lodash";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; 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 { 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"; 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 {} interface IState {}
const ACTIVE_SPACE_LS_KEY = "mx_active_space"; const ACTIVE_SPACE_LS_KEY = "mx_active_space";
export const HOME_SPACE = Symbol("home-space"); const metaSpaceOrder: MetaSpace[] = [MetaSpace.Home, MetaSpace.Favourites, MetaSpace.People, MetaSpace.Orphans];
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 MAX_SUGGESTED_ROOMS = 20; const MAX_SUGGESTED_ROOMS = 20;
// This setting causes the page to reload and can be costly if read frequently, so read it here only // 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 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] const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
return arr.reduce((result, room: Room) => { return arr.reduce((result, room: Room) => {
@ -105,30 +100,41 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
private notificationStateMap = new Map<SpaceKey, SpaceNotificationState>(); private notificationStateMap = new Map<SpaceKey, SpaceNotificationState>();
// Map from space key to Set of room IDs that should be shown as part of that space's filter // Map from space key to Set of room IDs that should be shown as part of that space's filter
private spaceFilteredRooms = new Map<SpaceKey, Set<string>>(); private spaceFilteredRooms = new Map<SpaceKey, Set<string>>();
// The space currently selected in the Space Panel - if null then Home is selected // The space currently selected in the Space Panel
private _activeSpace?: Room = null; private _activeSpace?: SpaceKey = MetaSpace.Home; // set properly by onReady
private _suggestedRooms: ISuggestedRoom[] = []; private _suggestedRooms: ISuggestedRoom[] = [];
private _invitedSpaces = new Set<Room>(); private _invitedSpaces = new Set<Room>();
private spaceOrderLocalEchoMap = new Map<string, string>(); private spaceOrderLocalEchoMap = new Map<string, string>();
private _restrictedJoinRuleSupport?: IRoomCapability; private _restrictedJoinRuleSupport?: IRoomCapability;
private _allRoomsInHome: boolean = SettingsStore.getValue("Spaces.allRoomsInHome"); private _allRoomsInHome: boolean = SettingsStore.getValue("Spaces.allRoomsInHome");
private _enabledMetaSpaces: MetaSpace[] = []; // set by onReady
constructor() { constructor() {
super(defaultDispatcher, {}); super(defaultDispatcher, {});
SettingsStore.monitorSetting("Spaces.allRoomsInHome", null); SettingsStore.monitorSetting("Spaces.allRoomsInHome", null);
SettingsStore.monitorSetting("Spaces.enabledMetaSpaces", null);
} }
public get invitedSpaces(): Room[] { public get invitedSpaces(): Room[] {
return Array.from(this._invitedSpaces); return Array.from(this._invitedSpaces);
} }
public get enabledMetaSpaces(): MetaSpace[] {
return this._enabledMetaSpaces;
}
public get spacePanelSpaces(): Room[] { public get spacePanelSpaces(): Room[] {
return this.rootSpaces; return this.rootSpaces;
} }
public get activeSpace(): Room | null { public get activeSpace(): SpaceKey {
return this._activeSpace || null; return this._activeSpace;
}
public get activeSpaceRoom(): Room | null {
if (this._activeSpace[0] !== "!") return null;
return this.matrixClient?.getRoom(this._activeSpace);
} }
public get suggestedRooms(): ISuggestedRoom[] { public get suggestedRooms(): ISuggestedRoom[] {
@ -139,12 +145,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
return this._allRoomsInHome; return this._allRoomsInHome;
} }
public setActiveRoomInSpace(space: Room | null): void { public setActiveRoomInSpace(space: SpaceKey): void {
if (space && !space.isSpaceRoom()) return; if (space[0] === "!" && !this.matrixClient?.getRoom(space)?.isSpaceRoom()) return;
if (space !== this.activeSpace) this.setActiveSpace(space); if (space !== this.activeSpace) this.setActiveSpace(space);
if (space) { if (space) {
const roomId = this.getNotificationState(space.roomId).getFirstRoomWithNotifications(); const roomId = this.getNotificationState(space).getFirstRoomWithNotifications();
defaultDispatcher.dispatch({ defaultDispatcher.dispatch({
action: "view_room", action: "view_room",
room_id: roomId, room_id: roomId,
@ -184,12 +190,20 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
* @param contextSwitch whether to switch the user's context, * @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. * 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) { public setActiveSpace(space: SpaceKey, contextSwitch = true) {
if (!this.matrixClient || space === this.activeSpace || (space && !space.isSpaceRoom())) return; 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._activeSpace = space;
this.emit(UPDATE_SELECTED_SPACE, this.activeSpace); this.emit(UPDATE_SELECTED_SPACE, this.activeSpace);
this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []); this.emit(UPDATE_SUGGESTED_ROOMS, this._suggestedRooms = []);
if (contextSwitch) { if (contextSwitch) {
// view last selected room from space // view last selected room from space
@ -198,7 +212,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
// if the space being selected is an invite then always view that invite // 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 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 // 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.matrixClient.getRoom(roomId)?.getMyMembership() === "join" &&
this.getSpaceFilteredRoomIds(space).has(roomId) this.getSpaceFilteredRoomIds(space).has(roomId)
) { ) {
@ -207,10 +221,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
room_id: roomId, room_id: roomId,
context_switch: true, context_switch: true,
}); });
} else if (space) { } else if (cliSpace) {
defaultDispatcher.dispatch({ defaultDispatcher.dispatch({
action: "view_room", action: "view_room",
room_id: space.roomId, room_id: space,
context_switch: true, context_switch: true,
}); });
} else { } else {
@ -221,22 +235,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
} }
// persist space selected // persist space selected
if (space) { window.localStorage.setItem(ACTIVE_SPACE_LS_KEY, space);
window.localStorage.setItem(ACTIVE_SPACE_LS_KEY, space.roomId);
} else {
window.localStorage.removeItem(ACTIVE_SPACE_LS_KEY);
}
if (space) { if (cliSpace) {
this.loadSuggestedRooms(space); this.loadSuggestedRooms(cliSpace);
} }
} }
private async loadSuggestedRooms(space: Room): Promise<void> { private async loadSuggestedRooms(space: Room): Promise<void> {
const suggestedRooms = await this.fetchSuggestedRooms(space); const suggestedRooms = await this.fetchSuggestedRooms(space);
if (this._activeSpace === space) { if (this._activeSpace === space.roomId) {
this._suggestedRooms = suggestedRooms; 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<IState> {
return this.parentMap.get(roomId) || new Set(); return this.parentMap.get(roomId) || new Set();
} }
public getSpaceFilteredRoomIds = (space: Room | null): Set<string> => { public getSpaceFilteredRoomIds = (space: SpaceKey): Set<string> => {
if (!space && this.allRoomsInHome) { if (space === MetaSpace.Home && this.allRoomsInHome) {
return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId)); 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(() => { private rebuild = throttle(() => {
@ -420,12 +430,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.parentMap = backrefs; this.parentMap = backrefs;
// if the currently selected space no longer exists, remove its selection // if the currently selected space no longer exists, remove its selection
if (this._activeSpace && detachedNodes.has(this._activeSpace)) { if (this._activeSpace[0] === "!" && detachedNodes.has(this.matrixClient.getRoom(this._activeSpace))) {
this.setActiveSpace(null, false); this.goToFirstSpace();
} }
this.onRoomsUpdate(); // TODO only do this if a change has happened 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 // 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)); this._invitedSpaces = new Set(this.sortRootSpaces(invitedSpaces));
@ -440,19 +450,22 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
if (this.allRoomsInHome) return true; if (this.allRoomsInHome) return true;
if (room.isSpaceRoom()) return false; if (room.isSpaceRoom()) return false;
return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space 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 || DMRoomMap.shared().getUserIdForRoomId(room.roomId); // put all DMs in the Home Space
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite); // show all favourites
}; };
// Update a given room due to its tag changing (e.g DM-ness or Fav-ness) // 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 // This can only change whether it shows up in the HOME_SPACE or not
private onRoomUpdate = (room: Room) => { private onRoomUpdate = (room: Room) => {
if (this.showInHomeSpace(room)) { const enabledMetaSpaces = new Set(this.enabledMetaSpaces);
this.spaceFilteredRooms.get(HOME_SPACE)?.add(room.roomId); // TODO more metaspace stuffs
this.emit(HOME_SPACE); if (enabledMetaSpaces.has(MetaSpace.Home)) {
} else if (!this.orphanedRooms.has(room.roomId)) { if (this.showInHomeSpace(room)) {
this.spaceFilteredRooms.get(HOME_SPACE)?.delete(room.roomId); this.spaceFilteredRooms.get(MetaSpace.Home)?.add(room.roomId);
this.emit(HOME_SPACE); 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<IState> {
const oldFilteredRooms = this.spaceFilteredRooms; const oldFilteredRooms = this.spaceFilteredRooms;
this.spaceFilteredRooms = new Map(); 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 // put all room invites in the Home Space
const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite"); const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite");
this.spaceFilteredRooms.set(HOME_SPACE, new Set<string>(invites.map(room => room.roomId))); this.spaceFilteredRooms.set(MetaSpace.Home, new Set(invites.map(r => r.roomId)));
visibleRooms.forEach(room => { visibleRooms.forEach(room => {
if (this.showInHomeSpace(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<string, Set<string>>(); const hiddenChildren = new EnhancedMap<string, Set<string>>();
visibleRooms.forEach(room => { visibleRooms.forEach(room => {
if (room.getMyMembership() !== "join") return; if (room.getMyMembership() !== "join") return;
@ -540,15 +576,23 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.emit(k); 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) => { 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 // Update NotificationStates
this.getNotificationState(s).setRooms(visibleRooms.filter(room => { this.getNotificationState(s).setRooms(visibleRooms.filter(room => {
if (!roomIds.has(room.roomId) || room.isSpaceRoom()) return false; if (!roomIds.has(room.roomId) || room.isSpaceRoom()) return false;
if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { if (dmBadgeSpace && DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
return s === HOME_SPACE; return s === dmBadgeSpace;
} }
return true; return true;
@ -575,7 +619,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
} }
// don't trigger a context switch when we are switching a space to match the chosen room // 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) => { private onRoom = (room: Room, newMembership?: string, oldMembership?: string) => {
@ -597,7 +641,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
const numSuggestedRooms = this._suggestedRooms.length; const numSuggestedRooms = this._suggestedRooms.length;
this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId); this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId);
if (numSuggestedRooms !== this._suggestedRooms.length) { 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 // if the room currently being viewed was just joined then switch to its related space
@ -622,10 +666,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
if (membership === "join" && room.roomId === RoomViewStore.getRoomId()) { if (membership === "join" && room.roomId === RoomViewStore.getRoomId()) {
// if the user was looking at the space and then joined: select that space // if the user was looking at the space and then joined: select that space
this.setActiveSpace(room, false); this.setActiveSpace(room.roomId, false);
} else if (membership === "leave" && room.roomId === this.activeSpace?.roomId) { } else if (membership === "leave" && room.roomId === this.activeSpace) {
// user's active space has gone away, go back to home // 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<IState> {
const rootSpaces = this.sortRootSpaces(this.rootSpaces); const rootSpaces = this.sortRootSpaces(this.rootSpaces);
if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) { if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) {
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<IState> {
this.emit(room.roomId); 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 this.matrixClient.getRoom(ev.getStateKey())?.getMyMembership() !== "join" && // target not joined
ev.getPrevContent().suggested !== ev.getContent().suggested // suggested flag changed ev.getPrevContent().suggested !== ev.getContent().suggested // suggested flag changed
) { ) {
@ -694,7 +738,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
if (order !== lastOrder) { if (order !== lastOrder) {
this.notifyIfOrderChanged(); 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 // 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 oldTags = lastEv?.getContent()?.tags || {};
const newTags = ev.getContent()?.tags || {}; const newTags = ev.getContent()?.tags || {};
@ -728,9 +772,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.parentMap = new EnhancedMap(); this.parentMap = new EnhancedMap();
this.notificationStateMap = new Map(); this.notificationStateMap = new Map();
this.spaceFilteredRooms = new Map(); this.spaceFilteredRooms = new Map();
this._activeSpace = null; this._activeSpace = MetaSpace.Home; // set properly by onReady
this._suggestedRooms = []; this._suggestedRooms = [];
this._invitedSpaces = new Set(); this._invitedSpaces = new Set();
this._enabledMetaSpaces = [];
} }
protected async onNotReady() { protected async onNotReady() {
@ -760,16 +805,27 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
?.["m.room_versions"]?.["org.matrix.msc3244.room_capabilities"]?.["restricted"]; ?.["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 await this.onSpaceUpdate(); // trigger an initial update
// restore selected state from last session if any and still valid // restore selected state from last session if any and still valid
const lastSpaceId = window.localStorage.getItem(ACTIVE_SPACE_LS_KEY); 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 // 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) { protected async onAction(payload: ActionPayload) {
if (!spacesEnabled) return; if (!spacesEnabled) return;
switch (payload.action) { switch (payload.action) {
@ -783,9 +839,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
if (room?.isSpaceRoom()) { if (room?.isSpaceRoom()) {
// Don't context switch when navigating to the space room // Don't context switch when navigating to the space room
// as it will cause you to end up in the wrong room // as it will cause you to end up in the wrong room
this.setActiveSpace(room, false); this.setActiveSpace(room.roomId, false);
} else if ( } else if (
(!this.allRoomsInHome || this.activeSpace) && (!this.allRoomsInHome || this.activeSpace[0] === "!") &&
!this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId) !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)
) { ) {
this.switchToRelatedSpace(roomId); this.switchToRelatedSpace(roomId);
@ -799,31 +855,54 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
} }
case "after_leave_room": case "after_leave_room":
if (this._activeSpace && payload.room_id === this._activeSpace.roomId) { if (this._activeSpace[0] === "!" && payload.room_id === this._activeSpace) {
this.setActiveSpace(null, false); // User has left the current space, go to first space
this.goToFirstSpace();
} }
break; break;
case Action.SwitchSpace: case Action.SwitchSpace: {
// 1 is Home, 2-9 are the spaces after Home // Metaspaces start at 1, Spaces follow
if (payload.num === 1) { if (payload.num < 1 || payload.num > 9) break;
this.setActiveSpace(null); const numMetaSpaces = this.enabledMetaSpaces.length;
} else if (payload.num > 0 && this.spacePanelSpaces.length > payload.num - 2) { if (payload.num <= numMetaSpaces) {
this.setActiveSpace(this.spacePanelSpaces[payload.num - 2]); 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; break;
}
case Action.SettingUpdated: { case Action.SettingUpdated: {
const settingUpdatedPayload = payload as SettingUpdatedPayload; const settingUpdatedPayload = payload as SettingUpdatedPayload;
if (settingUpdatedPayload.settingName === "Spaces.allRoomsInHome") { switch (settingUpdatedPayload.settingName) {
const newValue = SettingsStore.getValue("Spaces.allRoomsInHome"); case "Spaces.allRoomsInHome": {
if (this.allRoomsInHome !== newValue) { const newValue = SettingsStore.getValue("Spaces.allRoomsInHome");
this._allRoomsInHome = newValue; if (this.allRoomsInHome !== newValue) {
this.emit(UPDATE_HOME_BEHAVIOUR, this.allRoomsInHome); this._allRoomsInHome = newValue;
this.rebuild(); // rebuild everything 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;
} }
} }
} }

View file

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

View file

@ -21,7 +21,7 @@ import { inviteUsersToRoom } from "../RoomInvite";
import Modal, { IHandle } from "../Modal"; import Modal, { IHandle } from "../Modal";
import { _t } from "../languageHandler"; import { _t } from "../languageHandler";
import ErrorDialog from "../components/views/dialogs/ErrorDialog"; 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 Spinner from "../components/views/elements/Spinner";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";

View file

@ -18,13 +18,16 @@ import { EventEmitter } from "events";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; 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 "../skinned-sdk"; // Must be first for skinning to work
import SpaceStore, { import SpaceStore from "../../src/stores/spaces/SpaceStore";
import {
MetaSpace,
UPDATE_HOME_BEHAVIOUR, UPDATE_HOME_BEHAVIOUR,
UPDATE_INVITED_SPACES, UPDATE_INVITED_SPACES,
UPDATE_SELECTED_SPACE, UPDATE_SELECTED_SPACE,
UPDATE_TOP_LEVEL_SPACES, UPDATE_TOP_LEVEL_SPACES,
} from "../../src/stores/SpaceStore"; } from "../../src/stores/spaces";
import * as testUtils from "../utils/test-utils"; import * as testUtils from "../utils/test-utils";
import { mkEvent, stubClient } from "../test-utils"; import { mkEvent, stubClient } from "../test-utils";
import DMRoomMap from "../../src/utils/DMRoomMap"; import DMRoomMap from "../../src/utils/DMRoomMap";
@ -90,10 +93,18 @@ describe("SpaceStore", () => {
await emitProm; await emitProm;
}; };
beforeEach(() => { beforeEach(async () => {
jest.runAllTimers(); // run async dispatch jest.runAllTimers(); // run async dispatch
client.getVisibleRooms.mockReturnValue(rooms = []); 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 () => { afterEach(async () => {
await testUtils.resetAsyncStoreWithClient(store); await testUtils.resetAsyncStoreWithClient(store);
}); });
@ -377,69 +388,84 @@ describe("SpaceStore", () => {
}); });
it("home space contains orphaned rooms", () => { it("home space contains orphaned rooms", () => {
expect(store.getSpaceFilteredRoomIds(null).has(orphan1)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(orphan1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(null).has(orphan2)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(orphan2)).toBeTruthy();
}); });
it("home space contains favourites", () => { it("home space does not contain all favourites", () => {
expect(store.getSpaceFilteredRoomIds(null).has(fav1)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(fav1)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(null).has(fav2)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(fav2)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(null).has(fav3)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(fav3)).toBeFalsy();
}); });
it("home space contains dm rooms", () => { it("home space contains dm rooms", () => {
expect(store.getSpaceFilteredRoomIds(null).has(dm1)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(dm1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(null).has(dm2)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(dm2)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(null).has(dm3)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(dm3)).toBeTruthy();
}); });
it("home space contains invites", () => { 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", () => { 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 () => { it("all rooms space does contain rooms/low priority even if they are also shown in a space", async () => {
await setShowAllRooms(true); 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 () => { it("home space doesn't contain rooms/low priority if they are also shown in a space", async () => {
await setShowAllRooms(false); await setShowAllRooms(false);
expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeFalsy(); expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(room1)).toBeFalsy();
}); });
it("space contains child rooms", () => { it("space contains child rooms", () => {
const space = client.getRoom(space1); expect(store.getSpaceFilteredRoomIds(space1).has(fav1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space).has(fav1)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(space1).has(room1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space).has(room1)).toBeTruthy();
}); });
it("space contains child favourites", () => { it("space contains child favourites", () => {
const space = client.getRoom(space2); expect(store.getSpaceFilteredRoomIds(space2).has(fav1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space).has(fav1)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(space2).has(fav2)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space).has(fav2)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(space2).has(fav3)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space).has(fav3)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(space2).has(room1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space).has(room1)).toBeTruthy();
}); });
it("space contains child invites", () => { it("space contains child invites", () => {
const space = client.getRoom(space3); expect(store.getSpaceFilteredRoomIds(space3).has(invite2)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space).has(invite2)).toBeTruthy();
}); });
it("spaces contain dms which you have with members of that space", () => { it("spaces contain dms which you have with members of that space", () => {
expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm1)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(space1).has(dm1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm1)).toBeFalsy(); expect(store.getSpaceFilteredRoomIds(space2).has(dm1)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm1)).toBeFalsy(); expect(store.getSpaceFilteredRoomIds(space3).has(dm1)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm2)).toBeFalsy(); expect(store.getSpaceFilteredRoomIds(space1).has(dm2)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm2)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(space2).has(dm2)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm2)).toBeFalsy(); expect(store.getSpaceFilteredRoomIds(space3).has(dm2)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm3)).toBeFalsy(); expect(store.getSpaceFilteredRoomIds(space1).has(dm3)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm3)).toBeFalsy(); expect(store.getSpaceFilteredRoomIds(space2).has(dm3)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm3)).toBeFalsy(); expect(store.getSpaceFilteredRoomIds(space3).has(dm3)).toBeFalsy();
}); });
it("dms are only added to Notification States for only the Home Space", () => { 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", () => { 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", () => { 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.invitedSpaces).toStrictEqual([]);
expect(store.getChildSpaces(space1)).toStrictEqual([]); expect(store.getChildSpaces(space1)).toStrictEqual([]);
expect(store.getChildRooms(space1)).toStrictEqual([]); expect(store.getChildRooms(space1)).toStrictEqual([]);
expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(invite1)).toBeFalsy(); expect(store.getSpaceFilteredRoomIds(space1).has(invite1)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(null).has(invite1)).toBeFalsy(); expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite1)).toBeFalsy();
const invite = mkRoom(invite1); const invite = mkRoom(invite1);
invite.getMyMembership.mockReturnValue("invite"); invite.getMyMembership.mockReturnValue("invite");
@ -599,8 +625,8 @@ describe("SpaceStore", () => {
expect(store.invitedSpaces).toStrictEqual([]); expect(store.invitedSpaces).toStrictEqual([]);
expect(store.getChildSpaces(space1)).toStrictEqual([]); expect(store.getChildSpaces(space1)).toStrictEqual([]);
expect(store.getChildRooms(space1)).toStrictEqual([invite]); expect(store.getChildRooms(space1)).toStrictEqual([invite]);
expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(invite1)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(space1).has(invite1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(null).has(invite1)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite1)).toBeTruthy();
}); });
}); });
@ -614,49 +640,46 @@ describe("SpaceStore", () => {
]); ]);
mkSpace(space3).getMyMembership.mockReturnValue("invite"); mkSpace(space3).getMyMembership.mockReturnValue("invite");
await run(); await run();
store.setActiveSpace(null); store.setActiveSpace(MetaSpace.Home);
expect(store.activeSpace).toBe(null); expect(store.activeSpace).toBe(MetaSpace.Home);
}); });
afterEach(() => { afterEach(() => {
fn.mockClear(); fn.mockClear();
}); });
it("switch to home space", async () => { it("switch to home space", async () => {
store.setActiveSpace(client.getRoom(space1)); store.setActiveSpace(space1);
fn.mockClear(); fn.mockClear();
store.setActiveSpace(null); store.setActiveSpace(MetaSpace.Home);
expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, null); expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, MetaSpace.Home);
expect(store.activeSpace).toBe(null); expect(store.activeSpace).toBe(MetaSpace.Home);
}); });
it("switch to invited space", async () => { it("switch to invited space", async () => {
const space = client.getRoom(space3); store.setActiveSpace(space3);
store.setActiveSpace(space); expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space3);
expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space); expect(store.activeSpace).toBe(space3);
expect(store.activeSpace).toBe(space);
}); });
it("switch to top level space", async () => { it("switch to top level space", async () => {
const space = client.getRoom(space1); store.setActiveSpace(space1);
store.setActiveSpace(space); expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space1);
expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space); expect(store.activeSpace).toBe(space1);
expect(store.activeSpace).toBe(space);
}); });
it("switch to subspace", async () => { it("switch to subspace", async () => {
const space = client.getRoom(space2); store.setActiveSpace(space2);
store.setActiveSpace(space); expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space2);
expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space); expect(store.activeSpace).toBe(space2);
expect(store.activeSpace).toBe(space);
}); });
it("switch to unknown space is a nop", async () => { 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 const space = client.getRoom(room1); // not a space
store.setActiveSpace(space); store.setActiveSpace(space.roomId);
expect(fn).not.toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space); expect(fn).not.toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space.roomId);
expect(store.activeSpace).toBe(null); expect(store.activeSpace).toBe(MetaSpace.Home);
}); });
}); });
@ -678,6 +701,7 @@ describe("SpaceStore", () => {
}); });
afterEach(() => { afterEach(() => {
localStorage.clear(); localStorage.clear();
localStorage.setItem("mx_labs_feature_feature_spaces_metaspaces", "true");
defaultDispatcher.unregister(dispatcherRef); 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 () => { 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); viewRoom(room2);
store.setActiveSpace(client.getRoom(space2)); store.setActiveSpace(space2);
viewRoom(room2); viewRoom(room2);
store.setActiveSpace(client.getRoom(space1)); store.setActiveSpace(space1);
expect(getCurrentRoom()).toBe(room2); expect(getCurrentRoom()).toBe(room2);
}); });
it("last viewed room in target space is in the current space", async () => { it("last viewed room in target space is in the current space", async () => {
store.setActiveSpace(client.getRoom(space1)); store.setActiveSpace(space1);
viewRoom(room2); viewRoom(room2);
store.setActiveSpace(client.getRoom(space2)); store.setActiveSpace(space2);
expect(getCurrentRoom()).toBe(space2); expect(getCurrentRoom()).toBe(space2);
store.setActiveSpace(client.getRoom(space1)); store.setActiveSpace(space1);
expect(getCurrentRoom()).toBe(room2); expect(getCurrentRoom()).toBe(room2);
}); });
it("last viewed room in target space is not in the current space", async () => { it("last viewed room in target space is not in the current space", async () => {
store.setActiveSpace(client.getRoom(space1)); store.setActiveSpace(space1);
viewRoom(room1); viewRoom(room1);
store.setActiveSpace(client.getRoom(space2)); store.setActiveSpace(space2);
viewRoom(room2); viewRoom(room2);
store.setActiveSpace(client.getRoom(space1)); store.setActiveSpace(space1);
expect(getCurrentRoom()).toBe(room1); expect(getCurrentRoom()).toBe(room1);
}); });
it("last viewed room is target space is not known", async () => { it("last viewed room is target space is not known", async () => {
store.setActiveSpace(client.getRoom(space1)); store.setActiveSpace(space1);
viewRoom(room1); viewRoom(room1);
localStorage.setItem(`mx_space_context_${space2}`, orphan2); localStorage.setItem(`mx_space_context_${space2}`, orphan2);
store.setActiveSpace(client.getRoom(space2)); store.setActiveSpace(space2);
expect(getCurrentRoom()).toBe(space2); expect(getCurrentRoom()).toBe(space2);
}); });
it("last viewed room is target space is no longer in that space", async () => { it("last viewed room is target space is no longer in that space", async () => {
store.setActiveSpace(client.getRoom(space1)); store.setActiveSpace(space1);
viewRoom(room1); viewRoom(room1);
localStorage.setItem(`mx_space_context_${space2}`, 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 expect(getCurrentRoom()).toBe(space2); // Space home instead of room1
}); });
it("no last viewed room in target space", async () => { it("no last viewed room in target space", async () => {
store.setActiveSpace(client.getRoom(space1)); store.setActiveSpace(space1);
viewRoom(room1); viewRoom(room1);
store.setActiveSpace(client.getRoom(space2)); store.setActiveSpace(space2);
expect(getCurrentRoom()).toBe(space2); expect(getCurrentRoom()).toBe(space2);
}); });
it("no last viewed room in home space", async () => { it("no last viewed room in home space", async () => {
store.setActiveSpace(client.getRoom(space1)); store.setActiveSpace(space1);
viewRoom(room1); viewRoom(room1);
store.setActiveSpace(null); store.setActiveSpace(MetaSpace.Home);
expect(getCurrentRoom()).toBeNull(); // Home expect(getCurrentRoom()).toBeNull(); // Home
}); });
}); });
@ -767,38 +791,51 @@ describe("SpaceStore", () => {
it("no switch required, room is in current space", async () => { it("no switch required, room is in current space", async () => {
viewRoom(room1); viewRoom(room1);
store.setActiveSpace(client.getRoom(space1), false); store.setActiveSpace(space1, false);
viewRoom(room2); viewRoom(room2);
expect(store.activeSpace).toBe(client.getRoom(space1)); expect(store.activeSpace).toBe(space1);
}); });
it("switch to canonical parent space for room", async () => { it("switch to canonical parent space for room", async () => {
viewRoom(room1); viewRoom(room1);
store.setActiveSpace(client.getRoom(space2), false); store.setActiveSpace(space2, false);
viewRoom(room2); viewRoom(room2);
expect(store.activeSpace).toBe(client.getRoom(space2)); expect(store.activeSpace).toBe(space2);
}); });
it("switch to first containing space for room", async () => { it("switch to first containing space for room", async () => {
viewRoom(room2); viewRoom(room2);
store.setActiveSpace(client.getRoom(space2), false); store.setActiveSpace(space2, false);
viewRoom(room3); viewRoom(room3);
expect(store.activeSpace).toBe(client.getRoom(space1)); expect(store.activeSpace).toBe(space1);
}); });
it("switch to home for orphaned room", async () => { it("switch to home for orphaned room", async () => {
viewRoom(room1); viewRoom(room1);
store.setActiveSpace(client.getRoom(space1), false); store.setActiveSpace(space1, false);
viewRoom(orphan1); 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 () => { it("when switching rooms in the all rooms home space don't switch to related space", async () => {
await setShowAllRooms(true); await setShowAllRooms(true);
viewRoom(room2); viewRoom(room2);
store.setActiveSpace(null, false); store.setActiveSpace(MetaSpace.Home, false);
viewRoom(room1); viewRoom(room1);
expect(store.activeSpace).toBeNull(); expect(store.activeSpace).toBe(MetaSpace.Home);
}); });
}); });

View file

@ -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");

View file

@ -14,17 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import "../enable-metaspaces-labs";
import "../../skinned-sdk"; // Must be first for skinning to work import "../../skinned-sdk"; // Must be first for skinning to work
import { SpaceWatcher } from "../../../src/stores/room-list/SpaceWatcher"; import { SpaceWatcher } from "../../../src/stores/room-list/SpaceWatcher";
import type { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore"; import type { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore";
import SettingsStore from "../../../src/settings/SettingsStore"; 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 { stubClient } from "../../test-utils";
import { SettingLevel } from "../../../src/settings/SettingLevel"; import { SettingLevel } from "../../../src/settings/SettingLevel";
import * as testUtils from "../../utils/test-utils";
import { setupAsyncStoreWithClient } from "../../utils/test-utils"; import { setupAsyncStoreWithClient } from "../../utils/test-utils";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import * as testUtils from "../../utils/test-utils";
import { SpaceFilterCondition } from "../../../src/stores/room-list/filters/SpaceFilterCondition"; import { SpaceFilterCondition } from "../../../src/stores/room-list/filters/SpaceFilterCondition";
import DMRoomMap from "../../../src/utils/DMRoomMap";
let filter: SpaceFilterCondition = null; let filter: SpaceFilterCondition = null;
@ -33,8 +36,13 @@ const mockRoomListStore = {
removeFilter: () => filter = null, removeFilter: () => filter = null,
} as unknown as RoomListStoreClass; } as unknown as RoomListStoreClass;
const space1Id = "!space1:server"; const getUserIdForRoomId = jest.fn();
const space2Id = "!space2:server"; const getDMRoomsForUserId = jest.fn();
// @ts-ignore
DMRoomMap.sharedInstance = { getUserIdForRoomId, getDMRoomsForUserId };
const space1 = "!space1:server";
const space2 = "!space2:server";
describe("SpaceWatcher", () => { describe("SpaceWatcher", () => {
stubClient(); stubClient();
@ -50,17 +58,21 @@ describe("SpaceWatcher", () => {
await testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR); await testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR);
}; };
let space1;
let space2;
beforeEach(async () => { beforeEach(async () => {
filter = null; filter = null;
store.removeAllListeners(); store.removeAllListeners();
store.setActiveSpace(null); store.setActiveSpace(MetaSpace.Home);
client.getVisibleRooms.mockReturnValue(rooms = []); client.getVisibleRooms.mockReturnValue(rooms = []);
space1 = mkSpace(space1Id); mkSpace(space1);
space2 = mkSpace(space2Id); 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)); client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId));
await setupAsyncStoreWithClient(store, client); await setupAsyncStoreWithClient(store, client);
@ -80,14 +92,14 @@ describe("SpaceWatcher", () => {
expect(filter).toBeNull(); 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); await setShowAllRooms(true);
new SpaceWatcher(mockRoomListStore); new SpaceWatcher(mockRoomListStore);
await setShowAllRooms(false); await setShowAllRooms(false);
expect(filter).toBeInstanceOf(SpaceFilterCondition); expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBeNull(); expect(filter["space"]).toBe(MetaSpace.Home);
}); });
it("sets filter correctly for all -> space transition", async () => { it("sets filter correctly for all -> space transition", async () => {
@ -126,7 +138,43 @@ describe("SpaceWatcher", () => {
SpaceStore.instance.setActiveSpace(space1); SpaceStore.instance.setActiveSpace(space1);
expect(filter).toBeInstanceOf(SpaceFilterCondition); expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(space1); 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(); expect(filter).toBeNull();
}); });
@ -138,10 +186,36 @@ describe("SpaceWatcher", () => {
new SpaceWatcher(mockRoomListStore); new SpaceWatcher(mockRoomListStore);
expect(filter).toBeInstanceOf(SpaceFilterCondition); expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(space1); expect(filter["space"]).toBe(space1);
SpaceStore.instance.setActiveSpace(null); SpaceStore.instance.setActiveSpace(MetaSpace.Home);
expect(filter).toBeInstanceOf(SpaceFilterCondition); 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 () => { it("updates filter correctly for space -> space transition", async () => {