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/_PreferencesUserSettingsTab.scss";
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
@import "./views/settings/tabs/user/_SidebarUserSettingsTab.scss";
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
@import "./views/spaces/_SpaceBasicSettings.scss";
@import "./views/spaces/_SpaceChildrenPicker.scss";

View file

@ -189,15 +189,35 @@ $activeBorderColor: $secondary-content;
}
}
&.mx_SpaceButton_home .mx_SpaceButton_icon {
background-color: #ffffff;
&.mx_SpaceButton_home,
&.mx_SpaceButton_favourites,
&.mx_SpaceButton_people,
&.mx_SpaceButton_orphans {
.mx_SpaceButton_icon {
background-color: #ffffff;
&::before {
background-color: #3f3d3d;
mask-image: url('$(res)/img/element-icons/home.svg');
&::before {
background-color: #3f3d3d;
}
}
}
&.mx_SpaceButton_home .mx_SpaceButton_icon::before {
mask-image: url('$(res)/img/element-icons/home.svg');
}
&.mx_SpaceButton_favourites .mx_SpaceButton_icon::before {
mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg');
}
&.mx_SpaceButton_people .mx_SpaceButton_icon::before {
mask-image: url('$(res)/img/element-icons/room/members.svg');
}
&.mx_SpaceButton_orphans .mx_SpaceButton_icon::before {
mask-image: url('$(res)/img/element-icons/roomlist/hash-circle.svg');
}
&.mx_SpaceButton_new .mx_SpaceButton_icon {
background-color: $roomlist-button-bg-color;

View file

@ -37,6 +37,10 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/settings/preference.svg');
}
.mx_UserSettingsDialog_sidebarIcon::before {
mask-image: url('$(res)/img/element-icons/settings/sidebar.svg');
}
.mx_UserSettingsDialog_securityIcon::before {
mask-image: url('$(res)/img/element-icons/security.svg');
}

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 { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import VoipUserMapper from "../VoipUserMapper";
import { SpaceStoreClass } from "../stores/SpaceStore";
import { SpaceStoreClass } from "../stores/spaces/SpaceStore";
import TypingStore from "../stores/TypingStore";
import { EventIndexPeg } from "../indexing/EventIndexPeg";
import { VoiceRecordingStore } from "../stores/VoiceRecordingStore";

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,7 +28,8 @@ import RoomListStore from "../../stores/room-list/RoomListStore";
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
import { replaceableComponent } from "../../utils/replaceableComponent";
import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/SpaceStore";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/spaces";
interface IProps {
isMinimized: boolean;

View file

@ -88,7 +88,7 @@ import RoomStatusBar from "./RoomStatusBar";
import MessageComposer from '../views/rooms/MessageComposer';
import JumpToBottomButton from "../views/rooms/JumpToBottomButton";
import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
import SpaceStore from "../../stores/SpaceStore";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { logger } from "matrix-js-sdk/src/logger";
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@ import StyledRadioGroup, { IDefinition } from "../elements/StyledRadioGroup";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import RoomAvatar from "../avatars/RoomAvatar";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal";
import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog";
@ -67,8 +67,8 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
const editRestrictedRoomIds = async (): Promise<string[] | undefined> => {
let selected = restrictedAllowRoomIds;
if (!selected?.length && SpaceStore.instance.activeSpace) {
selected = [SpaceStore.instance.activeSpace.roomId];
if (!selected?.length && SpaceStore.instance.activeSpaceRoom) {
selected = [SpaceStore.instance.activeSpaceRoom.roomId];
}
const matrixClient = MatrixClientPeg.get();
@ -176,9 +176,9 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
{ moreText && <span>{ moreText }</span> }
</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.", {}, {
spaceName: () => <b>{ SpaceStore.instance.activeSpace.name }</b>,
spaceName: () => <b>{ SpaceStore.instance.activeSpaceRoom.name }</b>,
});
} else {
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",
rageshakeData: Object.fromEntries([
"Spaces.allRoomsInHome",
"Spaces.enabledMetaSpaces",
].map(k => [k, SettingsStore.getValue(k)])),
});
}}

View file

@ -34,13 +34,15 @@ import SpaceCreateMenu from "./SpaceCreateMenu";
import { SpaceButton, SpaceItem } from "./SpaceTreeLevel";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import SpaceStore, {
HOME_SPACE,
import SpaceStore from "../../../stores/spaces/SpaceStore";
import {
MetaSpace,
SpaceKey,
UPDATE_HOME_BEHAVIOUR,
UPDATE_INVITED_SPACES,
UPDATE_SELECTED_SPACE,
UPDATE_TOP_LEVEL_SPACES,
} from "../../../stores/SpaceStore";
} from "../../../stores/spaces";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
@ -53,17 +55,21 @@ import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel";
import UIStore from "../../../stores/UIStore";
const useSpaces = (): [Room[], Room[], Room | null] => {
const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => {
const invites = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_INVITED_SPACES, () => {
return SpaceStore.instance.invitedSpaces;
});
const spaces = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, () => {
return SpaceStore.instance.spacePanelSpaces;
});
const activeSpace = useEventEmitterState<Room>(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
const [metaSpaces, actualSpaces] = useEventEmitterState<[MetaSpace[], Room[]]>(
SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES,
() => [
SpaceStore.instance.enabledMetaSpaces,
SpaceStore.instance.spacePanelSpaces,
],
);
const activeSpace = useEventEmitterState<SpaceKey>(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
return SpaceStore.instance.activeSpace;
});
return [invites, spaces, activeSpace];
return [invites, metaSpaces, actualSpaces, activeSpace];
};
interface IInnerSpacePanelProps {
@ -99,37 +105,76 @@ const HomeButtonContextMenu = ({ onFinished, ...props }: ComponentProps<typeof S
</IconizedContextMenu>;
};
interface IHomeButtonProps {
interface IMetaSpaceButtonProps extends ComponentProps<typeof SpaceButton> {
selected: boolean;
isPanelCollapsed: boolean;
}
const HomeButton = ({ selected, isPanelCollapsed }: IHomeButtonProps) => {
const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
return SpaceStore.instance.allRoomsInHome;
});
type MetaSpaceButtonProps = Pick<IMetaSpaceButtonProps, "selected" | "isPanelCollapsed">;
const MetaSpaceButton = ({ selected, isPanelCollapsed, ...props }: IMetaSpaceButtonProps) => {
return <li
className={classNames("mx_SpaceItem", {
"collapsed": isPanelCollapsed,
})}
role="treeitem"
>
<SpaceButton
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")}
/>
<SpaceButton {...props} selected={selected} isNarrow={isPanelCollapsed} />
</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 = ({
isPanelCollapsed,
setPanelCollapsed,
@ -181,13 +226,25 @@ const CreateSpaceButton = ({
</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
const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCollapsed, setPanelCollapsed }) => {
const [invites, spaces, activeSpace] = useSpaces();
const [invites, metaSpaces, actualSpaces, activeSpace] = useSpaces();
const activeSpaces = activeSpace ? [activeSpace] : [];
const metaSpacesSection = metaSpaces.map(key => {
const Component = metaSpaceComponentMap[key];
return <Component key={key} selected={activeSpace === key} isPanelCollapsed={isPanelCollapsed} />;
});
return <div className="mx_SpaceTreeLevel">
<HomeButton selected={!activeSpace} isPanelCollapsed={isPanelCollapsed} />
{ metaSpacesSection }
{ invites.map(s => (
<SpaceItem
key={s.roomId}
@ -197,7 +254,7 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCo
onExpand={() => setPanelCollapsed(false)}
/>
)) }
{ spaces.map((s, i) => (
{ actualSpaces.map((s, i) => (
<Draggable key={s.roomId} draggableId={s.roomId} index={i}>
{ (provided, snapshot) => (
<SpaceItem

View file

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

View file

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

View file

@ -830,6 +830,7 @@
"Polls (under active development)": "Polls (under active development)",
"Show info about bridges in room settings": "Show info about bridges in room settings",
"New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)",
"Meta Spaces": "Meta Spaces",
"Don't send read receipts": "Don't send read receipts",
"Font size": "Font size",
"Use custom size": "Use custom size",
@ -1064,6 +1065,9 @@
"Show all rooms": "Show all rooms",
"All rooms": "All rooms",
"Options": "Options",
"Favourites": "Favourites",
"People": "People",
"Other rooms": "Other rooms",
"Spaces": "Spaces",
"Expand space panel": "Expand space panel",
"Collapse space panel": "Collapse space panel",
@ -1426,6 +1430,16 @@
"Learn more about how we use analytics.": "Learn more about how we use analytics.",
"Where you're signed in": "Where you're signed in",
"Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Manage your signed-in devices below. A device's name is visible to people you communicate with.",
"Sidebar": "Sidebar",
"Spaces are ways to group rooms and people.": "Spaces are ways to group rooms and people.",
"Spaces to show": "Spaces to show",
"Along with the spaces you're in, you can use some pre-built ones too.": "Along with the spaces you're in, you can use some pre-built ones too.",
"Home is useful for getting an overview of everything.": "Home is useful for getting an overview of everything.",
"Show all your rooms in Home, even if they're in a space.": "Show all your rooms in Home, even if they're in a space.",
"Automatically group all your favourite rooms and people together in one place.": "Automatically group all your favourite rooms and people together in one place.",
"Automatically group all your people together in one place.": "Automatically group all your people together in one place.",
"Rooms outside of a space": "Rooms outside of a space",
"Automatically group all your rooms that aren't part of a space in one place.": "Automatically group all your rooms that aren't part of a space in one place.",
"Default Device": "Default Device",
"No media permissions": "No media permissions",
"You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam",
@ -1670,8 +1684,6 @@
"Show Widgets": "Show Widgets",
"Search": "Search",
"Invites": "Invites",
"Favourites": "Favourites",
"People": "People",
"Start chat": "Start chat",
"Rooms": "Rooms",
"Add room": "Add room",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 { _t } from "../languageHandler";
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
import SpaceStore from "../stores/SpaceStore";
import SpaceStore from "../stores/spaces/SpaceStore";
import Spinner from "../components/views/elements/Spinner";
import { logger } from "matrix-js-sdk/src/logger";

View file

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

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