1
0
mirror of https://github.com/element-hq/element-web.git synced 2025-09-10 21:51:57 +03:00
Files
element-web/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts
David Langley c842b615db Move the room list to the new ListView(backed by react-virtuoso) (#30515)
* Move Room List to ListView

- Also remove Space/Enter handing from keyboard navigation we can just leave the default behaviour of those keys and handle via onClick

* Update rooms when the primary filter changes

Otherwise when changing spaces, the filter does not reset until the next update to the RVS is made.

* Fix stickyRow/scrollIntoView when switiching space or changing filters

- Also remove the rest of space/enter keyboard handling use

* Remove the rest of space/enter keyboard handling use

* Remove useCombinedRef and add @radix-ui/react-compose-refs as we already depend on it

- Also remove eact-virtualized dep

* Update RoomList unit test

* Update snapshots and unit tests

* Fix e2e tests

* Remove react-virtualized from tests

* Fix e2e flake

* Update more screenshots

* Fix e2e test case where were should scroll to the top when the active room is no longer in the list

* Move from gitpkg to package-patch

* Update to latest react virtuoso release/api.

Also pass spaceId to the room list and scroll the activeIndex into view when spaceId or primaryFilter change.

* Use listbox/option roles to improve ScreenReader experience

* Change onKeyDown e.stopPropogation to cover context menu

* lint

* Remove unneeded exposure of the listView ref

Also move scrollIntoViewOnChange to useCallback

* Update unit test and snapshot

* Fix e2e tests and update screenshots

* Fix unit test and snapshot

* Update more unit tests

* Fix keyboard shortcuts and e2e test

* Fix another e2e and unit test

* lint

* Improve the naming for RoomResult and the documentation on it's fields meaning.

Also update the login in RoomList to check for any change in filters, this is a bit more future proof for when we introduce multi select than using activePrimaryFilter.

* Put back and fix landmark tests

* Fix test import

* Add comment regarding context object getting rendered.

* onKeyDown should be optional

* Use SpaceKey type on RoomResult

* lint
2025-08-21 14:43:40 +00:00

854 lines
36 KiB
TypeScript

/*
Copyright 2025 New Vector Ltd.
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 { EventType, KnownMembership, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
import type { RoomNotificationState } from "../../../../src/stores/notifications/RoomNotificationState";
import { LISTS_UPDATE_EVENT, RoomListStoreV3Class } from "../../../../src/stores/room-list-v3/RoomListStoreV3";
import { AsyncStoreWithClient } from "../../../../src/stores/AsyncStoreWithClient";
import { RecencySorter } from "../../../../src/stores/room-list-v3/skip-list/sorters/RecencySorter";
import { mkEvent, mkMessage, mkSpace, mkStubRoom, stubClient, upsertRoomStateEvents } from "../../../test-utils";
import { getMockedRooms } from "./skip-list/getMockedRooms";
import { AlphabeticSorter } from "../../../../src/stores/room-list-v3/skip-list/sorters/AlphabeticSorter";
import dispatcher from "../../../../src/dispatcher/dispatcher";
import SpaceStore from "../../../../src/stores/spaces/SpaceStore";
import { MetaSpace, UPDATE_SELECTED_SPACE } from "../../../../src/stores/spaces";
import { DefaultTagID } from "../../../../src/stores/room-list/models";
import { FilterKey } from "../../../../src/stores/room-list-v3/skip-list/filters";
import { RoomNotificationStateStore } from "../../../../src/stores/notifications/RoomNotificationStateStore";
import DMRoomMap from "../../../../src/utils/DMRoomMap";
import { SortingAlgorithm } from "../../../../src/stores/room-list-v3/skip-list/sorters";
import SettingsStore from "../../../../src/settings/SettingsStore";
import * as utils from "../../../../src/utils/notifications";
import * as roomMute from "../../../../src/stores/room-list/utils/roomMute";
import { Action } from "../../../../src/dispatcher/actions";
describe("RoomListStoreV3", () => {
async function getRoomListStore() {
const client = stubClient();
const rooms = getMockedRooms(client);
client.getVisibleRooms = jest.fn().mockReturnValue(rooms);
jest.spyOn(AsyncStoreWithClient.prototype, "matrixClient", "get").mockReturnValue(client);
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
return { client, rooms, store, dispatcher };
}
beforeEach(() => {
jest.spyOn(SpaceStore.instance, "isRoomInSpace").mockImplementation((space) => space === MetaSpace.Home);
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => MetaSpace.Home);
jest.spyOn(SpaceStore.instance, "storeReadyPromise", "get").mockImplementation(() => Promise.resolve());
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation((room) => {
const state = {
isUnread: false,
} as unknown as RoomNotificationState;
return state;
});
jest.spyOn(DMRoomMap, "shared").mockImplementation((() => {
return {
getUserIdForRoomId: (id) => "",
};
}) as () => DMRoomMap);
});
afterEach(() => {
jest.restoreAllMocks();
});
it("Provides an unsorted list of rooms", async () => {
const { store, rooms } = await getRoomListStore();
expect(store.getRooms()).toEqual(rooms);
});
it("Provides a sorted list of rooms", async () => {
const { store, rooms, client } = await getRoomListStore();
const sorter = new RecencySorter(client.getSafeUserId());
const sortedRooms = sorter.sort(rooms);
expect(store.getSortedRooms()).toEqual(sortedRooms);
});
it("Provides a way to resort", async () => {
const { store, rooms, client } = await getRoomListStore();
// List is sorted by recency, sort by alphabetical now
store.resort(SortingAlgorithm.Alphabetic);
let sortedRooms = new AlphabeticSorter().sort(rooms);
expect(store.getSortedRooms()).toEqual(sortedRooms);
expect(store.activeSortAlgorithm).toEqual(SortingAlgorithm.Alphabetic);
// Go back to recency sorting
store.resort(SortingAlgorithm.Recency);
sortedRooms = new RecencySorter(client.getSafeUserId()).sort(rooms);
expect(store.getSortedRooms()).toEqual(sortedRooms);
expect(store.activeSortAlgorithm).toEqual(SortingAlgorithm.Recency);
});
it("Uses preferred sorter on startup", async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(() => {
return SortingAlgorithm.Alphabetic;
});
const { store } = await getRoomListStore();
expect(store.activeSortAlgorithm).toEqual(SortingAlgorithm.Alphabetic);
});
describe("Updates", () => {
it("Room is re-inserted on timeline event", async () => {
const { store, rooms, dispatcher } = await getRoomListStore();
// Let's pretend like a new timeline event came on the room in 37th index.
const room = rooms[37];
const event = mkMessage({ room: room.roomId, user: `@foo${3}:matrix.org`, ts: 1000, event: true });
room.timeline.push(event);
const payload = {
action: "MatrixActions.Room.timeline",
event,
isLiveEvent: true,
isLiveUnfilteredRoomTimelineEvent: true,
room,
};
const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
dispatcher.dispatch(payload, true);
expect(fn).toHaveBeenCalled();
expect(store.getSortedRooms()[0].roomId).toEqual(room.roomId);
});
it("Forgotten room is removed", async () => {
const { store, rooms, dispatcher } = await getRoomListStore();
const room = rooms[37];
// Room at index 37 should be in the store now
expect(store.getSortedRooms().map((r) => r.roomId)).toContain(room.roomId);
// Forget room at index 37
const payload = {
action: Action.AfterForgetRoom,
room: room,
};
const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
dispatcher.dispatch(payload, true);
// Room at index 37 should no longer be in the store
expect(fn).toHaveBeenCalled();
expect(store.getSortedRooms().map((r) => r.roomId)).not.toContain(room.roomId);
});
it.each([KnownMembership.Join, KnownMembership.Invite])(
"Room is removed when membership changes to leave",
async (membership) => {
const { store, rooms, dispatcher } = await getRoomListStore();
// Let's say the user leaves room at index 37
const room = rooms[37];
const payload = {
action: "MatrixActions.Room.myMembership",
oldMembership: membership,
membership: KnownMembership.Leave,
room,
};
const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
dispatcher.dispatch(payload, true);
expect(fn).toHaveBeenCalled();
expect(store.getSortedRooms()).not.toContain(room);
},
);
it("Room is not removed when user is kicked", async () => {
const { store, rooms, dispatcher, client } = await getRoomListStore();
// Let's say the user gets kicked out of room at index 37
const room = rooms[37];
const mockMember = room.getMember(client.getSafeUserId())!;
mockMember.isKicked = () => true;
room.getMember = () => mockMember;
const payload = {
action: "MatrixActions.Room.myMembership",
oldMembership: KnownMembership.Join,
membership: KnownMembership.Leave,
room,
};
const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
dispatcher.dispatch(payload, true);
expect(fn).toHaveBeenCalled();
expect(store.getSortedRooms()).toContain(room);
});
it("Predecessor room is removed on room upgrade", async () => {
const { store, rooms, client, dispatcher } = await getRoomListStore();
// Let's say that !foo32:matrix.org is being upgraded
const oldRoom = rooms[32];
// Create a new room with a predecessor event that points to oldRoom
const newRoom = new Room("!foonew:matrix.org", client, client.getSafeUserId(), {});
const createWithPredecessor = new MatrixEvent({
type: EventType.RoomCreate,
sender: "@foo:foo.org",
room_id: newRoom.roomId,
content: {
predecessor: { room_id: oldRoom.roomId, event_id: "tombstone_event_id" },
},
event_id: "$create",
state_key: "",
});
upsertRoomStateEvents(newRoom, [createWithPredecessor]);
const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
dispatcher.dispatch(
{
action: "MatrixActions.Room.myMembership",
oldMembership: KnownMembership.Invite,
membership: KnownMembership.Join,
room: newRoom,
},
true,
);
expect(fn).toHaveBeenCalled();
const roomIds = store.getSortedRooms().map((r) => r.roomId);
expect(roomIds).not.toContain(oldRoom.roomId);
expect(roomIds).toContain(newRoom.roomId);
});
it("Rooms are re-inserted on m.direct event", async () => {
const { store, dispatcher, client } = await getRoomListStore();
// Let's mock the client to return new rooms with the name "My DM Room"
client.getRoom = (roomId: string) => mkStubRoom(roomId, "My DM Room", client);
// Let's create a m.direct event that we can dispatch
const content = {
"@bar1:matrix.org": ["!foo1:matrix.org", "!foo2:matrix.org"],
"@bar2:matrix.org": ["!foo3:matrix.org", "!foo4:matrix.org"],
"@bar3:matrix.org": ["!foo5:matrix.org"],
};
const event = mkEvent({
event: true,
content,
user: "@foo:matrix.org",
type: EventType.Direct,
});
const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
// Do the actual dispatch
dispatcher.dispatch(
{
action: "MatrixActions.accountData",
event_type: EventType.Direct,
event,
},
true,
);
// Ensure only one emit occurs
expect(fn).toHaveBeenCalledTimes(1);
/*
When the dispatched event is processed by the room-list, the associated
rooms will be fetched via client.getRoom and will be re-inserted into the
skip list. We can confirm that this happened by checking if all the dm rooms
have the same name ("My DM Room") since we've mocked the client to return such rooms.
*/
const ids = [
"!foo1:matrix.org",
"!foo2:matrix.org",
"!foo3:matrix.org",
"!foo4:matrix.org",
"!foo5:matrix.org",
];
const rooms = store.getSortedRooms().filter((r) => ids.includes(r.roomId));
rooms.forEach((room) => expect(room.name).toBe("My DM Room"));
});
it("Room is re-inserted on tag change", async () => {
const { store, rooms, dispatcher } = await getRoomListStore();
const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
dispatcher.dispatch(
{
action: "MatrixActions.Room.tags",
room: rooms[10],
},
true,
);
expect(fn).toHaveBeenCalled();
});
it("Room is re-inserted on decryption", async () => {
const { store, rooms, client, dispatcher } = await getRoomListStore();
jest.spyOn(client, "getRoom").mockImplementation(() => rooms[10]);
const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
dispatcher.dispatch(
{
action: "MatrixActions.Event.decrypted",
event: { getRoomId: () => rooms[10].roomId },
},
true,
);
expect(fn).toHaveBeenCalled();
});
it("Logs a warning if room couldn't be found from room-id on decryption action", async () => {
const { store, client, dispatcher } = await getRoomListStore();
jest.spyOn(client, "getRoom").mockImplementation(() => null);
const warnSpy = jest.spyOn(logger, "warn");
const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
// Dispatch a decrypted action but the room does not exist.
dispatcher.dispatch(
{
action: "MatrixActions.Event.decrypted",
event: {
getRoomId: () => "!doesnotexist:matrix.org",
getId: () => "some-id",
},
},
true,
);
expect(warnSpy).toHaveBeenCalled();
expect(fn).not.toHaveBeenCalled();
});
describe("Update from read receipt", () => {
function getReadReceiptEvent(userId: string) {
const content = {
some_id: {
"m.read": {
[userId]: {
ts: 5000,
},
},
},
};
const event = mkEvent({
event: true,
content,
user: "@foo:matrix.org",
type: EventType.Receipt,
});
return event;
}
it("Room is re-inserted on read receipt from our user", async () => {
const { store, rooms, client, dispatcher } = await getRoomListStore();
const event = getReadReceiptEvent(client.getSafeUserId());
const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
dispatcher.dispatch(
{
action: "MatrixActions.Room.receipt",
room: rooms[10],
event,
},
true,
);
expect(fn).toHaveBeenCalled();
});
it("Read receipt from other users do not cause room to be re-inserted", async () => {
const { store, rooms, dispatcher } = await getRoomListStore();
const event = getReadReceiptEvent("@foobar:matrix.org");
const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
dispatcher.dispatch(
{
action: "MatrixActions.Room.receipt",
room: rooms[10],
event,
},
true,
);
expect(fn).not.toHaveBeenCalled();
});
});
/**
* Create a space and add it to rooms
* @param rooms An array of rooms to which the new space is added.
* @param inSpaceIndices A list of indices from which rooms are added to the space.
*/
function createSpace(rooms: Room[], inSpaceIndices: number[], client: MatrixClient) {
const roomIds = inSpaceIndices.map((i) => rooms[i].roomId);
const spaceRoom = mkSpace(client, "!space1:matrix.org", [], roomIds);
rooms.push(spaceRoom);
return { spaceRoom, roomIds };
}
function setupMocks(spaceRoom: Room, roomIds: string[]) {
jest.spyOn(SpaceStore.instance, "isRoomInSpace").mockImplementation((space, id) => {
if (space === MetaSpace.Home && !roomIds.includes(id)) return true;
if (space === spaceRoom.roomId && roomIds.includes(id)) return true;
return false;
});
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => spaceRoom.roomId);
}
function getClientAndRooms() {
const client = stubClient();
const rooms = getMockedRooms(client);
client.getVisibleRooms = jest.fn().mockReturnValue(rooms);
jest.spyOn(AsyncStoreWithClient.prototype, "matrixClient", "get").mockReturnValue(client);
return { client, rooms };
}
describe("Spaces", () => {
it("Newly created space is not added by the store", async () => {
const { client, rooms } = getClientAndRooms();
const infoSpy = jest.spyOn(logger, "info");
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
// Create a space and let the store know about it
const { spaceRoom } = createSpace(rooms, [6, 8, 13, 27, 75], client);
dispatcher.dispatch(
{
action: "MatrixActions.Room.myMembership",
oldMembership: KnownMembership.Leave,
membership: KnownMembership.Invite,
room: spaceRoom,
},
true,
);
// Space room should not be added
expect(store.getSortedRooms()).not.toContain(spaceRoom);
expect(infoSpy).toHaveBeenCalledWith(
expect.stringContaining("RoomListStoreV3: Refusing to add new room"),
);
});
it("Filtering by spaces work", async () => {
const { client, rooms } = getClientAndRooms();
// Let's choose 5 rooms to put in space
const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 13, 27, 75], client);
// Mock the space store
jest.spyOn(SpaceStore.instance, "isRoomInSpace").mockImplementation((space, id) => {
if (space === MetaSpace.Home && !roomIds.includes(id)) return true;
if (space === spaceRoom.roomId && roomIds.includes(id)) return true;
return false;
});
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
// The rooms which belong to the space should not be shown
const result = store.getSortedRoomsInActiveSpace().rooms.map((r) => r.roomId);
for (const id of roomIds) {
expect(result).not.toContain(id);
}
// Lets switch to the space
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => spaceRoom.roomId);
SpaceStore.instance.emit(UPDATE_SELECTED_SPACE);
expect(fn).toHaveBeenCalled();
const result2 = store.getSortedRoomsInActiveSpace().rooms.map((r) => r.roomId);
for (const id of roomIds) {
expect(result2).toContain(id);
}
});
});
describe("Filters", () => {
it("filters by both space and favourite", async () => {
const { client, rooms } = getClientAndRooms();
// Let's choose 5 rooms to put in space
const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 13, 27, 75], client);
// Let's say that 8, 27 an 75 are favourite rooms
[8, 27, 75].forEach((i) => {
rooms[i].tags[DefaultTagID.Favourite] = {};
});
setupMocks(spaceRoom, roomIds);
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
// Sorted, filtered rooms should be 8, 27 and 75
const result = store.getSortedRoomsInActiveSpace([FilterKey.FavouriteFilter]).rooms;
expect(result).toHaveLength(3);
for (const i of [8, 27, 75]) {
expect(result).toContain(rooms[i]);
}
});
it("filters are recalculated on room update", async () => {
const { client, rooms } = getClientAndRooms();
// Let's choose 5 rooms to put in space
const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 13, 27, 75], client);
// Let's say that 8, 27 an 75 are favourite rooms
[8, 27, 75].forEach((i) => {
rooms[i].tags[DefaultTagID.Favourite] = {};
});
setupMocks(spaceRoom, roomIds);
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
// Let's say 27 got unfavourited
const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
rooms[27].tags = {};
dispatcher.dispatch(
{
action: "MatrixActions.Room.tags",
room: rooms[27],
},
true,
);
expect(fn).toHaveBeenCalled();
// Sorted, filtered rooms should be 27 and 75
const result = store.getSortedRoomsInActiveSpace([FilterKey.FavouriteFilter]).rooms;
expect(result).toHaveLength(2);
for (const i of [8, 75]) {
expect(result).toContain(rooms[i]);
}
});
it("supports filtering unread rooms", async () => {
const { client, rooms } = getClientAndRooms();
// Let's choose 5 rooms to put in space
const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 13, 27, 75], client);
// Let's say 8, 27 are unread
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation((room) => {
const state = {
hasUnreadCount: [rooms[8], rooms[27]].includes(room),
} as unknown as RoomNotificationState;
return state;
});
setupMocks(spaceRoom, roomIds);
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
// Should only give us rooms at index 8 and 27
const result = store.getSortedRoomsInActiveSpace([FilterKey.UnreadFilter]).rooms;
expect(result).toHaveLength(2);
for (const i of [8, 27]) {
expect(result).toContain(rooms[i]);
}
});
it("unread filter matches rooms that are marked as unread", async () => {
const { client, rooms } = getClientAndRooms();
// Let's choose 5 rooms to put in space
const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 13, 27, 75], client);
setupMocks(spaceRoom, roomIds);
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
// Since there's no unread yet, we expect zero results
let result = store.getSortedRoomsInActiveSpace([FilterKey.UnreadFilter]).rooms;
expect(result).toHaveLength(0);
// Mock so that room at index 8 is marked as unread
jest.spyOn(utils, "getMarkedUnreadState").mockImplementation((room) => room.roomId === rooms[8].roomId);
dispatcher.dispatch(
{
action: "MatrixActions.Room.accountData",
room: rooms[8],
event_type: utils.MARKED_UNREAD_TYPE_STABLE,
},
true,
);
// Now we expect room at index 8 to show as unread
result = store.getSortedRoomsInActiveSpace([FilterKey.UnreadFilter]).rooms;
expect(result).toHaveLength(1);
expect(result).toContain(rooms[8]);
});
it("supports filtering by people and rooms", async () => {
const { client, rooms } = getClientAndRooms();
// Let's choose 5 rooms to put in space
const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 13, 27, 75], client);
// Let's say 8, 27 are dms
const ids = [8, 27].map((i) => rooms[i].roomId);
jest.spyOn(DMRoomMap, "shared").mockImplementation((() => {
return {
getUserIdForRoomId: (id) => (ids.includes(id) ? "@myuser:matrix.org" : ""),
};
}) as () => DMRoomMap);
setupMocks(spaceRoom, roomIds);
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
// Should only give us rooms at index 8 and 27
const peopleRooms = store.getSortedRoomsInActiveSpace([FilterKey.PeopleFilter]).rooms;
expect(peopleRooms).toHaveLength(2);
for (const i of [8, 27]) {
expect(peopleRooms).toContain(rooms[i]);
}
// Rest are normal rooms
const nonDms = store.getSortedRoomsInActiveSpace([FilterKey.RoomsFilter]).rooms;
expect(nonDms).toHaveLength(3);
for (const i of [6, 13, 75]) {
expect(nonDms).toContain(rooms[i]);
}
});
it("supports filtering invited rooms", async () => {
const { client, rooms } = getClientAndRooms();
// Let's add 5 rooms that we are invited to
const invitedRooms = getMockedRooms(client, 5);
for (const room of invitedRooms) {
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Invite);
}
rooms.push(...invitedRooms);
// Let's choose 5 rooms to put in space
const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 100, 101, 102, 103, 104], client);
setupMocks(spaceRoom, roomIds);
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
const result = store.getSortedRoomsInActiveSpace([FilterKey.InvitesFilter]).rooms;
expect(result).toHaveLength(5);
for (const room of invitedRooms) {
expect(result).toContain(room);
}
});
it("supports filtering by mentions", async () => {
const { client, rooms } = getClientAndRooms();
// Let's choose 5 rooms to put in space
const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 13, 27, 75], client);
// Let's say 8, 27 have mentions
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation((room) => {
const state = {
isMention: [rooms[8], rooms[27]].includes(room),
} as unknown as RoomNotificationState;
return state;
});
setupMocks(spaceRoom, roomIds);
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
// Should only give us rooms at index 8 and 27
const result = store.getSortedRoomsInActiveSpace([FilterKey.MentionsFilter]).rooms;
expect(result).toHaveLength(2);
for (const i of [8, 27]) {
expect(result).toContain(rooms[i]);
}
});
it("supports filtering low priority rooms", async () => {
const { client, rooms } = getClientAndRooms();
// Let's choose 5 rooms to put in space
const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 13, 27, 75], client);
// Let's say that 8, 27 an 75 are low priority rooms
[8, 27, 75].forEach((i) => {
rooms[i].tags[DefaultTagID.LowPriority] = {};
});
setupMocks(spaceRoom, roomIds);
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
// Sorted, filtered rooms should be 8, 27 and 75
const result = store.getSortedRoomsInActiveSpace([FilterKey.LowPriorityFilter]).rooms;
expect(result).toHaveLength(3);
for (const i of [8, 27, 75]) {
expect(result).toContain(rooms[i]);
}
});
it("supports multiple filters", async () => {
const { client, rooms } = getClientAndRooms();
// Let's choose 5 rooms to put in space
const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 13, 27, 75], client);
// Let's say that 8 is a favourite room
rooms[8].tags[DefaultTagID.Favourite] = {};
// Let's say 8, 27 are unread
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation((room) => {
const state = {
hasUnreadCount: [rooms[8], rooms[27]].includes(room),
} as unknown as RoomNotificationState;
return state;
});
setupMocks(spaceRoom, roomIds);
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
// Should give us only room at 8 since that's the only room which matches both filters
const result = store.getSortedRoomsInActiveSpace([
FilterKey.UnreadFilter,
FilterKey.FavouriteFilter,
]).rooms;
expect(result).toHaveLength(1);
expect(result).toContain(rooms[8]);
});
});
});
describe("Muted rooms", () => {
async function getRoomListStoreWithMutedRooms() {
const client = stubClient();
const rooms = getMockedRooms(client);
// Let's say that rooms 34, 84, 64, 14, 57 are muted
const mutedIndices = [34, 84, 64, 14, 57];
const mutedRooms = mutedIndices.map((i) => rooms[i]);
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation((room) => {
const state = {
muted: mutedRooms.includes(room),
} as unknown as RoomNotificationState;
return state;
});
client.getVisibleRooms = jest.fn().mockReturnValue(rooms);
jest.spyOn(AsyncStoreWithClient.prototype, "matrixClient", "get").mockReturnValue(client);
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
return { client, rooms, mutedIndices, mutedRooms, store, dispatcher };
}
it("Muted rooms are sorted to the bottom of the list", async () => {
const { store, mutedRooms, client } = await getRoomListStoreWithMutedRooms();
const lastFiveRooms = store.getSortedRooms().slice(95);
const expectedRooms = new RecencySorter(client.getSafeUserId()).sort(mutedRooms);
// We expect the muted rooms to be at the bottom sorted by recency
expect(lastFiveRooms).toEqual(expectedRooms);
});
it("Muted rooms are sorted within themselves", async () => {
const { store, rooms } = await getRoomListStoreWithMutedRooms();
// Let's say that rooms 14 and 34 get new messages in that order
let ts = 1000;
for (const room of [rooms[14], rooms[34]]) {
const event = mkMessage({ room: room.roomId, user: `@foo${3}:matrix.org`, ts: 1000, event: true });
room.timeline.push(event);
const payload = {
action: "MatrixActions.Room.timeline",
event,
isLiveEvent: true,
isLiveUnfilteredRoomTimelineEvent: true,
room,
};
dispatcher.dispatch(payload, true);
ts = ts + 1;
}
const lastFiveRooms = store.getSortedRooms().slice(95);
// The order previously would have been 84, 64, 57, 34, 14
// Expected new order is 34, 14, 84, 64, 57
const expectedRooms = [rooms[34], rooms[14], rooms[84], rooms[64], rooms[57]];
expect(lastFiveRooms).toEqual(expectedRooms);
});
it("Muted room is correctly sorted when unmuted", async () => {
const { store, mutedRooms, rooms, client } = await getRoomListStoreWithMutedRooms();
// Let's say that muted room 64 becomes un-muted.
const unmutedRoom = rooms[64];
jest.spyOn(roomMute, "getChangedOverrideRoomMutePushRules").mockImplementation(() => [unmutedRoom.roomId]);
client.getRoom = jest.fn().mockReturnValue(unmutedRoom);
const payload = {
action: "MatrixActions.accountData",
event_type: EventType.PushRules,
};
mutedRooms.splice(2, 1);
dispatcher.dispatch(payload, true);
const lastFiveRooms = store.getSortedRooms().slice(95);
// We expect room at index 64 to no longer be at the bottom
expect(lastFiveRooms).not.toContain(unmutedRoom);
// Room 64 should go to index 34 since we're sorting by recency
expect(store.getSortedRooms()[34]).toEqual(unmutedRoom);
});
});
describe("Low priority rooms", () => {
async function getRoomListStoreWithRooms() {
const client = stubClient();
const rooms = getMockedRooms(client);
// Let's say that rooms 34, 84, 64, 14, 57 are low priority
const lowPriorityIndices = [34, 84, 64, 14, 57];
const lowPriorityRooms = lowPriorityIndices.map((i) => rooms[i]);
for (const room of lowPriorityRooms) {
room.tags[DefaultTagID.LowPriority] = {};
}
// Let's say that rooms 14, 57, 65, 78, 82, 5, 36 are muted
const mutedIndices = [14, 57, 65, 78, 82, 5, 36];
const mutedRooms = mutedIndices.map((i) => rooms[i]);
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation((room) => {
const state = {
muted: mutedRooms.includes(room),
} as unknown as RoomNotificationState;
return state;
});
client.getVisibleRooms = jest.fn().mockReturnValue(rooms);
jest.spyOn(AsyncStoreWithClient.prototype, "matrixClient", "get").mockReturnValue(client);
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
// We expect the following order: Low Priority -> Low Priority & Muted -> Muted
const expectedRoomIds = [84, 64, 34, 57, 14, 82, 78, 65, 36, 5].map((i) => rooms[i].roomId);
return {
client,
rooms,
expectedRoomIds,
store,
dispatcher,
};
}
it("Low priority rooms are pushed to the bottom of the list just before muted rooms", async () => {
const { store, expectedRoomIds } = await getRoomListStoreWithRooms();
const result = store
.getSortedRooms()
.slice(90)
.map((r) => r.roomId);
expect(result).toEqual(expectedRoomIds);
});
});
});