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 ".";
|
||||
import { getCachedRoomIDForAlias } from "../../RoomAliasCache";
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
||||
import {
|
||||
flattenSpaceHierarchyWithCache,
|
||||
SpaceEntityMap,
|
||||
SpaceDescendantMap,
|
||||
flattenSpaceHierarchy,
|
||||
} from "./flattenSpaceHierarchy";
|
||||
import { PosthogAnalytics } from "../../PosthogAnalytics";
|
||||
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||
|
||||
interface IState {}
|
||||
interface IState { }
|
||||
|
||||
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>>();
|
||||
// Map from SpaceKey to SpaceNotificationState instance representing that space
|
||||
private notificationStateMap = new Map<SpaceKey, SpaceNotificationState>();
|
||||
// Map from space key to Set of room IDs that should be shown as part of that space's filter
|
||||
private spaceFilteredRooms = new Map<SpaceKey, Set<string>>(); // 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>>();
|
||||
// Map from SpaceKey to Set of room IDs that are direct descendants of that space
|
||||
private roomIdsBySpace: SpaceEntityMap = new Map<SpaceKey, Set<string>>(); // won't contain MetaSpace.People
|
||||
// Map from space id to Set of space keys that are direct descendants of that space
|
||||
// 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
|
||||
private _activeSpace?: SpaceKey = MetaSpace.Home; // set properly by onReady
|
||||
private _suggestedRooms: ISuggestedRoom[] = [];
|
||||
|
@ -352,16 +367,19 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
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();
|
||||
}
|
||||
|
||||
public isRoomInSpace(space: SpaceKey, roomId: string): boolean {
|
||||
public isRoomInSpace(space: SpaceKey, roomId: string, includeDescendantSpaces = true): boolean {
|
||||
if (space === MetaSpace.Home && this.allRoomsInHome) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.spaceFilteredRooms.get(space)?.has(roomId)) {
|
||||
if (this.getSpaceFilteredRoomIds(space, includeDescendantSpaces)?.has(roomId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -377,7 +395,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
if (!isMetaSpace(space) &&
|
||||
this.spaceFilteredUsers.get(space)?.has(dmPartner) &&
|
||||
this.getSpaceFilteredUserIds(space, includeDescendantSpaces)?.has(dmPartner) &&
|
||||
SettingsStore.getValue("Spaces.showPeopleInSpace", space)
|
||||
) {
|
||||
return true;
|
||||
|
@ -386,21 +404,46 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
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) {
|
||||
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) {
|
||||
return undefined;
|
||||
}
|
||||
if (isMetaSpace(space)) return undefined;
|
||||
return this.spaceFilteredUsers.get(space) || new Set();
|
||||
if (isMetaSpace(space)) {
|
||||
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 => {
|
||||
const stack = [rootSpace];
|
||||
while (stack.length) {
|
||||
|
@ -503,10 +546,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
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);
|
||||
this.roomIdsBySpace.delete(MetaSpace.Home);
|
||||
} else {
|
||||
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) {
|
||||
|
@ -521,14 +564,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
if (enabledMetaSpaces.has(MetaSpace.Home)) {
|
||||
this.rebuildHomeSpace();
|
||||
} else {
|
||||
this.spaceFilteredRooms.delete(MetaSpace.Home);
|
||||
this.roomIdsBySpace.delete(MetaSpace.Home);
|
||||
}
|
||||
|
||||
if (enabledMetaSpaces.has(MetaSpace.Favourites)) {
|
||||
const favourites = visibleRooms.filter(r => r.tags[DefaultTagID.Favourite]);
|
||||
this.spaceFilteredRooms.set(MetaSpace.Favourites, new Set(favourites.map(r => r.roomId)));
|
||||
this.roomIdsBySpace.set(MetaSpace.Favourites, new Set(favourites.map(r => r.roomId)));
|
||||
} else {
|
||||
this.spaceFilteredRooms.delete(MetaSpace.Favourites);
|
||||
this.roomIdsBySpace.delete(MetaSpace.Favourites);
|
||||
}
|
||||
|
||||
// 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
|
||||
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)) {
|
||||
|
@ -561,7 +604,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
if (!spaces) {
|
||||
spaces = [...this.spaceFilteredRooms.keys()];
|
||||
spaces = [...this.roomIdsBySpace.keys()];
|
||||
if (dmBadgeSpace === MetaSpace.People) {
|
||||
spaces.push(MetaSpace.People);
|
||||
}
|
||||
|
@ -573,13 +616,15 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
spaces.forEach((s) => {
|
||||
if (this.allRoomsInHome && s === MetaSpace.Home) return; // we'll be using the global notification state, skip
|
||||
|
||||
const flattenedRoomsForSpace = this.getSpaceFilteredRoomIds(s, true);
|
||||
|
||||
// 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 (room.isSpaceRoom() || !flattenedRoomsForSpace.has(room.roomId)) return false;
|
||||
|
||||
if (dmBadgeSpace && DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
|
||||
return s === dmBadgeSpace;
|
||||
|
@ -606,85 +651,39 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
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
|
||||
if (inSpace) {
|
||||
this.userIdsBySpace.get(space.roomId)?.add(userId);
|
||||
} else {
|
||||
this.userIdsBySpace.get(space.roomId)?.delete(userId);
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
const stack = [space.roomId];
|
||||
while (stack.length) {
|
||||
const spaceId = stack.pop();
|
||||
seen.add(spaceId);
|
||||
// bust cache
|
||||
this._aggregatedSpaceCache.userIdsBySpace.clear();
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
const affectedParentSpaceIds = this.getKnownParents(space.roomId, true);
|
||||
this.emit(space.roomId);
|
||||
affectedParentSpaceIds.forEach(spaceId => this.emit(spaceId));
|
||||
|
||||
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();
|
||||
const prevRoomsBySpace = this.roomIdsBySpace;
|
||||
const prevUsersBySpace = this.userIdsBySpace;
|
||||
const prevChildSpacesBySpace = this.childSpacesBySpace;
|
||||
|
||||
this.roomIdsBySpace = new Map();
|
||||
this.userIdsBySpace = new Map();
|
||||
this.childSpacesBySpace = new Map();
|
||||
|
||||
this.rebuildParentMap();
|
||||
// mutates this.roomIdsBySpace
|
||||
this.rebuildMetaSpaces();
|
||||
|
||||
const hiddenChildren = new EnhancedMap<string, Set<string>>();
|
||||
|
@ -698,26 +697,28 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
this.rootSpaces.forEach(s => {
|
||||
// traverse each space tree in DFS to build up the supersets as you go up,
|
||||
// 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
|
||||
|
||||
// reuse existing results if multiple similar branches exist
|
||||
if (this.spaceFilteredRooms.has(spaceId) && this.spaceFilteredUsers.has(spaceId)) {
|
||||
return [this.spaceFilteredRooms.get(spaceId), this.spaceFilteredUsers.get(spaceId)];
|
||||
if (this.roomIdsBySpace.has(spaceId) && this.userIdsBySpace.has(spaceId)) {
|
||||
return [this.roomIdsBySpace.get(spaceId), this.userIdsBySpace.get(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 space = this.matrixClient?.getRoom(spaceId);
|
||||
const userIds = new Set(space?.getMembers().filter(m => {
|
||||
return m.membership === "join" || m.membership === "invite";
|
||||
}).map(m => m.userId));
|
||||
|
||||
const newPath = new Set(parentPath).add(spaceId);
|
||||
|
||||
childSpaces.forEach(childSpace => {
|
||||
const [rooms, users] = fn(childSpace.roomId, newPath) ?? [];
|
||||
rooms?.forEach(roomId => roomIds.add(roomId));
|
||||
users?.forEach(userId => userIds.add(userId));
|
||||
traverseSpace(childSpace.roomId, newPath) ?? [];
|
||||
});
|
||||
hiddenChildren.get(spaceId)?.forEach(roomId => {
|
||||
roomIds.add(roomId);
|
||||
|
@ -727,33 +728,50 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
const expandedRoomIds = new Set(Array.from(roomIds).flatMap(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];
|
||||
};
|
||||
|
||||
fn(s.roomId, new Set());
|
||||
traverseSpace(s.roomId, new Set());
|
||||
});
|
||||
|
||||
const roomDiff = mapDiff(oldFilteredRooms, this.spaceFilteredRooms);
|
||||
const userDiff = mapDiff(oldFilteredUsers, this.spaceFilteredUsers);
|
||||
const roomDiff = mapDiff(prevRoomsBySpace, this.roomIdsBySpace);
|
||||
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
|
||||
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 => {
|
||||
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([
|
||||
...roomDiff.added,
|
||||
...userDiff.added,
|
||||
...spaceDiff.added,
|
||||
...roomDiff.removed,
|
||||
...userDiff.removed,
|
||||
...spaceDiff.removed,
|
||||
...roomsChanged,
|
||||
...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 => {
|
||||
this.emit(k);
|
||||
});
|
||||
|
@ -786,7 +804,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
|
||||
// otherwise, try to find a root space which contains this room
|
||||
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
|
||||
|
@ -869,6 +887,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
|
||||
private onRoomState = (ev: MatrixEvent) => {
|
||||
const room = this.matrixClient.getRoom(ev.getRoomId());
|
||||
|
||||
if (!room) return;
|
||||
|
||||
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
|
||||
private onRoomStateMembers = (ev: MatrixEvent) => {
|
||||
const room = this.matrixClient.getRoom(ev.getRoomId());
|
||||
|
||||
const userId = ev.getStateKey();
|
||||
if (room?.isSpaceRoom() && // only consider space rooms
|
||||
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) {
|
||||
if (this.enabledMetaSpaces.includes(MetaSpace.Favourites)) {
|
||||
if (room.tags[DefaultTagID.Favourite]) {
|
||||
this.spaceFilteredRooms.get(MetaSpace.Favourites).add(room.roomId);
|
||||
this.roomIdsBySpace.get(MetaSpace.Favourites).add(room.roomId);
|
||||
} else {
|
||||
this.spaceFilteredRooms.get(MetaSpace.Favourites).delete(room.roomId);
|
||||
this.roomIdsBySpace.get(MetaSpace.Favourites).delete(room.roomId);
|
||||
}
|
||||
this.emit(MetaSpace.Favourites);
|
||||
}
|
||||
|
@ -961,11 +981,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
const enabledMetaSpaces = new Set(this.enabledMetaSpaces);
|
||||
|
||||
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)) {
|
||||
homeRooms?.add(room.roomId);
|
||||
} else if (!this.spaceFilteredRooms.get(MetaSpace.Orphans).has(room.roomId)) {
|
||||
this.spaceFilteredRooms.get(MetaSpace.Home)?.delete(room.roomId);
|
||||
} else if (!this.roomIdsBySpace.get(MetaSpace.Orphans).has(room.roomId)) {
|
||||
this.roomIdsBySpace.get(MetaSpace.Home)?.delete(room.roomId);
|
||||
}
|
||||
|
||||
this.emit(MetaSpace.Home);
|
||||
|
@ -976,7 +996,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
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.Home);
|
||||
}
|
||||
|
@ -1006,8 +1026,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
this.rootSpaces = [];
|
||||
this.parentMap = new EnhancedMap();
|
||||
this.notificationStateMap = new Map();
|
||||
this.spaceFilteredRooms = new Map();
|
||||
this.spaceFilteredUsers = new Map();
|
||||
this.roomIdsBySpace = new Map();
|
||||
this.userIdsBySpace = new Map();
|
||||
this._aggregatedSpaceCache.roomIdsBySpace.clear();
|
||||
this._aggregatedSpaceCache.userIdsBySpace.clear();
|
||||
this._activeSpace = MetaSpace.Home; // set properly by onReady
|
||||
this._suggestedRooms = [];
|
||||
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 "../skinned-sdk"; // Must be first for skinning to work
|
||||
|
||||
import SpaceStore from "../../src/stores/spaces/SpaceStore";
|
||||
import {
|
||||
MetaSpace,
|
||||
|
@ -58,9 +59,11 @@ const invite2 = "!invite2:server";
|
|||
const room1 = "!room1:server";
|
||||
const room2 = "!room2:server";
|
||||
const room3 = "!room3:server";
|
||||
const room4 = "!room4:server";
|
||||
const space1 = "!space1:server";
|
||||
const space2 = "!space2:server";
|
||||
const space3 = "!space3:server";
|
||||
const space4 = "!space4:server";
|
||||
|
||||
const getUserIdForRoomId = jest.fn(roomId => {
|
||||
return {
|
||||
|
@ -303,11 +306,13 @@ describe("SpaceStore", () => {
|
|||
|
||||
describe("test fixture 1", () => {
|
||||
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);
|
||||
mkSpace(space1, [fav1, room1]);
|
||||
mkSpace(space2, [fav1, fav2, fav3, room1]);
|
||||
mkSpace(space3, [invite2]);
|
||||
mkSpace(space4, [room4, fav2, space2, space3]);
|
||||
|
||||
client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId));
|
||||
|
||||
[fav1, fav2, fav3].forEach(roomId => {
|
||||
|
@ -383,85 +388,144 @@ describe("SpaceStore", () => {
|
|||
await run();
|
||||
});
|
||||
|
||||
it("home space contains orphaned rooms", () => {
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, orphan1)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, orphan2)).toBeTruthy();
|
||||
});
|
||||
describe('isRoomInSpace()', () => {
|
||||
it("home space contains orphaned rooms", () => {
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, orphan1)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, orphan2)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("home space does not contain all favourites", () => {
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, fav1)).toBeFalsy();
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, fav2)).toBeFalsy();
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, fav3)).toBeFalsy();
|
||||
});
|
||||
it("home space does not contain all favourites", () => {
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, fav1)).toBeFalsy();
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, fav2)).toBeFalsy();
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, fav3)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("home space contains dm rooms", () => {
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, dm1)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, dm2)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, dm3)).toBeTruthy();
|
||||
});
|
||||
it("home space contains dm rooms", () => {
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, dm1)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, dm2)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, dm3)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("home space contains invites", () => {
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, invite1)).toBeTruthy();
|
||||
});
|
||||
it("home space contains invites", () => {
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, invite1)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("home space contains invites even if they are also shown in a space", () => {
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, invite2)).toBeTruthy();
|
||||
});
|
||||
it("home space contains invites even if they are also shown in a space", () => {
|
||||
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 () => {
|
||||
await setShowAllRooms(true);
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, room1)).toBeTruthy();
|
||||
});
|
||||
it(
|
||||
"all rooms space does contain rooms/low priority even if they are also shown in a space",
|
||||
async () => {
|
||||
await setShowAllRooms(true);
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, room1)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("favourites space does contain favourites even if they are also shown in a space", async () => {
|
||||
expect(store.isRoomInSpace(MetaSpace.Favourites, fav1)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(MetaSpace.Favourites, fav2)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(MetaSpace.Favourites, fav3)).toBeTruthy();
|
||||
});
|
||||
it("favourites space does contain favourites even if they are also shown in a space", async () => {
|
||||
expect(store.isRoomInSpace(MetaSpace.Favourites, fav1)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(MetaSpace.Favourites, fav2)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(MetaSpace.Favourites, fav3)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("people space does contain people even if they are also shown in a space", async () => {
|
||||
expect(store.isRoomInSpace(MetaSpace.People, dm1)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(MetaSpace.People, dm2)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(MetaSpace.People, dm3)).toBeTruthy();
|
||||
});
|
||||
it("people space does contain people even if they are also shown in a space", async () => {
|
||||
expect(store.isRoomInSpace(MetaSpace.People, dm1)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(MetaSpace.People, dm2)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(MetaSpace.People, dm3)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("orphans space does contain orphans even if they are also shown in all rooms", async () => {
|
||||
await setShowAllRooms(true);
|
||||
expect(store.isRoomInSpace(MetaSpace.Orphans, orphan1)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(MetaSpace.Orphans, orphan2)).toBeTruthy();
|
||||
});
|
||||
it("orphans space does contain orphans even if they are also shown in all rooms", async () => {
|
||||
await setShowAllRooms(true);
|
||||
expect(store.isRoomInSpace(MetaSpace.Orphans, orphan1)).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 () => {
|
||||
await setShowAllRooms(false);
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, room1)).toBeFalsy();
|
||||
});
|
||||
it("home space doesn't contain rooms/low priority if they are also shown in a space", async () => {
|
||||
await setShowAllRooms(false);
|
||||
expect(store.isRoomInSpace(MetaSpace.Home, room1)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("space contains child rooms", () => {
|
||||
expect(store.isRoomInSpace(space1, fav1)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(space1, room1)).toBeTruthy();
|
||||
});
|
||||
it("space contains child rooms", () => {
|
||||
expect(store.isRoomInSpace(space1, fav1)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(space1, room1)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("space contains child favourites", () => {
|
||||
expect(store.isRoomInSpace(space2, fav1)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(space2, fav2)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(space2, fav3)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(space2, 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("space contains child invites", () => {
|
||||
expect(store.isRoomInSpace(space3, 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("spaces contain dms which you have with members of that space", () => {
|
||||
expect(store.isRoomInSpace(space1, dm1)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(space2, dm1)).toBeFalsy();
|
||||
expect(store.isRoomInSpace(space3, dm1)).toBeFalsy();
|
||||
expect(store.isRoomInSpace(space1, dm2)).toBeFalsy();
|
||||
expect(store.isRoomInSpace(space2, dm2)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(space3, dm2)).toBeFalsy();
|
||||
expect(store.isRoomInSpace(space1, dm3)).toBeFalsy();
|
||||
expect(store.isRoomInSpace(space2, dm3)).toBeFalsy();
|
||||
expect(store.isRoomInSpace(space3, dm3)).toBeFalsy();
|
||||
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", () => {
|
||||
expect(store.isRoomInSpace(space2, fav1)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(space2, fav2)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(space2, fav3)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(space2, room1)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("space contains child invites", () => {
|
||||
expect(store.isRoomInSpace(space3, invite2)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("spaces contain dms which you have with members of that space", () => {
|
||||
expect(store.isRoomInSpace(space1, dm1)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(space2, dm1)).toBeFalsy();
|
||||
expect(store.isRoomInSpace(space3, dm1)).toBeFalsy();
|
||||
expect(store.isRoomInSpace(space1, dm2)).toBeFalsy();
|
||||
expect(store.isRoomInSpace(space2, dm2)).toBeTruthy();
|
||||
expect(store.isRoomInSpace(space3, dm2)).toBeFalsy();
|
||||
expect(store.isRoomInSpace(space1, dm3)).toBeFalsy();
|
||||
expect(store.isRoomInSpace(space2, 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 () => {
|
||||
|
@ -614,6 +678,115 @@ describe("SpaceStore", () => {
|
|||
expect(store.isRoomInSpace(space1, 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", () => {
|
||||
|
|
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