Don't aggregate rooms and users in SpaceStore (#7723)
* add direct child maps * track rooms, users and space children in flat hierarchy in spacestore Signed-off-by: Kerry Archibald <kerrya@element.io> * update spacefiltercondition to use new spacestore * remove unused code Signed-off-by: Kerry Archibald <kerrya@element.io> * typos Signed-off-by: Kerry Archibald <kerrya@element.io> * only build flattened rooms set once per space when updating notifs * copyright Signed-off-by: Kerry Archibald <kerrya@element.io> * remove unnecessary currying Signed-off-by: Kerry Archibald <kerrya@element.io> * rename SpaceStore spaceFilteredRooms => roomsIdsBySpace, spaceFilteredUsers => userIdsBySpace Signed-off-by: Kerry Archibald <kerrya@element.io> * cache aggregates rooms and users by space Signed-off-by: Kerry Archibald <kerrya@element.io> * emit events recursively up parent spaces on changes Signed-off-by: Kerry Archibald <kerrya@element.io> * exclude meta spaces from aggregate cache Signed-off-by: Kerry Archibald <kerrya@element.io> * stray log * fix emit on member update Signed-off-by: Kerry Archibald <kerrya@element.io> * call order Signed-off-by: Kerry Archibald <kerrya@element.io> * extend existing getKnownParents fn Signed-off-by: Kerry Archibald <kerrya@element.io> * refine types and comments Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
parent
658590e5bc
commit
08a0c6f86c
4 changed files with 642 additions and 175 deletions
|
@ -53,10 +53,16 @@ import {
|
||||||
} from ".";
|
} from ".";
|
||||||
import { getCachedRoomIDForAlias } from "../../RoomAliasCache";
|
import { getCachedRoomIDForAlias } from "../../RoomAliasCache";
|
||||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
||||||
|
import {
|
||||||
|
flattenSpaceHierarchyWithCache,
|
||||||
|
SpaceEntityMap,
|
||||||
|
SpaceDescendantMap,
|
||||||
|
flattenSpaceHierarchy,
|
||||||
|
} from "./flattenSpaceHierarchy";
|
||||||
import { PosthogAnalytics } from "../../PosthogAnalytics";
|
import { PosthogAnalytics } from "../../PosthogAnalytics";
|
||||||
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||||
|
|
||||||
interface IState {}
|
interface IState { }
|
||||||
|
|
||||||
const ACTIVE_SPACE_LS_KEY = "mx_active_space";
|
const ACTIVE_SPACE_LS_KEY = "mx_active_space";
|
||||||
|
|
||||||
|
@ -101,10 +107,19 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
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 SpaceKey to Set of room IDs that are direct descendants of that space
|
||||||
private spaceFilteredRooms = new Map<SpaceKey, Set<string>>(); // won't contain MetaSpace.People
|
private roomIdsBySpace: SpaceEntityMap = 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
|
// Map from space id to Set of space keys that are direct descendants of that space
|
||||||
private spaceFilteredUsers = new Map<Room["roomId"], Set<string>>();
|
// meta spaces do not have descendants
|
||||||
|
private childSpacesBySpace: SpaceDescendantMap = new Map<Room["roomId"], Set<Room["roomId"]>>();
|
||||||
|
// Map from space id to Set of user IDs that are direct descendants of that space
|
||||||
|
private userIdsBySpace: SpaceEntityMap = new Map<Room["roomId"], Set<string>>();
|
||||||
|
// cache that stores the aggregated lists of roomIdsBySpace and userIdsBySpace
|
||||||
|
// cleared on changes
|
||||||
|
private _aggregatedSpaceCache = {
|
||||||
|
roomIdsBySpace: new Map<SpaceKey, Set<string>>(),
|
||||||
|
userIdsBySpace: 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[] = [];
|
||||||
|
@ -352,16 +367,19 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
return sortBy(parents, r => r.roomId)?.[0] || null;
|
return sortBy(parents, r => r.roomId)?.[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getKnownParents(roomId: string): Set<string> {
|
public getKnownParents(roomId: string, includeAncestors?: boolean): Set<string> {
|
||||||
|
if (includeAncestors) {
|
||||||
|
return flattenSpaceHierarchy(this.parentMap, this.parentMap, roomId);
|
||||||
|
}
|
||||||
return this.parentMap.get(roomId) || new Set();
|
return this.parentMap.get(roomId) || new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
public isRoomInSpace(space: SpaceKey, roomId: string): boolean {
|
public isRoomInSpace(space: SpaceKey, roomId: string, includeDescendantSpaces = true): boolean {
|
||||||
if (space === MetaSpace.Home && this.allRoomsInHome) {
|
if (space === MetaSpace.Home && this.allRoomsInHome) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.spaceFilteredRooms.get(space)?.has(roomId)) {
|
if (this.getSpaceFilteredRoomIds(space, includeDescendantSpaces)?.has(roomId)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,7 +395,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isMetaSpace(space) &&
|
if (!isMetaSpace(space) &&
|
||||||
this.spaceFilteredUsers.get(space)?.has(dmPartner) &&
|
this.getSpaceFilteredUserIds(space, includeDescendantSpaces)?.has(dmPartner) &&
|
||||||
SettingsStore.getValue("Spaces.showPeopleInSpace", space)
|
SettingsStore.getValue("Spaces.showPeopleInSpace", space)
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -386,21 +404,46 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSpaceFilteredRoomIds = (space: SpaceKey): Set<string> => {
|
// get all rooms in a space
|
||||||
|
// including descendant spaces
|
||||||
|
public getSpaceFilteredRoomIds = (
|
||||||
|
space: SpaceKey, includeDescendantSpaces = true, useCache = true,
|
||||||
|
): 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));
|
||||||
}
|
}
|
||||||
return this.spaceFilteredRooms.get(space) || new Set();
|
|
||||||
|
// meta spaces never have descendants
|
||||||
|
// and the aggregate cache is not managed for meta spaces
|
||||||
|
if (!includeDescendantSpaces || isMetaSpace(space)) {
|
||||||
|
return this.roomIdsBySpace.get(space) || new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getAggregatedRoomIdsBySpace(this.roomIdsBySpace, this.childSpacesBySpace, space, useCache);
|
||||||
};
|
};
|
||||||
|
|
||||||
public getSpaceFilteredUserIds = (space: SpaceKey): Set<string> => {
|
public getSpaceFilteredUserIds = (
|
||||||
|
space: SpaceKey, includeDescendantSpaces = true, useCache = true,
|
||||||
|
): Set<string> => {
|
||||||
if (space === MetaSpace.Home && this.allRoomsInHome) {
|
if (space === MetaSpace.Home && this.allRoomsInHome) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (isMetaSpace(space)) return undefined;
|
if (isMetaSpace(space)) {
|
||||||
return this.spaceFilteredUsers.get(space) || new Set();
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// meta spaces never have descendants
|
||||||
|
// and the aggregate cache is not managed for meta spaces
|
||||||
|
if (!includeDescendantSpaces || isMetaSpace(space)) {
|
||||||
|
return this.userIdsBySpace.get(space) || new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getAggregatedUserIdsBySpace(this.userIdsBySpace, this.childSpacesBySpace, space, useCache);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private getAggregatedRoomIdsBySpace = flattenSpaceHierarchyWithCache(this._aggregatedSpaceCache.roomIdsBySpace);
|
||||||
|
private getAggregatedUserIdsBySpace = flattenSpaceHierarchyWithCache(this._aggregatedSpaceCache.userIdsBySpace);
|
||||||
|
|
||||||
private markTreeChildren = (rootSpace: Room, unseen: Set<Room>): void => {
|
private markTreeChildren = (rootSpace: Room, unseen: Set<Room>): void => {
|
||||||
const stack = [rootSpace];
|
const stack = [rootSpace];
|
||||||
while (stack.length) {
|
while (stack.length) {
|
||||||
|
@ -503,10 +546,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
private rebuildHomeSpace = () => {
|
private rebuildHomeSpace = () => {
|
||||||
if (this.allRoomsInHome) {
|
if (this.allRoomsInHome) {
|
||||||
// this is a special-case to not have to maintain a set of all rooms
|
// this is a special-case to not have to maintain a set of all rooms
|
||||||
this.spaceFilteredRooms.delete(MetaSpace.Home);
|
this.roomIdsBySpace.delete(MetaSpace.Home);
|
||||||
} else {
|
} else {
|
||||||
const rooms = new Set(this.matrixClient.getVisibleRooms().filter(this.showInHomeSpace).map(r => r.roomId));
|
const rooms = new Set(this.matrixClient.getVisibleRooms().filter(this.showInHomeSpace).map(r => r.roomId));
|
||||||
this.spaceFilteredRooms.set(MetaSpace.Home, rooms);
|
this.roomIdsBySpace.set(MetaSpace.Home, rooms);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.activeSpace === MetaSpace.Home) {
|
if (this.activeSpace === MetaSpace.Home) {
|
||||||
|
@ -521,14 +564,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
if (enabledMetaSpaces.has(MetaSpace.Home)) {
|
if (enabledMetaSpaces.has(MetaSpace.Home)) {
|
||||||
this.rebuildHomeSpace();
|
this.rebuildHomeSpace();
|
||||||
} else {
|
} else {
|
||||||
this.spaceFilteredRooms.delete(MetaSpace.Home);
|
this.roomIdsBySpace.delete(MetaSpace.Home);
|
||||||
}
|
}
|
||||||
|
|
||||||
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.roomIdsBySpace.set(MetaSpace.Favourites, new Set(favourites.map(r => r.roomId)));
|
||||||
} else {
|
} else {
|
||||||
this.spaceFilteredRooms.delete(MetaSpace.Favourites);
|
this.roomIdsBySpace.delete(MetaSpace.Favourites);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The People metaspace doesn't need maintaining
|
// The People metaspace doesn't need maintaining
|
||||||
|
@ -540,7 +583,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
// 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);
|
||||||
});
|
});
|
||||||
this.spaceFilteredRooms.set(MetaSpace.Orphans, new Set(orphans.map(r => r.roomId)));
|
this.roomIdsBySpace.set(MetaSpace.Orphans, new Set(orphans.map(r => r.roomId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMetaSpace(this.activeSpace)) {
|
if (isMetaSpace(this.activeSpace)) {
|
||||||
|
@ -561,7 +604,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!spaces) {
|
if (!spaces) {
|
||||||
spaces = [...this.spaceFilteredRooms.keys()];
|
spaces = [...this.roomIdsBySpace.keys()];
|
||||||
if (dmBadgeSpace === MetaSpace.People) {
|
if (dmBadgeSpace === MetaSpace.People) {
|
||||||
spaces.push(MetaSpace.People);
|
spaces.push(MetaSpace.People);
|
||||||
}
|
}
|
||||||
|
@ -573,13 +616,15 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
spaces.forEach((s) => {
|
spaces.forEach((s) => {
|
||||||
if (this.allRoomsInHome && s === MetaSpace.Home) return; // we'll be using the global notification state, skip
|
if (this.allRoomsInHome && s === MetaSpace.Home) return; // we'll be using the global notification state, skip
|
||||||
|
|
||||||
|
const flattenedRoomsForSpace = this.getSpaceFilteredRoomIds(s, true);
|
||||||
|
|
||||||
// Update NotificationStates
|
// Update NotificationStates
|
||||||
this.getNotificationState(s).setRooms(visibleRooms.filter(room => {
|
this.getNotificationState(s).setRooms(visibleRooms.filter(room => {
|
||||||
if (s === MetaSpace.People) {
|
if (s === MetaSpace.People) {
|
||||||
return this.isRoomInSpace(MetaSpace.People, room.roomId);
|
return this.isRoomInSpace(MetaSpace.People, room.roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room.isSpaceRoom() || !this.spaceFilteredRooms.get(s).has(room.roomId)) return false;
|
if (room.isSpaceRoom() || !flattenedRoomsForSpace.has(room.roomId)) return false;
|
||||||
|
|
||||||
if (dmBadgeSpace && DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
|
if (dmBadgeSpace && DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
|
||||||
return s === dmBadgeSpace;
|
return s === dmBadgeSpace;
|
||||||
|
@ -606,85 +651,39 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
return member.membership === "join" || member.membership === "invite";
|
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
|
// 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) => {
|
private onMemberUpdate = (space: Room, userId: string) => {
|
||||||
const inSpace = SpaceStoreClass.isInSpace(space.getMember(userId));
|
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) {
|
if (inSpace) {
|
||||||
// add to our list and to that of all of our parents
|
this.userIdsBySpace.get(space.roomId)?.add(userId);
|
||||||
this.spaceFilteredUsers.get(spaceId).add(userId);
|
|
||||||
} else {
|
} else {
|
||||||
// remove from our list and that of all of our parents until we hit a parent with this user
|
this.userIdsBySpace.get(space.roomId)?.delete(userId);
|
||||||
this.spaceFilteredUsers.get(spaceId).delete(userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.getKnownParents(spaceId).forEach(parentId => {
|
// bust cache
|
||||||
if (seen.has(parentId)) return;
|
this._aggregatedSpaceCache.userIdsBySpace.clear();
|
||||||
const parent = this.matrixClient.getRoom(parentId);
|
|
||||||
// because spaceFilteredUsers is cumulative, if we are removing from lower in the hierarchy,
|
const affectedParentSpaceIds = this.getKnownParents(space.roomId, true);
|
||||||
// but the member is present higher in the hierarchy we must take care not to wrongly over-remove them.
|
this.emit(space.roomId);
|
||||||
if (inSpace || !SpaceStoreClass.isInSpace(parent.getMember(userId))) {
|
affectedParentSpaceIds.forEach(spaceId => this.emit(spaceId));
|
||||||
stack.push(parentId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.switchSpaceIfNeeded();
|
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 = () => {
|
private onRoomsUpdate = () => {
|
||||||
const visibleRooms = this.matrixClient.getVisibleRooms();
|
const visibleRooms = this.matrixClient.getVisibleRooms();
|
||||||
|
|
||||||
const oldFilteredRooms = this.spaceFilteredRooms;
|
const prevRoomsBySpace = this.roomIdsBySpace;
|
||||||
const oldFilteredUsers = this.spaceFilteredUsers;
|
const prevUsersBySpace = this.userIdsBySpace;
|
||||||
this.spaceFilteredRooms = new Map();
|
const prevChildSpacesBySpace = this.childSpacesBySpace;
|
||||||
this.spaceFilteredUsers = new Map();
|
|
||||||
|
this.roomIdsBySpace = new Map();
|
||||||
|
this.userIdsBySpace = new Map();
|
||||||
|
this.childSpacesBySpace = new Map();
|
||||||
|
|
||||||
this.rebuildParentMap();
|
this.rebuildParentMap();
|
||||||
|
// mutates this.roomIdsBySpace
|
||||||
this.rebuildMetaSpaces();
|
this.rebuildMetaSpaces();
|
||||||
|
|
||||||
const hiddenChildren = new EnhancedMap<string, Set<string>>();
|
const hiddenChildren = new EnhancedMap<string, Set<string>>();
|
||||||
|
@ -698,26 +697,28 @@ 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>, Set<string>] => {
|
const traverseSpace = (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) && this.spaceFilteredUsers.has(spaceId)) {
|
if (this.roomIdsBySpace.has(spaceId) && this.userIdsBySpace.has(spaceId)) {
|
||||||
return [this.spaceFilteredRooms.get(spaceId), this.spaceFilteredUsers.get(spaceId)];
|
return [this.roomIdsBySpace.get(spaceId), this.userIdsBySpace.get(spaceId)];
|
||||||
}
|
}
|
||||||
|
|
||||||
const [childSpaces, childRooms] = partitionSpacesAndRooms(this.getChildren(spaceId));
|
const [childSpaces, childRooms] = partitionSpacesAndRooms(this.getChildren(spaceId));
|
||||||
|
|
||||||
|
this.childSpacesBySpace.set(spaceId, new Set(childSpaces.map(space => space.roomId)));
|
||||||
|
|
||||||
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 => {
|
const userIds = new Set(space?.getMembers().filter(m => {
|
||||||
return m.membership === "join" || m.membership === "invite";
|
return m.membership === "join" || m.membership === "invite";
|
||||||
}).map(m => m.userId));
|
}).map(m => m.userId));
|
||||||
|
|
||||||
const newPath = new Set(parentPath).add(spaceId);
|
const newPath = new Set(parentPath).add(spaceId);
|
||||||
|
|
||||||
childSpaces.forEach(childSpace => {
|
childSpaces.forEach(childSpace => {
|
||||||
const [rooms, users] = fn(childSpace.roomId, newPath) ?? [];
|
traverseSpace(childSpace.roomId, newPath) ?? [];
|
||||||
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);
|
||||||
|
@ -727,33 +728,50 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
const expandedRoomIds = new Set(Array.from(roomIds).flatMap(roomId => {
|
const expandedRoomIds = new Set(Array.from(roomIds).flatMap(roomId => {
|
||||||
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.spaceFilteredUsers.set(spaceId, userIds);
|
this.roomIdsBySpace.set(spaceId, expandedRoomIds);
|
||||||
|
|
||||||
|
this.userIdsBySpace.set(spaceId, userIds);
|
||||||
return [expandedRoomIds, userIds];
|
return [expandedRoomIds, userIds];
|
||||||
};
|
};
|
||||||
|
|
||||||
fn(s.roomId, new Set());
|
traverseSpace(s.roomId, new Set());
|
||||||
});
|
});
|
||||||
|
|
||||||
const roomDiff = mapDiff(oldFilteredRooms, this.spaceFilteredRooms);
|
const roomDiff = mapDiff(prevRoomsBySpace, this.roomIdsBySpace);
|
||||||
const userDiff = mapDiff(oldFilteredUsers, this.spaceFilteredUsers);
|
const userDiff = mapDiff(prevUsersBySpace, this.userIdsBySpace);
|
||||||
|
const spaceDiff = mapDiff(prevChildSpacesBySpace, this.childSpacesBySpace);
|
||||||
// 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 roomsChanged = roomDiff.changed.filter(k => {
|
const roomsChanged = roomDiff.changed.filter(k => {
|
||||||
return setHasDiff(oldFilteredRooms.get(k), this.spaceFilteredRooms.get(k));
|
return setHasDiff(prevRoomsBySpace.get(k), this.roomIdsBySpace.get(k));
|
||||||
});
|
});
|
||||||
const usersChanged = userDiff.changed.filter(k => {
|
const usersChanged = userDiff.changed.filter(k => {
|
||||||
return setHasDiff(oldFilteredUsers.get(k), this.spaceFilteredUsers.get(k));
|
return setHasDiff(prevUsersBySpace.get(k), this.userIdsBySpace.get(k));
|
||||||
|
});
|
||||||
|
const spacesChanged = spaceDiff.changed.filter(k => {
|
||||||
|
return setHasDiff(prevChildSpacesBySpace.get(k), this.childSpacesBySpace.get(k));
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeSet = new Set([
|
const changeSet = new Set([
|
||||||
...roomDiff.added,
|
...roomDiff.added,
|
||||||
...userDiff.added,
|
...userDiff.added,
|
||||||
|
...spaceDiff.added,
|
||||||
...roomDiff.removed,
|
...roomDiff.removed,
|
||||||
...userDiff.removed,
|
...userDiff.removed,
|
||||||
|
...spaceDiff.removed,
|
||||||
...roomsChanged,
|
...roomsChanged,
|
||||||
...usersChanged,
|
...usersChanged,
|
||||||
|
...spacesChanged,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const affectedParents = Array.from(changeSet).flatMap(
|
||||||
|
changedId => [...this.getKnownParents(changedId, true)],
|
||||||
|
);
|
||||||
|
affectedParents.forEach(parentId => changeSet.add(parentId));
|
||||||
|
// bust aggregate cache
|
||||||
|
this._aggregatedSpaceCache.roomIdsBySpace.clear();
|
||||||
|
this._aggregatedSpaceCache.userIdsBySpace.clear();
|
||||||
|
|
||||||
changeSet.forEach(k => {
|
changeSet.forEach(k => {
|
||||||
this.emit(k);
|
this.emit(k);
|
||||||
});
|
});
|
||||||
|
@ -786,7 +804,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
|
|
||||||
// otherwise, try to find a root space which contains this room
|
// otherwise, try to find a root space which contains this room
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(roomId))?.roomId;
|
parent = this.rootSpaces.find(s => this.isRoomInSpace(s.roomId, roomId))?.roomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, try to find a metaspace which contains this room
|
// otherwise, try to find a metaspace which contains this room
|
||||||
|
@ -869,6 +887,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
|
|
||||||
private onRoomState = (ev: MatrixEvent) => {
|
private onRoomState = (ev: MatrixEvent) => {
|
||||||
const room = this.matrixClient.getRoom(ev.getRoomId());
|
const room = this.matrixClient.getRoom(ev.getRoomId());
|
||||||
|
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
|
|
||||||
switch (ev.getType()) {
|
switch (ev.getType()) {
|
||||||
|
@ -917,6 +936,7 @@ 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());
|
||||||
|
|
||||||
const userId = ev.getStateKey();
|
const userId = ev.getStateKey();
|
||||||
if (room?.isSpaceRoom() && // only consider space rooms
|
if (room?.isSpaceRoom() && // only consider space rooms
|
||||||
DMRoomMap.shared().getDMRoomsForUserId(userId).length > 0 && // only consider members we have a DM with
|
DMRoomMap.shared().getDMRoomsForUserId(userId).length > 0 && // only consider members we have a DM with
|
||||||
|
@ -949,9 +969,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
private onRoomFavouriteChange(room: Room) {
|
private onRoomFavouriteChange(room: Room) {
|
||||||
if (this.enabledMetaSpaces.includes(MetaSpace.Favourites)) {
|
if (this.enabledMetaSpaces.includes(MetaSpace.Favourites)) {
|
||||||
if (room.tags[DefaultTagID.Favourite]) {
|
if (room.tags[DefaultTagID.Favourite]) {
|
||||||
this.spaceFilteredRooms.get(MetaSpace.Favourites).add(room.roomId);
|
this.roomIdsBySpace.get(MetaSpace.Favourites).add(room.roomId);
|
||||||
} else {
|
} else {
|
||||||
this.spaceFilteredRooms.get(MetaSpace.Favourites).delete(room.roomId);
|
this.roomIdsBySpace.get(MetaSpace.Favourites).delete(room.roomId);
|
||||||
}
|
}
|
||||||
this.emit(MetaSpace.Favourites);
|
this.emit(MetaSpace.Favourites);
|
||||||
}
|
}
|
||||||
|
@ -961,11 +981,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
const enabledMetaSpaces = new Set(this.enabledMetaSpaces);
|
const enabledMetaSpaces = new Set(this.enabledMetaSpaces);
|
||||||
|
|
||||||
if (!this.allRoomsInHome && enabledMetaSpaces.has(MetaSpace.Home)) {
|
if (!this.allRoomsInHome && enabledMetaSpaces.has(MetaSpace.Home)) {
|
||||||
const homeRooms = this.spaceFilteredRooms.get(MetaSpace.Home);
|
const homeRooms = this.roomIdsBySpace.get(MetaSpace.Home);
|
||||||
if (this.showInHomeSpace(room)) {
|
if (this.showInHomeSpace(room)) {
|
||||||
homeRooms?.add(room.roomId);
|
homeRooms?.add(room.roomId);
|
||||||
} else if (!this.spaceFilteredRooms.get(MetaSpace.Orphans).has(room.roomId)) {
|
} else if (!this.roomIdsBySpace.get(MetaSpace.Orphans).has(room.roomId)) {
|
||||||
this.spaceFilteredRooms.get(MetaSpace.Home)?.delete(room.roomId);
|
this.roomIdsBySpace.get(MetaSpace.Home)?.delete(room.roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit(MetaSpace.Home);
|
this.emit(MetaSpace.Home);
|
||||||
|
@ -976,7 +996,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enabledMetaSpaces.has(MetaSpace.Orphans) || enabledMetaSpaces.has(MetaSpace.Home)) {
|
if (enabledMetaSpaces.has(MetaSpace.Orphans) || enabledMetaSpaces.has(MetaSpace.Home)) {
|
||||||
if (isDm && this.spaceFilteredRooms.get(MetaSpace.Orphans).delete(room.roomId)) {
|
if (isDm && this.roomIdsBySpace.get(MetaSpace.Orphans).delete(room.roomId)) {
|
||||||
this.emit(MetaSpace.Orphans);
|
this.emit(MetaSpace.Orphans);
|
||||||
this.emit(MetaSpace.Home);
|
this.emit(MetaSpace.Home);
|
||||||
}
|
}
|
||||||
|
@ -1006,8 +1026,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
this.rootSpaces = [];
|
this.rootSpaces = [];
|
||||||
this.parentMap = new EnhancedMap();
|
this.parentMap = new EnhancedMap();
|
||||||
this.notificationStateMap = new Map();
|
this.notificationStateMap = new Map();
|
||||||
this.spaceFilteredRooms = new Map();
|
this.roomIdsBySpace = new Map();
|
||||||
this.spaceFilteredUsers = new Map();
|
this.userIdsBySpace = new Map();
|
||||||
|
this._aggregatedSpaceCache.roomIdsBySpace.clear();
|
||||||
|
this._aggregatedSpaceCache.userIdsBySpace.clear();
|
||||||
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();
|
||||||
|
|
76
src/stores/spaces/flattenSpaceHierarchy.ts
Normal file
76
src/stores/spaces/flattenSpaceHierarchy.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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 { SpaceKey } from ".";
|
||||||
|
|
||||||
|
export type SpaceEntityMap = Map<SpaceKey, Set<string>>;
|
||||||
|
export type SpaceDescendantMap = Map<SpaceKey, Set<SpaceKey>>;
|
||||||
|
|
||||||
|
const traverseSpaceDescendants = (
|
||||||
|
spaceDescendantMap: SpaceDescendantMap,
|
||||||
|
spaceId: SpaceKey,
|
||||||
|
flatSpace = new Set<SpaceKey>(),
|
||||||
|
): Set<SpaceKey> => {
|
||||||
|
flatSpace.add(spaceId);
|
||||||
|
const descendentSpaces = spaceDescendantMap.get(spaceId);
|
||||||
|
descendentSpaces?.forEach(
|
||||||
|
descendantSpaceId => {
|
||||||
|
if (!flatSpace.has(descendantSpaceId)) {
|
||||||
|
traverseSpaceDescendants(spaceDescendantMap, descendantSpaceId, flatSpace);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return flatSpace;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to traverse space heirachy and flatten
|
||||||
|
* @param spaceEntityMap ie map of rooms or dm userIds
|
||||||
|
* @param spaceDescendantMap map of spaces and their children
|
||||||
|
* @returns set of all rooms
|
||||||
|
*/
|
||||||
|
export const flattenSpaceHierarchy = (
|
||||||
|
spaceEntityMap: SpaceEntityMap,
|
||||||
|
spaceDescendantMap: SpaceDescendantMap,
|
||||||
|
spaceId: SpaceKey,
|
||||||
|
): Set<string> => {
|
||||||
|
const flattenedSpaceIds = traverseSpaceDescendants(spaceDescendantMap, spaceId);
|
||||||
|
const flattenedRooms = new Set<string>();
|
||||||
|
|
||||||
|
flattenedSpaceIds.forEach(id => {
|
||||||
|
const roomIds = spaceEntityMap.get(id);
|
||||||
|
roomIds?.forEach(flattenedRooms.add, flattenedRooms);
|
||||||
|
});
|
||||||
|
|
||||||
|
return flattenedRooms;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const flattenSpaceHierarchyWithCache = (cache: SpaceEntityMap) => (
|
||||||
|
spaceEntityMap: SpaceEntityMap,
|
||||||
|
spaceDescendantMap: SpaceDescendantMap,
|
||||||
|
spaceId: SpaceKey,
|
||||||
|
useCache = true,
|
||||||
|
): Set<string> => {
|
||||||
|
if (useCache && cache.has(spaceId)) {
|
||||||
|
return cache.get(spaceId);
|
||||||
|
}
|
||||||
|
const result = flattenSpaceHierarchy(spaceEntityMap, spaceDescendantMap, spaceId);
|
||||||
|
cache.set(spaceId, result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
|
||||||
import "../skinned-sdk"; // Must be first for skinning to work
|
import "../skinned-sdk"; // Must be first for skinning to work
|
||||||
|
|
||||||
import SpaceStore from "../../src/stores/spaces/SpaceStore";
|
import SpaceStore from "../../src/stores/spaces/SpaceStore";
|
||||||
import {
|
import {
|
||||||
MetaSpace,
|
MetaSpace,
|
||||||
|
@ -58,9 +59,11 @@ const invite2 = "!invite2:server";
|
||||||
const room1 = "!room1:server";
|
const room1 = "!room1:server";
|
||||||
const room2 = "!room2:server";
|
const room2 = "!room2:server";
|
||||||
const room3 = "!room3:server";
|
const room3 = "!room3:server";
|
||||||
|
const room4 = "!room4:server";
|
||||||
const space1 = "!space1:server";
|
const space1 = "!space1:server";
|
||||||
const space2 = "!space2:server";
|
const space2 = "!space2:server";
|
||||||
const space3 = "!space3:server";
|
const space3 = "!space3:server";
|
||||||
|
const space4 = "!space4:server";
|
||||||
|
|
||||||
const getUserIdForRoomId = jest.fn(roomId => {
|
const getUserIdForRoomId = jest.fn(roomId => {
|
||||||
return {
|
return {
|
||||||
|
@ -303,11 +306,13 @@ describe("SpaceStore", () => {
|
||||||
|
|
||||||
describe("test fixture 1", () => {
|
describe("test fixture 1", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
[fav1, fav2, fav3, dm1, dm2, dm3, orphan1, orphan2, invite1, invite2, room1, room2, room3]
|
[fav1, fav2, fav3, dm1, dm2, dm3, orphan1, orphan2, invite1, invite2, room1, room2, room3, room4]
|
||||||
.forEach(mkRoom);
|
.forEach(mkRoom);
|
||||||
mkSpace(space1, [fav1, room1]);
|
mkSpace(space1, [fav1, room1]);
|
||||||
mkSpace(space2, [fav1, fav2, fav3, room1]);
|
mkSpace(space2, [fav1, fav2, fav3, room1]);
|
||||||
mkSpace(space3, [invite2]);
|
mkSpace(space3, [invite2]);
|
||||||
|
mkSpace(space4, [room4, fav2, space2, space3]);
|
||||||
|
|
||||||
client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId));
|
client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId));
|
||||||
|
|
||||||
[fav1, fav2, fav3].forEach(roomId => {
|
[fav1, fav2, fav3].forEach(roomId => {
|
||||||
|
@ -383,6 +388,7 @@ describe("SpaceStore", () => {
|
||||||
await run();
|
await run();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isRoomInSpace()', () => {
|
||||||
it("home space contains orphaned rooms", () => {
|
it("home space contains orphaned rooms", () => {
|
||||||
expect(store.isRoomInSpace(MetaSpace.Home, orphan1)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.Home, orphan1)).toBeTruthy();
|
||||||
expect(store.isRoomInSpace(MetaSpace.Home, orphan2)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.Home, orphan2)).toBeTruthy();
|
||||||
|
@ -408,7 +414,9 @@ describe("SpaceStore", () => {
|
||||||
expect(store.isRoomInSpace(MetaSpace.Home, 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.isRoomInSpace(MetaSpace.Home, room1)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.Home, room1)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
@ -441,6 +449,51 @@ describe("SpaceStore", () => {
|
||||||
expect(store.isRoomInSpace(space1, room1)).toBeTruthy();
|
expect(store.isRoomInSpace(space1, room1)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns true for all sub-space child rooms when includeSubSpaceRooms is undefined", () => {
|
||||||
|
expect(store.isRoomInSpace(space4, room4)).toBeTruthy();
|
||||||
|
expect(store.isRoomInSpace(space4, fav2)).toBeTruthy();
|
||||||
|
// space2's rooms
|
||||||
|
expect(store.isRoomInSpace(space4, fav1)).toBeTruthy();
|
||||||
|
expect(store.isRoomInSpace(space4, fav3)).toBeTruthy();
|
||||||
|
expect(store.isRoomInSpace(space4, room1)).toBeTruthy();
|
||||||
|
// space3's rooms
|
||||||
|
expect(store.isRoomInSpace(space4, invite2)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true for all sub-space child rooms when includeSubSpaceRooms is true", () => {
|
||||||
|
expect(store.isRoomInSpace(space4, room4, true)).toBeTruthy();
|
||||||
|
expect(store.isRoomInSpace(space4, fav2, true)).toBeTruthy();
|
||||||
|
// space2's rooms
|
||||||
|
expect(store.isRoomInSpace(space4, fav1, true)).toBeTruthy();
|
||||||
|
expect(store.isRoomInSpace(space4, fav3, true)).toBeTruthy();
|
||||||
|
expect(store.isRoomInSpace(space4, room1, true)).toBeTruthy();
|
||||||
|
// space3's rooms
|
||||||
|
expect(store.isRoomInSpace(space4, invite2, true)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for all sub-space child rooms when includeSubSpaceRooms is false", () => {
|
||||||
|
// direct children
|
||||||
|
expect(store.isRoomInSpace(space4, room4, false)).toBeTruthy();
|
||||||
|
expect(store.isRoomInSpace(space4, fav2, false)).toBeTruthy();
|
||||||
|
// space2's rooms
|
||||||
|
expect(store.isRoomInSpace(space4, fav1, false)).toBeFalsy();
|
||||||
|
expect(store.isRoomInSpace(space4, fav3, false)).toBeFalsy();
|
||||||
|
expect(store.isRoomInSpace(space4, room1, false)).toBeFalsy();
|
||||||
|
// space3's rooms
|
||||||
|
expect(store.isRoomInSpace(space4, invite2, false)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("space contains all sub-space's child rooms", () => {
|
||||||
|
expect(store.isRoomInSpace(space4, room4)).toBeTruthy();
|
||||||
|
expect(store.isRoomInSpace(space4, fav2)).toBeTruthy();
|
||||||
|
// space2's rooms
|
||||||
|
expect(store.isRoomInSpace(space4, fav1)).toBeTruthy();
|
||||||
|
expect(store.isRoomInSpace(space4, fav3)).toBeTruthy();
|
||||||
|
expect(store.isRoomInSpace(space4, room1)).toBeTruthy();
|
||||||
|
// space3's rooms
|
||||||
|
expect(store.isRoomInSpace(space4, invite2)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it("space contains child favourites", () => {
|
it("space contains child favourites", () => {
|
||||||
expect(store.isRoomInSpace(space2, fav1)).toBeTruthy();
|
expect(store.isRoomInSpace(space2, fav1)).toBeTruthy();
|
||||||
expect(store.isRoomInSpace(space2, fav2)).toBeTruthy();
|
expect(store.isRoomInSpace(space2, fav2)).toBeTruthy();
|
||||||
|
@ -464,6 +517,17 @@ describe("SpaceStore", () => {
|
||||||
expect(store.isRoomInSpace(space3, dm3)).toBeFalsy();
|
expect(store.isRoomInSpace(space3, dm3)).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses cached aggregated rooms', () => {
|
||||||
|
const rooms = store.getSpaceFilteredRoomIds(space4, true);
|
||||||
|
expect(store.isRoomInSpace(space4, fav1)).toBeTruthy();
|
||||||
|
expect(store.isRoomInSpace(space4, fav3)).toBeTruthy();
|
||||||
|
expect(store.isRoomInSpace(space4, room1)).toBeTruthy();
|
||||||
|
|
||||||
|
// isRoomInSpace calls didn't rebuild room set
|
||||||
|
expect(rooms).toStrictEqual(store.getSpaceFilteredRoomIds(space4, true));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("dms are only added to Notification States for only the People Space", async () => {
|
it("dms are only added to Notification States for only the People Space", async () => {
|
||||||
[dm1, dm2, dm3].forEach(d => {
|
[dm1, dm2, dm3].forEach(d => {
|
||||||
expect(store.getNotificationState(MetaSpace.People)
|
expect(store.getNotificationState(MetaSpace.People)
|
||||||
|
@ -614,6 +678,115 @@ describe("SpaceStore", () => {
|
||||||
expect(store.isRoomInSpace(space1, invite1)).toBeTruthy();
|
expect(store.isRoomInSpace(space1, invite1)).toBeTruthy();
|
||||||
expect(store.isRoomInSpace(MetaSpace.Home, invite1)).toBeTruthy();
|
expect(store.isRoomInSpace(MetaSpace.Home, invite1)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('onRoomsUpdate()', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
[fav1, fav2, fav3, dm1, dm2, dm3, orphan1, orphan2, invite1, invite2, room1, room2, room3, room4]
|
||||||
|
.forEach(mkRoom);
|
||||||
|
mkSpace(space2, [fav1, fav2, fav3, room1]);
|
||||||
|
mkSpace(space3, [invite2]);
|
||||||
|
mkSpace(space4, [room4, fav2, space2, space3]);
|
||||||
|
mkSpace(space1, [fav1, room1, space4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const addChildRoom = (spaceId, childId) => {
|
||||||
|
const childEvent = mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: EventType.SpaceChild,
|
||||||
|
room: spaceId,
|
||||||
|
user: client.getUserId(),
|
||||||
|
skey: childId,
|
||||||
|
content: { via: [], canonical: true },
|
||||||
|
ts: Date.now(),
|
||||||
|
});
|
||||||
|
const spaceRoom = client.getRoom(spaceId);
|
||||||
|
spaceRoom.currentState.getStateEvents.mockImplementation(
|
||||||
|
testUtils.mockStateEventImplementation([childEvent]),
|
||||||
|
);
|
||||||
|
|
||||||
|
client.emit("RoomState.events", childEvent);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMember = (spaceId, user: RoomMember) => {
|
||||||
|
const memberEvent = mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: EventType.RoomMember,
|
||||||
|
room: spaceId,
|
||||||
|
user: client.getUserId(),
|
||||||
|
skey: user.userId,
|
||||||
|
content: { membership: 'join' },
|
||||||
|
ts: Date.now(),
|
||||||
|
});
|
||||||
|
const spaceRoom = client.getRoom(spaceId);
|
||||||
|
spaceRoom.currentState.getStateEvents.mockImplementation(
|
||||||
|
testUtils.mockStateEventImplementation([memberEvent]),
|
||||||
|
);
|
||||||
|
spaceRoom.getMember.mockReturnValue(user);
|
||||||
|
|
||||||
|
client.emit("RoomState.members", memberEvent);
|
||||||
|
};
|
||||||
|
|
||||||
|
it('emits events for parent spaces when child room is added', async () => {
|
||||||
|
await run();
|
||||||
|
|
||||||
|
const room5 = mkRoom('!room5:server');
|
||||||
|
const emitSpy = jest.spyOn(store, 'emit').mockClear();
|
||||||
|
// add room5 into space2
|
||||||
|
addChildRoom(space2, room5.roomId);
|
||||||
|
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(space2);
|
||||||
|
// space2 is subspace of space4
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(space4);
|
||||||
|
// space4 is a subspace of space1
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(space1);
|
||||||
|
expect(emitSpy).not.toHaveBeenCalledWith(space3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates rooms state when a child room is added', async () => {
|
||||||
|
await run();
|
||||||
|
const room5 = mkRoom('!room5:server');
|
||||||
|
|
||||||
|
expect(store.isRoomInSpace(space2, room5.roomId)).toBeFalsy();
|
||||||
|
expect(store.isRoomInSpace(space4, room5.roomId)).toBeFalsy();
|
||||||
|
|
||||||
|
// add room5 into space2
|
||||||
|
addChildRoom(space2, room5.roomId);
|
||||||
|
|
||||||
|
expect(store.isRoomInSpace(space2, room5.roomId)).toBeTruthy();
|
||||||
|
// space2 is subspace of space4
|
||||||
|
expect(store.isRoomInSpace(space4, room5.roomId)).toBeTruthy();
|
||||||
|
// space4 is subspace of space1
|
||||||
|
expect(store.isRoomInSpace(space1, room5.roomId)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits events for parent spaces when a member is added', async () => {
|
||||||
|
await run();
|
||||||
|
|
||||||
|
const emitSpy = jest.spyOn(store, 'emit').mockClear();
|
||||||
|
// add into space2
|
||||||
|
addMember(space2, dm1Partner);
|
||||||
|
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(space2);
|
||||||
|
// space2 is subspace of space4
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(space4);
|
||||||
|
// space4 is a subspace of space1
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(space1);
|
||||||
|
expect(emitSpy).not.toHaveBeenCalledWith(space3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates users state when a member is added', async () => {
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(store.getSpaceFilteredUserIds(space2)).toEqual(new Set([]));
|
||||||
|
|
||||||
|
// add into space2
|
||||||
|
addMember(space2, dm1Partner);
|
||||||
|
|
||||||
|
expect(store.getSpaceFilteredUserIds(space2)).toEqual(new Set([dm1Partner.userId]));
|
||||||
|
expect(store.getSpaceFilteredUserIds(space4)).toEqual(new Set([dm1Partner.userId]));
|
||||||
|
expect(store.getSpaceFilteredUserIds(space1)).toEqual(new Set([dm1Partner.userId]));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("active space switching tests", () => {
|
describe("active space switching tests", () => {
|
||||||
|
|
196
test/stores/room-list/filters/SpaceFilterCondition-test.ts
Normal file
196
test/stores/room-list/filters/SpaceFilterCondition-test.ts
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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 { mocked } from 'jest-mock';
|
||||||
|
|
||||||
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
|
import { FILTER_CHANGED } from "../../../../src/stores/room-list/filters/IFilterCondition";
|
||||||
|
import { SpaceFilterCondition } from "../../../../src/stores/room-list/filters/SpaceFilterCondition";
|
||||||
|
import { MetaSpace } from "../../../../src/stores/spaces";
|
||||||
|
import SpaceStore from "../../../../src/stores/spaces/SpaceStore";
|
||||||
|
|
||||||
|
jest.mock("../../../../src/settings/SettingsStore");
|
||||||
|
jest.mock("../../../../src/stores/spaces/SpaceStore", () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
class MockSpaceStore extends EventEmitter {
|
||||||
|
isRoomInSpace = jest.fn();
|
||||||
|
getSpaceFilteredUserIds = jest.fn().mockReturnValue(new Set<string>([]));
|
||||||
|
getSpaceFilteredRoomIds = jest.fn().mockReturnValue(new Set<string>([]));
|
||||||
|
}
|
||||||
|
return { instance: new MockSpaceStore() };
|
||||||
|
});
|
||||||
|
|
||||||
|
const SettingsStoreMock = mocked(SettingsStore);
|
||||||
|
const SpaceStoreInstanceMock = mocked(SpaceStore.instance);
|
||||||
|
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
describe('SpaceFilterCondition', () => {
|
||||||
|
const space1 = '!space1:server';
|
||||||
|
const space2 = '!space2:server';
|
||||||
|
const room1Id = '!r1:server';
|
||||||
|
const room2Id = '!r2:server';
|
||||||
|
const room3Id = '!r3:server';
|
||||||
|
const user1Id = '@u1:server';
|
||||||
|
const user2Id = '@u2:server';
|
||||||
|
const user3Id = '@u3:server';
|
||||||
|
const makeMockGetValue = (settings = {}) => (settingName, space) => settings[settingName]?.[space] || false;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
SettingsStoreMock.getValue.mockClear().mockImplementation(makeMockGetValue());
|
||||||
|
SpaceStoreInstanceMock.getSpaceFilteredUserIds.mockReturnValue(new Set([]));
|
||||||
|
SpaceStoreInstanceMock.isRoomInSpace.mockReturnValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const initFilter = (space): SpaceFilterCondition => {
|
||||||
|
const filter = new SpaceFilterCondition();
|
||||||
|
filter.updateSpace(space);
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
return filter;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('isVisible', () => {
|
||||||
|
const room1 = { roomId: room1Id } as unknown as Room;
|
||||||
|
it('calls isRoomInSpace correctly', () => {
|
||||||
|
const filter = initFilter(space1);
|
||||||
|
|
||||||
|
expect(filter.isVisible(room1)).toEqual(true);
|
||||||
|
expect(SpaceStoreInstanceMock.isRoomInSpace).toHaveBeenCalledWith(space1, room1Id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onStoreUpdate', () => {
|
||||||
|
it('emits filter changed event when updateSpace is called even without changes', async () => {
|
||||||
|
const filter = new SpaceFilterCondition();
|
||||||
|
const emitSpy = jest.spyOn(filter, 'emit');
|
||||||
|
filter.updateSpace(space1);
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(FILTER_CHANGED);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('showPeopleInSpace setting', () => {
|
||||||
|
it('emits filter changed event when setting changes', async () => {
|
||||||
|
// init filter with setting true for space1
|
||||||
|
SettingsStoreMock.getValue.mockImplementation(makeMockGetValue({
|
||||||
|
["Spaces.showPeopleInSpace"]: { [space1]: true },
|
||||||
|
}));
|
||||||
|
const filter = initFilter(space1);
|
||||||
|
const emitSpy = jest.spyOn(filter, 'emit');
|
||||||
|
|
||||||
|
SettingsStoreMock.getValue.mockClear().mockImplementation(makeMockGetValue({
|
||||||
|
["Spaces.showPeopleInSpace"]: { [space1]: false },
|
||||||
|
}));
|
||||||
|
|
||||||
|
SpaceStoreInstanceMock.emit(space1);
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(FILTER_CHANGED);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits filter changed event when setting is false and space changes to a meta space', async () => {
|
||||||
|
// init filter with setting true for space1
|
||||||
|
SettingsStoreMock.getValue.mockImplementation(makeMockGetValue({
|
||||||
|
["Spaces.showPeopleInSpace"]: { [space1]: false },
|
||||||
|
}));
|
||||||
|
const filter = initFilter(space1);
|
||||||
|
const emitSpy = jest.spyOn(filter, 'emit');
|
||||||
|
|
||||||
|
filter.updateSpace(MetaSpace.Home);
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(FILTER_CHANGED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not emit filter changed event on store update when nothing changed', async () => {
|
||||||
|
const filter = initFilter(space1);
|
||||||
|
const emitSpy = jest.spyOn(filter, 'emit');
|
||||||
|
SpaceStoreInstanceMock.emit(space1);
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
expect(emitSpy).not.toHaveBeenCalledWith(FILTER_CHANGED);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes listener when updateSpace is called', async () => {
|
||||||
|
const filter = initFilter(space1);
|
||||||
|
filter.updateSpace(space2);
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
const emitSpy = jest.spyOn(filter, 'emit');
|
||||||
|
|
||||||
|
// update mock so filter would emit change if it was listening to space1
|
||||||
|
SpaceStoreInstanceMock.getSpaceFilteredRoomIds.mockReturnValue(new Set([room1Id]));
|
||||||
|
SpaceStoreInstanceMock.emit(space1);
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
// no filter changed event
|
||||||
|
expect(emitSpy).not.toHaveBeenCalledWith(FILTER_CHANGED);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes listener when destroy is called', async () => {
|
||||||
|
const filter = initFilter(space1);
|
||||||
|
filter.destroy();
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
const emitSpy = jest.spyOn(filter, 'emit');
|
||||||
|
|
||||||
|
// update mock so filter would emit change if it was listening to space1
|
||||||
|
SpaceStoreInstanceMock.getSpaceFilteredRoomIds.mockReturnValue(new Set([room1Id]));
|
||||||
|
SpaceStoreInstanceMock.emit(space1);
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
// no filter changed event
|
||||||
|
expect(emitSpy).not.toHaveBeenCalledWith(FILTER_CHANGED);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when directChildRoomIds change', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
SpaceStoreInstanceMock.getSpaceFilteredRoomIds.mockReturnValue(new Set([room1Id, room2Id]));
|
||||||
|
});
|
||||||
|
const filterChangedCases = [
|
||||||
|
['room added', [room1Id, room2Id, room3Id]],
|
||||||
|
['room removed', [room1Id]],
|
||||||
|
['room swapped', [room1Id, room3Id]], // same number of rooms with changes
|
||||||
|
];
|
||||||
|
|
||||||
|
it.each(filterChangedCases)('%s', (_d, rooms) => {
|
||||||
|
const filter = initFilter(space1);
|
||||||
|
const emitSpy = jest.spyOn(filter, 'emit');
|
||||||
|
|
||||||
|
SpaceStoreInstanceMock.getSpaceFilteredRoomIds.mockReturnValue(new Set(rooms));
|
||||||
|
SpaceStoreInstanceMock.emit(space1);
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(FILTER_CHANGED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when user ids change', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
SpaceStoreInstanceMock.getSpaceFilteredUserIds.mockReturnValue(new Set([user1Id, user2Id]));
|
||||||
|
});
|
||||||
|
const filterChangedCases = [
|
||||||
|
['user added', [user1Id, user2Id, user3Id]],
|
||||||
|
['user removed', [user1Id]],
|
||||||
|
['user swapped', [user1Id, user3Id]], // same number of rooms with changes
|
||||||
|
];
|
||||||
|
|
||||||
|
it.each(filterChangedCases)('%s', (_d, rooms) => {
|
||||||
|
const filter = initFilter(space1);
|
||||||
|
const emitSpy = jest.spyOn(filter, 'emit');
|
||||||
|
|
||||||
|
SpaceStoreInstanceMock.getSpaceFilteredUserIds.mockReturnValue(new Set(rooms));
|
||||||
|
SpaceStoreInstanceMock.emit(space1);
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(FILTER_CHANGED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue