Space preferences for whether or not you see DMs in a Space (#7250)
This commit is contained in:
parent
5ee356daaa
commit
39c4b78371
22 changed files with 911 additions and 350 deletions
|
@ -112,6 +112,7 @@
|
||||||
@import "./views/dialogs/_SettingsDialog.scss";
|
@import "./views/dialogs/_SettingsDialog.scss";
|
||||||
@import "./views/dialogs/_ShareDialog.scss";
|
@import "./views/dialogs/_ShareDialog.scss";
|
||||||
@import "./views/dialogs/_SlashCommandHelpDialog.scss";
|
@import "./views/dialogs/_SlashCommandHelpDialog.scss";
|
||||||
|
@import "./views/dialogs/_SpacePreferencesDialog.scss";
|
||||||
@import "./views/dialogs/_SpaceSettingsDialog.scss";
|
@import "./views/dialogs/_SpaceSettingsDialog.scss";
|
||||||
@import "./views/dialogs/_SpotlightDialog.scss";
|
@import "./views/dialogs/_SpotlightDialog.scss";
|
||||||
@import "./views/dialogs/_TabbedIntegrationManagerDialog.scss";
|
@import "./views/dialogs/_TabbedIntegrationManagerDialog.scss";
|
||||||
|
|
|
@ -396,6 +396,10 @@ $activeBorderColor: $primary-content;
|
||||||
mask-image: url('$(res)/img/element-icons/roomlist/search.svg');
|
mask-image: url('$(res)/img/element-icons/roomlist/search.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SpacePanel_iconPreferences::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/settings/preference.svg');
|
||||||
|
}
|
||||||
|
|
||||||
.mx_SpacePanel_noIcon {
|
.mx_SpacePanel_noIcon {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,10 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Not actually a component but things shared by settings components
|
// Not actually a component but things shared by settings components
|
||||||
.mx_UserSettingsDialog, .mx_RoomSettingsDialog, .mx_SpaceSettingsDialog {
|
.mx_UserSettingsDialog,
|
||||||
|
.mx_RoomSettingsDialog,
|
||||||
|
.mx_SpaceSettingsDialog,
|
||||||
|
.mx_SpacePreferencesDialog {
|
||||||
width: 90vw;
|
width: 90vw;
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
// set the height too since tabbed view scrolls itself.
|
// set the height too since tabbed view scrolls itself.
|
||||||
|
|
34
res/css/views/dialogs/_SpacePreferencesDialog.scss
Normal file
34
res/css/views/dialogs/_SpacePreferencesDialog.scss
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
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_SpacePreferencesDialog {
|
||||||
|
width: 700px;
|
||||||
|
height: 400px;
|
||||||
|
|
||||||
|
.mx_TabbedView .mx_SettingsTab {
|
||||||
|
min-width: unset;
|
||||||
|
|
||||||
|
.mx_SettingsTab_section {
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
|
||||||
|
.mx_Checkbox + p {
|
||||||
|
color: $secondary-content;
|
||||||
|
margin: 0 20px 0 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ import {
|
||||||
showCreateNewRoom,
|
showCreateNewRoom,
|
||||||
showCreateNewSubspace,
|
showCreateNewSubspace,
|
||||||
showSpaceInvite,
|
showSpaceInvite,
|
||||||
|
showSpacePreferences,
|
||||||
showSpaceSettings,
|
showSpaceSettings,
|
||||||
} from "../../../utils/space";
|
} from "../../../utils/space";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
@ -166,6 +167,14 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onPreferencesClick = (ev: ButtonEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
showSpacePreferences(space);
|
||||||
|
onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
const onExploreRoomsClick = (ev: ButtonEvent) => {
|
const onExploreRoomsClick = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
@ -193,6 +202,11 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
|
||||||
label={canAddRooms ? _t("Manage & explore rooms") : _t("Explore rooms")}
|
label={canAddRooms ? _t("Manage & explore rooms") : _t("Explore rooms")}
|
||||||
onClick={onExploreRoomsClick}
|
onClick={onExploreRoomsClick}
|
||||||
/>
|
/>
|
||||||
|
<IconizedContextMenuOption
|
||||||
|
iconClassName="mx_SpacePanel_iconPreferences"
|
||||||
|
label={_t("Preferences")}
|
||||||
|
onClick={onPreferencesClick}
|
||||||
|
/>
|
||||||
{ settingsOption }
|
{ settingsOption }
|
||||||
{ leaveOption }
|
{ leaveOption }
|
||||||
{ devtoolsOption }
|
{ devtoolsOption }
|
||||||
|
|
99
src/components/views/dialogs/SpacePreferencesDialog.tsx
Normal file
99
src/components/views/dialogs/SpacePreferencesDialog.tsx
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
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 { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
import { _t, _td } from '../../../languageHandler';
|
||||||
|
import BaseDialog from "../dialogs/BaseDialog";
|
||||||
|
import { IDialogProps } from "./IDialogProps";
|
||||||
|
import TabbedView, { Tab } from "../../structures/TabbedView";
|
||||||
|
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||||
|
import { useSettingValue } from "../../../hooks/useSettings";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
|
import RoomName from "../elements/RoomName";
|
||||||
|
|
||||||
|
export enum SpacePreferenceTab {
|
||||||
|
Appearance = "SPACE_PREFERENCE_APPEARANCE_TAB",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
space: Room;
|
||||||
|
initialTabId?: SpacePreferenceTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SpacePreferencesAppearanceTab = ({ space }: Pick<IProps, "space">) => {
|
||||||
|
const showPeople = useSettingValue("Spaces.showPeopleInSpace", space.roomId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_SettingsTab">
|
||||||
|
<div className="mx_SettingsTab_heading">{ _t("Sections to show") }</div>
|
||||||
|
|
||||||
|
<div className="mx_SettingsTab_section">
|
||||||
|
<StyledCheckbox
|
||||||
|
checked={!!showPeople}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
SettingsStore.setValue(
|
||||||
|
"Spaces.showPeopleInSpace",
|
||||||
|
space.roomId,
|
||||||
|
SettingLevel.ROOM_ACCOUNT,
|
||||||
|
!showPeople,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ _t("People") }
|
||||||
|
</StyledCheckbox>
|
||||||
|
<p>
|
||||||
|
{ _t("This groups your chats with members of this space. " +
|
||||||
|
"Turning this off will hide those chats from your view of %(spaceName)s.", {
|
||||||
|
spaceName: space.name,
|
||||||
|
}) }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SpacePreferencesDialog: React.FC<IProps> = ({ space, initialTabId, onFinished }) => {
|
||||||
|
const tabs = [
|
||||||
|
new Tab(
|
||||||
|
SpacePreferenceTab.Appearance,
|
||||||
|
_td("Appearance"),
|
||||||
|
"mx_RoomSettingsDialog_notificationsIcon",
|
||||||
|
<SpacePreferencesAppearanceTab space={space} />,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog
|
||||||
|
className="mx_SpacePreferencesDialog"
|
||||||
|
hasCancel
|
||||||
|
onFinished={onFinished}
|
||||||
|
title={_t("Preferences")}
|
||||||
|
fixedWidth={false}
|
||||||
|
>
|
||||||
|
<h4>
|
||||||
|
<RoomName room={space} />
|
||||||
|
</h4>
|
||||||
|
<div className="mx_SettingsDialog_content">
|
||||||
|
<TabbedView tabs={tabs} initialTabId={initialTabId} />
|
||||||
|
</div>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SpacePreferencesDialog;
|
|
@ -52,7 +52,7 @@ export enum UserTab {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps extends IDialogProps {
|
interface IProps extends IDialogProps {
|
||||||
initialTabId?: string;
|
initialTabId?: UserTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
|
|
@ -129,7 +129,7 @@ const NewRoomIntro = () => {
|
||||||
let parentSpace: Room;
|
let parentSpace: Room;
|
||||||
if (
|
if (
|
||||||
SpaceStore.instance.activeSpaceRoom?.canInvite(cli.getUserId()) &&
|
SpaceStore.instance.activeSpaceRoom?.canInvite(cli.getUserId()) &&
|
||||||
SpaceStore.instance.getSpaceFilteredRoomIds(SpaceStore.instance.activeSpace).has(room.roomId)
|
SpaceStore.instance.isRoomInSpace(SpaceStore.instance.activeSpace, room.roomId)
|
||||||
) {
|
) {
|
||||||
parentSpace = SpaceStore.instance.activeSpaceRoom;
|
parentSpace = SpaceStore.instance.activeSpaceRoom;
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,7 @@ import {
|
||||||
SpaceKey,
|
SpaceKey,
|
||||||
UPDATE_SUGGESTED_ROOMS,
|
UPDATE_SUGGESTED_ROOMS,
|
||||||
UPDATE_SELECTED_SPACE,
|
UPDATE_SELECTED_SPACE,
|
||||||
|
isMetaSpace,
|
||||||
} from "../../../stores/spaces";
|
} from "../../../stores/spaces";
|
||||||
import { shouldShowSpaceInvite, showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../../utils/space";
|
import { shouldShowSpaceInvite, showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../../utils/space";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
@ -62,6 +63,7 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||||
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void;
|
onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void;
|
||||||
|
@ -493,10 +495,10 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onExplore = () => {
|
private onExplore = () => {
|
||||||
if (this.props.activeSpace[0] === "!") {
|
if (!isMetaSpace(this.props.activeSpace)) {
|
||||||
defaultDispatcher.dispatch({
|
defaultDispatcher.dispatch({
|
||||||
action: "view_room",
|
action: "view_room",
|
||||||
room_id: SpaceStore.instance.activeSpace,
|
room_id: this.props.activeSpace,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
|
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
|
||||||
|
@ -611,7 +613,12 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
if (
|
if (
|
||||||
(this.props.activeSpace === MetaSpace.Favourites && orderedTagId !== DefaultTagID.Favourite) ||
|
(this.props.activeSpace === MetaSpace.Favourites && orderedTagId !== DefaultTagID.Favourite) ||
|
||||||
(this.props.activeSpace === MetaSpace.People && orderedTagId !== DefaultTagID.DM) ||
|
(this.props.activeSpace === MetaSpace.People && orderedTagId !== DefaultTagID.DM) ||
|
||||||
(this.props.activeSpace === MetaSpace.Orphans && orderedTagId === DefaultTagID.DM)
|
(this.props.activeSpace === MetaSpace.Orphans && orderedTagId === DefaultTagID.DM) ||
|
||||||
|
(
|
||||||
|
!isMetaSpace(this.props.activeSpace) &&
|
||||||
|
orderedTagId === DefaultTagID.DM &&
|
||||||
|
!SettingsStore.getValue("Spaces.showPeopleInSpace", this.props.activeSpace)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
alwaysVisible = false;
|
alwaysVisible = false;
|
||||||
}
|
}
|
||||||
|
@ -668,7 +675,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
kind="link"
|
kind="link"
|
||||||
onClick={this.onExplore}
|
onClick={this.onExplore}
|
||||||
>
|
>
|
||||||
{ this.props.activeSpace[0] === "!" ? _t("Explore rooms") : _t("Explore all public rooms") }
|
{ !isMetaSpace(this.props.activeSpace) ? _t("Explore rooms") : _t("Explore all public rooms") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,7 +176,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
);
|
);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
collapsed: collapsed,
|
collapsed,
|
||||||
childSpaces: this.childSpaces,
|
childSpaces: this.childSpaces,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2727,6 +2727,8 @@
|
||||||
"Link to selected message": "Link to selected message",
|
"Link to selected message": "Link to selected message",
|
||||||
"Link to room": "Link to room",
|
"Link to room": "Link to room",
|
||||||
"Command Help": "Command Help",
|
"Command Help": "Command Help",
|
||||||
|
"Sections to show": "Sections to show",
|
||||||
|
"This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s.": "This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s.",
|
||||||
"Space settings": "Space settings",
|
"Space settings": "Space settings",
|
||||||
"Settings - %(spaceName)s": "Settings - %(spaceName)s",
|
"Settings - %(spaceName)s": "Settings - %(spaceName)s",
|
||||||
"Spaces you're in": "Spaces you're in",
|
"Spaces you're in": "Spaces you're in",
|
||||||
|
|
|
@ -872,6 +872,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
[MetaSpace.Home]: true,
|
[MetaSpace.Home]: true,
|
||||||
}, false),
|
}, false),
|
||||||
},
|
},
|
||||||
|
"Spaces.showPeopleInSpace": {
|
||||||
|
supportedLevels: [SettingLevel.ROOM_ACCOUNT],
|
||||||
|
default: true,
|
||||||
|
controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", null),
|
||||||
|
},
|
||||||
"showCommunitiesInsteadOfSpaces": {
|
"showCommunitiesInsteadOfSpaces": {
|
||||||
displayName: _td("Display Communities instead of Spaces"),
|
displayName: _td("Display Communities instead of Spaces"),
|
||||||
description: _td("Temporarily show communities instead of Spaces for this session. " +
|
description: _td("Temporarily show communities instead of Spaces for this session. " +
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { SettingLevel } from "./SettingLevel";
|
||||||
|
|
||||||
export type CallbackFn = (changedInRoomId: string, atLevel: SettingLevel, newValAtLevel: any) => void;
|
export type CallbackFn = (changedInRoomId: string, atLevel: SettingLevel, newValAtLevel: any) => void;
|
||||||
|
|
||||||
const IRRELEVANT_ROOM = Symbol("irrelevant-room");
|
const IRRELEVANT_ROOM: string = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generalized management class for dealing with watchers on a per-handler (per-level)
|
* Generalized management class for dealing with watchers on a per-handler (per-level)
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import { RoomListStoreClass } from "./RoomListStore";
|
import { RoomListStoreClass } from "./RoomListStore";
|
||||||
import { SpaceFilterCondition } from "./filters/SpaceFilterCondition";
|
import { SpaceFilterCondition } from "./filters/SpaceFilterCondition";
|
||||||
import SpaceStore from "../spaces/SpaceStore";
|
import SpaceStore from "../spaces/SpaceStore";
|
||||||
import { MetaSpace, SpaceKey, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces";
|
import { isMetaSpace, MetaSpace, SpaceKey, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Watches for changes in spaces to manage the filter on the provided RoomListStore
|
* Watches for changes in spaces to manage the filter on the provided RoomListStore
|
||||||
|
@ -66,7 +66,7 @@ export class SpaceWatcher {
|
||||||
};
|
};
|
||||||
|
|
||||||
private updateFilter = () => {
|
private updateFilter = () => {
|
||||||
if (this.activeSpace[0] === "!") {
|
if (!isMetaSpace(this.activeSpace)) {
|
||||||
SpaceStore.instance.traverseSpace(this.activeSpace, roomId => {
|
SpaceStore.instance.traverseSpace(this.activeSpace, roomId => {
|
||||||
this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
|
this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { IDestroyable } from "../../../utils/IDestroyable";
|
||||||
import SpaceStore from "../../spaces/SpaceStore";
|
import SpaceStore from "../../spaces/SpaceStore";
|
||||||
import { MetaSpace, SpaceKey } from "../../spaces";
|
import { MetaSpace, SpaceKey } from "../../spaces";
|
||||||
import { setHasDiff } from "../../../utils/sets";
|
import { setHasDiff } from "../../../utils/sets";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A filter condition for the room list which reveals rooms which
|
* A filter condition for the room list which reveals rooms which
|
||||||
|
@ -31,6 +32,8 @@ import { setHasDiff } from "../../../utils/sets";
|
||||||
*/
|
*/
|
||||||
export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable {
|
export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable {
|
||||||
private roomIds = new Set<string>();
|
private roomIds = new Set<string>();
|
||||||
|
private userIds = new Set<string>();
|
||||||
|
private showPeopleInSpace = true;
|
||||||
private space: SpaceKey = MetaSpace.Home;
|
private space: SpaceKey = MetaSpace.Home;
|
||||||
|
|
||||||
public get kind(): FilterKind {
|
public get kind(): FilterKind {
|
||||||
|
@ -38,7 +41,7 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
|
||||||
}
|
}
|
||||||
|
|
||||||
public isVisible(room: Room): boolean {
|
public isVisible(room: Room): boolean {
|
||||||
return this.roomIds.has(room.roomId);
|
return SpaceStore.instance.isRoomInSpace(this.space, room.roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onStoreUpdate = async (): Promise<void> => {
|
private onStoreUpdate = async (): Promise<void> => {
|
||||||
|
@ -46,7 +49,18 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
|
||||||
// clone the set as it may be mutated by the space store internally
|
// clone the set as it may be mutated by the space store internally
|
||||||
this.roomIds = new Set(SpaceStore.instance.getSpaceFilteredRoomIds(this.space));
|
this.roomIds = new Set(SpaceStore.instance.getSpaceFilteredRoomIds(this.space));
|
||||||
|
|
||||||
if (setHasDiff(beforeRoomIds, this.roomIds)) {
|
const beforeUserIds = this.userIds;
|
||||||
|
// clone the set as it may be mutated by the space store internally
|
||||||
|
this.userIds = new Set(SpaceStore.instance.getSpaceFilteredUserIds(this.space));
|
||||||
|
|
||||||
|
const beforeShowPeopleInSpace = this.showPeopleInSpace;
|
||||||
|
this.showPeopleInSpace = this.space[0] !== "!" ||
|
||||||
|
SettingsStore.getValue("Spaces.showPeopleInSpace", this.space);
|
||||||
|
|
||||||
|
if (beforeShowPeopleInSpace !== this.showPeopleInSpace ||
|
||||||
|
setHasDiff(beforeRoomIds, this.roomIds) ||
|
||||||
|
setHasDiff(beforeUserIds, this.userIds)
|
||||||
|
) {
|
||||||
this.emit(FILTER_CHANGED);
|
this.emit(FILTER_CHANGED);
|
||||||
// XXX: Room List Store has a bug where updates to the pre-filter during a local echo of a
|
// XXX: Room List Store has a bug where updates to the pre-filter during a local echo of a
|
||||||
// tags transition seem to be ignored, so refire in the next tick to work around it
|
// tags transition seem to be ignored, so refire in the next tick to work around it
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { IRoomCapability } from "matrix-js-sdk/src/client";
|
import { IRoomCapability } from "matrix-js-sdk/src/client";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
|
||||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
|
@ -32,15 +33,15 @@ import { SpaceNotificationState } from "../notifications/SpaceNotificationState"
|
||||||
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
|
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
|
||||||
import { DefaultTagID } from "../room-list/models";
|
import { DefaultTagID } from "../room-list/models";
|
||||||
import { EnhancedMap, mapDiff } from "../../utils/maps";
|
import { EnhancedMap, mapDiff } from "../../utils/maps";
|
||||||
import { setHasDiff } from "../../utils/sets";
|
import { setDiff, setHasDiff } from "../../utils/sets";
|
||||||
import RoomViewStore from "../RoomViewStore";
|
import RoomViewStore from "../RoomViewStore";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { arrayHasDiff, arrayHasOrderChange } from "../../utils/arrays";
|
import { arrayHasDiff, arrayHasOrderChange } from "../../utils/arrays";
|
||||||
import { objectDiff } from "../../utils/objects";
|
|
||||||
import { reorderLexicographically } from "../../utils/stringOrderField";
|
import { reorderLexicographically } from "../../utils/stringOrderField";
|
||||||
import { TAG_ORDER } from "../../components/views/rooms/RoomList";
|
import { TAG_ORDER } from "../../components/views/rooms/RoomList";
|
||||||
import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";
|
import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";
|
||||||
import {
|
import {
|
||||||
|
isMetaSpace,
|
||||||
ISuggestedRoom,
|
ISuggestedRoom,
|
||||||
MetaSpace,
|
MetaSpace,
|
||||||
SpaceKey,
|
SpaceKey,
|
||||||
|
@ -51,6 +52,7 @@ import {
|
||||||
UPDATE_TOP_LEVEL_SPACES,
|
UPDATE_TOP_LEVEL_SPACES,
|
||||||
} from ".";
|
} from ".";
|
||||||
import { getCachedRoomIDForAlias } from "../../RoomAliasCache";
|
import { getCachedRoomIDForAlias } from "../../RoomAliasCache";
|
||||||
|
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
||||||
|
|
||||||
interface IState {}
|
interface IState {}
|
||||||
|
|
||||||
|
@ -93,14 +95,14 @@ const getRoomFn: FetchRoomFn = (room: Room) => {
|
||||||
export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
// The spaces representing the roots of the various tree-like hierarchies
|
// The spaces representing the roots of the various tree-like hierarchies
|
||||||
private rootSpaces: Room[] = [];
|
private rootSpaces: Room[] = [];
|
||||||
// The list of rooms not present in any currently joined spaces
|
|
||||||
private orphanedRooms = new Set<string>();
|
|
||||||
// Map from room ID to set of spaces which list it as a child
|
// Map from room ID to set of spaces which list it as a child
|
||||||
private parentMap = new EnhancedMap<string, Set<string>>();
|
private parentMap = new EnhancedMap<string, Set<string>>();
|
||||||
// Map from SpaceKey to SpaceNotificationState instance representing that space
|
// Map from SpaceKey to SpaceNotificationState instance representing that space
|
||||||
private notificationStateMap = new Map<SpaceKey, SpaceNotificationState>();
|
private notificationStateMap = new Map<SpaceKey, SpaceNotificationState>();
|
||||||
// Map from space key to Set of room IDs that should be shown as part of that space's filter
|
// Map from space key to Set of room IDs that should be shown as part of that space's filter
|
||||||
private spaceFilteredRooms = new Map<SpaceKey, Set<string>>();
|
private spaceFilteredRooms = new Map<SpaceKey, Set<string>>(); // won't contain MetaSpace.People
|
||||||
|
// Map from space ID to Set of user IDs that should be shown as part of that space's filter
|
||||||
|
private spaceFilteredUsers = new Map<Room["roomId"], Set<string>>();
|
||||||
// The space currently selected in the Space Panel
|
// The space currently selected in the Space Panel
|
||||||
private _activeSpace?: SpaceKey = MetaSpace.Home; // set properly by onReady
|
private _activeSpace?: SpaceKey = MetaSpace.Home; // set properly by onReady
|
||||||
private _suggestedRooms: ISuggestedRoom[] = [];
|
private _suggestedRooms: ISuggestedRoom[] = [];
|
||||||
|
@ -115,6 +117,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
|
|
||||||
SettingsStore.monitorSetting("Spaces.allRoomsInHome", null);
|
SettingsStore.monitorSetting("Spaces.allRoomsInHome", null);
|
||||||
SettingsStore.monitorSetting("Spaces.enabledMetaSpaces", null);
|
SettingsStore.monitorSetting("Spaces.enabledMetaSpaces", null);
|
||||||
|
SettingsStore.monitorSetting("Spaces.showPeopleInSpace", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get invitedSpaces(): Room[] {
|
public get invitedSpaces(): Room[] {
|
||||||
|
@ -134,7 +137,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public get activeSpaceRoom(): Room | null {
|
public get activeSpaceRoom(): Room | null {
|
||||||
if (this._activeSpace[0] !== "!") return null;
|
if (isMetaSpace(this._activeSpace)) return null;
|
||||||
return this.matrixClient?.getRoom(this._activeSpace);
|
return this.matrixClient?.getRoom(this._activeSpace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,7 +150,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public setActiveRoomInSpace(space: SpaceKey): void {
|
public setActiveRoomInSpace(space: SpaceKey): void {
|
||||||
if (space[0] === "!" && !this.matrixClient?.getRoom(space)?.isSpaceRoom()) return;
|
if (!isMetaSpace(space) && !this.matrixClient?.getRoom(space)?.isSpaceRoom()) return;
|
||||||
if (space !== this.activeSpace) this.setActiveSpace(space);
|
if (space !== this.activeSpace) this.setActiveSpace(space);
|
||||||
|
|
||||||
if (space) {
|
if (space) {
|
||||||
|
@ -195,7 +198,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
if (!space || !this.matrixClient || space === this.activeSpace) return;
|
if (!space || !this.matrixClient || space === this.activeSpace) return;
|
||||||
|
|
||||||
let cliSpace: Room;
|
let cliSpace: Room;
|
||||||
if (space[0] === "!") {
|
if (!isMetaSpace(space)) {
|
||||||
cliSpace = this.matrixClient.getRoom(space);
|
cliSpace = this.matrixClient.getRoom(space);
|
||||||
if (!cliSpace?.isSpaceRoom()) return;
|
if (!cliSpace?.isSpaceRoom()) return;
|
||||||
} else if (!this.enabledMetaSpaces.includes(space as MetaSpace)) {
|
} else if (!this.enabledMetaSpaces.includes(space as MetaSpace)) {
|
||||||
|
@ -215,7 +218,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
// else view space home or home depending on what is being clicked on
|
// else view space home or home depending on what is being clicked on
|
||||||
if (cliSpace?.getMyMembership() !== "invite" &&
|
if (cliSpace?.getMyMembership() !== "invite" &&
|
||||||
this.matrixClient.getRoom(roomId)?.getMyMembership() === "join" &&
|
this.matrixClient.getRoom(roomId)?.getMyMembership() === "join" &&
|
||||||
this.getSpaceFilteredRoomIds(space).has(roomId)
|
this.isRoomInSpace(space, roomId)
|
||||||
) {
|
) {
|
||||||
defaultDispatcher.dispatch({
|
defaultDispatcher.dispatch({
|
||||||
action: "view_room",
|
action: "view_room",
|
||||||
|
@ -349,6 +352,36 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
return this.parentMap.get(roomId) || new Set();
|
return this.parentMap.get(roomId) || new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isRoomInSpace(space: SpaceKey, roomId: string): boolean {
|
||||||
|
if (space === MetaSpace.Home && this.allRoomsInHome) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.spaceFilteredRooms.get(space)?.has(roomId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||||
|
if (!dmPartner) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// beyond this point we know this is a DM
|
||||||
|
|
||||||
|
if (space === MetaSpace.Home || space === MetaSpace.People) {
|
||||||
|
// these spaces contain all DMs
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMetaSpace(space) &&
|
||||||
|
this.spaceFilteredUsers.get(space)?.has(dmPartner) &&
|
||||||
|
SettingsStore.getValue("Spaces.showPeopleInSpace", space)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public getSpaceFilteredRoomIds = (space: SpaceKey): Set<string> => {
|
public getSpaceFilteredRoomIds = (space: SpaceKey): Set<string> => {
|
||||||
if (space === MetaSpace.Home && this.allRoomsInHome) {
|
if (space === MetaSpace.Home && this.allRoomsInHome) {
|
||||||
return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId));
|
return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId));
|
||||||
|
@ -356,162 +389,147 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
return this.spaceFilteredRooms.get(space) || new Set();
|
return this.spaceFilteredRooms.get(space) || new Set();
|
||||||
};
|
};
|
||||||
|
|
||||||
private rebuild = throttle(() => {
|
public getSpaceFilteredUserIds = (space: SpaceKey): Set<string> => {
|
||||||
if (!this.matrixClient) return;
|
if (space === MetaSpace.Home && this.allRoomsInHome) {
|
||||||
|
return undefined;
|
||||||
const [visibleSpaces, visibleRooms] = partitionSpacesAndRooms(this.matrixClient.getVisibleRooms());
|
|
||||||
const [joinedSpaces, invitedSpaces] = visibleSpaces.reduce((arr, s) => {
|
|
||||||
if (s.getMyMembership() === "join") {
|
|
||||||
arr[0].push(s);
|
|
||||||
} else if (s.getMyMembership() === "invite") {
|
|
||||||
arr[1].push(s);
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}, [[], []]);
|
|
||||||
|
|
||||||
// exclude invited spaces from unseenChildren as they will be forcibly shown at the top level of the treeview
|
|
||||||
const unseenChildren = new Set<Room>([...visibleRooms, ...joinedSpaces]);
|
|
||||||
const backrefs = new EnhancedMap<string, Set<string>>();
|
|
||||||
|
|
||||||
// Sort spaces by room ID to force the cycle breaking to be deterministic
|
|
||||||
const spaces = sortBy(joinedSpaces, space => space.roomId);
|
|
||||||
|
|
||||||
// TODO handle cleaning up links when a Space is removed
|
|
||||||
spaces.forEach(space => {
|
|
||||||
const children = this.getChildren(space.roomId);
|
|
||||||
children.forEach(child => {
|
|
||||||
unseenChildren.delete(child);
|
|
||||||
|
|
||||||
backrefs.getOrCreate(child.roomId, new Set()).add(space.roomId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren));
|
|
||||||
|
|
||||||
// somewhat algorithm to handle full-cycles
|
|
||||||
const detachedNodes = new Set<Room>(spaces);
|
|
||||||
|
|
||||||
const markTreeChildren = (rootSpace: Room, unseen: Set<Room>) => {
|
|
||||||
const stack = [rootSpace];
|
|
||||||
while (stack.length) {
|
|
||||||
const op = stack.pop();
|
|
||||||
unseen.delete(op);
|
|
||||||
this.getChildSpaces(op.roomId).forEach(space => {
|
|
||||||
if (unseen.has(space)) {
|
|
||||||
stack.push(space);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rootSpaces.forEach(rootSpace => {
|
|
||||||
markTreeChildren(rootSpace, detachedNodes);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle spaces forming fully cyclical relationships.
|
|
||||||
// In order, assume each detachedNode is a root unless it has already
|
|
||||||
// been claimed as the child of prior detached node.
|
|
||||||
// Work from a copy of the detachedNodes set as it will be mutated as part of this operation.
|
|
||||||
Array.from(detachedNodes).forEach(detachedNode => {
|
|
||||||
if (!detachedNodes.has(detachedNode)) return;
|
|
||||||
// declare this detached node a new root, find its children, without ever looping back to it
|
|
||||||
detachedNodes.delete(detachedNode);
|
|
||||||
rootSpaces.push(detachedNode);
|
|
||||||
markTreeChildren(detachedNode, detachedNodes);
|
|
||||||
|
|
||||||
// TODO only consider a detached node a root space if it has no *parents other than the ones forming cycles
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO neither of these handle an A->B->C->A with an additional C->D
|
|
||||||
// detachedNodes.forEach(space => {
|
|
||||||
// rootSpaces.push(space);
|
|
||||||
// });
|
|
||||||
|
|
||||||
this.orphanedRooms = new Set(orphanedRooms.map(r => r.roomId));
|
|
||||||
this.rootSpaces = this.sortRootSpaces(rootSpaces);
|
|
||||||
this.parentMap = backrefs;
|
|
||||||
|
|
||||||
// if the currently selected space no longer exists, remove its selection
|
|
||||||
if (this._activeSpace[0] === "!" && detachedNodes.has(this.matrixClient.getRoom(this._activeSpace))) {
|
|
||||||
this.goToFirstSpace();
|
|
||||||
}
|
}
|
||||||
|
if (isMetaSpace(space)) return undefined;
|
||||||
this.onRoomsUpdate(); // TODO only do this if a change has happened
|
return this.spaceFilteredUsers.get(space) || new Set();
|
||||||
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));
|
|
||||||
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
|
|
||||||
}, 100, { trailing: true, leading: true });
|
|
||||||
|
|
||||||
private onSpaceUpdate = () => {
|
|
||||||
this.rebuild();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private showInHomeSpace = (room: Room) => {
|
private markTreeChildren = (rootSpace: Room, unseen: Set<Room>): void => {
|
||||||
if (this.allRoomsInHome) return true;
|
const stack = [rootSpace];
|
||||||
if (room.isSpaceRoom()) return false;
|
while (stack.length) {
|
||||||
return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space
|
const space = stack.pop();
|
||||||
|| DMRoomMap.shared().getUserIdForRoomId(room.roomId); // put all DMs in the Home Space
|
unseen.delete(space);
|
||||||
};
|
this.getChildSpaces(space.roomId).forEach(space => {
|
||||||
|
if (unseen.has(space)) {
|
||||||
// Update a given room due to its tag changing (e.g DM-ness or Fav-ness)
|
stack.push(space);
|
||||||
// This can only change whether it shows up in the HOME_SPACE or not
|
|
||||||
private onRoomUpdate = (room: Room) => {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onSpaceMembersChange = (ev: MatrixEvent) => {
|
|
||||||
// skip this update if we do not have a DM with this user
|
|
||||||
if (DMRoomMap.shared().getDMRoomsForUserId(ev.getStateKey()).length < 1) return;
|
|
||||||
this.onRoomsUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onRoomsUpdate = throttle(() => {
|
|
||||||
// TODO resolve some updates as deltas
|
|
||||||
const visibleRooms = this.matrixClient.getVisibleRooms();
|
|
||||||
|
|
||||||
const oldFilteredRooms = this.spaceFilteredRooms;
|
|
||||||
this.spaceFilteredRooms = new Map();
|
|
||||||
|
|
||||||
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(MetaSpace.Home, new Set(invites.map(r => r.roomId)));
|
|
||||||
|
|
||||||
visibleRooms.forEach(room => {
|
|
||||||
if (this.showInHomeSpace(room)) {
|
|
||||||
this.spaceFilteredRooms.get(MetaSpace.Home).add(room.roomId);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private findRootSpaces = (joinedSpaces: Room[]): Room[] => {
|
||||||
|
// exclude invited spaces from unseenChildren as they will be forcibly shown at the top level of the treeview
|
||||||
|
const unseenSpaces = new Set(joinedSpaces);
|
||||||
|
|
||||||
|
joinedSpaces.forEach(space => {
|
||||||
|
this.getChildSpaces(space.roomId).forEach(subspace => {
|
||||||
|
unseenSpaces.delete(subspace);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Consider any spaces remaining in unseenSpaces as root,
|
||||||
|
// given they are not children of any known spaces.
|
||||||
|
// The hierarchy from these roots may not yet be exhaustive due to the possibility of full-cycles.
|
||||||
|
const rootSpaces = Array.from(unseenSpaces);
|
||||||
|
|
||||||
|
// Next we need to determine the roots of any remaining full-cycles.
|
||||||
|
// We sort spaces by room ID to force the cycle breaking to be deterministic.
|
||||||
|
const detachedNodes = new Set<Room>(sortBy(joinedSpaces, space => space.roomId));
|
||||||
|
|
||||||
|
// Mark any nodes which are children of our existing root spaces as attached.
|
||||||
|
rootSpaces.forEach(rootSpace => {
|
||||||
|
this.markTreeChildren(rootSpace, detachedNodes);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle spaces forming fully cyclical relationships.
|
||||||
|
// In order, assume each remaining detachedNode is a root unless it has already
|
||||||
|
// been claimed as the child of prior detached node.
|
||||||
|
// Work from a copy of the detachedNodes set as it will be mutated as part of this operation.
|
||||||
|
// TODO consider sorting by number of in-refs to favour nodes with fewer parents.
|
||||||
|
Array.from(detachedNodes).forEach(detachedNode => {
|
||||||
|
if (!detachedNodes.has(detachedNode)) return; // already claimed, skip
|
||||||
|
// declare this detached node a new root, find its children, without ever looping back to it
|
||||||
|
rootSpaces.push(detachedNode); // consider this node a new root space
|
||||||
|
this.markTreeChildren(detachedNode, detachedNodes); // declare this node and its children attached
|
||||||
|
});
|
||||||
|
|
||||||
|
return rootSpaces;
|
||||||
|
};
|
||||||
|
|
||||||
|
private rebuildSpaceHierarchy = () => {
|
||||||
|
const visibleSpaces = this.matrixClient.getVisibleRooms().filter(r => r.isSpaceRoom());
|
||||||
|
const [joinedSpaces, invitedSpaces] = visibleSpaces.reduce(([joined, invited], s) => {
|
||||||
|
switch (getEffectiveMembership(s.getMyMembership())) {
|
||||||
|
case EffectiveMembership.Join:
|
||||||
|
joined.push(s);
|
||||||
|
break;
|
||||||
|
case EffectiveMembership.Invite:
|
||||||
|
invited.push(s);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return [joined, invited];
|
||||||
|
}, [[], []] as [Room[], Room[]]);
|
||||||
|
|
||||||
|
const rootSpaces = this.findRootSpaces(joinedSpaces);
|
||||||
|
const oldRootSpaces = this.rootSpaces;
|
||||||
|
this.rootSpaces = this.sortRootSpaces(rootSpaces);
|
||||||
|
|
||||||
|
this.onRoomsUpdate();
|
||||||
|
|
||||||
|
if (arrayHasOrderChange(oldRootSpaces, this.rootSpaces)) {
|
||||||
|
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces, this.enabledMetaSpaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldInvitedSpaces = this._invitedSpaces;
|
||||||
|
this._invitedSpaces = new Set(this.sortRootSpaces(invitedSpaces));
|
||||||
|
if (setHasDiff(oldInvitedSpaces, this._invitedSpaces)) {
|
||||||
|
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private rebuildParentMap = () => {
|
||||||
|
const joinedSpaces = this.matrixClient.getVisibleRooms().filter(r => {
|
||||||
|
return r.isSpaceRoom() && r.getMyMembership() === "join";
|
||||||
|
});
|
||||||
|
|
||||||
|
this.parentMap = new EnhancedMap<string, Set<string>>();
|
||||||
|
joinedSpaces.forEach(space => {
|
||||||
|
const children = this.getChildren(space.roomId);
|
||||||
|
children.forEach(child => {
|
||||||
|
this.parentMap.getOrCreate(child.roomId, new Set()).add(space.roomId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private rebuildHomeSpace = () => {
|
||||||
|
if (this.allRoomsInHome) {
|
||||||
|
// this is a special-case to not have to maintain a set of all rooms
|
||||||
|
this.spaceFilteredRooms.delete(MetaSpace.Home);
|
||||||
|
} else {
|
||||||
|
const rooms = new Set(this.matrixClient.getVisibleRooms().filter(this.showInHomeSpace).map(r => r.roomId));
|
||||||
|
this.spaceFilteredRooms.set(MetaSpace.Home, rooms);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.activeSpace === MetaSpace.Home) {
|
||||||
|
this.switchSpaceIfNeeded();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private rebuildMetaSpaces = () => {
|
||||||
|
const enabledMetaSpaces = new Set(this.enabledMetaSpaces);
|
||||||
|
const visibleRooms = this.matrixClient.getVisibleRooms();
|
||||||
|
|
||||||
|
if (enabledMetaSpaces.has(MetaSpace.Home)) {
|
||||||
|
this.rebuildHomeSpace();
|
||||||
|
} else {
|
||||||
|
this.spaceFilteredRooms.delete(MetaSpace.Home);
|
||||||
|
}
|
||||||
|
|
||||||
// populate the Favourites metaspace if it is enabled
|
|
||||||
if (enabledMetaSpaces.has(MetaSpace.Favourites)) {
|
if (enabledMetaSpaces.has(MetaSpace.Favourites)) {
|
||||||
const favourites = visibleRooms.filter(r => r.tags[DefaultTagID.Favourite]);
|
const favourites = visibleRooms.filter(r => r.tags[DefaultTagID.Favourite]);
|
||||||
this.spaceFilteredRooms.set(MetaSpace.Favourites, new Set(favourites.map(r => r.roomId)));
|
this.spaceFilteredRooms.set(MetaSpace.Favourites, new Set(favourites.map(r => r.roomId)));
|
||||||
|
} else {
|
||||||
|
this.spaceFilteredRooms.delete(MetaSpace.Favourites);
|
||||||
}
|
}
|
||||||
|
|
||||||
// populate the People metaspace if it is enabled
|
// The People metaspace doesn't need maintaining
|
||||||
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
|
// Populate the orphans space if the Home space is enabled as it is a superset of it.
|
||||||
if (enabledMetaSpaces.has(MetaSpace.Orphans)) {
|
// Home is effectively a super set of People + Orphans with the addition of having all invites too.
|
||||||
|
if (enabledMetaSpaces.has(MetaSpace.Orphans) || enabledMetaSpaces.has(MetaSpace.Home)) {
|
||||||
const orphans = visibleRooms.filter(r => {
|
const orphans = visibleRooms.filter(r => {
|
||||||
// filter out DMs and rooms with >0 parents
|
// filter out DMs and rooms with >0 parents
|
||||||
return !this.parentMap.get(r.roomId)?.size && !DMRoomMap.shared().getUserIdForRoomId(r.roomId);
|
return !this.parentMap.get(r.roomId)?.size && !DMRoomMap.shared().getUserIdForRoomId(r.roomId);
|
||||||
|
@ -519,6 +537,150 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
this.spaceFilteredRooms.set(MetaSpace.Orphans, new Set(orphans.map(r => r.roomId)));
|
this.spaceFilteredRooms.set(MetaSpace.Orphans, new Set(orphans.map(r => r.roomId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isMetaSpace(this.activeSpace)) {
|
||||||
|
this.switchSpaceIfNeeded();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private updateNotificationStates = (spaces?: SpaceKey[]) => {
|
||||||
|
const enabledMetaSpaces = new Set(this.enabledMetaSpaces);
|
||||||
|
const visibleRooms = this.matrixClient.getVisibleRooms();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!spaces) {
|
||||||
|
spaces = [...this.spaceFilteredRooms.keys()];
|
||||||
|
if (dmBadgeSpace === MetaSpace.People) {
|
||||||
|
spaces.push(MetaSpace.People);
|
||||||
|
}
|
||||||
|
if (enabledMetaSpaces.has(MetaSpace.Home) && !this.allRoomsInHome) {
|
||||||
|
spaces.push(MetaSpace.Home);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spaces.forEach((s) => {
|
||||||
|
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 (s === MetaSpace.People) {
|
||||||
|
return this.isRoomInSpace(MetaSpace.People, room.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (room.isSpaceRoom() || !this.spaceFilteredRooms.get(s).has(room.roomId)) return false;
|
||||||
|
|
||||||
|
if (dmBadgeSpace && DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
|
||||||
|
return s === dmBadgeSpace;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dmBadgeSpace !== MetaSpace.People) {
|
||||||
|
this.notificationStateMap.delete(MetaSpace.People);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private showInHomeSpace = (room: Room): boolean => {
|
||||||
|
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
|
||||||
|
room.getMyMembership() === "invite"; // put all invites in the Home Space
|
||||||
|
};
|
||||||
|
|
||||||
|
private static isInSpace(member: RoomMember): boolean {
|
||||||
|
return member.membership === "join" || member.membership === "invite";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getSpaceMembers(space: Room): string[] {
|
||||||
|
return space.getMembers().filter(SpaceStoreClass.isInSpace).map(m => m.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method for resolving the impact of a single user's membership change in the given Space and its hierarchy
|
||||||
|
private onMemberUpdate = (space: Room, userId: string) => {
|
||||||
|
const inSpace = SpaceStoreClass.isInSpace(space.getMember(userId));
|
||||||
|
|
||||||
|
if (this.spaceFilteredUsers.get(space.roomId).has(userId)) {
|
||||||
|
if (inSpace) return; // nothing to do, user was already joined to subspace
|
||||||
|
if (this.getChildSpaces(space.roomId).some(s => this.spaceFilteredUsers.get(s.roomId).has(userId))) {
|
||||||
|
return; // nothing to do, this user leaving will have no effect as they are in a subspace
|
||||||
|
}
|
||||||
|
} else if (!inSpace) {
|
||||||
|
return; // nothing to do, user already not in the list
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const stack = [space.roomId];
|
||||||
|
while (stack.length) {
|
||||||
|
const spaceId = stack.pop();
|
||||||
|
seen.add(spaceId);
|
||||||
|
|
||||||
|
if (inSpace) {
|
||||||
|
// add to our list and to that of all of our parents
|
||||||
|
this.spaceFilteredUsers.get(spaceId).add(userId);
|
||||||
|
} else {
|
||||||
|
// remove from our list and that of all of our parents until we hit a parent with this user
|
||||||
|
this.spaceFilteredUsers.get(spaceId).delete(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getKnownParents(spaceId).forEach(parentId => {
|
||||||
|
if (seen.has(parentId)) return;
|
||||||
|
const parent = this.matrixClient.getRoom(parentId);
|
||||||
|
// because spaceFilteredUsers is cumulative, if we are removing from lower in the hierarchy,
|
||||||
|
// but the member is present higher in the hierarchy we must take care not to wrongly over-remove them.
|
||||||
|
if (inSpace || !SpaceStoreClass.isInSpace(parent.getMember(userId))) {
|
||||||
|
stack.push(parentId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.switchSpaceIfNeeded();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onMembersUpdate = (space: Room, seen = new Set<string>()) => {
|
||||||
|
// Update this space's membership list
|
||||||
|
const userIds = new Set(SpaceStoreClass.getSpaceMembers(space));
|
||||||
|
// We only need to look one level with children
|
||||||
|
// as any further descendants will already be in their parent's superset
|
||||||
|
this.getChildSpaces(space.roomId).forEach(subspace => {
|
||||||
|
SpaceStoreClass.getSpaceMembers(subspace).forEach(userId => {
|
||||||
|
userIds.add(userId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.spaceFilteredUsers.set(space.roomId, userIds);
|
||||||
|
this.emit(space.roomId);
|
||||||
|
|
||||||
|
// Traverse all parents and update them too
|
||||||
|
this.getKnownParents(space.roomId).forEach(parentId => {
|
||||||
|
if (seen.has(parentId)) return;
|
||||||
|
const parent = this.matrixClient.getRoom(parentId);
|
||||||
|
if (parent) {
|
||||||
|
const newSeen = new Set(seen);
|
||||||
|
newSeen.add(parentId);
|
||||||
|
this.onMembersUpdate(parent, newSeen);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRoomsUpdate = () => {
|
||||||
|
const visibleRooms = this.matrixClient.getVisibleRooms();
|
||||||
|
|
||||||
|
const oldFilteredRooms = this.spaceFilteredRooms;
|
||||||
|
const oldFilteredUsers = this.spaceFilteredUsers;
|
||||||
|
this.spaceFilteredRooms = new Map();
|
||||||
|
this.spaceFilteredUsers = new Map();
|
||||||
|
|
||||||
|
this.rebuildParentMap();
|
||||||
|
this.rebuildMetaSpaces();
|
||||||
|
|
||||||
const hiddenChildren = new EnhancedMap<string, Set<string>>();
|
const hiddenChildren = new EnhancedMap<string, Set<string>>();
|
||||||
visibleRooms.forEach(room => {
|
visibleRooms.forEach(room => {
|
||||||
if (room.getMyMembership() !== "join") return;
|
if (room.getMyMembership() !== "join") return;
|
||||||
|
@ -530,31 +692,26 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
this.rootSpaces.forEach(s => {
|
this.rootSpaces.forEach(s => {
|
||||||
// traverse each space tree in DFS to build up the supersets as you go up,
|
// traverse each space tree in DFS to build up the supersets as you go up,
|
||||||
// reusing results from like subtrees.
|
// reusing results from like subtrees.
|
||||||
const fn = (spaceId: string, parentPath: Set<string>): Set<string> => {
|
const fn = (spaceId: string, parentPath: Set<string>): [Set<string>, Set<string>] => {
|
||||||
if (parentPath.has(spaceId)) return; // prevent cycles
|
if (parentPath.has(spaceId)) return; // prevent cycles
|
||||||
|
|
||||||
// reuse existing results if multiple similar branches exist
|
// reuse existing results if multiple similar branches exist
|
||||||
if (this.spaceFilteredRooms.has(spaceId)) {
|
if (this.spaceFilteredRooms.has(spaceId) && this.spaceFilteredUsers.has(spaceId)) {
|
||||||
return this.spaceFilteredRooms.get(spaceId);
|
return [this.spaceFilteredRooms.get(spaceId), this.spaceFilteredUsers.get(spaceId)];
|
||||||
}
|
}
|
||||||
|
|
||||||
const [childSpaces, childRooms] = partitionSpacesAndRooms(this.getChildren(spaceId));
|
const [childSpaces, childRooms] = partitionSpacesAndRooms(this.getChildren(spaceId));
|
||||||
const roomIds = new Set(childRooms.map(r => r.roomId));
|
const roomIds = new Set(childRooms.map(r => r.roomId));
|
||||||
const space = this.matrixClient?.getRoom(spaceId);
|
const space = this.matrixClient?.getRoom(spaceId);
|
||||||
|
const userIds = new Set(space?.getMembers().filter(m => {
|
||||||
// Add relevant DMs
|
return m.membership === "join" || m.membership === "invite";
|
||||||
space?.getMembers().forEach(member => {
|
}).map(m => m.userId));
|
||||||
if (member.membership !== "join" && member.membership !== "invite") return;
|
|
||||||
DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => {
|
|
||||||
roomIds.add(roomId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const newPath = new Set(parentPath).add(spaceId);
|
const newPath = new Set(parentPath).add(spaceId);
|
||||||
childSpaces.forEach(childSpace => {
|
childSpaces.forEach(childSpace => {
|
||||||
fn(childSpace.roomId, newPath)?.forEach(roomId => {
|
const [rooms, users] = fn(childSpace.roomId, newPath) ?? [];
|
||||||
roomIds.add(roomId);
|
rooms?.forEach(roomId => roomIds.add(roomId));
|
||||||
});
|
users?.forEach(userId => userIds.add(userId));
|
||||||
});
|
});
|
||||||
hiddenChildren.get(spaceId)?.forEach(roomId => {
|
hiddenChildren.get(spaceId)?.forEach(roomId => {
|
||||||
roomIds.add(roomId);
|
roomIds.add(roomId);
|
||||||
|
@ -565,42 +722,59 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
return this.matrixClient.getRoomUpgradeHistory(roomId, true).map(r => r.roomId);
|
return this.matrixClient.getRoomUpgradeHistory(roomId, true).map(r => r.roomId);
|
||||||
}));
|
}));
|
||||||
this.spaceFilteredRooms.set(spaceId, expandedRoomIds);
|
this.spaceFilteredRooms.set(spaceId, expandedRoomIds);
|
||||||
return expandedRoomIds;
|
this.spaceFilteredUsers.set(spaceId, userIds);
|
||||||
|
return [expandedRoomIds, userIds];
|
||||||
};
|
};
|
||||||
|
|
||||||
fn(s.roomId, new Set());
|
fn(s.roomId, new Set());
|
||||||
});
|
});
|
||||||
|
|
||||||
const diff = mapDiff(oldFilteredRooms, this.spaceFilteredRooms);
|
const roomDiff = mapDiff(oldFilteredRooms, this.spaceFilteredRooms);
|
||||||
|
const userDiff = mapDiff(oldFilteredUsers, this.spaceFilteredUsers);
|
||||||
// filter out keys which changed by reference only by checking whether the sets differ
|
// filter out keys which changed by reference only by checking whether the sets differ
|
||||||
const changed = diff.changed.filter(k => setHasDiff(oldFilteredRooms.get(k), this.spaceFilteredRooms.get(k)));
|
const roomsChanged = roomDiff.changed.filter(k => {
|
||||||
[...diff.added, ...diff.removed, ...changed].forEach(k => {
|
return setHasDiff(oldFilteredRooms.get(k), this.spaceFilteredRooms.get(k));
|
||||||
|
});
|
||||||
|
const usersChanged = userDiff.changed.filter(k => {
|
||||||
|
return setHasDiff(oldFilteredUsers.get(k), this.spaceFilteredUsers.get(k));
|
||||||
|
});
|
||||||
|
|
||||||
|
const changeSet = new Set([
|
||||||
|
...roomDiff.added,
|
||||||
|
...userDiff.added,
|
||||||
|
...roomDiff.removed,
|
||||||
|
...userDiff.removed,
|
||||||
|
...roomsChanged,
|
||||||
|
...usersChanged,
|
||||||
|
]);
|
||||||
|
|
||||||
|
changeSet.forEach(k => {
|
||||||
this.emit(k);
|
this.emit(k);
|
||||||
});
|
});
|
||||||
|
|
||||||
let dmBadgeSpace: MetaSpace;
|
if (changeSet.has(this.activeSpace)) {
|
||||||
// only show badges on dms on the most relevant space if such exists
|
this.switchSpaceIfNeeded();
|
||||||
if (enabledMetaSpaces.has(MetaSpace.People)) {
|
|
||||||
dmBadgeSpace = MetaSpace.People;
|
|
||||||
} else if (enabledMetaSpaces.has(MetaSpace.Home)) {
|
|
||||||
dmBadgeSpace = MetaSpace.Home;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.spaceFilteredRooms.forEach((roomIds, s) => {
|
const notificationStatesToUpdate = [...changeSet];
|
||||||
if (this.allRoomsInHome && s === MetaSpace.Home) return; // we'll be using the global notification state, skip
|
if (this.enabledMetaSpaces.includes(MetaSpace.People) &&
|
||||||
|
userDiff.added.length + userDiff.removed.length + usersChanged.length > 0
|
||||||
|
) {
|
||||||
|
notificationStatesToUpdate.push(MetaSpace.People);
|
||||||
|
}
|
||||||
|
this.updateNotificationStates(notificationStatesToUpdate);
|
||||||
|
};
|
||||||
|
|
||||||
// Update NotificationStates
|
private switchSpaceIfNeeded = throttle(() => {
|
||||||
this.getNotificationState(s).setRooms(visibleRooms.filter(room => {
|
const roomId = RoomViewStore.getRoomId();
|
||||||
if (!roomIds.has(room.roomId) || room.isSpaceRoom()) return false;
|
if (this.isRoomInSpace(this.activeSpace, roomId)) return;
|
||||||
|
|
||||||
if (dmBadgeSpace && DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
|
if (this.matrixClient.getRoom(roomId)?.isSpaceRoom()) {
|
||||||
return s === dmBadgeSpace;
|
this.goToFirstSpace(true);
|
||||||
}
|
} else {
|
||||||
|
this.switchToRelatedSpace(roomId);
|
||||||
return true;
|
}
|
||||||
}));
|
}, 100, { leading: true, trailing: true });
|
||||||
});
|
|
||||||
}, 100, { trailing: true, leading: true });
|
|
||||||
|
|
||||||
private switchToRelatedSpace = (roomId: string) => {
|
private switchToRelatedSpace = (roomId: string) => {
|
||||||
if (this.suggestedRooms.find(r => r.room_id === roomId)) return;
|
if (this.suggestedRooms.find(r => r.room_id === roomId)) return;
|
||||||
|
@ -616,11 +790,15 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
// otherwise, try to find a metaspace which contains this room
|
// otherwise, try to find a metaspace which contains this room
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
// search meta spaces in reverse as Home is the first and least specific one
|
// search meta spaces in reverse as Home is the first and least specific one
|
||||||
parent = [...this.enabledMetaSpaces].reverse().find(s => this.getSpaceFilteredRoomIds(s).has(roomId));
|
parent = [...this.enabledMetaSpaces].reverse().find(s => this.isRoomInSpace(s, roomId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't trigger a context switch when we are switching a space to match the chosen room
|
// don't trigger a context switch when we are switching a space to match the chosen room
|
||||||
this.setActiveSpace(parent ?? MetaSpace.Home, false); // TODO
|
if (parent) {
|
||||||
|
this.setActiveSpace(parent, false);
|
||||||
|
} else {
|
||||||
|
this.goToFirstSpace();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRoom = (room: Room, newMembership?: string, oldMembership?: string) => {
|
private onRoom = (room: Room, newMembership?: string, oldMembership?: string) => {
|
||||||
|
@ -632,10 +810,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
const membership = newMembership || roomMembership;
|
const membership = newMembership || roomMembership;
|
||||||
|
|
||||||
if (!room.isSpaceRoom()) {
|
if (!room.isSpaceRoom()) {
|
||||||
// this.onRoomUpdate(room);
|
this.onRoomsUpdate();
|
||||||
// this.onRoomsUpdate();
|
|
||||||
// ideally we only need onRoomsUpdate here but it doesn't rebuild parentMap so always adds new rooms to Home
|
|
||||||
this.rebuild();
|
|
||||||
|
|
||||||
if (membership === "join") {
|
if (membership === "join") {
|
||||||
// the user just joined a room, remove it from the suggested list if it was there
|
// the user just joined a room, remove it from the suggested list if it was there
|
||||||
|
@ -655,13 +830,21 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
|
|
||||||
// Space
|
// Space
|
||||||
if (membership === "invite") {
|
if (membership === "invite") {
|
||||||
|
const len = this._invitedSpaces.size;
|
||||||
this._invitedSpaces.add(room);
|
this._invitedSpaces.add(room);
|
||||||
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
|
if (len !== this._invitedSpaces.size) {
|
||||||
|
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
|
||||||
|
}
|
||||||
} else if (oldMembership === "invite" && membership !== "join") {
|
} else if (oldMembership === "invite" && membership !== "join") {
|
||||||
this._invitedSpaces.delete(room);
|
if (this._invitedSpaces.delete(room)) {
|
||||||
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
|
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.onSpaceUpdate();
|
this.rebuildSpaceHierarchy();
|
||||||
|
// fire off updates to all parent listeners
|
||||||
|
this.parentMap.get(room.roomId)?.forEach((parentId) => {
|
||||||
|
this.emit(parentId);
|
||||||
|
});
|
||||||
this.emit(room.roomId);
|
this.emit(room.roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -687,28 +870,36 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
|
|
||||||
switch (ev.getType()) {
|
switch (ev.getType()) {
|
||||||
case EventType.SpaceChild:
|
case EventType.SpaceChild: {
|
||||||
|
const target = this.matrixClient.getRoom(ev.getStateKey());
|
||||||
|
|
||||||
if (room.isSpaceRoom()) {
|
if (room.isSpaceRoom()) {
|
||||||
this.onSpaceUpdate();
|
if (target?.isSpaceRoom()) {
|
||||||
|
this.rebuildSpaceHierarchy();
|
||||||
|
this.emit(target.roomId);
|
||||||
|
} else {
|
||||||
|
this.onRoomsUpdate();
|
||||||
|
}
|
||||||
this.emit(room.roomId);
|
this.emit(room.roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room.roomId === this.activeSpace && // current space
|
if (room.roomId === this.activeSpace && // current space
|
||||||
this.matrixClient.getRoom(ev.getStateKey())?.getMyMembership() !== "join" && // target not joined
|
target?.getMyMembership() !== "join" && // target not joined
|
||||||
ev.getPrevContent().suggested !== ev.getContent().suggested // suggested flag changed
|
ev.getPrevContent().suggested !== ev.getContent().suggested // suggested flag changed
|
||||||
) {
|
) {
|
||||||
this.loadSuggestedRooms(room);
|
this.loadSuggestedRooms(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case EventType.SpaceParent:
|
case EventType.SpaceParent:
|
||||||
// TODO rebuild the space parent and not the room - check permissions?
|
// TODO rebuild the space parent and not the room - check permissions?
|
||||||
// TODO confirm this after implementing parenting behaviour
|
// TODO confirm this after implementing parenting behaviour
|
||||||
if (room.isSpaceRoom()) {
|
if (room.isSpaceRoom()) {
|
||||||
this.onSpaceUpdate();
|
this.rebuildSpaceHierarchy();
|
||||||
} else if (!this.allRoomsInHome) {
|
} else {
|
||||||
this.onRoomUpdate(room);
|
this.onRoomsUpdate();
|
||||||
}
|
}
|
||||||
this.emit(room.roomId);
|
this.emit(room.roomId);
|
||||||
break;
|
break;
|
||||||
|
@ -724,8 +915,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
// listening for m.room.member events in onRoomState above doesn't work as the Member object isn't updated by then
|
// listening for m.room.member events in onRoomState above doesn't work as the Member object isn't updated by then
|
||||||
private onRoomStateMembers = (ev: MatrixEvent) => {
|
private onRoomStateMembers = (ev: MatrixEvent) => {
|
||||||
const room = this.matrixClient.getRoom(ev.getRoomId());
|
const room = this.matrixClient.getRoom(ev.getRoomId());
|
||||||
if (room?.isSpaceRoom()) {
|
const userId = ev.getStateKey();
|
||||||
this.onSpaceMembersChange(ev);
|
if (room?.isSpaceRoom() && // only consider space rooms
|
||||||
|
DMRoomMap.shared().getDMRoomsForUserId(userId).length > 0 && // only consider members we have a DM with
|
||||||
|
ev.getPrevContent().membership !== ev.getContent().membership // only consider when membership changes
|
||||||
|
) {
|
||||||
|
this.onMemberUpdate(room, userId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -744,35 +939,73 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
const oldTags = lastEv?.getContent()?.tags || {};
|
const oldTags = lastEv?.getContent()?.tags || {};
|
||||||
const newTags = ev.getContent()?.tags || {};
|
const newTags = ev.getContent()?.tags || {};
|
||||||
if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) {
|
if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) {
|
||||||
this.onRoomUpdate(room);
|
this.onRoomFavouriteChange(room);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onAccountData = (ev: MatrixEvent, prevEvent?: MatrixEvent) => {
|
private onRoomFavouriteChange(room: Room) {
|
||||||
if (!this.allRoomsInHome && ev.getType() === EventType.Direct) {
|
if (this.enabledMetaSpaces.includes(MetaSpace.Favourites)) {
|
||||||
const lastContent = prevEvent?.getContent() ?? {};
|
if (room.tags[DefaultTagID.Favourite]) {
|
||||||
const content = ev.getContent();
|
this.spaceFilteredRooms.get(MetaSpace.Favourites).add(room.roomId);
|
||||||
|
} else {
|
||||||
|
this.spaceFilteredRooms.get(MetaSpace.Favourites).delete(room.roomId);
|
||||||
|
}
|
||||||
|
this.emit(MetaSpace.Favourites);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const diff = objectDiff<Record<string, string[]>>(lastContent, content);
|
private onRoomDmChange(room: Room, isDm: boolean): void {
|
||||||
// filter out keys which changed by reference only by checking whether the sets differ
|
const enabledMetaSpaces = new Set(this.enabledMetaSpaces);
|
||||||
const changed = diff.changed.filter(k => arrayHasDiff(lastContent[k], content[k]));
|
|
||||||
// DM tag changes, refresh relevant rooms
|
if (!this.allRoomsInHome && enabledMetaSpaces.has(MetaSpace.Home)) {
|
||||||
new Set([...diff.added, ...diff.removed, ...changed]).forEach(roomId => {
|
const homeRooms = this.spaceFilteredRooms.get(MetaSpace.Home);
|
||||||
|
if (this.showInHomeSpace(room)) {
|
||||||
|
homeRooms?.add(room.roomId);
|
||||||
|
} else if (!this.spaceFilteredRooms.get(MetaSpace.Orphans).has(room.roomId)) {
|
||||||
|
this.spaceFilteredRooms.get(MetaSpace.Home)?.delete(room.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit(MetaSpace.Home);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enabledMetaSpaces.has(MetaSpace.People)) {
|
||||||
|
this.emit(MetaSpace.People);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enabledMetaSpaces.has(MetaSpace.Orphans) || enabledMetaSpaces.has(MetaSpace.Home)) {
|
||||||
|
if (isDm && this.spaceFilteredRooms.get(MetaSpace.Orphans).delete(room.roomId)) {
|
||||||
|
this.emit(MetaSpace.Orphans);
|
||||||
|
this.emit(MetaSpace.Home);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onAccountData = (ev: MatrixEvent, prevEv?: MatrixEvent) => {
|
||||||
|
if (ev.getType() === EventType.Direct) {
|
||||||
|
const previousRooms = new Set(Object.values(prevEv?.getContent<Record<string, string[]>>() ?? {}).flat());
|
||||||
|
const currentRooms = new Set(Object.values(ev.getContent<Record<string, string[]>>()).flat());
|
||||||
|
|
||||||
|
const diff = setDiff(previousRooms, currentRooms);
|
||||||
|
[...diff.added, ...diff.removed].forEach(roomId => {
|
||||||
const room = this.matrixClient?.getRoom(roomId);
|
const room = this.matrixClient?.getRoom(roomId);
|
||||||
if (room) {
|
if (room) {
|
||||||
this.onRoomUpdate(room);
|
this.onRoomDmChange(room, currentRooms.has(roomId));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (diff.removed.length > 0) {
|
||||||
|
this.switchSpaceIfNeeded();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
protected async reset() {
|
protected async reset() {
|
||||||
this.rootSpaces = [];
|
this.rootSpaces = [];
|
||||||
this.orphanedRooms = new Set();
|
|
||||||
this.parentMap = new EnhancedMap();
|
this.parentMap = new EnhancedMap();
|
||||||
this.notificationStateMap = new Map();
|
this.notificationStateMap = new Map();
|
||||||
this.spaceFilteredRooms = new Map();
|
this.spaceFilteredRooms = new Map();
|
||||||
|
this.spaceFilteredUsers = new Map();
|
||||||
this._activeSpace = MetaSpace.Home; // set properly by onReady
|
this._activeSpace = MetaSpace.Home; // set properly by onReady
|
||||||
this._suggestedRooms = [];
|
this._suggestedRooms = [];
|
||||||
this._invitedSpaces = new Set();
|
this._invitedSpaces = new Set();
|
||||||
|
@ -809,17 +1042,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
const enabledMetaSpaces = SettingsStore.getValue("Spaces.enabledMetaSpaces");
|
const enabledMetaSpaces = SettingsStore.getValue("Spaces.enabledMetaSpaces");
|
||||||
this._enabledMetaSpaces = metaSpaceOrder.filter(k => enabledMetaSpaces[k]) as MetaSpace[];
|
this._enabledMetaSpaces = metaSpaceOrder.filter(k => enabledMetaSpaces[k]) as MetaSpace[];
|
||||||
|
|
||||||
await this.onSpaceUpdate(); // trigger an initial update
|
this.rebuildSpaceHierarchy(); // trigger an initial update
|
||||||
|
|
||||||
// restore selected state from last session if any and still valid
|
// restore selected state from last session if any and still valid
|
||||||
const lastSpaceId = window.localStorage.getItem(ACTIVE_SPACE_LS_KEY);
|
const lastSpaceId = window.localStorage.getItem(ACTIVE_SPACE_LS_KEY);
|
||||||
if (lastSpaceId && (
|
const valid = (lastSpaceId && !isMetaSpace(lastSpaceId))
|
||||||
lastSpaceId[0] === "!" ? this.matrixClient.getRoom(lastSpaceId) : enabledMetaSpaces[lastSpaceId]
|
? this.matrixClient.getRoom(lastSpaceId)
|
||||||
)) {
|
: enabledMetaSpaces[lastSpaceId];
|
||||||
|
if (valid) {
|
||||||
// don't context switch here as it may break permalinks
|
// don't context switch here as it may break permalinks
|
||||||
this.setActiveSpace(lastSpaceId, false);
|
this.setActiveSpace(lastSpaceId, false);
|
||||||
} else {
|
} else {
|
||||||
this.goToFirstSpace();
|
this.switchSpaceIfNeeded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -828,7 +1062,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onAction(payload: ActionPayload) {
|
protected async onAction(payload: ActionPayload) {
|
||||||
if (!spacesEnabled) return;
|
if (!spacesEnabled || !this.matrixClient) return;
|
||||||
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case "view_room": {
|
case "view_room": {
|
||||||
// Don't auto-switch rooms when reacting to a context-switch
|
// Don't auto-switch rooms when reacting to a context-switch
|
||||||
|
@ -842,12 +1077,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
|
|
||||||
if (!roomId) return; // we'll get re-fired with the room ID shortly
|
if (!roomId) return; // we'll get re-fired with the room ID shortly
|
||||||
|
|
||||||
const room = this.matrixClient?.getRoom(roomId);
|
const room = this.matrixClient.getRoom(roomId);
|
||||||
if (room?.isSpaceRoom()) {
|
if (room?.isSpaceRoom()) {
|
||||||
// Don't context switch when navigating to the space room
|
// Don't context switch when navigating to the space room
|
||||||
// as it will cause you to end up in the wrong room
|
// as it will cause you to end up in the wrong room
|
||||||
this.setActiveSpace(room.roomId, false);
|
this.setActiveSpace(room.roomId, false);
|
||||||
} else if (!this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) {
|
} else if (!this.isRoomInSpace(this.activeSpace, roomId)) {
|
||||||
this.switchToRelatedSpace(roomId);
|
this.switchToRelatedSpace(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -866,9 +1101,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "after_leave_room":
|
case "after_leave_room":
|
||||||
if (this._activeSpace[0] === "!" && payload.room_id === this._activeSpace) {
|
if (!isMetaSpace(this._activeSpace) && payload.room_id === this._activeSpace) {
|
||||||
// User has left the current space, go to first space
|
// User has left the current space, go to first space
|
||||||
this.goToFirstSpace();
|
this.goToFirstSpace(true);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -892,7 +1127,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
if (this.allRoomsInHome !== newValue) {
|
if (this.allRoomsInHome !== newValue) {
|
||||||
this._allRoomsInHome = newValue;
|
this._allRoomsInHome = newValue;
|
||||||
this.emit(UPDATE_HOME_BEHAVIOUR, this.allRoomsInHome);
|
this.emit(UPDATE_HOME_BEHAVIOUR, this.allRoomsInHome);
|
||||||
this.rebuild(); // rebuild everything
|
if (this.enabledMetaSpaces.includes(MetaSpace.Home)) {
|
||||||
|
this.rebuildHomeSpace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -901,18 +1138,39 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
const newValue = SettingsStore.getValue("Spaces.enabledMetaSpaces");
|
const newValue = SettingsStore.getValue("Spaces.enabledMetaSpaces");
|
||||||
const enabledMetaSpaces = metaSpaceOrder.filter(k => newValue[k]) as MetaSpace[];
|
const enabledMetaSpaces = metaSpaceOrder.filter(k => newValue[k]) as MetaSpace[];
|
||||||
if (arrayHasDiff(this._enabledMetaSpaces, enabledMetaSpaces)) {
|
if (arrayHasDiff(this._enabledMetaSpaces, enabledMetaSpaces)) {
|
||||||
|
const hadPeopleOrHomeEnabled = this.enabledMetaSpaces.some(s => {
|
||||||
|
return s === MetaSpace.Home || s === MetaSpace.People;
|
||||||
|
});
|
||||||
this._enabledMetaSpaces = enabledMetaSpaces;
|
this._enabledMetaSpaces = enabledMetaSpaces;
|
||||||
// if a metaspace currently being viewed was remove, go to another one
|
const hasPeopleOrHomeEnabled = this.enabledMetaSpaces.some(s => {
|
||||||
if (this.activeSpace[0] !== "!" &&
|
return s === MetaSpace.Home || s === MetaSpace.People;
|
||||||
!enabledMetaSpaces.includes(this.activeSpace as MetaSpace)
|
});
|
||||||
) {
|
|
||||||
this.goToFirstSpace();
|
// if a metaspace currently being viewed was removed, go to another one
|
||||||
|
if (isMetaSpace(this.activeSpace) && !newValue[this.activeSpace]) {
|
||||||
|
this.switchSpaceIfNeeded();
|
||||||
}
|
}
|
||||||
|
this.rebuildMetaSpaces();
|
||||||
|
|
||||||
|
if (hadPeopleOrHomeEnabled !== hasPeopleOrHomeEnabled) {
|
||||||
|
// in this case we have to rebuild everything as DM badges will move to/from real spaces
|
||||||
|
this.updateNotificationStates();
|
||||||
|
} else {
|
||||||
|
this.updateNotificationStates(enabledMetaSpaces);
|
||||||
|
}
|
||||||
|
|
||||||
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces, this.enabledMetaSpaces);
|
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces, this.enabledMetaSpaces);
|
||||||
this.rebuild(); // rebuild everything
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "Spaces.showPeopleInSpace":
|
||||||
|
// getSpaceFilteredUserIds will return the appropriate value
|
||||||
|
this.emit(settingUpdatedPayload.roomId);
|
||||||
|
if (!this.enabledMetaSpaces.some(s => s === MetaSpace.Home || s === MetaSpace.People)) {
|
||||||
|
this.updateNotificationStates([settingUpdatedPayload.roomId]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,3 +53,10 @@ export type SpaceKey = MetaSpace | Room["roomId"];
|
||||||
export interface ISuggestedRoom extends IHierarchyRoom {
|
export interface ISuggestedRoom extends IHierarchyRoom {
|
||||||
viaServers: string[];
|
viaServers: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isMetaSpace(spaceKey: SpaceKey): boolean {
|
||||||
|
return spaceKey === MetaSpace.Home ||
|
||||||
|
spaceKey === MetaSpace.Favourites ||
|
||||||
|
spaceKey === MetaSpace.People ||
|
||||||
|
spaceKey === MetaSpace.Orphans;
|
||||||
|
}
|
||||||
|
|
|
@ -184,6 +184,8 @@ export function arrayHasDiff(a: any[], b: any[]): boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Diff<T> = { added: T[], removed: T[] };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a diff on two arrays. The result is what is different with the
|
* Performs a diff on two arrays. The result is what is different with the
|
||||||
* first array (`added` in the returned object means objects in B that aren't
|
* first array (`added` in the returned object means objects in B that aren't
|
||||||
|
@ -192,7 +194,7 @@ export function arrayHasDiff(a: any[], b: any[]): boolean {
|
||||||
* @param b The second array. Must be defined.
|
* @param b The second array. Must be defined.
|
||||||
* @returns The diff between the arrays.
|
* @returns The diff between the arrays.
|
||||||
*/
|
*/
|
||||||
export function arrayDiff<T>(a: T[], b: T[]): { added: T[], removed: T[] } {
|
export function arrayDiff<T>(a: T[], b: T[]): Diff<T> {
|
||||||
return {
|
return {
|
||||||
added: b.filter(i => !a.includes(i)),
|
added: b.filter(i => !a.includes(i)),
|
||||||
removed: a.filter(i => !b.includes(i)),
|
removed: a.filter(i => !b.includes(i)),
|
||||||
|
|
|
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { arrayDiff, Diff } from "./arrays";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if two sets are different through a shallow comparison.
|
* Determines if two sets are different through a shallow comparison.
|
||||||
* @param a The first set. Must be defined.
|
* @param a The first set. Must be defined.
|
||||||
|
@ -32,3 +34,13 @@ export function setHasDiff<T>(a: Set<T>, b: Set<T>): boolean {
|
||||||
return true; // different lengths means they are naturally diverged
|
return true; // different lengths means they are naturally diverged
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the values added and removed between two sets.
|
||||||
|
* @param a The first set. Must be defined.
|
||||||
|
* @param b The second set. Must be defined.
|
||||||
|
* @returns The difference between the values in each set.
|
||||||
|
*/
|
||||||
|
export function setDiff<T>(a: Set<T>, b: Set<T>): Diff<T> {
|
||||||
|
return arrayDiff(Array.from(a), Array.from(b));
|
||||||
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ import Spinner from "../components/views/elements/Spinner";
|
||||||
import dis from "../dispatcher/dispatcher";
|
import dis from "../dispatcher/dispatcher";
|
||||||
import LeaveSpaceDialog from "../components/views/dialogs/LeaveSpaceDialog";
|
import LeaveSpaceDialog from "../components/views/dialogs/LeaveSpaceDialog";
|
||||||
import CreateSpaceFromCommunityDialog from "../components/views/dialogs/CreateSpaceFromCommunityDialog";
|
import CreateSpaceFromCommunityDialog from "../components/views/dialogs/CreateSpaceFromCommunityDialog";
|
||||||
|
import SpacePreferencesDialog, { SpacePreferenceTab } from "../components/views/dialogs/SpacePreferencesDialog";
|
||||||
|
|
||||||
export const shouldShowSpaceSettings = (space: Room) => {
|
export const shouldShowSpaceSettings = (space: Room) => {
|
||||||
const userId = space.client.getUserId();
|
const userId = space.client.getUserId();
|
||||||
|
@ -197,3 +198,10 @@ export const createSpaceFromCommunity = (cli: MatrixClient, groupId: string): Pr
|
||||||
groupId,
|
groupId,
|
||||||
}, "mx_CreateSpaceFromCommunityDialog_wrapper").finished as Promise<[string?]>;
|
}, "mx_CreateSpaceFromCommunityDialog_wrapper").finished as Promise<[string?]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const showSpacePreferences = (space: Room, initialTabId?: SpacePreferenceTab): Promise<unknown> => {
|
||||||
|
return Modal.createTrackedDialog("Space preferences", "", SpacePreferencesDialog, {
|
||||||
|
initialTabId,
|
||||||
|
space,
|
||||||
|
}, null, false, true).finished;
|
||||||
|
};
|
||||||
|
|
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from "events";
|
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
|
||||||
|
@ -40,11 +39,6 @@ jest.useFakeTimers();
|
||||||
|
|
||||||
const testUserId = "@test:user";
|
const testUserId = "@test:user";
|
||||||
|
|
||||||
const getUserIdForRoomId = jest.fn();
|
|
||||||
const getDMRoomsForUserId = jest.fn();
|
|
||||||
// @ts-ignore
|
|
||||||
DMRoomMap.sharedInstance = { getUserIdForRoomId, getDMRoomsForUserId };
|
|
||||||
|
|
||||||
const fav1 = "!fav1:server";
|
const fav1 = "!fav1:server";
|
||||||
const fav2 = "!fav2:server";
|
const fav2 = "!fav2:server";
|
||||||
const fav3 = "!fav3:server";
|
const fav3 = "!fav3:server";
|
||||||
|
@ -68,6 +62,28 @@ const space1 = "!space1:server";
|
||||||
const space2 = "!space2:server";
|
const space2 = "!space2:server";
|
||||||
const space3 = "!space3:server";
|
const space3 = "!space3:server";
|
||||||
|
|
||||||
|
const getUserIdForRoomId = jest.fn(roomId => {
|
||||||
|
return {
|
||||||
|
[dm1]: dm1Partner.userId,
|
||||||
|
[dm2]: dm2Partner.userId,
|
||||||
|
[dm3]: dm3Partner.userId,
|
||||||
|
}[roomId];
|
||||||
|
});
|
||||||
|
const getDMRoomsForUserId = jest.fn(userId => {
|
||||||
|
switch (userId) {
|
||||||
|
case dm1Partner.userId:
|
||||||
|
return [dm1];
|
||||||
|
case dm2Partner.userId:
|
||||||
|
return [dm2];
|
||||||
|
case dm3Partner.userId:
|
||||||
|
return [dm3];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
DMRoomMap.sharedInstance = { getUserIdForRoomId, getDMRoomsForUserId };
|
||||||
|
|
||||||
describe("SpaceStore", () => {
|
describe("SpaceStore", () => {
|
||||||
stubClient();
|
stubClient();
|
||||||
const store = SpaceStore.instance;
|
const store = SpaceStore.instance;
|
||||||
|
@ -306,26 +322,6 @@ describe("SpaceStore", () => {
|
||||||
client.getRoom(roomId).getMyMembership.mockReturnValue("invite");
|
client.getRoom(roomId).getMyMembership.mockReturnValue("invite");
|
||||||
});
|
});
|
||||||
|
|
||||||
getUserIdForRoomId.mockImplementation(roomId => {
|
|
||||||
return {
|
|
||||||
[dm1]: dm1Partner.userId,
|
|
||||||
[dm2]: dm2Partner.userId,
|
|
||||||
[dm3]: dm3Partner.userId,
|
|
||||||
}[roomId];
|
|
||||||
});
|
|
||||||
getDMRoomsForUserId.mockImplementation(userId => {
|
|
||||||
switch (userId) {
|
|
||||||
case dm1Partner.userId:
|
|
||||||
return [dm1];
|
|
||||||
case dm2Partner.userId:
|
|
||||||
return [dm2];
|
|
||||||
case dm3Partner.userId:
|
|
||||||
return [dm3];
|
|
||||||
default:
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// have dmPartner1 be in space1 with you
|
// have dmPartner1 be in space1 with you
|
||||||
const mySpace1Member = new RoomMember(space1, testUserId);
|
const mySpace1Member = new RoomMember(space1, testUserId);
|
||||||
mySpace1Member.membership = "join";
|
mySpace1Member.membership = "join";
|
||||||
|
@ -388,103 +384,104 @@ describe("SpaceStore", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("home space contains orphaned rooms", () => {
|
it("home space contains orphaned rooms", () => {
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(orphan1)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.Home, orphan1)).toBeTruthy();
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(orphan2)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.Home, orphan2)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("home space does not contain all favourites", () => {
|
it("home space does not contain all favourites", () => {
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(fav1)).toBeFalsy();
|
expect(store.isRoomInSpace(MetaSpace.Home, fav1)).toBeFalsy();
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(fav2)).toBeFalsy();
|
expect(store.isRoomInSpace(MetaSpace.Home, fav2)).toBeFalsy();
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(fav3)).toBeFalsy();
|
expect(store.isRoomInSpace(MetaSpace.Home, fav3)).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("home space contains dm rooms", () => {
|
it("home space contains dm rooms", () => {
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(dm1)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.Home, dm1)).toBeTruthy();
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(dm2)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.Home, dm2)).toBeTruthy();
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(dm3)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.Home, dm3)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("home space contains invites", () => {
|
it("home space contains invites", () => {
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite1)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.Home, invite1)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("home space contains invites even if they are also shown in a space", () => {
|
it("home space contains invites even if they are also shown in a space", () => {
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite2)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.Home, invite2)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("all rooms space does contain rooms/low priority even if they are also shown in a space", async () => {
|
it("all rooms space does contain rooms/low priority even if they are also shown in a space", async () => {
|
||||||
await setShowAllRooms(true);
|
await setShowAllRooms(true);
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(room1)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.Home, room1)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("favourites space does contain favourites even if they are also shown in a space", async () => {
|
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.isRoomInSpace(MetaSpace.Favourites, fav1)).toBeTruthy();
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Favourites).has(fav2)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.Favourites, fav2)).toBeTruthy();
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Favourites).has(fav3)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.Favourites, fav3)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("people space does contain people even if they are also shown in a space", async () => {
|
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.isRoomInSpace(MetaSpace.People, dm1)).toBeTruthy();
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.People).has(dm2)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.People, dm2)).toBeTruthy();
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.People).has(dm3)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.People, dm3)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("orphans space does contain orphans even if they are also shown in all rooms", async () => {
|
it("orphans space does contain orphans even if they are also shown in all rooms", async () => {
|
||||||
await setShowAllRooms(true);
|
await setShowAllRooms(true);
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Orphans).has(orphan1)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.Orphans, orphan1)).toBeTruthy();
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Orphans).has(orphan2)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.Orphans, orphan2)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("home space doesn't contain rooms/low priority if they are also shown in a space", async () => {
|
it("home space doesn't contain rooms/low priority if they are also shown in a space", async () => {
|
||||||
await setShowAllRooms(false);
|
await setShowAllRooms(false);
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(room1)).toBeFalsy();
|
expect(store.isRoomInSpace(MetaSpace.Home, room1)).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("space contains child rooms", () => {
|
it("space contains child rooms", () => {
|
||||||
expect(store.getSpaceFilteredRoomIds(space1).has(fav1)).toBeTruthy();
|
expect(store.isRoomInSpace(space1, fav1)).toBeTruthy();
|
||||||
expect(store.getSpaceFilteredRoomIds(space1).has(room1)).toBeTruthy();
|
expect(store.isRoomInSpace(space1, room1)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("space contains child favourites", () => {
|
it("space contains child favourites", () => {
|
||||||
expect(store.getSpaceFilteredRoomIds(space2).has(fav1)).toBeTruthy();
|
expect(store.isRoomInSpace(space2, fav1)).toBeTruthy();
|
||||||
expect(store.getSpaceFilteredRoomIds(space2).has(fav2)).toBeTruthy();
|
expect(store.isRoomInSpace(space2, fav2)).toBeTruthy();
|
||||||
expect(store.getSpaceFilteredRoomIds(space2).has(fav3)).toBeTruthy();
|
expect(store.isRoomInSpace(space2, fav3)).toBeTruthy();
|
||||||
expect(store.getSpaceFilteredRoomIds(space2).has(room1)).toBeTruthy();
|
expect(store.isRoomInSpace(space2, room1)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("space contains child invites", () => {
|
it("space contains child invites", () => {
|
||||||
expect(store.getSpaceFilteredRoomIds(space3).has(invite2)).toBeTruthy();
|
expect(store.isRoomInSpace(space3, invite2)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("spaces contain dms which you have with members of that space", () => {
|
it("spaces contain dms which you have with members of that space", () => {
|
||||||
expect(store.getSpaceFilteredRoomIds(space1).has(dm1)).toBeTruthy();
|
expect(store.isRoomInSpace(space1, dm1)).toBeTruthy();
|
||||||
expect(store.getSpaceFilteredRoomIds(space2).has(dm1)).toBeFalsy();
|
expect(store.isRoomInSpace(space2, dm1)).toBeFalsy();
|
||||||
expect(store.getSpaceFilteredRoomIds(space3).has(dm1)).toBeFalsy();
|
expect(store.isRoomInSpace(space3, dm1)).toBeFalsy();
|
||||||
expect(store.getSpaceFilteredRoomIds(space1).has(dm2)).toBeFalsy();
|
expect(store.isRoomInSpace(space1, dm2)).toBeFalsy();
|
||||||
expect(store.getSpaceFilteredRoomIds(space2).has(dm2)).toBeTruthy();
|
expect(store.isRoomInSpace(space2, dm2)).toBeTruthy();
|
||||||
expect(store.getSpaceFilteredRoomIds(space3).has(dm2)).toBeFalsy();
|
expect(store.isRoomInSpace(space3, dm2)).toBeFalsy();
|
||||||
expect(store.getSpaceFilteredRoomIds(space1).has(dm3)).toBeFalsy();
|
expect(store.isRoomInSpace(space1, dm3)).toBeFalsy();
|
||||||
expect(store.getSpaceFilteredRoomIds(space2).has(dm3)).toBeFalsy();
|
expect(store.isRoomInSpace(space2, dm3)).toBeFalsy();
|
||||||
expect(store.getSpaceFilteredRoomIds(space3).has(dm3)).toBeFalsy();
|
expect(store.isRoomInSpace(space3, dm3)).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("dms are only added to Notification States for only the Home Space", () => {
|
it("dms are only added to Notification States for only the People Space", async () => {
|
||||||
// XXX: All rooms space is forcibly enabled, as part of a future PR test Home space better
|
[dm1, dm2, dm3].forEach(d => {
|
||||||
// [dm1, dm2, dm3].forEach(d => {
|
expect(store.getNotificationState(MetaSpace.People)
|
||||||
// expect(store.getNotificationState(HOME_SPACE).rooms.map(r => r.roomId).includes(d)).toBeTruthy();
|
.rooms.map(r => r.roomId).includes(d)).toBeTruthy();
|
||||||
// });
|
});
|
||||||
[space1, space2, space3].forEach(s => {
|
[space1, space2, space3, MetaSpace.Home, MetaSpace.Orphans, MetaSpace.Favourites].forEach(s => {
|
||||||
[dm1, dm2, dm3].forEach(d => {
|
[dm1, dm2, dm3].forEach(d => {
|
||||||
expect(store.getNotificationState(s).rooms.map(r => r.roomId).includes(d)).toBeFalsy();
|
expect(store.getNotificationState(s).rooms.map(r => r.roomId).includes(d)).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("orphan rooms are added to Notification States for only the Home Space", () => {
|
it("orphan rooms are added to Notification States for only the Home Space", async () => {
|
||||||
// XXX: All rooms space is forcibly enabled, as part of a future PR test Home space better
|
await setShowAllRooms(false);
|
||||||
// [orphan1, orphan2].forEach(d => {
|
[orphan1, orphan2].forEach(d => {
|
||||||
// expect(store.getNotificationState(HOME_SPACE).rooms.map(r => r.roomId).includes(d)).toBeTruthy();
|
expect(store.getNotificationState(MetaSpace.Home)
|
||||||
// });
|
.rooms.map(r => r.roomId).includes(d)).toBeTruthy();
|
||||||
|
});
|
||||||
[space1, space2, space3].forEach(s => {
|
[space1, space2, space3].forEach(s => {
|
||||||
[orphan1, orphan2].forEach(d => {
|
[orphan1, orphan2].forEach(d => {
|
||||||
expect(store.getNotificationState(s).rooms.map(r => r.roomId).includes(d)).toBeFalsy();
|
expect(store.getNotificationState(s).rooms.map(r => r.roomId).includes(d)).toBeFalsy();
|
||||||
|
@ -517,33 +514,22 @@ describe("SpaceStore", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("honours m.space.parent if sender has permission in parent space", () => {
|
it("honours m.space.parent if sender has permission in parent space", () => {
|
||||||
expect(store.getSpaceFilteredRoomIds(space2).has(room2)).toBeTruthy();
|
expect(store.isRoomInSpace(space2, room2)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not honour m.space.parent if sender does not have permission in parent space", () => {
|
it("does not honour m.space.parent if sender does not have permission in parent space", () => {
|
||||||
expect(store.getSpaceFilteredRoomIds(space3).has(room3)).toBeFalsy();
|
expect(store.isRoomInSpace(space3, room3)).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("hierarchy resolution update tests", () => {
|
describe("hierarchy resolution update tests", () => {
|
||||||
let emitter: EventEmitter;
|
|
||||||
beforeEach(async () => {
|
|
||||||
emitter = new EventEmitter();
|
|
||||||
client.on.mockImplementation(emitter.on.bind(emitter));
|
|
||||||
client.removeListener.mockImplementation(emitter.removeListener.bind(emitter));
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
client.on.mockReset();
|
|
||||||
client.removeListener.mockReset();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates state when spaces are joined", async () => {
|
it("updates state when spaces are joined", async () => {
|
||||||
await run();
|
await run();
|
||||||
expect(store.spacePanelSpaces).toStrictEqual([]);
|
expect(store.spacePanelSpaces).toStrictEqual([]);
|
||||||
const space = mkSpace(space1);
|
const space = mkSpace(space1);
|
||||||
const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES);
|
const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES);
|
||||||
emitter.emit("Room", space);
|
client.emit("Room", space);
|
||||||
await prom;
|
await prom;
|
||||||
expect(store.spacePanelSpaces).toStrictEqual([space]);
|
expect(store.spacePanelSpaces).toStrictEqual([space]);
|
||||||
expect(store.invitedSpaces).toStrictEqual([]);
|
expect(store.invitedSpaces).toStrictEqual([]);
|
||||||
|
@ -556,7 +542,7 @@ describe("SpaceStore", () => {
|
||||||
expect(store.spacePanelSpaces).toStrictEqual([space]);
|
expect(store.spacePanelSpaces).toStrictEqual([space]);
|
||||||
space.getMyMembership.mockReturnValue("leave");
|
space.getMyMembership.mockReturnValue("leave");
|
||||||
const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES);
|
const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES);
|
||||||
emitter.emit("Room.myMembership", space, "leave", "join");
|
client.emit("Room.myMembership", space, "leave", "join");
|
||||||
await prom;
|
await prom;
|
||||||
expect(store.spacePanelSpaces).toStrictEqual([]);
|
expect(store.spacePanelSpaces).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
|
@ -568,7 +554,7 @@ describe("SpaceStore", () => {
|
||||||
const space = mkSpace(space1);
|
const space = mkSpace(space1);
|
||||||
space.getMyMembership.mockReturnValue("invite");
|
space.getMyMembership.mockReturnValue("invite");
|
||||||
const prom = testUtils.emitPromise(store, UPDATE_INVITED_SPACES);
|
const prom = testUtils.emitPromise(store, UPDATE_INVITED_SPACES);
|
||||||
emitter.emit("Room", space);
|
client.emit("Room", space);
|
||||||
await prom;
|
await prom;
|
||||||
expect(store.spacePanelSpaces).toStrictEqual([]);
|
expect(store.spacePanelSpaces).toStrictEqual([]);
|
||||||
expect(store.invitedSpaces).toStrictEqual([space]);
|
expect(store.invitedSpaces).toStrictEqual([space]);
|
||||||
|
@ -583,7 +569,7 @@ describe("SpaceStore", () => {
|
||||||
expect(store.invitedSpaces).toStrictEqual([space]);
|
expect(store.invitedSpaces).toStrictEqual([space]);
|
||||||
space.getMyMembership.mockReturnValue("join");
|
space.getMyMembership.mockReturnValue("join");
|
||||||
const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES);
|
const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES);
|
||||||
emitter.emit("Room.myMembership", space, "join", "invite");
|
client.emit("Room.myMembership", space, "join", "invite");
|
||||||
await prom;
|
await prom;
|
||||||
expect(store.spacePanelSpaces).toStrictEqual([space]);
|
expect(store.spacePanelSpaces).toStrictEqual([space]);
|
||||||
expect(store.invitedSpaces).toStrictEqual([]);
|
expect(store.invitedSpaces).toStrictEqual([]);
|
||||||
|
@ -598,7 +584,7 @@ describe("SpaceStore", () => {
|
||||||
expect(store.invitedSpaces).toStrictEqual([space]);
|
expect(store.invitedSpaces).toStrictEqual([space]);
|
||||||
space.getMyMembership.mockReturnValue("leave");
|
space.getMyMembership.mockReturnValue("leave");
|
||||||
const prom = testUtils.emitPromise(store, UPDATE_INVITED_SPACES);
|
const prom = testUtils.emitPromise(store, UPDATE_INVITED_SPACES);
|
||||||
emitter.emit("Room.myMembership", space, "leave", "invite");
|
client.emit("Room.myMembership", space, "leave", "invite");
|
||||||
await prom;
|
await prom;
|
||||||
expect(store.spacePanelSpaces).toStrictEqual([]);
|
expect(store.spacePanelSpaces).toStrictEqual([]);
|
||||||
expect(store.invitedSpaces).toStrictEqual([]);
|
expect(store.invitedSpaces).toStrictEqual([]);
|
||||||
|
@ -612,21 +598,21 @@ describe("SpaceStore", () => {
|
||||||
expect(store.invitedSpaces).toStrictEqual([]);
|
expect(store.invitedSpaces).toStrictEqual([]);
|
||||||
expect(store.getChildSpaces(space1)).toStrictEqual([]);
|
expect(store.getChildSpaces(space1)).toStrictEqual([]);
|
||||||
expect(store.getChildRooms(space1)).toStrictEqual([]);
|
expect(store.getChildRooms(space1)).toStrictEqual([]);
|
||||||
expect(store.getSpaceFilteredRoomIds(space1).has(invite1)).toBeFalsy();
|
expect(store.isRoomInSpace(space1, invite1)).toBeFalsy();
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite1)).toBeFalsy();
|
expect(store.isRoomInSpace(MetaSpace.Home, invite1)).toBeFalsy();
|
||||||
|
|
||||||
const invite = mkRoom(invite1);
|
const invite = mkRoom(invite1);
|
||||||
invite.getMyMembership.mockReturnValue("invite");
|
invite.getMyMembership.mockReturnValue("invite");
|
||||||
const prom = testUtils.emitPromise(store, space1);
|
const prom = testUtils.emitPromise(store, space1);
|
||||||
emitter.emit("Room", space);
|
client.emit("Room", space);
|
||||||
await prom;
|
await prom;
|
||||||
|
|
||||||
expect(store.spacePanelSpaces).toStrictEqual([space]);
|
expect(store.spacePanelSpaces).toStrictEqual([space]);
|
||||||
expect(store.invitedSpaces).toStrictEqual([]);
|
expect(store.invitedSpaces).toStrictEqual([]);
|
||||||
expect(store.getChildSpaces(space1)).toStrictEqual([]);
|
expect(store.getChildSpaces(space1)).toStrictEqual([]);
|
||||||
expect(store.getChildRooms(space1)).toStrictEqual([invite]);
|
expect(store.getChildRooms(space1)).toStrictEqual([invite]);
|
||||||
expect(store.getSpaceFilteredRoomIds(space1).has(invite1)).toBeTruthy();
|
expect(store.isRoomInSpace(space1, invite1)).toBeTruthy();
|
||||||
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite1)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.Home, invite1)).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -817,7 +803,7 @@ describe("SpaceStore", () => {
|
||||||
expect(store.activeSpace).toBe(MetaSpace.Orphans);
|
expect(store.activeSpace).toBe(MetaSpace.Orphans);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("switch to first space when selected metaspace is disabled", async () => {
|
it("switch to first valid space when selected metaspace is disabled", async () => {
|
||||||
store.setActiveSpace(MetaSpace.People, false);
|
store.setActiveSpace(MetaSpace.People, false);
|
||||||
expect(store.activeSpace).toBe(MetaSpace.People);
|
expect(store.activeSpace).toBe(MetaSpace.People);
|
||||||
await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, {
|
await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, {
|
||||||
|
@ -827,7 +813,7 @@ describe("SpaceStore", () => {
|
||||||
[MetaSpace.Orphans]: true,
|
[MetaSpace.Orphans]: true,
|
||||||
});
|
});
|
||||||
jest.runAllTimers();
|
jest.runAllTimers();
|
||||||
expect(store.activeSpace).toBe(MetaSpace.Favourites);
|
expect(store.activeSpace).toBe(MetaSpace.Orphans);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when switching rooms in the all rooms home space don't switch to related space", async () => {
|
it("when switching rooms in the all rooms home space don't switch to related space", async () => {
|
||||||
|
@ -889,4 +875,105 @@ describe("SpaceStore", () => {
|
||||||
expect(fn).toBeCalledWith("!c:server");
|
expect(fn).toBeCalledWith("!c:server");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("test user flow", async () => {
|
||||||
|
// init the store
|
||||||
|
await run();
|
||||||
|
await setShowAllRooms(false);
|
||||||
|
|
||||||
|
// receive invite to space
|
||||||
|
const rootSpace = mkSpace(space1, [room1, room2, space2]);
|
||||||
|
rootSpace.getMyMembership.mockReturnValue("invite");
|
||||||
|
client.emit("Room", rootSpace);
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(SpaceStore.instance.invitedSpaces).toStrictEqual([rootSpace]);
|
||||||
|
expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([]);
|
||||||
|
|
||||||
|
// accept invite to space
|
||||||
|
rootSpace.getMyMembership.mockReturnValue("join");
|
||||||
|
client.emit("Room.myMembership", rootSpace, "join", "invite");
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]);
|
||||||
|
expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([rootSpace]);
|
||||||
|
|
||||||
|
// join room in space
|
||||||
|
expect(SpaceStore.instance.isRoomInSpace(space1, room1)).toBeFalsy();
|
||||||
|
const rootSpaceRoom1 = mkRoom(room1);
|
||||||
|
rootSpaceRoom1.getMyMembership.mockReturnValue("join");
|
||||||
|
client.emit("Room", rootSpaceRoom1);
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]);
|
||||||
|
expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([rootSpace]);
|
||||||
|
expect(SpaceStore.instance.isRoomInSpace(space1, room1)).toBeTruthy();
|
||||||
|
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Home, room1)).toBeFalsy();
|
||||||
|
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Favourites, room1)).toBeFalsy();
|
||||||
|
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.People, room1)).toBeFalsy();
|
||||||
|
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Orphans, room1)).toBeFalsy();
|
||||||
|
|
||||||
|
// receive room invite
|
||||||
|
expect(SpaceStore.instance.isRoomInSpace(space1, room2)).toBeFalsy();
|
||||||
|
const rootSpaceRoom2 = mkRoom(room2);
|
||||||
|
rootSpaceRoom2.getMyMembership.mockReturnValue("invite");
|
||||||
|
client.emit("Room", rootSpaceRoom2);
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]);
|
||||||
|
expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([rootSpace]);
|
||||||
|
expect(SpaceStore.instance.isRoomInSpace(space1, room2)).toBeTruthy();
|
||||||
|
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Home, room2)).toBeTruthy();
|
||||||
|
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Favourites, room2)).toBeFalsy();
|
||||||
|
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.People, room2)).toBeFalsy();
|
||||||
|
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Orphans, room2)).toBeFalsy();
|
||||||
|
|
||||||
|
// start DM in space
|
||||||
|
const myRootSpaceMember = new RoomMember(space1, testUserId);
|
||||||
|
myRootSpaceMember.membership = "join";
|
||||||
|
const rootSpaceFriend = new RoomMember(space1, dm1Partner.userId);
|
||||||
|
rootSpaceFriend.membership = "join";
|
||||||
|
rootSpace.getMembers.mockReturnValue([
|
||||||
|
myRootSpaceMember,
|
||||||
|
rootSpaceFriend,
|
||||||
|
]);
|
||||||
|
rootSpace.getMember.mockImplementation(userId => {
|
||||||
|
switch (userId) {
|
||||||
|
case testUserId:
|
||||||
|
return myRootSpaceMember;
|
||||||
|
case dm1Partner.userId:
|
||||||
|
return rootSpaceFriend;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(SpaceStore.instance.getSpaceFilteredUserIds(space1).has(dm1Partner.userId)).toBeFalsy();
|
||||||
|
client.emit("RoomState.members", mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: EventType.RoomMember,
|
||||||
|
content: {
|
||||||
|
membership: "join",
|
||||||
|
},
|
||||||
|
skey: dm1Partner.userId,
|
||||||
|
user: dm1Partner.userId,
|
||||||
|
room: space1,
|
||||||
|
}));
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(SpaceStore.instance.getSpaceFilteredUserIds(space1).has(dm1Partner.userId)).toBeTruthy();
|
||||||
|
const dm1Room = mkRoom(dm1);
|
||||||
|
dm1Room.getMyMembership.mockReturnValue("join");
|
||||||
|
client.emit("Room", dm1Room);
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]);
|
||||||
|
expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([rootSpace]);
|
||||||
|
expect(SpaceStore.instance.isRoomInSpace(space1, dm1)).toBeTruthy();
|
||||||
|
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Home, dm1)).toBeTruthy();
|
||||||
|
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Favourites, dm1)).toBeFalsy();
|
||||||
|
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.People, dm1)).toBeTruthy();
|
||||||
|
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Orphans, dm1)).toBeFalsy();
|
||||||
|
|
||||||
|
// join subspace
|
||||||
|
const subspace = mkSpace(space2);
|
||||||
|
subspace.getMyMembership.mockReturnValue("join");
|
||||||
|
const prom = testUtils.emitPromise(SpaceStore.instance, space1);
|
||||||
|
client.emit("Room", subspace);
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]);
|
||||||
|
expect(SpaceStore.instance.spacePanelSpaces.map(r => r.roomId)).toStrictEqual([rootSpace.roomId]);
|
||||||
|
await prom;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import EventEmitter from "events";
|
||||||
import ShallowRenderer from 'react-test-renderer/shallow';
|
import ShallowRenderer from 'react-test-renderer/shallow';
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
|
@ -43,6 +44,8 @@ export function stubClient() {
|
||||||
* @returns {object} MatrixClient stub
|
* @returns {object} MatrixClient stub
|
||||||
*/
|
*/
|
||||||
export function createTestClient() {
|
export function createTestClient() {
|
||||||
|
const eventEmitter = new EventEmitter();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getHomeserverUrl: jest.fn(),
|
getHomeserverUrl: jest.fn(),
|
||||||
getIdentityServerUrl: jest.fn(),
|
getIdentityServerUrl: jest.fn(),
|
||||||
|
@ -57,8 +60,9 @@ export function createTestClient() {
|
||||||
getVisibleRooms: jest.fn().mockReturnValue([]),
|
getVisibleRooms: jest.fn().mockReturnValue([]),
|
||||||
getGroups: jest.fn().mockReturnValue([]),
|
getGroups: jest.fn().mockReturnValue([]),
|
||||||
loginFlows: jest.fn(),
|
loginFlows: jest.fn(),
|
||||||
on: jest.fn(),
|
on: eventEmitter.on.bind(eventEmitter),
|
||||||
removeListener: jest.fn(),
|
emit: eventEmitter.emit.bind(eventEmitter),
|
||||||
|
removeListener: eventEmitter.removeListener.bind(eventEmitter),
|
||||||
isRoomEncrypted: jest.fn().mockReturnValue(false),
|
isRoomEncrypted: jest.fn().mockReturnValue(false),
|
||||||
peekInRoom: jest.fn().mockResolvedValue(mkStubRoom()),
|
peekInRoom: jest.fn().mockResolvedValue(mkStubRoom()),
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue