= ({
notifBadge =
SpaceStore.instance.setActiveRoomInSpace(space || null)}
+ onClick={() => SpaceStore.instance.setActiveRoomInSpace(spaceKey ?? space.roomId)}
forceCount={false}
notification={notificationState}
aria-label={ariaLabel}
@@ -116,7 +117,7 @@ export const SpaceButton: React.FC = ({
mx_SpaceButton_narrow: isNarrow,
})}
title={label}
- onClick={onClick}
+ onClick={spaceKey ? () => SpaceStore.instance.setActiveSpace(spaceKey) : props.onClick}
onContextMenu={openMenu}
forceHide={!isNarrow || menuDisplayed}
inputRef={handle}
@@ -146,7 +147,7 @@ export const SpaceButton: React.FC = ({
interface IItemProps extends InputHTMLAttributes {
space?: Room;
- activeSpaces: Room[];
+ activeSpaces: SpaceKey[];
isNested?: boolean;
isPanelCollapsed?: boolean;
onExpand?: Function;
@@ -258,7 +259,7 @@ export class SpaceItem extends React.PureComponent {
private onClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
- SpaceStore.instance.setActiveSpace(this.props.space);
+ SpaceStore.instance.setActiveSpace(this.props.space.roomId);
};
render() {
@@ -316,7 +317,7 @@ export class SpaceItem extends React.PureComponent {
{...restDragHandleProps}
space={space}
className={isInvite ? "mx_SpaceButton_invite" : undefined}
- selected={activeSpaces.includes(space)}
+ selected={activeSpaces.includes(space.roomId)}
label={space.name}
contextMenuTooltip={_t("Space options")}
notificationState={notificationState}
@@ -337,7 +338,7 @@ export class SpaceItem extends React.PureComponent {
interface ITreeLevelProps {
spaces: Room[];
- activeSpaces: Room[];
+ activeSpaces: SpaceKey[];
isNested?: boolean;
parents: Set;
}
diff --git a/src/createRoom.ts b/src/createRoom.ts
index 6394cb6849..f4d796cb4b 100644
--- a/src/createRoom.ts
+++ b/src/createRoom.ts
@@ -40,7 +40,7 @@ import GroupStore from "./stores/GroupStore";
import CountlyAnalytics from "./CountlyAnalytics";
import { isJoinedOrNearlyJoined } from "./utils/membership";
import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
-import SpaceStore from "./stores/SpaceStore";
+import SpaceStore from "./stores/spaces/SpaceStore";
import { makeSpaceParentEvent } from "./utils/space";
import { Action } from "./dispatcher/actions";
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index a25ee37386..1fab6eda59 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -830,6 +830,7 @@
"Polls (under active development)": "Polls (under active development)",
"Show info about bridges in room settings": "Show info about bridges in room settings",
"New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)",
+ "Meta Spaces": "Meta Spaces",
"Don't send read receipts": "Don't send read receipts",
"Font size": "Font size",
"Use custom size": "Use custom size",
@@ -1064,6 +1065,9 @@
"Show all rooms": "Show all rooms",
"All rooms": "All rooms",
"Options": "Options",
+ "Favourites": "Favourites",
+ "People": "People",
+ "Other rooms": "Other rooms",
"Spaces": "Spaces",
"Expand space panel": "Expand space panel",
"Collapse space panel": "Collapse space panel",
@@ -1426,6 +1430,16 @@
"Learn more about how we use analytics.": "Learn more about how we use analytics.",
"Where you're signed in": "Where you're signed in",
"Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Manage your signed-in devices below. A device's name is visible to people you communicate with.",
+ "Sidebar": "Sidebar",
+ "Spaces are ways to group rooms and people.": "Spaces are ways to group rooms and people.",
+ "Spaces to show": "Spaces to show",
+ "Along with the spaces you're in, you can use some pre-built ones too.": "Along with the spaces you're in, you can use some pre-built ones too.",
+ "Home is useful for getting an overview of everything.": "Home is useful for getting an overview of everything.",
+ "Show all your rooms in Home, even if they're in a space.": "Show all your rooms in Home, even if they're in a space.",
+ "Automatically group all your favourite rooms and people together in one place.": "Automatically group all your favourite rooms and people together in one place.",
+ "Automatically group all your people together in one place.": "Automatically group all your people together in one place.",
+ "Rooms outside of a space": "Rooms outside of a space",
+ "Automatically group all your rooms that aren't part of a space in one place.": "Automatically group all your rooms that aren't part of a space in one place.",
"Default Device": "Default Device",
"No media permissions": "No media permissions",
"You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam",
@@ -1670,8 +1684,6 @@
"Show Widgets": "Show Widgets",
"Search": "Search",
"Invites": "Invites",
- "Favourites": "Favourites",
- "People": "People",
"Start chat": "Start chat",
"Rooms": "Rooms",
"Add room": "Add room",
diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx
index 8b25309120..c263317dc4 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -42,6 +42,7 @@ import ReducedMotionController from './controllers/ReducedMotionController';
import IncompatibleController from "./controllers/IncompatibleController";
import PseudonymousAnalyticsController from './controllers/PseudonymousAnalyticsController';
import NewLayoutSwitcherController from './controllers/NewLayoutSwitcherController';
+import { MetaSpace } from "../stores/spaces";
// These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
const LEVELS_ROOM_SETTINGS = [
@@ -283,6 +284,16 @@ export const SETTINGS: {[setting: string]: ISetting} = {
default: false,
controller: new NewLayoutSwitcherController(),
},
+ "feature_spaces_metaspaces": {
+ isFeature: true,
+ supportedLevels: LEVELS_FEATURE,
+ displayName: _td("Meta Spaces"),
+ default: false,
+ controller: new OrderedMultiController([
+ new IncompatibleController("showCommunitiesInsteadOfSpaces"),
+ new ReloadOnChangeController(),
+ ]),
+ },
"RoomList.backgroundImage": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: null,
@@ -755,6 +766,15 @@ export const SETTINGS: {[setting: string]: ISetting} = {
default: false,
controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", null),
},
+ "Spaces.enabledMetaSpaces": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ default: {
+ [MetaSpace.Home]: true,
+ },
+ controller: new IncompatibleController("feature_spaces_metaspaces", {
+ [MetaSpace.Home]: true,
+ }, false),
+ },
"showCommunitiesInsteadOfSpaces": {
displayName: _td("Display Communities instead of Spaces"),
description: _td("Temporarily show communities instead of Spaces for this session. " +
diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts
index 8a85ca354f..7c33901ae4 100644
--- a/src/stores/BreadcrumbsStore.ts
+++ b/src/stores/BreadcrumbsStore.ts
@@ -22,7 +22,7 @@ import defaultDispatcher from "../dispatcher/dispatcher";
import { arrayHasDiff } from "../utils/arrays";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { SettingLevel } from "../settings/SettingLevel";
-import SpaceStore from "./SpaceStore";
+import SpaceStore from "./spaces/SpaceStore";
import { Action } from "../dispatcher/actions";
import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";
diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts
index c4b1f012b1..9fbfcb32e2 100644
--- a/src/stores/room-list/RoomListStore.ts
+++ b/src/stores/room-list/RoomListStore.ts
@@ -35,7 +35,7 @@ import { NameFilterCondition } from "./filters/NameFilterCondition";
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
import { VisibilityProvider } from "./filters/VisibilityProvider";
import { SpaceWatcher } from "./SpaceWatcher";
-import SpaceStore from "../SpaceStore";
+import SpaceStore from "../spaces/SpaceStore";
import { Action } from "../../dispatcher/actions";
import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";
diff --git a/src/stores/room-list/SpaceWatcher.ts b/src/stores/room-list/SpaceWatcher.ts
index fe2eb1e881..e7d6e78206 100644
--- a/src/stores/room-list/SpaceWatcher.ts
+++ b/src/stores/room-list/SpaceWatcher.ts
@@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { Room } from "matrix-js-sdk/src/models/room";
-
import { RoomListStoreClass } from "./RoomListStore";
import { SpaceFilterCondition } from "./filters/SpaceFilterCondition";
-import SpaceStore, { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../SpaceStore";
+import SpaceStore from "../spaces/SpaceStore";
+import { MetaSpace, SpaceKey, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces";
/**
* Watches for changes in spaces to manage the filter on the provided RoomListStore
@@ -26,11 +25,11 @@ import SpaceStore, { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../Spa
export class SpaceWatcher {
private readonly filter = new SpaceFilterCondition();
// we track these separately to the SpaceStore as we need to observe transitions
- private activeSpace: Room = SpaceStore.instance.activeSpace;
+ private activeSpace: SpaceKey = SpaceStore.instance.activeSpace;
private allRoomsInHome: boolean = SpaceStore.instance.allRoomsInHome;
constructor(private store: RoomListStoreClass) {
- if (!this.allRoomsInHome || this.activeSpace) {
+ if (SpaceWatcher.needsFilter(this.activeSpace, this.allRoomsInHome)) {
this.updateFilter();
store.addFilter(this.filter);
}
@@ -38,21 +37,26 @@ export class SpaceWatcher {
SpaceStore.instance.on(UPDATE_HOME_BEHAVIOUR, this.onHomeBehaviourUpdated);
}
- private onSelectedSpaceUpdated = (activeSpace?: Room, allRoomsInHome = this.allRoomsInHome) => {
+ private static needsFilter(spaceKey: SpaceKey, allRoomsInHome: boolean): boolean {
+ return !(spaceKey === MetaSpace.Home && allRoomsInHome);
+ }
+
+ private onSelectedSpaceUpdated = (activeSpace: SpaceKey, allRoomsInHome = this.allRoomsInHome) => {
if (activeSpace === this.activeSpace && allRoomsInHome === this.allRoomsInHome) return; // nop
- const oldActiveSpace = this.activeSpace;
- const oldAllRoomsInHome = this.allRoomsInHome;
+ const neededFilter = SpaceWatcher.needsFilter(this.activeSpace, this.allRoomsInHome);
+ const needsFilter = SpaceWatcher.needsFilter(activeSpace, allRoomsInHome);
+
this.activeSpace = activeSpace;
this.allRoomsInHome = allRoomsInHome;
- if (activeSpace || !allRoomsInHome) {
+ if (needsFilter) {
this.updateFilter();
}
- if (oldAllRoomsInHome && !oldActiveSpace) {
+ if (!neededFilter && needsFilter) {
this.store.addFilter(this.filter);
- } else if (allRoomsInHome && !activeSpace) {
+ } else if (neededFilter && !needsFilter) {
this.store.removeFilter(this.filter);
}
};
@@ -62,8 +66,8 @@ export class SpaceWatcher {
};
private updateFilter = () => {
- if (this.activeSpace) {
- SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => {
+ if (this.activeSpace[0] === "!") {
+ SpaceStore.instance.traverseSpace(this.activeSpace, roomId => {
this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
});
}
diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts
index 754e1c1d94..c812edee48 100644
--- a/src/stores/room-list/algorithms/Algorithm.ts
+++ b/src/stores/room-list/algorithms/Algorithm.ts
@@ -34,7 +34,7 @@ import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } f
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
import { getListAlgorithmInstance } from "./list-ordering";
import { VisibilityProvider } from "../filters/VisibilityProvider";
-import SpaceStore from "../../SpaceStore";
+import SpaceStore from "../../spaces/SpaceStore";
import { logger } from "matrix-js-sdk/src/logger";
diff --git a/src/stores/room-list/filters/SpaceFilterCondition.ts b/src/stores/room-list/filters/SpaceFilterCondition.ts
index 0e6965d843..fd815bf86f 100644
--- a/src/stores/room-list/filters/SpaceFilterCondition.ts
+++ b/src/stores/room-list/filters/SpaceFilterCondition.ts
@@ -19,7 +19,8 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
import { IDestroyable } from "../../../utils/IDestroyable";
-import SpaceStore, { HOME_SPACE } from "../../SpaceStore";
+import SpaceStore from "../../spaces/SpaceStore";
+import { MetaSpace, SpaceKey } from "../../spaces";
import { setHasDiff } from "../../../utils/sets";
/**
@@ -30,7 +31,7 @@ import { setHasDiff } from "../../../utils/sets";
*/
export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable {
private roomIds = new Set();
- private space: Room = null;
+ private space: SpaceKey = MetaSpace.Home;
public get kind(): FilterKind {
return FilterKind.Prefilter;
@@ -55,15 +56,13 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
}
};
- private getSpaceEventKey = (space: Room | null) => space ? space.roomId : HOME_SPACE;
-
- public updateSpace(space: Room) {
- SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
- SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate);
+ public updateSpace(space: SpaceKey) {
+ SpaceStore.instance.off(this.space, this.onStoreUpdate);
+ SpaceStore.instance.on(this.space = space, this.onStoreUpdate);
this.onStoreUpdate(); // initial update from the change to the space
}
public destroy(): void {
- SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
+ SpaceStore.instance.off(this.space, this.onStoreUpdate);
}
}
diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts
index f63b622053..18b68da301 100644
--- a/src/stores/room-list/filters/VisibilityProvider.ts
+++ b/src/stores/room-list/filters/VisibilityProvider.ts
@@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import CallHandler from "../../../CallHandler";
import { RoomListCustomisations } from "../../../customisations/RoomList";
import VoipUserMapper from "../../../VoipUserMapper";
-import SpaceStore from "../../SpaceStore";
+import SpaceStore from "../../spaces/SpaceStore";
export class VisibilityProvider {
private static internalInstance: VisibilityProvider;
diff --git a/src/stores/SpaceStore.ts b/src/stores/spaces/SpaceStore.ts
similarity index 76%
rename from src/stores/SpaceStore.ts
rename to src/stores/spaces/SpaceStore.ts
index ea5ff56aea..5cea148b78 100644
--- a/src/stores/SpaceStore.ts
+++ b/src/stores/spaces/SpaceStore.ts
@@ -18,56 +18,51 @@ import { ListIteratee, Many, sortBy, throttle } from "lodash";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
-import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces";
import { IRoomCapability } from "matrix-js-sdk/src/client";
-
-import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
-import defaultDispatcher from "../dispatcher/dispatcher";
-import { ActionPayload } from "../dispatcher/payloads";
-import RoomListStore from "./room-list/RoomListStore";
-import SettingsStore from "../settings/SettingsStore";
-import DMRoomMap from "../utils/DMRoomMap";
-import { FetchRoomFn } from "./notifications/ListNotificationState";
-import { SpaceNotificationState } from "./notifications/SpaceNotificationState";
-import { RoomNotificationStateStore } from "./notifications/RoomNotificationStateStore";
-import { DefaultTagID } from "./room-list/models";
-import { EnhancedMap, mapDiff } from "../utils/maps";
-import { setHasDiff } from "../utils/sets";
-import RoomViewStore from "./RoomViewStore";
-import { Action } from "../dispatcher/actions";
-import { arrayHasDiff, arrayHasOrderChange } from "../utils/arrays";
-import { objectDiff } from "../utils/objects";
-import { reorderLexicographically } from "../utils/stringOrderField";
-import { TAG_ORDER } from "../components/views/rooms/RoomList";
-import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";
-
import { logger } from "matrix-js-sdk/src/logger";
-type SpaceKey = string | symbol;
+import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
+import defaultDispatcher from "../../dispatcher/dispatcher";
+import { ActionPayload } from "../../dispatcher/payloads";
+import RoomListStore from "../room-list/RoomListStore";
+import SettingsStore from "../../settings/SettingsStore";
+import DMRoomMap from "../../utils/DMRoomMap";
+import { FetchRoomFn } from "../notifications/ListNotificationState";
+import { SpaceNotificationState } from "../notifications/SpaceNotificationState";
+import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
+import { DefaultTagID } from "../room-list/models";
+import { EnhancedMap, mapDiff } from "../../utils/maps";
+import { setHasDiff } from "../../utils/sets";
+import RoomViewStore from "../RoomViewStore";
+import { Action } from "../../dispatcher/actions";
+import { arrayHasDiff, arrayHasOrderChange } from "../../utils/arrays";
+import { objectDiff } from "../../utils/objects";
+import { reorderLexicographically } from "../../utils/stringOrderField";
+import { TAG_ORDER } from "../../components/views/rooms/RoomList";
+import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";
+import {
+ ISuggestedRoom,
+ MetaSpace,
+ SpaceKey,
+ UPDATE_HOME_BEHAVIOUR,
+ UPDATE_INVITED_SPACES,
+ UPDATE_SELECTED_SPACE,
+ UPDATE_SUGGESTED_ROOMS,
+ UPDATE_TOP_LEVEL_SPACES,
+} from ".";
interface IState {}
const ACTIVE_SPACE_LS_KEY = "mx_active_space";
-export const HOME_SPACE = Symbol("home-space");
-export const SUGGESTED_ROOMS = Symbol("suggested-rooms");
-
-export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
-export const UPDATE_INVITED_SPACES = Symbol("invited-spaces");
-export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
-export const UPDATE_HOME_BEHAVIOUR = Symbol("home-behaviour");
-// Space Room ID/HOME_SPACE will be emitted when a Space's children change
-
-export interface ISuggestedRoom extends IHierarchyRoom {
- viaServers: string[];
-}
+const metaSpaceOrder: MetaSpace[] = [MetaSpace.Home, MetaSpace.Favourites, MetaSpace.People, MetaSpace.Orphans];
const MAX_SUGGESTED_ROOMS = 20;
// This setting causes the page to reload and can be costly if read frequently, so read it here only
const spacesEnabled = !SettingsStore.getValue("showCommunitiesInsteadOfSpaces");
-const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "HOME_SPACE"}`;
+const getSpaceContextKey = (space: SpaceKey) => `mx_space_context_${space}`;
const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
return arr.reduce((result, room: Room) => {
@@ -105,30 +100,41 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
private notificationStateMap = new Map();
// Map from space key to Set of room IDs that should be shown as part of that space's filter
private spaceFilteredRooms = new Map>();
- // The space currently selected in the Space Panel - if null then Home is selected
- private _activeSpace?: Room = null;
+ // The space currently selected in the Space Panel
+ private _activeSpace?: SpaceKey = MetaSpace.Home; // set properly by onReady
private _suggestedRooms: ISuggestedRoom[] = [];
private _invitedSpaces = new Set();
private spaceOrderLocalEchoMap = new Map();
private _restrictedJoinRuleSupport?: IRoomCapability;
private _allRoomsInHome: boolean = SettingsStore.getValue("Spaces.allRoomsInHome");
+ private _enabledMetaSpaces: MetaSpace[] = []; // set by onReady
constructor() {
super(defaultDispatcher, {});
SettingsStore.monitorSetting("Spaces.allRoomsInHome", null);
+ SettingsStore.monitorSetting("Spaces.enabledMetaSpaces", null);
}
public get invitedSpaces(): Room[] {
return Array.from(this._invitedSpaces);
}
+ public get enabledMetaSpaces(): MetaSpace[] {
+ return this._enabledMetaSpaces;
+ }
+
public get spacePanelSpaces(): Room[] {
return this.rootSpaces;
}
- public get activeSpace(): Room | null {
- return this._activeSpace || null;
+ public get activeSpace(): SpaceKey {
+ return this._activeSpace;
+ }
+
+ public get activeSpaceRoom(): Room | null {
+ if (this._activeSpace[0] !== "!") return null;
+ return this.matrixClient?.getRoom(this._activeSpace);
}
public get suggestedRooms(): ISuggestedRoom[] {
@@ -139,12 +145,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
return this._allRoomsInHome;
}
- public setActiveRoomInSpace(space: Room | null): void {
- if (space && !space.isSpaceRoom()) return;
+ public setActiveRoomInSpace(space: SpaceKey): void {
+ if (space[0] === "!" && !this.matrixClient?.getRoom(space)?.isSpaceRoom()) return;
if (space !== this.activeSpace) this.setActiveSpace(space);
if (space) {
- const roomId = this.getNotificationState(space.roomId).getFirstRoomWithNotifications();
+ const roomId = this.getNotificationState(space).getFirstRoomWithNotifications();
defaultDispatcher.dispatch({
action: "view_room",
room_id: roomId,
@@ -184,12 +190,20 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
* @param contextSwitch whether to switch the user's context,
* should not be done when the space switch is done implicitly due to another event like switching room.
*/
- public setActiveSpace(space: Room | null, contextSwitch = true) {
- if (!this.matrixClient || space === this.activeSpace || (space && !space.isSpaceRoom())) return;
+ public setActiveSpace(space: SpaceKey, contextSwitch = true) {
+ if (!space || !this.matrixClient || space === this.activeSpace) return;
+
+ let cliSpace: Room;
+ if (space[0] === "!") {
+ cliSpace = this.matrixClient.getRoom(space);
+ if (!cliSpace?.isSpaceRoom()) return;
+ } else if (!this.enabledMetaSpaces.includes(space as MetaSpace)) {
+ return;
+ }
this._activeSpace = space;
this.emit(UPDATE_SELECTED_SPACE, this.activeSpace);
- this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []);
+ this.emit(UPDATE_SUGGESTED_ROOMS, this._suggestedRooms = []);
if (contextSwitch) {
// view last selected room from space
@@ -198,7 +212,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// if the space being selected is an invite then always view that invite
// else if the last viewed room in this space is joined then view that
// else view space home or home depending on what is being clicked on
- if (space?.getMyMembership() !== "invite" &&
+ if (cliSpace?.getMyMembership() !== "invite" &&
this.matrixClient.getRoom(roomId)?.getMyMembership() === "join" &&
this.getSpaceFilteredRoomIds(space).has(roomId)
) {
@@ -207,10 +221,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
room_id: roomId,
context_switch: true,
});
- } else if (space) {
+ } else if (cliSpace) {
defaultDispatcher.dispatch({
action: "view_room",
- room_id: space.roomId,
+ room_id: space,
context_switch: true,
});
} else {
@@ -221,22 +235,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
// persist space selected
- if (space) {
- window.localStorage.setItem(ACTIVE_SPACE_LS_KEY, space.roomId);
- } else {
- window.localStorage.removeItem(ACTIVE_SPACE_LS_KEY);
- }
+ window.localStorage.setItem(ACTIVE_SPACE_LS_KEY, space);
- if (space) {
- this.loadSuggestedRooms(space);
+ if (cliSpace) {
+ this.loadSuggestedRooms(cliSpace);
}
}
private async loadSuggestedRooms(space: Room): Promise {
const suggestedRooms = await this.fetchSuggestedRooms(space);
- if (this._activeSpace === space) {
+ if (this._activeSpace === space.roomId) {
this._suggestedRooms = suggestedRooms;
- this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
+ this.emit(UPDATE_SUGGESTED_ROOMS, this._suggestedRooms);
}
}
@@ -337,11 +347,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
return this.parentMap.get(roomId) || new Set();
}
- public getSpaceFilteredRoomIds = (space: Room | null): Set => {
- if (!space && this.allRoomsInHome) {
+ public getSpaceFilteredRoomIds = (space: SpaceKey): Set => {
+ if (space === MetaSpace.Home && this.allRoomsInHome) {
return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId));
}
- return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set();
+ return this.spaceFilteredRooms.get(space) || new Set();
};
private rebuild = throttle(() => {
@@ -420,12 +430,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
this.parentMap = backrefs;
// if the currently selected space no longer exists, remove its selection
- if (this._activeSpace && detachedNodes.has(this._activeSpace)) {
- this.setActiveSpace(null, false);
+ if (this._activeSpace[0] === "!" && detachedNodes.has(this.matrixClient.getRoom(this._activeSpace))) {
+ this.goToFirstSpace();
}
this.onRoomsUpdate(); // TODO only do this if a change has happened
- this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
+ this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces, this.enabledMetaSpaces);
// build initial state of invited spaces as we would have missed the emitted events about the room at launch
this._invitedSpaces = new Set(this.sortRootSpaces(invitedSpaces));
@@ -440,19 +450,22 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
if (this.allRoomsInHome) return true;
if (room.isSpaceRoom()) return false;
return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space
- || DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space
- || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite); // show all favourites
+ || DMRoomMap.shared().getUserIdForRoomId(room.roomId); // put all DMs in the Home Space
};
// Update a given room due to its tag changing (e.g DM-ness or Fav-ness)
// This can only change whether it shows up in the HOME_SPACE or not
private onRoomUpdate = (room: Room) => {
- if (this.showInHomeSpace(room)) {
- this.spaceFilteredRooms.get(HOME_SPACE)?.add(room.roomId);
- this.emit(HOME_SPACE);
- } else if (!this.orphanedRooms.has(room.roomId)) {
- this.spaceFilteredRooms.get(HOME_SPACE)?.delete(room.roomId);
- this.emit(HOME_SPACE);
+ const enabledMetaSpaces = new Set(this.enabledMetaSpaces);
+ // TODO more metaspace stuffs
+ if (enabledMetaSpaces.has(MetaSpace.Home)) {
+ if (this.showInHomeSpace(room)) {
+ this.spaceFilteredRooms.get(MetaSpace.Home)?.add(room.roomId);
+ this.emit(MetaSpace.Home);
+ } else if (!this.orphanedRooms.has(room.roomId)) {
+ this.spaceFilteredRooms.get(MetaSpace.Home)?.delete(room.roomId);
+ this.emit(MetaSpace.Home);
+ }
}
};
@@ -469,18 +482,41 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
const oldFilteredRooms = this.spaceFilteredRooms;
this.spaceFilteredRooms = new Map();
- if (!this.allRoomsInHome) {
+ const enabledMetaSpaces = new Set(this.enabledMetaSpaces);
+ // populate the Home metaspace if it is enabled and is not set to all rooms
+ if (enabledMetaSpaces.has(MetaSpace.Home) && !this.allRoomsInHome) {
// put all room invites in the Home Space
const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite");
- this.spaceFilteredRooms.set(HOME_SPACE, new Set(invites.map(room => room.roomId)));
+ this.spaceFilteredRooms.set(MetaSpace.Home, new Set(invites.map(r => r.roomId)));
visibleRooms.forEach(room => {
if (this.showInHomeSpace(room)) {
- this.spaceFilteredRooms.get(HOME_SPACE).add(room.roomId);
+ this.spaceFilteredRooms.get(MetaSpace.Home).add(room.roomId);
}
});
}
+ // populate the Favourites metaspace if it is enabled
+ if (enabledMetaSpaces.has(MetaSpace.Favourites)) {
+ const favourites = visibleRooms.filter(r => r.tags[DefaultTagID.Favourite]);
+ this.spaceFilteredRooms.set(MetaSpace.Favourites, new Set(favourites.map(r => r.roomId)));
+ }
+
+ // populate the People metaspace if it is enabled
+ if (enabledMetaSpaces.has(MetaSpace.People)) {
+ const people = visibleRooms.filter(r => DMRoomMap.shared().getUserIdForRoomId(r.roomId));
+ this.spaceFilteredRooms.set(MetaSpace.People, new Set(people.map(r => r.roomId)));
+ }
+
+ // populate the Orphans metaspace if it is enabled
+ if (enabledMetaSpaces.has(MetaSpace.Orphans)) {
+ const orphans = visibleRooms.filter(r => {
+ // filter out DMs and rooms with >0 parents
+ return !this.parentMap.get(r.roomId)?.size && !DMRoomMap.shared().getUserIdForRoomId(r.roomId);
+ });
+ this.spaceFilteredRooms.set(MetaSpace.Orphans, new Set(orphans.map(r => r.roomId)));
+ }
+
const hiddenChildren = new EnhancedMap>();
visibleRooms.forEach(room => {
if (room.getMyMembership() !== "join") return;
@@ -540,15 +576,23 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
this.emit(k);
});
+ let dmBadgeSpace: MetaSpace;
+ // only show badges on dms on the most relevant space if such exists
+ if (enabledMetaSpaces.has(MetaSpace.People)) {
+ dmBadgeSpace = MetaSpace.People;
+ } else if (enabledMetaSpaces.has(MetaSpace.Home)) {
+ dmBadgeSpace = MetaSpace.Home;
+ }
+
this.spaceFilteredRooms.forEach((roomIds, s) => {
- if (this.allRoomsInHome && s === HOME_SPACE) return; // we'll be using the global notification state, skip
+ if (this.allRoomsInHome && s === MetaSpace.Home) return; // we'll be using the global notification state, skip
// Update NotificationStates
this.getNotificationState(s).setRooms(visibleRooms.filter(room => {
if (!roomIds.has(room.roomId) || room.isSpaceRoom()) return false;
- if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
- return s === HOME_SPACE;
+ if (dmBadgeSpace && DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
+ return s === dmBadgeSpace;
}
return true;
@@ -575,7 +619,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
// don't trigger a context switch when we are switching a space to match the chosen room
- this.setActiveSpace(parent || null, false);
+ this.setActiveSpace(parent?.roomId ?? MetaSpace.Home, false); // TODO
};
private onRoom = (room: Room, newMembership?: string, oldMembership?: string) => {
@@ -597,7 +641,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
const numSuggestedRooms = this._suggestedRooms.length;
this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId);
if (numSuggestedRooms !== this._suggestedRooms.length) {
- this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
+ this.emit(UPDATE_SUGGESTED_ROOMS, this._suggestedRooms);
}
// if the room currently being viewed was just joined then switch to its related space
@@ -622,10 +666,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
if (membership === "join" && room.roomId === RoomViewStore.getRoomId()) {
// if the user was looking at the space and then joined: select that space
- this.setActiveSpace(room, false);
- } else if (membership === "leave" && room.roomId === this.activeSpace?.roomId) {
+ this.setActiveSpace(room.roomId, false);
+ } else if (membership === "leave" && room.roomId === this.activeSpace) {
// user's active space has gone away, go back to home
- this.setActiveSpace(null, true);
+ this.goToFirstSpace(true);
}
};
@@ -633,7 +677,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
const rootSpaces = this.sortRootSpaces(this.rootSpaces);
if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) {
this.rootSpaces = rootSpaces;
- this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
+ this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces, this.enabledMetaSpaces);
}
}
@@ -648,7 +692,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
this.emit(room.roomId);
}
- if (room === this.activeSpace && // current space
+ if (room.roomId === this.activeSpace && // current space
this.matrixClient.getRoom(ev.getStateKey())?.getMyMembership() !== "join" && // target not joined
ev.getPrevContent().suggested !== ev.getContent().suggested // suggested flag changed
) {
@@ -694,7 +738,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
if (order !== lastOrder) {
this.notifyIfOrderChanged();
}
- } else if (ev.getType() === EventType.Tag && !this.allRoomsInHome) {
+ } else if (ev.getType() === EventType.Tag) {
// If the room was in favourites and now isn't or the opposite then update its position in the trees
const oldTags = lastEv?.getContent()?.tags || {};
const newTags = ev.getContent()?.tags || {};
@@ -728,9 +772,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
this.parentMap = new EnhancedMap();
this.notificationStateMap = new Map();
this.spaceFilteredRooms = new Map();
- this._activeSpace = null;
+ this._activeSpace = MetaSpace.Home; // set properly by onReady
this._suggestedRooms = [];
this._invitedSpaces = new Set();
+ this._enabledMetaSpaces = [];
}
protected async onNotReady() {
@@ -760,16 +805,27 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
?.["m.room_versions"]?.["org.matrix.msc3244.room_capabilities"]?.["restricted"];
});
+ const enabledMetaSpaces = SettingsStore.getValue("Spaces.enabledMetaSpaces");
+ this._enabledMetaSpaces = metaSpaceOrder.filter(k => enabledMetaSpaces[k]) as MetaSpace[];
+
await this.onSpaceUpdate(); // trigger an initial update
// restore selected state from last session if any and still valid
const lastSpaceId = window.localStorage.getItem(ACTIVE_SPACE_LS_KEY);
- if (lastSpaceId) {
+ if (lastSpaceId && (
+ lastSpaceId[0] === "!" ? this.matrixClient.getRoom(lastSpaceId) : enabledMetaSpaces[lastSpaceId]
+ )) {
// don't context switch here as it may break permalinks
- this.setActiveSpace(this.matrixClient.getRoom(lastSpaceId), false);
+ this.setActiveSpace(lastSpaceId, false);
+ } else {
+ this.goToFirstSpace();
}
}
+ private goToFirstSpace(contextSwitch = false) {
+ this.setActiveSpace(this.enabledMetaSpaces[0] ?? this.spacePanelSpaces[0]?.roomId, contextSwitch);
+ }
+
protected async onAction(payload: ActionPayload) {
if (!spacesEnabled) return;
switch (payload.action) {
@@ -783,9 +839,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
if (room?.isSpaceRoom()) {
// Don't context switch when navigating to the space room
// as it will cause you to end up in the wrong room
- this.setActiveSpace(room, false);
+ this.setActiveSpace(room.roomId, false);
} else if (
- (!this.allRoomsInHome || this.activeSpace) &&
+ (!this.allRoomsInHome || this.activeSpace[0] === "!") &&
!this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)
) {
this.switchToRelatedSpace(roomId);
@@ -799,31 +855,54 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
case "after_leave_room":
- if (this._activeSpace && payload.room_id === this._activeSpace.roomId) {
- this.setActiveSpace(null, false);
+ if (this._activeSpace[0] === "!" && payload.room_id === this._activeSpace) {
+ // User has left the current space, go to first space
+ this.goToFirstSpace();
}
break;
- case Action.SwitchSpace:
- // 1 is Home, 2-9 are the spaces after Home
- if (payload.num === 1) {
- this.setActiveSpace(null);
- } else if (payload.num > 0 && this.spacePanelSpaces.length > payload.num - 2) {
- this.setActiveSpace(this.spacePanelSpaces[payload.num - 2]);
+ case Action.SwitchSpace: {
+ // Metaspaces start at 1, Spaces follow
+ if (payload.num < 1 || payload.num > 9) break;
+ const numMetaSpaces = this.enabledMetaSpaces.length;
+ if (payload.num <= numMetaSpaces) {
+ this.setActiveSpace(this.enabledMetaSpaces[payload.num - 1]);
+ } else if (this.spacePanelSpaces.length > payload.num - numMetaSpaces - 1) {
+ this.setActiveSpace(this.spacePanelSpaces[payload.num - numMetaSpaces - 1].roomId);
}
break;
+ }
case Action.SettingUpdated: {
const settingUpdatedPayload = payload as SettingUpdatedPayload;
- if (settingUpdatedPayload.settingName === "Spaces.allRoomsInHome") {
- const newValue = SettingsStore.getValue("Spaces.allRoomsInHome");
- if (this.allRoomsInHome !== newValue) {
- this._allRoomsInHome = newValue;
- this.emit(UPDATE_HOME_BEHAVIOUR, this.allRoomsInHome);
- this.rebuild(); // rebuild everything
+ switch (settingUpdatedPayload.settingName) {
+ case "Spaces.allRoomsInHome": {
+ const newValue = SettingsStore.getValue("Spaces.allRoomsInHome");
+ if (this.allRoomsInHome !== newValue) {
+ this._allRoomsInHome = newValue;
+ this.emit(UPDATE_HOME_BEHAVIOUR, this.allRoomsInHome);
+ this.rebuild(); // rebuild everything
+ }
+ break;
+ }
+
+ case "Spaces.enabledMetaSpaces": {
+ const newValue = SettingsStore.getValue("Spaces.enabledMetaSpaces");
+ const enabledMetaSpaces = metaSpaceOrder.filter(k => newValue[k]) as MetaSpace[];
+ if (arrayHasDiff(this._enabledMetaSpaces, enabledMetaSpaces)) {
+ this._enabledMetaSpaces = enabledMetaSpaces;
+ // if a metaspace currently being viewed was remove, go to another one
+ if (this.activeSpace[0] !== "!" &&
+ !enabledMetaSpaces.includes(this.activeSpace as MetaSpace)
+ ) {
+ this.goToFirstSpace();
+ }
+ this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces, this.enabledMetaSpaces);
+ this.rebuild(); // rebuild everything
+ }
+ break;
}
}
- break;
}
}
}
diff --git a/src/stores/SpaceTreeLevelLayoutStore.ts b/src/stores/spaces/SpaceTreeLevelLayoutStore.ts
similarity index 100%
rename from src/stores/SpaceTreeLevelLayoutStore.ts
rename to src/stores/spaces/SpaceTreeLevelLayoutStore.ts
diff --git a/src/stores/spaces/index.ts b/src/stores/spaces/index.ts
new file mode 100644
index 0000000000..7816932b05
--- /dev/null
+++ b/src/stores/spaces/index.ts
@@ -0,0 +1,40 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { Room } from "matrix-js-sdk/src/models/room";
+import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces";
+
+// The consts & types are moved out here to prevent cyclical imports
+
+export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
+export const UPDATE_INVITED_SPACES = Symbol("invited-spaces");
+export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
+export const UPDATE_HOME_BEHAVIOUR = Symbol("home-behaviour");
+export const UPDATE_SUGGESTED_ROOMS = Symbol("suggested-rooms");
+// Space Key will be emitted when a Space's children change
+
+export enum MetaSpace {
+ Home = "home-space",
+ Favourites = "favourites-space",
+ People = "people-space",
+ Orphans = "orphans-space",
+}
+
+export type SpaceKey = MetaSpace | Room["roomId"];
+
+export interface ISuggestedRoom extends IHierarchyRoom {
+ viaServers: string[];
+}
diff --git a/src/utils/RoomUpgrade.ts b/src/utils/RoomUpgrade.ts
index b9ea93d7fc..902c5d00ca 100644
--- a/src/utils/RoomUpgrade.ts
+++ b/src/utils/RoomUpgrade.ts
@@ -21,7 +21,7 @@ import { inviteUsersToRoom } from "../RoomInvite";
import Modal, { IHandle } from "../Modal";
import { _t } from "../languageHandler";
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
-import SpaceStore from "../stores/SpaceStore";
+import SpaceStore from "../stores/spaces/SpaceStore";
import Spinner from "../components/views/elements/Spinner";
import { logger } from "matrix-js-sdk/src/logger";
diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts
index ccbf0af402..d6d0566811 100644
--- a/test/stores/SpaceStore-test.ts
+++ b/test/stores/SpaceStore-test.ts
@@ -18,13 +18,16 @@ import { EventEmitter } from "events";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
+import "./enable-metaspaces-labs";
import "../skinned-sdk"; // Must be first for skinning to work
-import SpaceStore, {
+import SpaceStore from "../../src/stores/spaces/SpaceStore";
+import {
+ MetaSpace,
UPDATE_HOME_BEHAVIOUR,
UPDATE_INVITED_SPACES,
UPDATE_SELECTED_SPACE,
UPDATE_TOP_LEVEL_SPACES,
-} from "../../src/stores/SpaceStore";
+} from "../../src/stores/spaces";
import * as testUtils from "../utils/test-utils";
import { mkEvent, stubClient } from "../test-utils";
import DMRoomMap from "../../src/utils/DMRoomMap";
@@ -90,10 +93,18 @@ describe("SpaceStore", () => {
await emitProm;
};
- beforeEach(() => {
+ beforeEach(async () => {
jest.runAllTimers(); // run async dispatch
client.getVisibleRooms.mockReturnValue(rooms = []);
+
+ await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, {
+ [MetaSpace.Home]: true,
+ [MetaSpace.Favourites]: true,
+ [MetaSpace.People]: true,
+ [MetaSpace.Orphans]: true,
+ });
});
+
afterEach(async () => {
await testUtils.resetAsyncStoreWithClient(store);
});
@@ -377,69 +388,84 @@ describe("SpaceStore", () => {
});
it("home space contains orphaned rooms", () => {
- expect(store.getSpaceFilteredRoomIds(null).has(orphan1)).toBeTruthy();
- expect(store.getSpaceFilteredRoomIds(null).has(orphan2)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(orphan1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(orphan2)).toBeTruthy();
});
- it("home space contains favourites", () => {
- expect(store.getSpaceFilteredRoomIds(null).has(fav1)).toBeTruthy();
- expect(store.getSpaceFilteredRoomIds(null).has(fav2)).toBeTruthy();
- expect(store.getSpaceFilteredRoomIds(null).has(fav3)).toBeTruthy();
+ it("home space does not contain all favourites", () => {
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(fav1)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(fav2)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(fav3)).toBeFalsy();
});
it("home space contains dm rooms", () => {
- expect(store.getSpaceFilteredRoomIds(null).has(dm1)).toBeTruthy();
- expect(store.getSpaceFilteredRoomIds(null).has(dm2)).toBeTruthy();
- expect(store.getSpaceFilteredRoomIds(null).has(dm3)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(dm1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(dm2)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(dm3)).toBeTruthy();
});
it("home space contains invites", () => {
- expect(store.getSpaceFilteredRoomIds(null).has(invite1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite1)).toBeTruthy();
});
it("home space contains invites even if they are also shown in a space", () => {
- expect(store.getSpaceFilteredRoomIds(null).has(invite2)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite2)).toBeTruthy();
});
it("all rooms space does contain rooms/low priority even if they are also shown in a space", async () => {
await setShowAllRooms(true);
- expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(room1)).toBeTruthy();
+ });
+
+ it("favourites space does contain favourites even if they are also shown in a space", async () => {
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Favourites).has(fav1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Favourites).has(fav2)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Favourites).has(fav3)).toBeTruthy();
+ });
+
+ it("people space does contain people even if they are also shown in a space", async () => {
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.People).has(dm1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.People).has(dm2)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.People).has(dm3)).toBeTruthy();
+ });
+
+ it("orphans space does contain orphans even if they are also shown in all rooms", async () => {
+ await setShowAllRooms(true);
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Orphans).has(orphan1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Orphans).has(orphan2)).toBeTruthy();
});
it("home space doesn't contain rooms/low priority if they are also shown in a space", async () => {
await setShowAllRooms(false);
- expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(room1)).toBeFalsy();
});
it("space contains child rooms", () => {
- const space = client.getRoom(space1);
- expect(store.getSpaceFilteredRoomIds(space).has(fav1)).toBeTruthy();
- expect(store.getSpaceFilteredRoomIds(space).has(room1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(space1).has(fav1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(space1).has(room1)).toBeTruthy();
});
it("space contains child favourites", () => {
- const space = client.getRoom(space2);
- expect(store.getSpaceFilteredRoomIds(space).has(fav1)).toBeTruthy();
- expect(store.getSpaceFilteredRoomIds(space).has(fav2)).toBeTruthy();
- expect(store.getSpaceFilteredRoomIds(space).has(fav3)).toBeTruthy();
- expect(store.getSpaceFilteredRoomIds(space).has(room1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(space2).has(fav1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(space2).has(fav2)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(space2).has(fav3)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(space2).has(room1)).toBeTruthy();
});
it("space contains child invites", () => {
- const space = client.getRoom(space3);
- expect(store.getSpaceFilteredRoomIds(space).has(invite2)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(space3).has(invite2)).toBeTruthy();
});
it("spaces contain dms which you have with members of that space", () => {
- expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm1)).toBeTruthy();
- expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm1)).toBeFalsy();
- expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm1)).toBeFalsy();
- expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm2)).toBeFalsy();
- expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm2)).toBeTruthy();
- expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm2)).toBeFalsy();
- expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm3)).toBeFalsy();
- expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm3)).toBeFalsy();
- expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm3)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(space1).has(dm1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(space2).has(dm1)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(space3).has(dm1)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(space1).has(dm2)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(space2).has(dm2)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(space3).has(dm2)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(space1).has(dm3)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(space2).has(dm3)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(space3).has(dm3)).toBeFalsy();
});
it("dms are only added to Notification States for only the Home Space", () => {
@@ -491,11 +517,11 @@ describe("SpaceStore", () => {
});
it("honours m.space.parent if sender has permission in parent space", () => {
- expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(room2)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(space2).has(room2)).toBeTruthy();
});
it("does not honour m.space.parent if sender does not have permission in parent space", () => {
- expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(room3)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(space3).has(room3)).toBeFalsy();
});
});
});
@@ -586,8 +612,8 @@ describe("SpaceStore", () => {
expect(store.invitedSpaces).toStrictEqual([]);
expect(store.getChildSpaces(space1)).toStrictEqual([]);
expect(store.getChildRooms(space1)).toStrictEqual([]);
- expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(invite1)).toBeFalsy();
- expect(store.getSpaceFilteredRoomIds(null).has(invite1)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(space1).has(invite1)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite1)).toBeFalsy();
const invite = mkRoom(invite1);
invite.getMyMembership.mockReturnValue("invite");
@@ -599,8 +625,8 @@ describe("SpaceStore", () => {
expect(store.invitedSpaces).toStrictEqual([]);
expect(store.getChildSpaces(space1)).toStrictEqual([]);
expect(store.getChildRooms(space1)).toStrictEqual([invite]);
- expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(invite1)).toBeTruthy();
- expect(store.getSpaceFilteredRoomIds(null).has(invite1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(space1).has(invite1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite1)).toBeTruthy();
});
});
@@ -614,49 +640,46 @@ describe("SpaceStore", () => {
]);
mkSpace(space3).getMyMembership.mockReturnValue("invite");
await run();
- store.setActiveSpace(null);
- expect(store.activeSpace).toBe(null);
+ store.setActiveSpace(MetaSpace.Home);
+ expect(store.activeSpace).toBe(MetaSpace.Home);
});
afterEach(() => {
fn.mockClear();
});
it("switch to home space", async () => {
- store.setActiveSpace(client.getRoom(space1));
+ store.setActiveSpace(space1);
fn.mockClear();
- store.setActiveSpace(null);
- expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, null);
- expect(store.activeSpace).toBe(null);
+ store.setActiveSpace(MetaSpace.Home);
+ expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, MetaSpace.Home);
+ expect(store.activeSpace).toBe(MetaSpace.Home);
});
it("switch to invited space", async () => {
- const space = client.getRoom(space3);
- store.setActiveSpace(space);
- expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space);
- expect(store.activeSpace).toBe(space);
+ store.setActiveSpace(space3);
+ expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space3);
+ expect(store.activeSpace).toBe(space3);
});
it("switch to top level space", async () => {
- const space = client.getRoom(space1);
- store.setActiveSpace(space);
- expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space);
- expect(store.activeSpace).toBe(space);
+ store.setActiveSpace(space1);
+ expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space1);
+ expect(store.activeSpace).toBe(space1);
});
it("switch to subspace", async () => {
- const space = client.getRoom(space2);
- store.setActiveSpace(space);
- expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space);
- expect(store.activeSpace).toBe(space);
+ store.setActiveSpace(space2);
+ expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space2);
+ expect(store.activeSpace).toBe(space2);
});
it("switch to unknown space is a nop", async () => {
- expect(store.activeSpace).toBe(null);
+ expect(store.activeSpace).toBe(MetaSpace.Home);
const space = client.getRoom(room1); // not a space
- store.setActiveSpace(space);
- expect(fn).not.toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space);
- expect(store.activeSpace).toBe(null);
+ store.setActiveSpace(space.roomId);
+ expect(fn).not.toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space.roomId);
+ expect(store.activeSpace).toBe(MetaSpace.Home);
});
});
@@ -678,6 +701,7 @@ describe("SpaceStore", () => {
});
afterEach(() => {
localStorage.clear();
+ localStorage.setItem("mx_labs_feature_feature_spaces_metaspaces", "true");
defaultDispatcher.unregister(dispatcherRef);
});
@@ -687,59 +711,59 @@ describe("SpaceStore", () => {
};
it("last viewed room in target space is the current viewed and in both spaces", async () => {
- store.setActiveSpace(client.getRoom(space1));
+ store.setActiveSpace(space1);
viewRoom(room2);
- store.setActiveSpace(client.getRoom(space2));
+ store.setActiveSpace(space2);
viewRoom(room2);
- store.setActiveSpace(client.getRoom(space1));
+ store.setActiveSpace(space1);
expect(getCurrentRoom()).toBe(room2);
});
it("last viewed room in target space is in the current space", async () => {
- store.setActiveSpace(client.getRoom(space1));
+ store.setActiveSpace(space1);
viewRoom(room2);
- store.setActiveSpace(client.getRoom(space2));
+ store.setActiveSpace(space2);
expect(getCurrentRoom()).toBe(space2);
- store.setActiveSpace(client.getRoom(space1));
+ store.setActiveSpace(space1);
expect(getCurrentRoom()).toBe(room2);
});
it("last viewed room in target space is not in the current space", async () => {
- store.setActiveSpace(client.getRoom(space1));
+ store.setActiveSpace(space1);
viewRoom(room1);
- store.setActiveSpace(client.getRoom(space2));
+ store.setActiveSpace(space2);
viewRoom(room2);
- store.setActiveSpace(client.getRoom(space1));
+ store.setActiveSpace(space1);
expect(getCurrentRoom()).toBe(room1);
});
it("last viewed room is target space is not known", async () => {
- store.setActiveSpace(client.getRoom(space1));
+ store.setActiveSpace(space1);
viewRoom(room1);
localStorage.setItem(`mx_space_context_${space2}`, orphan2);
- store.setActiveSpace(client.getRoom(space2));
+ store.setActiveSpace(space2);
expect(getCurrentRoom()).toBe(space2);
});
it("last viewed room is target space is no longer in that space", async () => {
- store.setActiveSpace(client.getRoom(space1));
+ store.setActiveSpace(space1);
viewRoom(room1);
localStorage.setItem(`mx_space_context_${space2}`, room1);
- store.setActiveSpace(client.getRoom(space2));
+ store.setActiveSpace(space2);
expect(getCurrentRoom()).toBe(space2); // Space home instead of room1
});
it("no last viewed room in target space", async () => {
- store.setActiveSpace(client.getRoom(space1));
+ store.setActiveSpace(space1);
viewRoom(room1);
- store.setActiveSpace(client.getRoom(space2));
+ store.setActiveSpace(space2);
expect(getCurrentRoom()).toBe(space2);
});
it("no last viewed room in home space", async () => {
- store.setActiveSpace(client.getRoom(space1));
+ store.setActiveSpace(space1);
viewRoom(room1);
- store.setActiveSpace(null);
+ store.setActiveSpace(MetaSpace.Home);
expect(getCurrentRoom()).toBeNull(); // Home
});
});
@@ -767,38 +791,51 @@ describe("SpaceStore", () => {
it("no switch required, room is in current space", async () => {
viewRoom(room1);
- store.setActiveSpace(client.getRoom(space1), false);
+ store.setActiveSpace(space1, false);
viewRoom(room2);
- expect(store.activeSpace).toBe(client.getRoom(space1));
+ expect(store.activeSpace).toBe(space1);
});
it("switch to canonical parent space for room", async () => {
viewRoom(room1);
- store.setActiveSpace(client.getRoom(space2), false);
+ store.setActiveSpace(space2, false);
viewRoom(room2);
- expect(store.activeSpace).toBe(client.getRoom(space2));
+ expect(store.activeSpace).toBe(space2);
});
it("switch to first containing space for room", async () => {
viewRoom(room2);
- store.setActiveSpace(client.getRoom(space2), false);
+ store.setActiveSpace(space2, false);
viewRoom(room3);
- expect(store.activeSpace).toBe(client.getRoom(space1));
+ expect(store.activeSpace).toBe(space1);
});
it("switch to home for orphaned room", async () => {
viewRoom(room1);
- store.setActiveSpace(client.getRoom(space1), false);
+ store.setActiveSpace(space1, false);
viewRoom(orphan1);
- expect(store.activeSpace).toBeNull();
+ expect(store.activeSpace).toBe(MetaSpace.Home);
+ });
+
+ it("switch to first space when selected metaspace is disabled", async () => {
+ store.setActiveSpace(MetaSpace.People, false);
+ expect(store.activeSpace).toBe(MetaSpace.People);
+ await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, {
+ [MetaSpace.Home]: false,
+ [MetaSpace.Favourites]: true,
+ [MetaSpace.People]: false,
+ [MetaSpace.Orphans]: true,
+ });
+ jest.runAllTimers();
+ expect(store.activeSpace).toBe(MetaSpace.Favourites);
});
it("when switching rooms in the all rooms home space don't switch to related space", async () => {
await setShowAllRooms(true);
viewRoom(room2);
- store.setActiveSpace(null, false);
+ store.setActiveSpace(MetaSpace.Home, false);
viewRoom(room1);
- expect(store.activeSpace).toBeNull();
+ expect(store.activeSpace).toBe(MetaSpace.Home);
});
});
diff --git a/test/stores/enable-metaspaces-labs.ts b/test/stores/enable-metaspaces-labs.ts
new file mode 100644
index 0000000000..f22132a0d6
--- /dev/null
+++ b/test/stores/enable-metaspaces-labs.ts
@@ -0,0 +1,17 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+localStorage.setItem("mx_labs_feature_feature_spaces_metaspaces", "true");
diff --git a/test/stores/room-list/SpaceWatcher-test.ts b/test/stores/room-list/SpaceWatcher-test.ts
index cb2394349a..42ffbe5333 100644
--- a/test/stores/room-list/SpaceWatcher-test.ts
+++ b/test/stores/room-list/SpaceWatcher-test.ts
@@ -14,17 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import "../enable-metaspaces-labs";
import "../../skinned-sdk"; // Must be first for skinning to work
import { SpaceWatcher } from "../../../src/stores/room-list/SpaceWatcher";
import type { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore";
import SettingsStore from "../../../src/settings/SettingsStore";
-import SpaceStore, { UPDATE_HOME_BEHAVIOUR } from "../../../src/stores/SpaceStore";
+import SpaceStore from "../../../src/stores/spaces/SpaceStore";
+import { MetaSpace, UPDATE_HOME_BEHAVIOUR } from "../../../src/stores/spaces";
import { stubClient } from "../../test-utils";
import { SettingLevel } from "../../../src/settings/SettingLevel";
+import * as testUtils from "../../utils/test-utils";
import { setupAsyncStoreWithClient } from "../../utils/test-utils";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
-import * as testUtils from "../../utils/test-utils";
import { SpaceFilterCondition } from "../../../src/stores/room-list/filters/SpaceFilterCondition";
+import DMRoomMap from "../../../src/utils/DMRoomMap";
let filter: SpaceFilterCondition = null;
@@ -33,8 +36,13 @@ const mockRoomListStore = {
removeFilter: () => filter = null,
} as unknown as RoomListStoreClass;
-const space1Id = "!space1:server";
-const space2Id = "!space2:server";
+const getUserIdForRoomId = jest.fn();
+const getDMRoomsForUserId = jest.fn();
+// @ts-ignore
+DMRoomMap.sharedInstance = { getUserIdForRoomId, getDMRoomsForUserId };
+
+const space1 = "!space1:server";
+const space2 = "!space2:server";
describe("SpaceWatcher", () => {
stubClient();
@@ -50,17 +58,21 @@ describe("SpaceWatcher", () => {
await testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR);
};
- let space1;
- let space2;
-
beforeEach(async () => {
filter = null;
store.removeAllListeners();
- store.setActiveSpace(null);
+ store.setActiveSpace(MetaSpace.Home);
client.getVisibleRooms.mockReturnValue(rooms = []);
- space1 = mkSpace(space1Id);
- space2 = mkSpace(space2Id);
+ mkSpace(space1);
+ mkSpace(space2);
+
+ await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, {
+ [MetaSpace.Home]: true,
+ [MetaSpace.Favourites]: true,
+ [MetaSpace.People]: true,
+ [MetaSpace.Orphans]: true,
+ });
client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId));
await setupAsyncStoreWithClient(store, client);
@@ -80,14 +92,14 @@ describe("SpaceWatcher", () => {
expect(filter).toBeNull();
});
- it("sets space=null filter for all -> home transition", async () => {
+ it("sets space=Home filter for all -> home transition", async () => {
await setShowAllRooms(true);
new SpaceWatcher(mockRoomListStore);
await setShowAllRooms(false);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
- expect(filter["space"]).toBeNull();
+ expect(filter["space"]).toBe(MetaSpace.Home);
});
it("sets filter correctly for all -> space transition", async () => {
@@ -126,7 +138,43 @@ describe("SpaceWatcher", () => {
SpaceStore.instance.setActiveSpace(space1);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(space1);
- SpaceStore.instance.setActiveSpace(null);
+ SpaceStore.instance.setActiveSpace(MetaSpace.Home);
+
+ expect(filter).toBeNull();
+ });
+
+ it("removes filter for favourites -> all transition", async () => {
+ await setShowAllRooms(true);
+ new SpaceWatcher(mockRoomListStore);
+
+ SpaceStore.instance.setActiveSpace(MetaSpace.Favourites);
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(MetaSpace.Favourites);
+ SpaceStore.instance.setActiveSpace(MetaSpace.Home);
+
+ expect(filter).toBeNull();
+ });
+
+ it("removes filter for people -> all transition", async () => {
+ await setShowAllRooms(true);
+ new SpaceWatcher(mockRoomListStore);
+
+ SpaceStore.instance.setActiveSpace(MetaSpace.People);
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(MetaSpace.People);
+ SpaceStore.instance.setActiveSpace(MetaSpace.Home);
+
+ expect(filter).toBeNull();
+ });
+
+ it("removes filter for orphans -> all transition", async () => {
+ await setShowAllRooms(true);
+ new SpaceWatcher(mockRoomListStore);
+
+ SpaceStore.instance.setActiveSpace(MetaSpace.Orphans);
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(MetaSpace.Orphans);
+ SpaceStore.instance.setActiveSpace(MetaSpace.Home);
expect(filter).toBeNull();
});
@@ -138,10 +186,36 @@ describe("SpaceWatcher", () => {
new SpaceWatcher(mockRoomListStore);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(space1);
- SpaceStore.instance.setActiveSpace(null);
+ SpaceStore.instance.setActiveSpace(MetaSpace.Home);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
- expect(filter["space"]).toBe(null);
+ expect(filter["space"]).toBe(MetaSpace.Home);
+ });
+
+ it("updates filter correctly for space -> orphans transition", async () => {
+ await setShowAllRooms(false);
+ SpaceStore.instance.setActiveSpace(space1);
+
+ new SpaceWatcher(mockRoomListStore);
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(space1);
+ SpaceStore.instance.setActiveSpace(MetaSpace.Orphans);
+
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(MetaSpace.Orphans);
+ });
+
+ it("updates filter correctly for orphans -> people transition", async () => {
+ await setShowAllRooms(false);
+ SpaceStore.instance.setActiveSpace(MetaSpace.Orphans);
+
+ new SpaceWatcher(mockRoomListStore);
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(MetaSpace.Orphans);
+ SpaceStore.instance.setActiveSpace(MetaSpace.People);
+
+ expect(filter).toBeInstanceOf(SpaceFilterCondition);
+ expect(filter["space"]).toBe(MetaSpace.People);
});
it("updates filter correctly for space -> space transition", async () => {