Remove legacy room list (#34040)

* refactor(room-list): migrate SpaceStore off the legacy room list store

SpaceStore.setActiveRoomInSpace iterated the legacy RoomListStore's
`orderedLists` in `TAG_ORDER`; switch it to the space-aware
RoomListStoreV3.getSortedRoomsInActiveSpace() accessor. This drops the
last non-UI dependency on the legacy store and on `TAG_ORDER` (exported
from LegacyRoomList, deleted next).

* feat(room-list)!: remove the legacy room list UI

Delete the old sublist-based room list and its components now that the
new RoomListPanel is the default. Removed: LegacyRoomList,
LegacyRoomListHeader, RoomSublist, ExtraTile, RoomTile (+ Subtitle/
CallSummary), RoomBreadcrumbs and RoomSearch, plus their styles and
tests.

LeftPanel collapses to the RoomListPanel-only path. The shared
`contextMenuBelow` helper is relocated into RoomResultContextMenus (its
only remaining consumer).

* feat(room-list)!: remove the legacy RoomListStore

The legacy sublist-based room list UI is gone, so the old
`stores/room-list` store (Algorithm, sorters, filters, layout store,
space
watcher) has no remaining consumers. Delete the directory and its tests.

MatrixChat.forgetRoom no longer calls the legacy `manualRoomUpdate`; the
new room list store removes the room on the `AfterForgetRoom` dispatch
that still fires. Drop the `mxRoomListStore`/`mxRoomListLayoutStore`
globals and the now-dead test imports.

* feat(room-list)!: remove the feature_new_room_list labs flag

The new room list is now the only room list, so remove the
feature_new_room_list labs flag and make its enabled behaviour
unconditional everywhere it was gated:

- LoggedInView: always use the resizable layout and
NEW_ROOM_LIST_MIN_WIDTH;
  drop the collapsible/minimized legacy path.
- SpaceStore: People and Favourites are dropped from metaSpaceOrder (per
the
  long-standing TODO on the removed accessor).
- MessagePreviewStore: stop appending thread replies to previews.
- Settings, SidebarUserSettingsTab, PreferencesUserSettingsTab,
  QuickSettingsButton, SpacePanel, LandmarkNavigation: drop the flag
reads and
  legacy branches.

Update the tests that toggled the flag; the People/Favourites meta space
tests covered behaviour that the flag (default on) already disabled.

* feat(room-list)!: remove the dead legacy left-panel resizer

LoggedInView still built the old `Resizer`/`CollapseDistributor` over an
`lp-resizer` ResizeHandle and persisted `mx_lhs_size`. That handle is no
longer rendered (the resizable layout is now driven by
LeftResizablePanelView + ResizerViewModel, which persists its own state
via RoomList.panelSize/RoomList.isPanelCollapsed), so the old resizer
was
inert dead code left over from the legacy room list.

Remove createResizer/loadResizer/loadResizerPreferences, the
_resizeContainer/resizeHandler refs, the ResizeHandle render, the
mx_lhs_size handling and NEW_ROOM_LIST_MIN_WIDTH, plus the unit tests
that
exercised the mocked resizer.

* feat(room-list)!: update i18n files

* refactor(room-list): remove the now-unused collapseLhs state

`collapseLhs` is write-only since the left panel no longer collapses: it
was last read by LoggedInView's `shouldUseMinimizedUI`, removed with the
feature_new_room_list flag. Drop it from MatrixChat's IState (and its
assignments), collapsing the hide/show_left_panel handlers to just the
`notifyLeftHandleResized()` call they still need, and from
LoggedInView's
IProps and the test props.

* fix(room-list): instantiate message previewers lazily

Removing the unused SettingsStore import from MessagePreviewStore (when
the
feature_new_room_list flag was dropped) changed module load order and
exposed a latent circular dependency: ReactionEventPreview imports
MessagePreviewStore, which eagerly did `new ReactionEventPreview()` at
module-eval — so importing ReactionEventPreview first (as its unit test
does) hit "ReactionEventPreview is not a constructor".

Construct the previewers lazily on first use (cached) instead of at
module
load, so nothing dereferences a mid-evaluation module. Fixes
ReactionEventPreview-test.

* test(room-list): remove `feature_new_room_list` labs flag in e2e tests

* chore: remove remaining `newRoomList` flag

* chore: cleanup theme files

* fix: restore the re-resizable TouchEvent polyfill

* chore: remove usage of breadcrumbs settings in BreadcrumbStore

* Revert "fix(room-list): instantiate message previewers lazily"

This reverts commit 4e6eedfff0449c68a96c0470a4eb425b5aec5512.

* chore: remove unused function in BreadCrumbStore

* test: remove unused fuction of BreadcrumStore in tests

* test: add tests for RoomResultContextMenu
This commit is contained in:
Florian Duros
2026-07-02 09:07:37 +00:00
committed by GitHub
parent 522ec3aa57
commit b68ecdb860
103 changed files with 91 additions and 11086 deletions
@@ -27,41 +27,6 @@ describe("BreadcrumbsStore", () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
});
it("does not meet room requirements if there are not enough rooms", () => {
// We don't have enough rooms, so we don't meet requirements
mocked(client.getVisibleRooms).mockReturnValue(fakeRooms(2));
expect(store.meetsRoomRequirement).toBe(false);
});
it("meets room requirements if there are enough rooms", () => {
// We do have enough rooms to show breadcrumbs
mocked(client.getVisibleRooms).mockReturnValue(fakeRooms(25));
expect(store.meetsRoomRequirement).toBe(true);
});
describe("And the feature_dynamic_room_predecessors is enabled", () => {
beforeEach(() => {
// Turn on feature_dynamic_room_predecessors setting
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === "feature_dynamic_room_predecessors",
);
});
it("passes through the dynamic room precessors flag", () => {
mocked(client.getVisibleRooms).mockReturnValue(fakeRooms(25));
expect(store.meetsRoomRequirement).toBeTruthy();
expect(client.getVisibleRooms).toHaveBeenCalledWith(true);
});
});
describe("And the feature_dynamic_room_predecessors is not enabled", () => {
it("passes through the dynamic room precessors flag", () => {
mocked(client.getVisibleRooms).mockReturnValue(fakeRooms(25));
expect(store.meetsRoomRequirement).toBeTruthy();
expect(client.getVisibleRooms).toHaveBeenCalledWith(false);
});
});
describe("If the feature_dynamic_room_predecessors is not enabled", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
@@ -155,17 +120,6 @@ describe("BreadcrumbsStore", () => {
await flushPromises();
}
/**
* Create as many fake rooms in an array as you ask for.
*/
function fakeRooms(howMany: number): Room[] {
const ret: Room[] = [];
for (let i = 0; i < howMany; i++) {
ret.push(fakeRoom());
}
return ret;
}
let roomIdx = 0;
function fakeRoom(): Room {
@@ -37,7 +37,7 @@ import SettingsStore from "../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Action } from "../../../src/dispatcher/actions";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import RoomListStore from "../../../src/stores/room-list/RoomListStore";
import RoomListStoreV3 from "../../../src/stores/room-list-v3/RoomListStoreV3";
import { DefaultTagID } from "../../../src/stores/room-list-v3/skip-list/tag";
import { RoomNotificationStateStore } from "../../../src/stores/notifications/RoomNotificationStateStore";
import { NotificationLevel } from "../../../src/stores/notifications/NotificationLevel";
@@ -141,8 +141,6 @@ describe("SpaceStore", () => {
});
afterEach(async () => {
// Disable the new room list feature flag
await SettingsStore.setValue("feature_new_room_list", null, SettingLevel.DEVICE, false);
await testUtils.resetAsyncStoreWithClient(store);
});
@@ -444,12 +442,6 @@ describe("SpaceStore", () => {
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("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();
@@ -573,27 +565,6 @@ describe("SpaceStore", () => {
});
});
it("dms are only added to Notification States for only the People Space", async () => {
[dm1, dm2, dm3].forEach((d) => {
expect(
store
.getNotificationState(MetaSpace.People)
.rooms.map((r) => r.roomId)
.includes(d),
).toBeTruthy();
});
[space1, space2, space3, MetaSpace.Home, MetaSpace.Orphans, MetaSpace.Favourites].forEach((s) => {
[dm1, dm2, dm3].forEach((d) => {
expect(
store
.getNotificationState(s)
.rooms.map((r) => r.roomId)
.includes(d),
).toBeFalsy();
});
});
});
it("orphan rooms are added to Notification States for only the Home Space", async () => {
await setShowAllRooms(false);
[orphan1, orphan2].forEach((d) => {
@@ -710,29 +681,6 @@ describe("SpaceStore", () => {
});
});
it("should add new DM Invites to the People Space Notification State", async () => {
mkRoom(dm1);
mocked(client.getRoom(dm1)!).getMyMembership.mockReturnValue(KnownMembership.Join);
mocked(client).getRoom.mockImplementation((roomId) => rooms.find((room) => room.roomId === roomId) || null);
await run();
mkRoom(dm2);
const cliDm2 = client.getRoom(dm2)!;
mocked(cliDm2).getMyMembership.mockReturnValue(KnownMembership.Invite);
mocked(client).getRoom.mockImplementation((roomId) => rooms.find((room) => room.roomId === roomId) || null);
client.emit(RoomEvent.MyMembership, cliDm2, KnownMembership.Invite);
[dm1, dm2].forEach((d) => {
expect(
store
.getNotificationState(MetaSpace.People)
.rooms.map((r) => r.roomId)
.includes(d),
).toBeTruthy();
});
});
describe("hierarchy resolution update tests", () => {
it("updates state when spaces are joined", async () => {
await run();
@@ -1199,16 +1147,15 @@ describe("SpaceStore", () => {
});
it("switch to first valid space when selected metaspace is disabled", async () => {
store.setActiveSpace(MetaSpace.People, false);
expect(store.activeSpace).toBe(MetaSpace.People);
viewRoom(room1);
store.setActiveSpace(MetaSpace.Orphans, false);
expect(store.activeSpace).toBe(MetaSpace.Orphans);
await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, {
[MetaSpace.Home]: false,
[MetaSpace.Favourites]: true,
[MetaSpace.People]: false,
[MetaSpace.Orphans]: true,
[MetaSpace.Home]: true,
[MetaSpace.Orphans]: false,
});
jest.runAllTimers();
expect(store.activeSpace).toBe(MetaSpace.Orphans);
expect(store.activeSpace).toBe(space1);
});
it("when switching rooms in the all rooms home space don't switch to related space", async () => {
@@ -1402,12 +1349,9 @@ describe("SpaceStore", () => {
removeListener();
});
it("Favourites and People meta spaces should not be returned when the feature_new_room_list labs flag is enabled", async () => {
// Enable the new room list
await SettingsStore.setValue("feature_new_room_list", null, SettingLevel.DEVICE, true);
it("Favourites and People meta spaces should not be returned", async () => {
await run();
// Favourites and People meta spaces should not be returned
// Favourites and People meta spaces are not part of the meta space order
expect(SpaceStore.instance.enabledMetaSpaces).toStrictEqual([MetaSpace.Home, MetaSpace.Orphans]);
});
@@ -1515,8 +1459,9 @@ describe("SpaceStore", () => {
const state = RoomNotificationStateStore.instance.getRoomState(room);
// @ts-ignore
state._level = NotificationLevel.Notification;
jest.spyOn(RoomListStore.instance, "orderedLists", "get").mockReturnValue({
[DefaultTagID.Untagged]: [room],
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: MetaSpace.Home,
sections: [{ tag: DefaultTagID.Untagged, rooms: [room] }],
});
// init the store
@@ -1,349 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import {
ConditionKind,
EventType,
type IPushRule,
MatrixEvent,
PendingEventOrdering,
PushRuleActionName,
Room,
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { mocked } from "jest-mock";
import defaultDispatcher, { type MatrixDispatcher } from "../../../../src/dispatcher/dispatcher";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import SettingsStore, { type CallbackFn } from "../../../../src/settings/SettingsStore";
import { ListAlgorithm, SortAlgorithm } from "../../../../src/stores/room-list/algorithms/models";
import { OrderedDefaultTagIDs, RoomUpdateCause } from "../../../../src/stores/room-list/models";
import RoomListStore, { RoomListStoreClass } from "../../../../src/stores/room-list/RoomListStore";
import DMRoomMap from "../../../../src/utils/DMRoomMap";
import { flushPromises, stubClient, upsertRoomStateEvents } from "../../../test-utils";
import { DEFAULT_PUSH_RULES, makePushRule } from "../../../test-utils/pushRules";
describe("RoomListStore", () => {
const client = stubClient();
const newRoomId = "!roomid:example.com";
const roomNoPredecessorId = "!roomnopreid:example.com";
const oldRoomId = "!oldroomid:example.com";
const userId = "@user:example.com";
const createWithPredecessor = new MatrixEvent({
type: EventType.RoomCreate,
sender: userId,
room_id: newRoomId,
content: {
predecessor: { room_id: oldRoomId, event_id: "tombstone_event_id" },
},
event_id: "$create",
state_key: "",
});
const createNoPredecessor = new MatrixEvent({
type: EventType.RoomCreate,
sender: userId,
room_id: newRoomId,
content: {},
event_id: "$create",
state_key: "",
});
const predecessor = new MatrixEvent({
type: EventType.RoomPredecessor,
sender: userId,
room_id: newRoomId,
content: {
predecessor_room_id: oldRoomId,
last_known_event_id: "tombstone_event_id",
},
event_id: "$pred",
state_key: "",
});
const roomWithPredecessorEvent = new Room(newRoomId, client, userId, {});
upsertRoomStateEvents(roomWithPredecessorEvent, [predecessor]);
const roomWithCreatePredecessor = new Room(newRoomId, client, userId, {});
upsertRoomStateEvents(roomWithCreatePredecessor, [createWithPredecessor]);
const roomNoPredecessor = new Room(roomNoPredecessorId, client, userId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
upsertRoomStateEvents(roomNoPredecessor, [createNoPredecessor]);
const oldRoom = new Room(oldRoomId, client, userId, {});
const normalRoom = new Room("!normal:server.org", client, userId);
client.getRoom = jest.fn().mockImplementation((roomId) => {
switch (roomId) {
case newRoomId:
return roomWithCreatePredecessor;
case oldRoomId:
return oldRoom;
case normalRoom.roomId:
return normalRoom;
default:
return null;
}
});
beforeAll(async () => {
await (RoomListStore.instance as RoomListStoreClass).makeReady(client);
});
it.each(OrderedDefaultTagIDs)("defaults to importance ordering for %s=", (tagId) => {
expect(RoomListStore.instance.getTagSorting(tagId)).toBe(SortAlgorithm.Recent);
});
it.each(OrderedDefaultTagIDs)("defaults to activity ordering for %s=", (tagId) => {
expect(RoomListStore.instance.getListOrder(tagId)).toBe(ListAlgorithm.Natural);
});
function createStore(): { store: RoomListStoreClass; handleRoomUpdate: jest.Mock<any, any> } {
const fakeDispatcher = { register: jest.fn() } as unknown as MatrixDispatcher;
const store = new RoomListStoreClass(fakeDispatcher);
// @ts-ignore accessing private member to set client
store.readyStore.matrixClient = client;
const handleRoomUpdate = jest.fn();
// @ts-ignore accessing private member to mock it
store.algorithm.handleRoomUpdate = handleRoomUpdate;
return { store, handleRoomUpdate };
}
it("Removes old room if it finds a predecessor in the create event", () => {
// Given a store we can spy on
const { store, handleRoomUpdate } = createStore();
mocked(client.getRoomUpgradeHistory).mockImplementation((roomId) =>
roomId === roomWithCreatePredecessor.roomId ? [oldRoom, roomWithCreatePredecessor] : [],
);
// When we tell it we joined a new room that has an old room as
// predecessor in the create event
const payload = {
oldMembership: KnownMembership.Invite,
membership: KnownMembership.Join,
room: roomWithCreatePredecessor,
};
store.onDispatchMyMembership(payload);
// Then the old room is removed
expect(handleRoomUpdate).toHaveBeenCalledWith(oldRoom, RoomUpdateCause.RoomRemoved);
// And the new room is added
expect(handleRoomUpdate).toHaveBeenCalledWith(roomWithCreatePredecessor, RoomUpdateCause.NewRoom);
});
it("Does not remove old room if there is no predecessor in the create event", () => {
// Given a store we can spy on
const { store, handleRoomUpdate } = createStore();
// When we tell it we joined a new room with no predecessor
const payload = {
oldMembership: KnownMembership.Invite,
membership: KnownMembership.Join,
room: roomNoPredecessor,
};
store.onDispatchMyMembership(payload);
// Then the new room is added
expect(handleRoomUpdate).toHaveBeenCalledWith(roomNoPredecessor, RoomUpdateCause.NewRoom);
// And no other updates happen
expect(handleRoomUpdate).toHaveBeenCalledTimes(1);
});
it("Lists all rooms that the client says are visible", () => {
// Given 3 rooms that are visible according to the client
const room1 = new Room("!r1:e.com", client, userId, { pendingEventOrdering: PendingEventOrdering.Detached });
const room2 = new Room("!r2:e.com", client, userId, { pendingEventOrdering: PendingEventOrdering.Detached });
const room3 = new Room("!r3:e.com", client, userId, { pendingEventOrdering: PendingEventOrdering.Detached });
room1.updateMyMembership(KnownMembership.Join);
room2.updateMyMembership(KnownMembership.Join);
room3.updateMyMembership(KnownMembership.Join);
DMRoomMap.makeShared(client);
const { store } = createStore();
client.getVisibleRooms = jest.fn().mockReturnValue([room1, room2, room3]);
// When we make the list of rooms
store.regenerateAllLists({ trigger: false });
// Then the list contains all 3
expect(store.orderedLists).toMatchObject({
"im.vector.fake.recent": [room1, room2, room3],
});
// We asked not to use MSC3946 when we asked the client for the visible rooms
expect(client.getVisibleRooms).toHaveBeenCalledWith(false);
expect(client.getVisibleRooms).toHaveBeenCalledTimes(1);
});
it("Watches the feature flag setting", () => {
jest.spyOn(SettingsStore, "watchSetting").mockReturnValue("dyn_pred_ref");
jest.spyOn(SettingsStore, "unwatchSetting");
// When we create a store
const { store } = createStore();
// Then we watch the feature flag
expect(SettingsStore.watchSetting).toHaveBeenCalledWith(
"feature_dynamic_room_predecessors",
null,
expect.any(Function),
);
// And when we unmount it
store.componentWillUnmount();
// Then we unwatch it.
expect(SettingsStore.unwatchSetting).toHaveBeenCalledWith("dyn_pred_ref");
});
it("Regenerates all lists when the feature flag is set", () => {
// Given a store allowing us to spy on any use of SettingsStore
let featureFlagValue = false;
jest.spyOn(SettingsStore, "getValue").mockImplementation(() => featureFlagValue);
let watchCallback: CallbackFn<any> | undefined;
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((_settingName, _roomId, callbackFn) => {
watchCallback = callbackFn;
return "dyn_pred_ref";
});
jest.spyOn(SettingsStore, "unwatchSetting");
const { store } = createStore();
client.getVisibleRooms = jest.fn().mockReturnValue([]);
// Sanity: no calculation has happened yet
expect(client.getVisibleRooms).toHaveBeenCalledTimes(0);
// When we calculate for the first time
store.regenerateAllLists({ trigger: false });
// Then we use the current feature flag value (false)
expect(client.getVisibleRooms).toHaveBeenCalledWith(false);
expect(client.getVisibleRooms).toHaveBeenCalledTimes(1);
// But when we update the feature flag
featureFlagValue = true;
watchCallback!(
"feature_dynamic_room_predecessors",
"",
SettingLevel.DEFAULT,
featureFlagValue,
featureFlagValue,
);
// Then we recalculate and passed the updated value (true)
expect(client.getVisibleRooms).toHaveBeenCalledWith(true);
expect(client.getVisibleRooms).toHaveBeenCalledTimes(2);
});
describe("When feature_dynamic_room_predecessors = true", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === "feature_dynamic_room_predecessors",
);
});
afterEach(() => {
jest.spyOn(SettingsStore, "getValue").mockReset();
});
it("Removes old room if it finds a predecessor in the m.predecessor event", () => {
// Given a store we can spy on
const { store, handleRoomUpdate } = createStore();
// When we tell it we joined a new room that has an old room as
// predecessor in the create event
const payload = {
oldMembership: KnownMembership.Invite,
membership: KnownMembership.Join,
room: roomWithPredecessorEvent,
};
store.onDispatchMyMembership(payload);
// Then the old room is removed
expect(handleRoomUpdate).toHaveBeenCalledWith(oldRoom, RoomUpdateCause.RoomRemoved);
// And the new room is added
expect(handleRoomUpdate).toHaveBeenCalledWith(roomWithPredecessorEvent, RoomUpdateCause.NewRoom);
});
it("Passes the feature flag on to the client when asking for visible rooms", () => {
// Given a store that we can ask for a room list
DMRoomMap.makeShared(client);
const { store } = createStore();
client.getVisibleRooms = jest.fn().mockReturnValue([]);
// When we make the list of rooms
store.regenerateAllLists({ trigger: false });
// We asked to use MSC3946 when we asked the client for the visible rooms
expect(client.getVisibleRooms).toHaveBeenCalledWith(true);
expect(client.getVisibleRooms).toHaveBeenCalledTimes(1);
});
});
describe("room updates", () => {
const makeStore = async () => {
const store = new RoomListStoreClass(defaultDispatcher);
await store.start();
return store;
};
describe("push rules updates", () => {
const makePushRulesEvent = (overrideRules: IPushRule[] = []): MatrixEvent => {
return new MatrixEvent({
type: EventType.PushRules,
content: {
global: {
...DEFAULT_PUSH_RULES.global,
override: overrideRules,
},
},
});
};
it("triggers a room update when room mutes have changed", async () => {
const rule = makePushRule(normalRoom.roomId, {
actions: [PushRuleActionName.DontNotify],
conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: normalRoom.roomId }],
});
const event = makePushRulesEvent([rule]);
const previousEvent = makePushRulesEvent();
const store = await makeStore();
// @ts-ignore private property alg
const algorithmSpy = jest.spyOn(store.algorithm, "handleRoomUpdate").mockReturnValue(undefined);
// @ts-ignore cheat and call protected fn
store.onAction({ action: "MatrixActions.accountData", event, previousEvent });
await flushPromises();
expect(algorithmSpy).toHaveBeenCalledWith(normalRoom, RoomUpdateCause.PossibleMuteChange);
});
it("handles when a muted room is unknown by the room list", async () => {
const rule = makePushRule(normalRoom.roomId, {
actions: [PushRuleActionName.DontNotify],
conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: normalRoom.roomId }],
});
const unknownRoomRule = makePushRule("!unknown:server.org", {
conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: "!unknown:server.org" }],
});
const event = makePushRulesEvent([unknownRoomRule, rule]);
const previousEvent = makePushRulesEvent();
const store = await makeStore();
// @ts-ignore private property alg
const algorithmSpy = jest.spyOn(store.algorithm, "handleRoomUpdate").mockReturnValue(undefined);
// @ts-ignore cheat and call protected fn
store.onAction({ action: "MatrixActions.accountData", event, previousEvent });
await flushPromises();
// only one call to update made for normalRoom
expect(algorithmSpy).toHaveBeenCalledTimes(1);
expect(algorithmSpy).toHaveBeenCalledWith(normalRoom, RoomUpdateCause.PossibleMuteChange);
});
});
});
});
@@ -1,218 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { mocked } from "jest-mock";
import { type Room } from "matrix-js-sdk/src/matrix";
import { SpaceWatcher } from "../../../../src/stores/room-list/SpaceWatcher";
import type { RoomListStoreClass } from "../../../../src/stores/room-list/RoomListStore";
import SettingsStore from "../../../../src/settings/SettingsStore";
import SpaceStore from "../../../../src/stores/spaces/SpaceStore";
import { MetaSpace, UPDATE_HOME_BEHAVIOUR } from "../../../../src/stores/spaces";
import { stubClient, mkSpace, emitPromise, setupAsyncStoreWithClient } from "../../../test-utils";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { SpaceFilterCondition } from "../../../../src/stores/room-list/filters/SpaceFilterCondition";
import DMRoomMap from "../../../../src/utils/DMRoomMap";
let filter: SpaceFilterCondition | null = null;
const mockRoomListStore = {
addFilter: (f: SpaceFilterCondition) => (filter = f),
removeFilter: (): void => {
filter = null;
},
} as unknown as RoomListStoreClass;
const getUserIdForRoomId = jest.fn();
const getDMRoomsForUserId = jest.fn();
// @ts-ignore
DMRoomMap.sharedInstance = { getUserIdForRoomId, getDMRoomsForUserId };
const space1 = "!space1:server";
const space2 = "!space2:server";
describe("SpaceWatcher", () => {
stubClient();
const store = SpaceStore.instance;
const client = mocked(MatrixClientPeg.safeGet());
let rooms: Room[] = [];
const mkSpaceForRooms = (spaceId: string, children: string[] = []) => mkSpace(client, spaceId, rooms, children);
const setShowAllRooms = async (value: boolean) => {
if (store.allRoomsInHome === value) return;
await SettingsStore.setValue("Spaces.allRoomsInHome", null, SettingLevel.DEVICE, value);
await emitPromise(store, UPDATE_HOME_BEHAVIOUR);
};
beforeEach(async () => {
filter = null;
store.removeAllListeners();
store.setActiveSpace(MetaSpace.Home);
client.getVisibleRooms.mockReturnValue((rooms = []));
mkSpaceForRooms(space1);
mkSpaceForRooms(space2);
await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, {
[MetaSpace.Home]: true,
[MetaSpace.Favourites]: true,
[MetaSpace.People]: true,
[MetaSpace.Orphans]: true,
});
client.getRoom.mockImplementation((roomId) => rooms.find((room) => room.roomId === roomId) || null);
await setupAsyncStoreWithClient(store, client);
});
it("initialises sanely with home behaviour", async () => {
await setShowAllRooms(false);
new SpaceWatcher(mockRoomListStore);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
});
it("initialises sanely with all behaviour", async () => {
await setShowAllRooms(true);
new SpaceWatcher(mockRoomListStore);
expect(filter).toBeNull();
});
it("sets space=Home filter for all -> home transition", async () => {
await setShowAllRooms(true);
new SpaceWatcher(mockRoomListStore);
await setShowAllRooms(false);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter!["space"]).toBe(MetaSpace.Home);
});
it("sets filter correctly for all -> space transition", async () => {
await setShowAllRooms(true);
new SpaceWatcher(mockRoomListStore);
SpaceStore.instance.setActiveSpace(space1);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter!["space"]).toBe(space1);
});
it("removes filter for home -> all transition", async () => {
await setShowAllRooms(false);
new SpaceWatcher(mockRoomListStore);
await setShowAllRooms(true);
expect(filter).toBeNull();
});
it("sets filter correctly for home -> space transition", async () => {
await setShowAllRooms(false);
new SpaceWatcher(mockRoomListStore);
SpaceStore.instance.setActiveSpace(space1);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter!["space"]).toBe(space1);
});
it("removes filter for space -> all transition", async () => {
await setShowAllRooms(true);
new SpaceWatcher(mockRoomListStore);
SpaceStore.instance.setActiveSpace(space1);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter!["space"]).toBe(space1);
SpaceStore.instance.setActiveSpace(MetaSpace.Home);
expect(filter).toBeNull();
});
// The new room list is now forced on, which removes the favourites and people meta spaces.
// So no need to test these transitions any more.
it("removes filter for orphans -> all transition", async () => {
await setShowAllRooms(true);
new SpaceWatcher(mockRoomListStore);
SpaceStore.instance.setActiveSpace(MetaSpace.Orphans);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter!["space"]).toBe(MetaSpace.Orphans);
SpaceStore.instance.setActiveSpace(MetaSpace.Home);
expect(filter).toBeNull();
});
it("updates filter correctly for space -> home transition", async () => {
await setShowAllRooms(false);
SpaceStore.instance.setActiveSpace(space1);
new SpaceWatcher(mockRoomListStore);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter!["space"]).toBe(space1);
SpaceStore.instance.setActiveSpace(MetaSpace.Home);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter!["space"]).toBe(MetaSpace.Home);
});
it("updates filter correctly for space -> orphans transition", async () => {
await setShowAllRooms(false);
SpaceStore.instance.setActiveSpace(space1);
new SpaceWatcher(mockRoomListStore);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter!["space"]).toBe(space1);
SpaceStore.instance.setActiveSpace(MetaSpace.Orphans);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter!["space"]).toBe(MetaSpace.Orphans);
});
it("updates filter correctly for space -> space transition", async () => {
await setShowAllRooms(false);
SpaceStore.instance.setActiveSpace(space1);
new SpaceWatcher(mockRoomListStore);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter!["space"]).toBe(space1);
SpaceStore.instance.setActiveSpace(space2);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter!["space"]).toBe(space2);
});
it("doesn't change filter when changing showAllRooms mode to true", async () => {
await setShowAllRooms(false);
SpaceStore.instance.setActiveSpace(space1);
new SpaceWatcher(mockRoomListStore);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter!["space"]).toBe(space1);
await setShowAllRooms(true);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter!["space"]).toBe(space1);
});
it("doesn't change filter when changing showAllRooms mode to false", async () => {
await setShowAllRooms(true);
SpaceStore.instance.setActiveSpace(space1);
new SpaceWatcher(mockRoomListStore);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter!["space"]).toBe(space1);
await setShowAllRooms(false);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter!["space"]).toBe(space1);
});
});
@@ -1,102 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { mocked, type MockedObject } from "jest-mock";
import { PendingEventOrdering, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { Widget } from "matrix-widget-api";
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
import {
stubClient,
setupAsyncStoreWithClient,
useMockedCalls,
MockedCall,
useMockMediaDevices,
} from "../../../../test-utils";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { DefaultTagID } from "../../../../../src/stores/room-list-v3/skip-list/tag";
import { SortAlgorithm, ListAlgorithm } from "../../../../../src/stores/room-list/algorithms/models";
import "../../../../../src/stores/room-list/RoomListStore"; // must be imported before Algorithm to avoid cycles
import { Algorithm } from "../../../../../src/stores/room-list/algorithms/Algorithm";
import { CallStore } from "../../../../../src/stores/CallStore";
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
import { ConnectionState } from "../../../../../src/models/Call";
import { type WidgetMessaging } from "../../../../../src/stores/widgets/WidgetMessaging";
describe("Algorithm", () => {
useMockedCalls();
let client: MockedObject<MatrixClient>;
let algorithm: Algorithm;
beforeEach(() => {
useMockMediaDevices();
stubClient();
client = mocked(MatrixClientPeg.safeGet());
DMRoomMap.makeShared(client);
algorithm = new Algorithm();
algorithm.start();
algorithm.populateTags(
{ [DefaultTagID.Untagged]: SortAlgorithm.Alphabetic },
{ [DefaultTagID.Untagged]: ListAlgorithm.Natural },
);
});
afterEach(() => {
algorithm.stop();
});
it("sticks rooms with calls to the top when they're connected", async () => {
const room = new Room("!1:example.org", client, "@alice:example.org", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
const roomWithCall = new Room("!2:example.org", client, "@alice:example.org", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
client.getRoom.mockImplementation((roomId) => {
switch (roomId) {
case room.roomId:
return room;
case roomWithCall.roomId:
return roomWithCall;
default:
return null;
}
});
client.getRooms.mockReturnValue([room, roomWithCall]);
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
client.reEmitter.reEmit(roomWithCall, [RoomStateEvent.Events]);
for (const room of client.getRooms()) jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
algorithm.setKnownRooms(client.getRooms());
setupAsyncStoreWithClient(CallStore.instance, client);
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
MockedCall.create(roomWithCall, "1");
const call = CallStore.instance.getCall(roomWithCall.roomId);
if (!(call instanceof MockedCall)) throw new Error("Failed to create call");
const widget = new Widget(call.widget);
WidgetMessagingStore.instance.storeMessaging(widget, roomWithCall.roomId, {
stop: () => {},
} as unknown as WidgetMessaging);
// End of setup
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([room, roomWithCall]);
call.setConnectionState(ConnectionState.Connected);
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([roomWithCall, room]);
await call.disconnect();
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([room, roomWithCall]);
});
});
@@ -1,177 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { Room, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { mkMessage, mkRoom, stubClient } from "../../../../test-utils";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import "../../../../../src/stores/room-list/RoomListStore";
import { RecentAlgorithm } from "../../../../../src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import { makeThreadEvent, mkThread } from "../../../../test-utils/threads";
import { DefaultTagID } from "../../../../../src/stores/room-list-v3/skip-list/tag";
describe("RecentAlgorithm", () => {
let algorithm: RecentAlgorithm;
let cli: MatrixClient;
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.safeGet();
algorithm = new RecentAlgorithm();
});
describe("getLastTs", () => {
it("returns the last ts", () => {
const room = new Room("room123", cli, "@john:matrix.org");
const event1 = mkMessage({
room: room.roomId,
msg: "Hello world!",
user: "@alice:matrix.org",
ts: 5,
event: true,
});
const event2 = mkMessage({
room: room.roomId,
msg: "Howdy!",
user: "@bob:matrix.org",
ts: 10,
event: true,
});
room.getMyMembership = () => KnownMembership.Join;
room.addLiveEvents([event1], { addToState: true });
expect(algorithm.getLastTs(room, "@jane:matrix.org")).toBe(5);
expect(algorithm.getLastTs(room, "@john:matrix.org")).toBe(5);
room.addLiveEvents([event2], { addToState: true });
expect(algorithm.getLastTs(room, "@jane:matrix.org")).toBe(10);
expect(algorithm.getLastTs(room, "@john:matrix.org")).toBe(10);
});
it("returns a fake ts for rooms without a timeline", () => {
const room = mkRoom(cli, "!new:example.org");
// @ts-ignore
room.timeline = undefined;
expect(algorithm.getLastTs(room, "@john:matrix.org")).toBe(Number.MAX_SAFE_INTEGER);
});
it("works when not a member", () => {
const room = mkRoom(cli, "!new:example.org");
room.getMyMembership.mockReturnValue(KnownMembership.Invite);
expect(algorithm.getLastTs(room, "@john:matrix.org")).toBe(Number.MAX_SAFE_INTEGER);
});
});
describe("sortRooms", () => {
it("orders rooms per last message ts", () => {
const room1 = new Room("room1", cli, "@bob:matrix.org");
const room2 = new Room("room2", cli, "@bob:matrix.org");
room1.getMyMembership = () => KnownMembership.Join;
room2.getMyMembership = () => KnownMembership.Join;
const evt = mkMessage({
room: room1.roomId,
msg: "Hello world!",
user: "@alice:matrix.org",
ts: 5,
event: true,
});
const evt2 = mkMessage({
room: room2.roomId,
msg: "Hello world!",
user: "@alice:matrix.org",
ts: 2,
event: true,
});
room1.addLiveEvents([evt], { addToState: true });
room2.addLiveEvents([evt2], { addToState: true });
expect(algorithm.sortRooms([room2, room1], DefaultTagID.Untagged)).toEqual([room1, room2]);
});
it("orders rooms without messages first", () => {
const room1 = new Room("room1", cli, "@bob:matrix.org");
const room2 = new Room("room2", cli, "@bob:matrix.org");
room1.getMyMembership = () => KnownMembership.Join;
room2.getMyMembership = () => KnownMembership.Join;
const evt = mkMessage({
room: room1.roomId,
msg: "Hello world!",
user: "@alice:matrix.org",
ts: 5,
event: true,
});
room1.addLiveEvents([evt], { addToState: true });
expect(algorithm.sortRooms([room2, room1], DefaultTagID.Untagged)).toEqual([room2, room1]);
const { events } = mkThread({
room: room1,
client: cli,
authorId: "@bob:matrix.org",
participantUserIds: ["@bob:matrix.org"],
ts: 12,
});
room1.addLiveEvents(events, { addToState: true });
});
it("orders rooms based on thread replies too", () => {
const room1 = new Room("room1", cli, "@bob:matrix.org");
const room2 = new Room("room2", cli, "@bob:matrix.org");
room1.getMyMembership = () => KnownMembership.Join;
room2.getMyMembership = () => KnownMembership.Join;
const { rootEvent, events: events1 } = mkThread({
room: room1,
client: cli,
authorId: "@bob:matrix.org",
participantUserIds: ["@bob:matrix.org"],
ts: 12,
length: 5,
});
room1.addLiveEvents(events1, { addToState: true });
const { events: events2 } = mkThread({
room: room2,
client: cli,
authorId: "@bob:matrix.org",
participantUserIds: ["@bob:matrix.org"],
ts: 14,
length: 10,
});
room2.addLiveEvents(events2, { addToState: true });
expect(algorithm.sortRooms([room1, room2], DefaultTagID.Untagged)).toEqual([room2, room1]);
const threadReply = makeThreadEvent({
user: "@bob:matrix.org",
room: room1.roomId,
event: true,
msg: `hello world`,
rootEventId: rootEvent.getId()!,
replyToEventId: rootEvent.getId()!,
// replies are 1ms after each other
ts: 50,
});
room1.addLiveEvents([threadReply], { addToState: true });
expect(algorithm.sortRooms([room1, room2], DefaultTagID.Untagged)).toEqual([room1, room2]);
});
});
});
@@ -1,363 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { ConditionKind, MatrixEvent, PushRuleActionName, Room, RoomEvent } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { RoomNotificationStateStore } from "../../../../../../src/stores/notifications/RoomNotificationStateStore";
import { ImportanceAlgorithm } from "../../../../../../src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm";
import { SortAlgorithm } from "../../../../../../src/stores/room-list/algorithms/models";
import * as RoomNotifs from "../../../../../../src/RoomNotifs";
import { DefaultTagID } from "../../../../../../src/stores/room-list-v3/skip-list/tag";
import { RoomUpdateCause } from "../../../../../../src/stores/room-list/models";
import { NotificationLevel } from "../../../../../../src/stores/notifications/NotificationLevel";
import { AlphabeticAlgorithm } from "../../../../../../src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../../test-utils";
import { RecentAlgorithm } from "../../../../../../src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import { DEFAULT_PUSH_RULES, makePushRule } from "../../../../../test-utils/pushRules";
describe("ImportanceAlgorithm", () => {
const userId = "@alice:server.org";
const tagId = DefaultTagID.Favourite;
const makeRoom = (id: string, name: string, order?: number): Room => {
const room = new Room(id, client, userId);
room.name = name;
const tagEvent = new MatrixEvent({
type: "m.tag",
content: {
tags: {
[tagId]: {
order,
},
},
},
});
room.addTags(tagEvent);
return room;
};
const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
});
const roomA = makeRoom("!aaa:server.org", "Alpha", 2);
const roomB = makeRoom("!bbb:server.org", "Bravo", 5);
const roomC = makeRoom("!ccc:server.org", "Charlie", 1);
const roomD = makeRoom("!ddd:server.org", "Delta", 4);
const roomE = makeRoom("!eee:server.org", "Echo", 3);
const roomX = makeRoom("!xxx:server.org", "Xylophone", 99);
const muteRoomARule = makePushRule(roomA.roomId, {
actions: [PushRuleActionName.DontNotify],
conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: roomA.roomId }],
});
const muteRoomBRule = makePushRule(roomB.roomId, {
actions: [PushRuleActionName.DontNotify],
conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: roomB.roomId }],
});
client.pushRules = {
global: {
...DEFAULT_PUSH_RULES.global,
override: [...DEFAULT_PUSH_RULES.global.override!, muteRoomARule, muteRoomBRule],
},
};
const unreadStates: Record<string, ReturnType<(typeof RoomNotifs)["determineUnreadState"]>> = {
red: { symbol: null, count: 1, level: NotificationLevel.Highlight, invited: false },
grey: { symbol: null, count: 1, level: NotificationLevel.Notification, invited: false },
none: { symbol: null, count: 0, level: NotificationLevel.None, invited: false },
};
beforeEach(() => {
jest.spyOn(RoomNotifs, "determineUnreadState").mockReturnValue({
symbol: null,
count: 0,
level: NotificationLevel.None,
invited: false,
});
});
const setupAlgorithm = (sortAlgorithm: SortAlgorithm, rooms?: Room[]) => {
const algorithm = new ImportanceAlgorithm(tagId, sortAlgorithm);
algorithm.setRooms(rooms || [roomA, roomB, roomC]);
return algorithm;
};
describe("When sortAlgorithm is alphabetical", () => {
const sortAlgorithm = SortAlgorithm.Alphabetic;
beforeEach(async () => {
// destroy roomMap so we can start fresh
// @ts-ignore private property
RoomNotificationStateStore.instance.roomMap = new Map<Room, RoomNotificationState>();
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
jest.spyOn(RoomNotifs, "determineUnreadState")
.mockClear()
.mockImplementation((room) => {
switch (room) {
// b and e have red notifs
case roomB:
case roomE:
return unreadStates.red;
// c is grey
case roomC:
return unreadStates.grey;
default:
return unreadStates.none;
}
});
});
it("orders rooms by alpha when they have the same notif state", () => {
jest.spyOn(RoomNotifs, "determineUnreadState").mockReturnValue({
symbol: null,
count: 0,
level: NotificationLevel.None,
invited: false,
});
const algorithm = setupAlgorithm(sortAlgorithm);
// sorted according to alpha
expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC]);
});
it("orders rooms by notification state then alpha", () => {
const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]);
expect(algorithm.orderedRooms).toEqual([
// alpha within red
roomB,
roomE,
// grey
roomC,
// alpha within none
roomA,
roomD,
]);
});
describe("handleRoomUpdate", () => {
it("removes a room", () => {
const algorithm = setupAlgorithm(sortAlgorithm);
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.RoomRemoved);
expect(shouldTriggerUpdate).toBe(true);
expect(algorithm.orderedRooms).toEqual([roomB, roomC]);
// no re-sorting on a remove
expect(AlphabeticAlgorithm.prototype.sortRooms).not.toHaveBeenCalled();
});
it("warns and returns without change when removing a room that is not indexed", () => {
jest.spyOn(logger, "warn").mockReturnValue(undefined);
const algorithm = setupAlgorithm(sortAlgorithm);
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.RoomRemoved);
expect(shouldTriggerUpdate).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(`Tried to remove unknown room from ${tagId}: ${roomD.roomId}`);
});
it("adds a new room", () => {
const algorithm = setupAlgorithm(sortAlgorithm);
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.NewRoom);
expect(shouldTriggerUpdate).toBe(true);
// inserted according to notif state
expect(algorithm.orderedRooms).toEqual([roomB, roomE, roomC, roomA]);
// only sorted within category
expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomB, roomE], tagId);
});
it("throws for an unhandled update cause", () => {
const algorithm = setupAlgorithm(sortAlgorithm);
expect(() =>
algorithm.handleRoomUpdate(roomA, "something unexpected" as unknown as RoomUpdateCause),
).toThrow("Unsupported update cause: something unexpected");
});
it("ignores a mute change", () => {
// muted rooms are not pushed to the bottom when sort is alpha
const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]);
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.PossibleMuteChange);
expect(shouldTriggerUpdate).toBe(false);
// no sorting
expect(AlphabeticAlgorithm.prototype.sortRooms).not.toHaveBeenCalled();
});
describe("time and read receipt updates", () => {
it("throws for when a room is not indexed", () => {
const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]);
expect(() => algorithm.handleRoomUpdate(roomX, RoomUpdateCause.Timeline)).toThrow(
`Room ${roomX.roomId} has no index in ${tagId}`,
);
});
it("re-sorts category when updated room has not changed category", () => {
const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]);
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.Timeline);
expect(shouldTriggerUpdate).toBe(true);
expect(algorithm.orderedRooms).toEqual([roomB, roomE, roomC, roomA, roomD]);
// only sorted within category
expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledTimes(1);
expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomB, roomE], tagId);
});
it("re-sorts category when updated room has changed category", () => {
const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]);
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
// change roomE to unreadState.none
jest.spyOn(RoomNotifs, "determineUnreadState").mockImplementation((room) => {
switch (room) {
// b and e have red notifs
case roomB:
return unreadStates.red;
// c is grey
case roomC:
return unreadStates.grey;
case roomE:
default:
return unreadStates.none;
}
});
// @ts-ignore don't bother mocking rest of emit properties
roomE.emit(RoomEvent.Timeline, new MatrixEvent({ type: "whatever", room_id: roomE.roomId }));
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.Timeline);
expect(shouldTriggerUpdate).toBe(true);
expect(algorithm.orderedRooms).toEqual([roomB, roomC, roomA, roomD, roomE]);
// only sorted within roomE's new category
expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledTimes(1);
expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomA, roomD, roomE], tagId);
});
});
});
});
describe("When sortAlgorithm is recent", () => {
const sortAlgorithm = SortAlgorithm.Recent;
// mock recent algorithm sorting
const fakeRecentOrder = [roomC, roomB, roomE, roomD, roomA];
beforeEach(async () => {
// destroy roomMap so we can start fresh
// @ts-ignore private property
RoomNotificationStateStore.instance.roomMap = new Map<Room, RoomNotificationState>();
jest.spyOn(RecentAlgorithm.prototype, "sortRooms")
.mockClear()
.mockImplementation((rooms: Room[]) =>
fakeRecentOrder.filter((sortedRoom) => rooms.includes(sortedRoom)),
);
jest.spyOn(RoomNotifs, "determineUnreadState")
.mockClear()
.mockImplementation((room) => {
switch (room) {
// b, c and e have red notifs
case roomB:
case roomE:
case roomC:
return unreadStates.red;
default:
return unreadStates.none;
}
});
});
it("orders rooms by recent when they have the same notif state", () => {
jest.spyOn(RoomNotifs, "determineUnreadState").mockReturnValue({
symbol: null,
count: 0,
level: NotificationLevel.None,
invited: false,
});
const algorithm = setupAlgorithm(sortAlgorithm);
// sorted according to recent
expect(algorithm.orderedRooms).toEqual([roomC, roomB, roomA]);
});
it("orders rooms by notification state then recent", () => {
const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]);
expect(algorithm.orderedRooms).toEqual([
// recent within red
roomC,
roomE,
// recent within none
roomD,
// muted
roomB,
roomA,
]);
});
describe("handleRoomUpdate", () => {
it("removes a room", () => {
const algorithm = setupAlgorithm(sortAlgorithm);
jest.spyOn(RecentAlgorithm.prototype, "sortRooms").mockClear();
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.RoomRemoved);
expect(shouldTriggerUpdate).toBe(true);
expect(algorithm.orderedRooms).toEqual([roomC, roomB]);
// no re-sorting on a remove
expect(RecentAlgorithm.prototype.sortRooms).not.toHaveBeenCalled();
});
it("warns and returns without change when removing a room that is not indexed", () => {
jest.spyOn(logger, "warn").mockReturnValue(undefined);
const algorithm = setupAlgorithm(sortAlgorithm);
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.RoomRemoved);
expect(shouldTriggerUpdate).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(`Tried to remove unknown room from ${tagId}: ${roomD.roomId}`);
});
it("adds a new room", () => {
const algorithm = setupAlgorithm(sortAlgorithm);
jest.spyOn(RecentAlgorithm.prototype, "sortRooms").mockClear();
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.NewRoom);
expect(shouldTriggerUpdate).toBe(true);
// inserted according to notif state and mute
expect(algorithm.orderedRooms).toEqual([roomC, roomE, roomB, roomA]);
// only sorted within category
expect(RecentAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomE, roomC], tagId);
});
it("re-sorts on a mute change", () => {
const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]);
jest.spyOn(RecentAlgorithm.prototype, "sortRooms").mockClear();
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.PossibleMuteChange);
expect(shouldTriggerUpdate).toBe(true);
expect(RecentAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomC, roomE], tagId);
});
});
});
});
@@ -1,282 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { ConditionKind, EventType, MatrixEvent, PushRuleActionName, Room, ClientEvent } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { NaturalAlgorithm } from "../../../../../../src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm";
import { SortAlgorithm } from "../../../../../../src/stores/room-list/algorithms/models";
import { DefaultTagID } from "../../../../../../src/stores/room-list-v3/skip-list/tag";
import { RoomUpdateCause } from "../../../../../../src/stores/room-list/models";
import { AlphabeticAlgorithm } from "../../../../../../src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm";
import { RecentAlgorithm } from "../../../../../../src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import { RoomNotificationStateStore } from "../../../../../../src/stores/notifications/RoomNotificationStateStore";
import * as RoomNotifs from "../../../../../../src/RoomNotifs";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../../test-utils";
import { DEFAULT_PUSH_RULES, makePushRule } from "../../../../../test-utils/pushRules";
import { NotificationLevel } from "../../../../../../src/stores/notifications/NotificationLevel";
describe("NaturalAlgorithm", () => {
const userId = "@alice:server.org";
const tagId = DefaultTagID.Favourite;
const makeRoom = (id: string, name: string): Room => {
const room = new Room(id, client, userId);
room.name = name;
return room;
};
const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
});
const roomA = makeRoom("!aaa:server.org", "Alpha");
const roomB = makeRoom("!bbb:server.org", "Bravo");
const roomC = makeRoom("!ccc:server.org", "Charlie");
const roomD = makeRoom("!ddd:server.org", "Delta");
const roomE = makeRoom("!eee:server.org", "Echo");
const roomX = makeRoom("!xxx:server.org", "Xylophone");
const muteRoomARule = makePushRule(roomA.roomId, {
actions: [PushRuleActionName.DontNotify],
conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: roomA.roomId }],
});
const muteRoomDRule = makePushRule(roomD.roomId, {
actions: [PushRuleActionName.DontNotify],
conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: roomD.roomId }],
});
client.pushRules = {
global: {
...DEFAULT_PUSH_RULES.global,
override: [...DEFAULT_PUSH_RULES.global!.override!, muteRoomARule, muteRoomDRule],
},
};
const setupAlgorithm = (sortAlgorithm: SortAlgorithm, rooms?: Room[]) => {
const algorithm = new NaturalAlgorithm(tagId, sortAlgorithm);
algorithm.setRooms(rooms || [roomA, roomB, roomC]);
return algorithm;
};
describe("When sortAlgorithm is alphabetical", () => {
const sortAlgorithm = SortAlgorithm.Alphabetic;
beforeEach(async () => {
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
});
it("orders rooms by alpha", () => {
const algorithm = setupAlgorithm(sortAlgorithm);
// sorted according to alpha
expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC]);
});
describe("handleRoomUpdate", () => {
it("removes a room", () => {
const algorithm = setupAlgorithm(sortAlgorithm);
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.RoomRemoved);
expect(shouldTriggerUpdate).toBe(true);
expect(algorithm.orderedRooms).toEqual([roomB, roomC]);
});
it("warns when removing a room that is not indexed", () => {
jest.spyOn(logger, "warn").mockReturnValue(undefined);
const algorithm = setupAlgorithm(sortAlgorithm);
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.RoomRemoved);
expect(shouldTriggerUpdate).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(`Tried to remove unknown room from ${tagId}: ${roomD.roomId}`);
});
it("adds a new room", () => {
const algorithm = setupAlgorithm(sortAlgorithm);
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.NewRoom);
expect(shouldTriggerUpdate).toBe(true);
expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC, roomE]);
// only sorted within category
expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith(
[roomA, roomB, roomC, roomE],
tagId,
);
});
it("adds a new muted room", () => {
const algorithm = setupAlgorithm(sortAlgorithm, [roomA, roomB, roomE]);
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.NewRoom);
expect(shouldTriggerUpdate).toBe(true);
// muted room mixed in main category
expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomD, roomE]);
// only sorted within category
expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledTimes(1);
});
it("ignores a mute change update", () => {
const algorithm = setupAlgorithm(sortAlgorithm);
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.PossibleMuteChange);
expect(shouldTriggerUpdate).toBe(false);
expect(AlphabeticAlgorithm.prototype.sortRooms).not.toHaveBeenCalled();
});
it("throws for an unhandled update cause", () => {
const algorithm = setupAlgorithm(sortAlgorithm);
expect(() =>
algorithm.handleRoomUpdate(roomA, "something unexpected" as unknown as RoomUpdateCause),
).toThrow("Unsupported update cause: something unexpected");
});
describe("time and read receipt updates", () => {
it("handles when a room is not indexed", () => {
const algorithm = setupAlgorithm(sortAlgorithm);
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomX, RoomUpdateCause.Timeline);
// for better or worse natural alg sets this to true
expect(shouldTriggerUpdate).toBe(true);
expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC]);
});
it("re-sorts rooms when timeline updates", () => {
const algorithm = setupAlgorithm(sortAlgorithm);
jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear();
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.Timeline);
expect(shouldTriggerUpdate).toBe(true);
expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC]);
// only sorted within category
expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledTimes(1);
expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomA, roomB, roomC], tagId);
});
});
});
});
describe("When sortAlgorithm is recent", () => {
const sortAlgorithm = SortAlgorithm.Recent;
// mock recent algorithm sorting
const fakeRecentOrder = [roomC, roomA, roomB, roomD, roomE];
beforeEach(async () => {
// destroy roomMap so we can start fresh
// @ts-ignore private property
RoomNotificationStateStore.instance.roomMap = new Map<Room, RoomNotificationState>();
jest.spyOn(RecentAlgorithm.prototype, "sortRooms")
.mockClear()
.mockImplementation((rooms: Room[]) =>
fakeRecentOrder.filter((sortedRoom) => rooms.includes(sortedRoom)),
);
jest.spyOn(RoomNotifs, "determineUnreadState").mockReturnValue({
symbol: null,
count: 0,
level: NotificationLevel.None,
invited: false,
});
});
it("orders rooms by recent with muted rooms to the bottom", () => {
const algorithm = setupAlgorithm(sortAlgorithm);
// sorted according to recent
expect(algorithm.orderedRooms).toEqual([roomC, roomB, roomA]);
});
describe("handleRoomUpdate", () => {
it("removes a room", () => {
const algorithm = setupAlgorithm(sortAlgorithm);
jest.spyOn(RecentAlgorithm.prototype, "sortRooms").mockClear();
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.RoomRemoved);
expect(shouldTriggerUpdate).toBe(true);
expect(algorithm.orderedRooms).toEqual([roomC, roomB]);
// no re-sorting on a remove
expect(RecentAlgorithm.prototype.sortRooms).not.toHaveBeenCalled();
});
it("warns and returns without change when removing a room that is not indexed", () => {
jest.spyOn(logger, "warn").mockReturnValue(undefined);
const algorithm = setupAlgorithm(sortAlgorithm);
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.RoomRemoved);
expect(shouldTriggerUpdate).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(`Tried to remove unknown room from ${tagId}: ${roomD.roomId}`);
});
it("adds a new room", () => {
const algorithm = setupAlgorithm(sortAlgorithm);
jest.spyOn(RecentAlgorithm.prototype, "sortRooms").mockClear();
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.NewRoom);
expect(shouldTriggerUpdate).toBe(true);
// inserted according to mute then recentness
expect(algorithm.orderedRooms).toEqual([roomC, roomB, roomE, roomA]);
// only sorted within category, muted roomA is not resorted
expect(RecentAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomC, roomB, roomE], tagId);
});
it("does not re-sort on possible mute change when room did not change effective mutedness", () => {
const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]);
jest.spyOn(RecentAlgorithm.prototype, "sortRooms").mockClear();
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.PossibleMuteChange);
expect(shouldTriggerUpdate).toBe(false);
expect(RecentAlgorithm.prototype.sortRooms).not.toHaveBeenCalled();
});
it("re-sorts on a mute change", () => {
const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]);
jest.spyOn(RecentAlgorithm.prototype, "sortRooms").mockClear();
// mute roomE
const muteRoomERule = makePushRule(roomE.roomId, {
actions: [PushRuleActionName.DontNotify],
conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: roomE.roomId }],
});
const pushRulesEvent = new MatrixEvent({ type: EventType.PushRules });
client.pushRules!.global!.override!.push(muteRoomERule);
client.emit(ClientEvent.AccountData, pushRulesEvent);
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.PossibleMuteChange);
expect(shouldTriggerUpdate).toBe(true);
expect(algorithm.orderedRooms).toEqual([
// unmuted, sorted by recent
roomC,
roomB,
// muted, sorted by recent
roomA,
roomD,
roomE,
]);
// only sorted muted category
expect(RecentAlgorithm.prototype.sortRooms).toHaveBeenCalledTimes(1);
expect(RecentAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomA, roomD, roomE], tagId);
});
});
});
});
@@ -1,198 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { mocked } from "jest-mock";
import { type Room } from "matrix-js-sdk/src/matrix";
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, type SpaceKey } 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-require-imports
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: Record<string, any> = {}) =>
(settingName: string, space: SpaceKey) =>
settings[settingName]?.[space] || false;
beforeEach(() => {
jest.resetAllMocks();
SettingsStoreMock.getValue.mockClear().mockImplementation(makeMockGetValue());
SpaceStoreInstanceMock.getSpaceFilteredUserIds.mockReturnValue(new Set([]));
SpaceStoreInstanceMock.isRoomInSpace.mockReturnValue(true);
});
const initFilter = (space: SpaceKey): 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);
});
});
});
});