Merge matrix-react-sdk into element-web
Merge remote-tracking branch 'repomerge/t3chguy/repomerge' into t3chguy/repo-merge Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
commit
f0ee7f7905
3265 changed files with 484599 additions and 699 deletions
376
test/unit-tests/stores/room-list/MessagePreviewStore-test.ts
Normal file
376
test/unit-tests/stores/room-list/MessagePreviewStore-test.ts
Normal file
|
@ -0,0 +1,376 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Mocked, mocked } from "jest-mock";
|
||||
import {
|
||||
EventStatus,
|
||||
EventTimeline,
|
||||
EventType,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
PendingEventOrdering,
|
||||
RelationType,
|
||||
Room,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MessagePreviewStore } from "../../../../src/stores/room-list/MessagePreviewStore";
|
||||
import { mkEvent, mkMessage, mkReaction, setupAsyncStoreWithClient, stubClient } from "../../../test-utils";
|
||||
import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
||||
import { mkThread } from "../../../test-utils/threads";
|
||||
|
||||
describe("MessagePreviewStore", () => {
|
||||
let client: Mocked<MatrixClient>;
|
||||
let room: Room;
|
||||
let nonRenderedRoom: Room;
|
||||
let store: MessagePreviewStore;
|
||||
|
||||
async function addEvent(
|
||||
store: MessagePreviewStore,
|
||||
room: Room,
|
||||
event: MatrixEvent,
|
||||
fireAction = true,
|
||||
): Promise<void> {
|
||||
room.addLiveEvents([event]);
|
||||
if (fireAction) {
|
||||
// @ts-ignore private access
|
||||
await store.onAction({
|
||||
action: "MatrixActions.Room.timeline",
|
||||
event,
|
||||
isLiveEvent: true,
|
||||
isLiveUnfilteredRoomTimelineEvent: true,
|
||||
room,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function addPendingEvent(
|
||||
store: MessagePreviewStore,
|
||||
room: Room,
|
||||
event: MatrixEvent,
|
||||
fireAction = true,
|
||||
): Promise<void> {
|
||||
room.addPendingEvent(event, "txid");
|
||||
if (fireAction) {
|
||||
// @ts-ignore private access
|
||||
await store.onLocalEchoUpdated(event, room);
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePendingEvent(event: MatrixEvent, eventStatus: EventStatus, fireAction = true): Promise<void> {
|
||||
room.updatePendingEvent(event, eventStatus);
|
||||
if (fireAction) {
|
||||
// @ts-ignore private access
|
||||
await store.onLocalEchoUpdated(event, room);
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
client = mocked(stubClient());
|
||||
room = new Room("!roomId:server", client, client.getSafeUserId(), {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
nonRenderedRoom = new Room("!roomId2:server", client, client.getSafeUserId(), {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
mocked(client.getRoom).mockReturnValue(room);
|
||||
|
||||
store = MessagePreviewStore.testInstance();
|
||||
await store.start();
|
||||
await setupAsyncStoreWithClient(store, client);
|
||||
});
|
||||
|
||||
it("should ignore edits for events other than the latest one", async () => {
|
||||
const firstMessage = mkMessage({
|
||||
user: "@sender:server",
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
msg: "First message",
|
||||
});
|
||||
await addEvent(store, room, firstMessage, false);
|
||||
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.Untagged))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: First message"`,
|
||||
);
|
||||
|
||||
const secondMessage = mkMessage({
|
||||
user: "@sender:server",
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
msg: "Second message",
|
||||
});
|
||||
await addEvent(store, room, secondMessage);
|
||||
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.Untagged))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: Second message"`,
|
||||
);
|
||||
|
||||
const firstMessageEdit = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: "@sender:server",
|
||||
room: room.roomId,
|
||||
content: {
|
||||
"body": "* First Message Edit",
|
||||
"m.new_content": {
|
||||
body: "First Message Edit",
|
||||
},
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Replace,
|
||||
event_id: firstMessage.getId()!,
|
||||
},
|
||||
},
|
||||
});
|
||||
await addEvent(store, room, firstMessageEdit);
|
||||
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.Untagged))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: Second message"`,
|
||||
);
|
||||
|
||||
const secondMessageEdit = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: "@sender:server",
|
||||
room: room.roomId,
|
||||
content: {
|
||||
"body": "* Second Message Edit",
|
||||
"m.new_content": {
|
||||
body: "Second Message Edit",
|
||||
},
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Replace,
|
||||
event_id: secondMessage.getId()!,
|
||||
},
|
||||
},
|
||||
});
|
||||
await addEvent(store, room, secondMessageEdit);
|
||||
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.Untagged))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: Second Message Edit"`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should not display a redacted edit", async () => {
|
||||
const firstMessage = mkMessage({
|
||||
user: "@sender:server",
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
msg: "First message",
|
||||
});
|
||||
await addEvent(store, room, firstMessage, false);
|
||||
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.Untagged))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: First message"`,
|
||||
);
|
||||
|
||||
const secondMessage = mkMessage({
|
||||
user: "@sender:server",
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
msg: "Second message",
|
||||
});
|
||||
await addEvent(store, room, secondMessage);
|
||||
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.Untagged))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: Second message"`,
|
||||
);
|
||||
|
||||
const secondMessageEdit = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: "@sender:server",
|
||||
room: room.roomId,
|
||||
content: {
|
||||
"body": "* Second Message Edit",
|
||||
"m.new_content": {
|
||||
body: "Second Message Edit",
|
||||
},
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Replace,
|
||||
event_id: secondMessage.getId()!,
|
||||
},
|
||||
},
|
||||
});
|
||||
await addEvent(store, room, secondMessageEdit);
|
||||
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.Untagged))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: Second Message Edit"`,
|
||||
);
|
||||
|
||||
secondMessage.makeRedacted(secondMessage, room);
|
||||
|
||||
await addEvent(store, room, secondMessage);
|
||||
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.Untagged))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: First message"`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should ignore edits to unknown events", async () => {
|
||||
await expect(store.getPreviewForRoom(room, DefaultTagID.DM)).resolves.toBeNull();
|
||||
|
||||
const firstMessage = mkMessage({
|
||||
user: "@sender:server",
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
msg: "First message",
|
||||
});
|
||||
await addEvent(store, room, firstMessage, true);
|
||||
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.DM))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: First message"`,
|
||||
);
|
||||
|
||||
const randomEdit = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: "@sender:server",
|
||||
room: room.roomId,
|
||||
content: {
|
||||
"body": "* Second Message Edit",
|
||||
"m.new_content": {
|
||||
body: "Second Message Edit",
|
||||
},
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Replace,
|
||||
event_id: "!other-event:server",
|
||||
},
|
||||
},
|
||||
});
|
||||
await addEvent(store, room, randomEdit);
|
||||
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.Untagged))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: First message"`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should generate correct preview for message events in DMs", async () => {
|
||||
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!.getJoinedMemberCount = jest.fn().mockReturnValue(2);
|
||||
|
||||
await expect(store.getPreviewForRoom(room, DefaultTagID.DM)).resolves.toBeNull();
|
||||
|
||||
const firstMessage = mkMessage({
|
||||
user: "@sender:server",
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
msg: "First message",
|
||||
});
|
||||
await addEvent(store, room, firstMessage);
|
||||
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.DM))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: First message"`,
|
||||
);
|
||||
|
||||
const secondMessage = mkMessage({
|
||||
user: "@sender:server",
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
msg: "Second message",
|
||||
});
|
||||
await addEvent(store, room, secondMessage);
|
||||
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.DM))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: Second message"`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should generate the correct preview for a reaction", async () => {
|
||||
const firstMessage = mkMessage({
|
||||
user: "@sender:server",
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
msg: "First message",
|
||||
});
|
||||
await addEvent(store, room, firstMessage);
|
||||
|
||||
const reaction = mkReaction(firstMessage);
|
||||
await addEvent(store, room, reaction);
|
||||
|
||||
const preview = await store.getPreviewForRoom(room, DefaultTagID.Untagged);
|
||||
expect(preview).toBeDefined();
|
||||
expect(preview?.isThreadReply).toBe(false);
|
||||
expect(preview?.text).toMatchInlineSnapshot(`"@sender:server reacted 🙃 to First message"`);
|
||||
});
|
||||
|
||||
it("should generate the correct preview for a reaction on a thread root", async () => {
|
||||
const { rootEvent, thread } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: client.getSafeUserId(),
|
||||
participantUserIds: [client.getSafeUserId()],
|
||||
});
|
||||
await addEvent(store, room, rootEvent);
|
||||
|
||||
const reaction = mkReaction(rootEvent, { ts: 42 });
|
||||
reaction.setThread(thread);
|
||||
await addEvent(store, room, reaction);
|
||||
|
||||
const preview = await store.getPreviewForRoom(room, DefaultTagID.Untagged);
|
||||
expect(preview).toBeDefined();
|
||||
expect(preview?.isThreadReply).toBe(false);
|
||||
expect(preview?.text).toContain("You reacted 🙃 to root event message");
|
||||
});
|
||||
|
||||
it("should handle local echos correctly", async () => {
|
||||
const firstMessage = mkMessage({
|
||||
user: "@sender:server",
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
msg: "First message",
|
||||
});
|
||||
|
||||
await addEvent(store, room, firstMessage);
|
||||
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.Untagged))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: First message"`,
|
||||
);
|
||||
|
||||
const secondMessage = mkMessage({
|
||||
user: "@sender:server",
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
msg: "Second message",
|
||||
});
|
||||
secondMessage.status = EventStatus.NOT_SENT;
|
||||
|
||||
await addPendingEvent(store, room, secondMessage);
|
||||
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.Untagged))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: Second message"`,
|
||||
);
|
||||
|
||||
await updatePendingEvent(secondMessage, EventStatus.CANCELLED);
|
||||
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.Untagged))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: First message"`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should not generate previews for rooms not rendered", async () => {
|
||||
const firstMessage = mkMessage({
|
||||
user: "@sender:server",
|
||||
event: true,
|
||||
room: nonRenderedRoom.roomId,
|
||||
msg: "First message",
|
||||
});
|
||||
|
||||
await addEvent(store, room, firstMessage);
|
||||
|
||||
const secondMessage = mkMessage({
|
||||
user: "@sender:server",
|
||||
event: true,
|
||||
room: nonRenderedRoom.roomId,
|
||||
msg: "Second message",
|
||||
});
|
||||
secondMessage.status = EventStatus.NOT_SENT;
|
||||
|
||||
await addPendingEvent(store, room, secondMessage);
|
||||
|
||||
// @ts-ignore private access
|
||||
expect(store.previews.has(nonRenderedRoom.roomId)).toBeFalsy();
|
||||
});
|
||||
});
|
394
test/unit-tests/stores/room-list/RoomListStore-test.ts
Normal file
394
test/unit-tests/stores/room-list/RoomListStore-test.ts
Normal file
|
@ -0,0 +1,394 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
ConditionKind,
|
||||
EventType,
|
||||
IPushRule,
|
||||
JoinRule,
|
||||
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, { MatrixDispatcher } from "../../../../src/dispatcher/dispatcher";
|
||||
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||
import SettingsStore, { CallbackFn } from "../../../../src/settings/SettingsStore";
|
||||
import { ListAlgorithm, SortAlgorithm } from "../../../../src/stores/room-list/algorithms/models";
|
||||
import { DefaultTagID, 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, mkRoom } 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();
|
||||
|
||||
// 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 | undefined;
|
||||
jest.spyOn(SettingsStore, "watchSetting").mockImplementation(
|
||||
(_settingName: string, _roomId: string | null, callbackFn: 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Correctly tags rooms", () => {
|
||||
it("renders Public and Knock rooms in Conferences section", () => {
|
||||
const videoRoomPrivate = "!videoRoomPrivate_server";
|
||||
const videoRoomPublic = "!videoRoomPublic_server";
|
||||
const videoRoomKnock = "!videoRoomKnock_server";
|
||||
|
||||
const rooms: Room[] = [];
|
||||
RoomListStore.instance;
|
||||
mkRoom(client, videoRoomPrivate, rooms);
|
||||
mkRoom(client, videoRoomPublic, rooms);
|
||||
mkRoom(client, videoRoomKnock, rooms);
|
||||
|
||||
mocked(client).getRoom.mockImplementation((roomId) => rooms.find((room) => room.roomId === roomId) || null);
|
||||
mocked(client).getRooms.mockImplementation(() => rooms);
|
||||
|
||||
const videoRoomKnockRoom = client.getRoom(videoRoomKnock);
|
||||
(videoRoomKnockRoom!.getJoinRule as jest.Mock).mockReturnValue(JoinRule.Knock);
|
||||
|
||||
const videoRoomPrivateRoom = client.getRoom(videoRoomPrivate);
|
||||
(videoRoomPrivateRoom!.getJoinRule as jest.Mock).mockReturnValue(JoinRule.Invite);
|
||||
|
||||
const videoRoomPublicRoom = client.getRoom(videoRoomPublic);
|
||||
(videoRoomPublicRoom!.getJoinRule as jest.Mock).mockReturnValue(JoinRule.Public);
|
||||
|
||||
[videoRoomPrivateRoom, videoRoomPublicRoom, videoRoomKnockRoom].forEach((room) => {
|
||||
(room!.isCallRoom as jest.Mock).mockReturnValue(true);
|
||||
});
|
||||
|
||||
expect(
|
||||
RoomListStore.instance
|
||||
.getTagsForRoom(client.getRoom(videoRoomPublic)!)
|
||||
.includes(DefaultTagID.Conference),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
RoomListStore.instance
|
||||
.getTagsForRoom(client.getRoom(videoRoomKnock)!)
|
||||
.includes(DefaultTagID.Conference),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
RoomListStore.instance
|
||||
.getTagsForRoom(client.getRoom(videoRoomPrivate)!)
|
||||
.includes(DefaultTagID.Conference),
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
341
test/unit-tests/stores/room-list/SlidingRoomListStore-test.ts
Normal file
341
test/unit-tests/stores/room-list/SlidingRoomListStore-test.ts
Normal file
|
@ -0,0 +1,341 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
import { mocked } from "jest-mock";
|
||||
import { SlidingSync, SlidingSyncEvent } from "matrix-js-sdk/src/sliding-sync";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
LISTS_UPDATE_EVENT,
|
||||
SlidingRoomListStoreClass,
|
||||
SlidingSyncSortToFilter,
|
||||
} from "../../../../src/stores/room-list/SlidingRoomListStore";
|
||||
import { SpaceStoreClass } from "../../../../src/stores/spaces/SpaceStore";
|
||||
import { MockEventEmitter, stubClient, untilEmission } from "../../../test-utils";
|
||||
import { TestSdkContext } from "../../TestSdkContext";
|
||||
import { SlidingSyncManager } from "../../../../src/SlidingSyncManager";
|
||||
import { RoomViewStore } from "../../../../src/stores/RoomViewStore";
|
||||
import { MatrixDispatcher } from "../../../../src/dispatcher/dispatcher";
|
||||
import { SortAlgorithm } from "../../../../src/stores/room-list/algorithms/models";
|
||||
import { DefaultTagID, TagID } from "../../../../src/stores/room-list/models";
|
||||
import { MetaSpace, UPDATE_SELECTED_SPACE } from "../../../../src/stores/spaces";
|
||||
import { LISTS_LOADING_EVENT } from "../../../../src/stores/room-list/RoomListStore";
|
||||
import { UPDATE_EVENT } from "../../../../src/stores/AsyncStore";
|
||||
|
||||
jest.mock("../../../../src/SlidingSyncManager");
|
||||
const MockSlidingSyncManager = <jest.Mock<SlidingSyncManager>>(<unknown>SlidingSyncManager);
|
||||
|
||||
describe("SlidingRoomListStore", () => {
|
||||
let store: SlidingRoomListStoreClass;
|
||||
let context: TestSdkContext;
|
||||
let dis: MatrixDispatcher;
|
||||
let activeSpace: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
context = new TestSdkContext();
|
||||
context.client = stubClient();
|
||||
context._SpaceStore = new MockEventEmitter<SpaceStoreClass>({
|
||||
traverseSpace: jest.fn(),
|
||||
get activeSpace() {
|
||||
return activeSpace;
|
||||
},
|
||||
}) as SpaceStoreClass;
|
||||
context._SlidingSyncManager = new MockSlidingSyncManager();
|
||||
context._SlidingSyncManager.slidingSync = mocked(
|
||||
new MockEventEmitter({
|
||||
getListData: jest.fn(),
|
||||
}) as unknown as SlidingSync,
|
||||
);
|
||||
context._RoomViewStore = mocked(
|
||||
new MockEventEmitter({
|
||||
getRoomId: jest.fn(),
|
||||
}) as unknown as RoomViewStore,
|
||||
);
|
||||
mocked(context._SlidingSyncManager.ensureListRegistered).mockResolvedValue({
|
||||
ranges: [[0, 10]],
|
||||
});
|
||||
|
||||
dis = new MatrixDispatcher();
|
||||
store = new SlidingRoomListStoreClass(dis, context);
|
||||
});
|
||||
|
||||
describe("spaces", () => {
|
||||
it("alters 'filters.spaces' on the DefaultTagID.Untagged list when the selected space changes", async () => {
|
||||
await store.start(); // call onReady
|
||||
const spaceRoomId = "!foo:bar";
|
||||
|
||||
const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => {
|
||||
return listName === DefaultTagID.Untagged && !isLoading;
|
||||
});
|
||||
|
||||
// change the active space
|
||||
activeSpace = spaceRoomId;
|
||||
context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false);
|
||||
await p;
|
||||
|
||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, {
|
||||
filters: expect.objectContaining({
|
||||
spaces: [spaceRoomId],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("gracefully handles subspaces in the home metaspace", async () => {
|
||||
const subspace = "!sub:space";
|
||||
mocked(context._SpaceStore!.traverseSpace).mockImplementation(
|
||||
(spaceId: string, fn: (roomId: string) => void) => {
|
||||
fn(subspace);
|
||||
},
|
||||
);
|
||||
activeSpace = MetaSpace.Home;
|
||||
await store.start(); // call onReady
|
||||
|
||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, {
|
||||
filters: expect.objectContaining({
|
||||
spaces: [subspace],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("alters 'filters.spaces' on the DefaultTagID.Untagged list if it loads with an active space", async () => {
|
||||
// change the active space before we are ready
|
||||
const spaceRoomId = "!foo2:bar";
|
||||
activeSpace = spaceRoomId;
|
||||
const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => {
|
||||
return listName === DefaultTagID.Untagged && !isLoading;
|
||||
});
|
||||
await store.start(); // call onReady
|
||||
await p;
|
||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(
|
||||
DefaultTagID.Untagged,
|
||||
expect.objectContaining({
|
||||
filters: expect.objectContaining({
|
||||
spaces: [spaceRoomId],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("includes subspaces in 'filters.spaces' when the selected space has subspaces", async () => {
|
||||
await store.start(); // call onReady
|
||||
const spaceRoomId = "!foo:bar";
|
||||
const subSpace1 = "!ss1:bar";
|
||||
const subSpace2 = "!ss2:bar";
|
||||
|
||||
const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => {
|
||||
return listName === DefaultTagID.Untagged && !isLoading;
|
||||
});
|
||||
|
||||
mocked(context._SpaceStore!.traverseSpace).mockImplementation(
|
||||
(spaceId: string, fn: (roomId: string) => void) => {
|
||||
if (spaceId === spaceRoomId) {
|
||||
fn(subSpace1);
|
||||
fn(subSpace2);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// change the active space
|
||||
activeSpace = spaceRoomId;
|
||||
context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false);
|
||||
await p;
|
||||
|
||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, {
|
||||
filters: expect.objectContaining({
|
||||
spaces: [spaceRoomId, subSpace1, subSpace2],
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("setTagSorting alters the 'sort' option in the list", async () => {
|
||||
const tagId: TagID = "foo";
|
||||
await store.setTagSorting(tagId, SortAlgorithm.Alphabetic);
|
||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(tagId, {
|
||||
sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic],
|
||||
});
|
||||
expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Alphabetic);
|
||||
|
||||
await store.setTagSorting(tagId, SortAlgorithm.Recent);
|
||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(tagId, {
|
||||
sort: SlidingSyncSortToFilter[SortAlgorithm.Recent],
|
||||
});
|
||||
expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Recent);
|
||||
});
|
||||
|
||||
it("getTagsForRoom gets the tags for the room", async () => {
|
||||
await store.start();
|
||||
const roomA = "!a:localhost";
|
||||
const roomB = "!b:localhost";
|
||||
const keyToListData: Record<string, { joinedCount: number; roomIndexToRoomId: Record<number, string> }> = {
|
||||
[DefaultTagID.Untagged]: {
|
||||
joinedCount: 10,
|
||||
roomIndexToRoomId: {
|
||||
0: roomA,
|
||||
1: roomB,
|
||||
},
|
||||
},
|
||||
[DefaultTagID.Favourite]: {
|
||||
joinedCount: 2,
|
||||
roomIndexToRoomId: {
|
||||
0: roomB,
|
||||
},
|
||||
},
|
||||
};
|
||||
mocked(context._SlidingSyncManager!.slidingSync!.getListData).mockImplementation((key: string) => {
|
||||
return keyToListData[key] || null;
|
||||
});
|
||||
|
||||
expect(store.getTagsForRoom(new Room(roomA, context.client!, context.client!.getUserId()!))).toEqual([
|
||||
DefaultTagID.Untagged,
|
||||
]);
|
||||
expect(store.getTagsForRoom(new Room(roomB, context.client!, context.client!.getUserId()!))).toEqual([
|
||||
DefaultTagID.Favourite,
|
||||
DefaultTagID.Untagged,
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits LISTS_UPDATE_EVENT when slidingSync lists update", async () => {
|
||||
await store.start();
|
||||
const roomA = "!a:localhost";
|
||||
const roomB = "!b:localhost";
|
||||
const roomC = "!c:localhost";
|
||||
const tagId = DefaultTagID.Favourite;
|
||||
const joinCount = 10;
|
||||
const roomIndexToRoomId = {
|
||||
// mixed to ensure we sort
|
||||
1: roomB,
|
||||
2: roomC,
|
||||
0: roomA,
|
||||
};
|
||||
const rooms = [
|
||||
new Room(roomA, context.client!, context.client!.getUserId()!),
|
||||
new Room(roomB, context.client!, context.client!.getUserId()!),
|
||||
new Room(roomC, context.client!, context.client!.getUserId()!),
|
||||
];
|
||||
mocked(context.client!.getRoom).mockImplementation((roomId: string) => {
|
||||
switch (roomId) {
|
||||
case roomA:
|
||||
return rooms[0];
|
||||
case roomB:
|
||||
return rooms[1];
|
||||
case roomC:
|
||||
return rooms[2];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const p = untilEmission(store, LISTS_UPDATE_EVENT);
|
||||
context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
|
||||
await p;
|
||||
expect(store.getCount(tagId)).toEqual(joinCount);
|
||||
expect(store.orderedLists[tagId]).toEqual(rooms);
|
||||
});
|
||||
|
||||
it("sets the sticky room on the basis of the viewed room in RoomViewStore", async () => {
|
||||
await store.start();
|
||||
// seed the store with 3 rooms
|
||||
const roomIdA = "!a:localhost";
|
||||
const roomIdB = "!b:localhost";
|
||||
const roomIdC = "!c:localhost";
|
||||
const tagId = DefaultTagID.Favourite;
|
||||
const joinCount = 10;
|
||||
const roomIndexToRoomId = {
|
||||
// mixed to ensure we sort
|
||||
1: roomIdB,
|
||||
2: roomIdC,
|
||||
0: roomIdA,
|
||||
};
|
||||
const roomA = new Room(roomIdA, context.client!, context.client!.getUserId()!);
|
||||
const roomB = new Room(roomIdB, context.client!, context.client!.getUserId()!);
|
||||
const roomC = new Room(roomIdC, context.client!, context.client!.getUserId()!);
|
||||
mocked(context.client!.getRoom).mockImplementation((roomId: string) => {
|
||||
switch (roomId) {
|
||||
case roomIdA:
|
||||
return roomA;
|
||||
case roomIdB:
|
||||
return roomB;
|
||||
case roomIdC:
|
||||
return roomC;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(context._SlidingSyncManager!.slidingSync!.getListData).mockImplementation((key: string) => {
|
||||
if (key !== tagId) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
roomIndexToRoomId: roomIndexToRoomId,
|
||||
joinedCount: joinCount,
|
||||
};
|
||||
});
|
||||
let p = untilEmission(store, LISTS_UPDATE_EVENT);
|
||||
context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
|
||||
await p;
|
||||
expect(store.orderedLists[tagId]).toEqual([roomA, roomB, roomC]);
|
||||
|
||||
// make roomB sticky and inform the store
|
||||
mocked(context.roomViewStore.getRoomId).mockReturnValue(roomIdB);
|
||||
context.roomViewStore.emit(UPDATE_EVENT);
|
||||
|
||||
// bump room C to the top, room B should not move from i=1 despite the list update saying to
|
||||
roomIndexToRoomId[0] = roomIdC;
|
||||
roomIndexToRoomId[1] = roomIdA;
|
||||
roomIndexToRoomId[2] = roomIdB;
|
||||
p = untilEmission(store, LISTS_UPDATE_EVENT);
|
||||
context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
|
||||
await p;
|
||||
|
||||
// check that B didn't move and that A was put below B
|
||||
expect(store.orderedLists[tagId]).toEqual([roomC, roomB, roomA]);
|
||||
|
||||
// make room C sticky: rooms should move as a result, without needing an additional list update
|
||||
mocked(context.roomViewStore.getRoomId).mockReturnValue(roomIdC);
|
||||
p = untilEmission(store, LISTS_UPDATE_EVENT);
|
||||
context.roomViewStore.emit(UPDATE_EVENT);
|
||||
await p;
|
||||
expect(store.orderedLists[tagId].map((r) => r.roomId)).toEqual([roomC, roomA, roomB].map((r) => r.roomId));
|
||||
});
|
||||
|
||||
it("gracefully handles unknown room IDs", async () => {
|
||||
await store.start();
|
||||
const roomIdA = "!a:localhost";
|
||||
const roomIdB = "!b:localhost"; // does not exist
|
||||
const roomIdC = "!c:localhost";
|
||||
const roomIndexToRoomId = {
|
||||
0: roomIdA,
|
||||
1: roomIdB, // does not exist
|
||||
2: roomIdC,
|
||||
};
|
||||
const tagId = DefaultTagID.Favourite;
|
||||
const joinCount = 10;
|
||||
// seed the store with 2 rooms
|
||||
const roomA = new Room(roomIdA, context.client!, context.client!.getUserId()!);
|
||||
const roomC = new Room(roomIdC, context.client!, context.client!.getUserId()!);
|
||||
mocked(context.client!.getRoom).mockImplementation((roomId: string) => {
|
||||
switch (roomId) {
|
||||
case roomIdA:
|
||||
return roomA;
|
||||
case roomIdC:
|
||||
return roomC;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(context._SlidingSyncManager!.slidingSync!.getListData).mockImplementation((key: string) => {
|
||||
if (key !== tagId) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
roomIndexToRoomId: roomIndexToRoomId,
|
||||
joinedCount: joinCount,
|
||||
};
|
||||
});
|
||||
const p = untilEmission(store, LISTS_UPDATE_EVENT);
|
||||
context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
|
||||
await p;
|
||||
expect(store.orderedLists[tagId]).toEqual([roomA, roomC]);
|
||||
});
|
||||
});
|
252
test/unit-tests/stores/room-list/SpaceWatcher-test.ts
Normal file
252
test/unit-tests/stores/room-list/SpaceWatcher-test.ts
Normal file
|
@ -0,0 +1,252 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { 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();
|
||||
});
|
||||
|
||||
it("removes filter for favourites -> all transition", async () => {
|
||||
await setShowAllRooms(true);
|
||||
new SpaceWatcher(mockRoomListStore);
|
||||
|
||||
SpaceStore.instance.setActiveSpace(MetaSpace.Favourites);
|
||||
expect(filter).toBeInstanceOf(SpaceFilterCondition);
|
||||
expect(filter!["space"]).toBe(MetaSpace.Favourites);
|
||||
SpaceStore.instance.setActiveSpace(MetaSpace.Home);
|
||||
|
||||
expect(filter).toBeNull();
|
||||
});
|
||||
|
||||
it("removes filter for people -> all transition", async () => {
|
||||
await setShowAllRooms(true);
|
||||
new SpaceWatcher(mockRoomListStore);
|
||||
|
||||
SpaceStore.instance.setActiveSpace(MetaSpace.People);
|
||||
expect(filter).toBeInstanceOf(SpaceFilterCondition);
|
||||
expect(filter!["space"]).toBe(MetaSpace.People);
|
||||
SpaceStore.instance.setActiveSpace(MetaSpace.Home);
|
||||
|
||||
expect(filter).toBeNull();
|
||||
});
|
||||
|
||||
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 orphans -> people transition", async () => {
|
||||
await setShowAllRooms(false);
|
||||
SpaceStore.instance.setActiveSpace(MetaSpace.Orphans);
|
||||
|
||||
new SpaceWatcher(mockRoomListStore);
|
||||
expect(filter).toBeInstanceOf(SpaceFilterCondition);
|
||||
expect(filter!["space"]).toBe(MetaSpace.Orphans);
|
||||
SpaceStore.instance.setActiveSpace(MetaSpace.People);
|
||||
|
||||
expect(filter).toBeInstanceOf(SpaceFilterCondition);
|
||||
expect(filter!["space"]).toBe(MetaSpace.People);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
101
test/unit-tests/stores/room-list/algorithms/Algorithm-test.ts
Normal file
101
test/unit-tests/stores/room-list/algorithms/Algorithm-test.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked, 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 type { ClientWidgetApi } from "matrix-widget-api";
|
||||
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/models";
|
||||
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";
|
||||
|
||||
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 === null) throw new Error("Failed to create call");
|
||||
|
||||
const widget = new Widget(call.widget);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, roomWithCall.roomId, {
|
||||
stop: () => {},
|
||||
} as unknown as ClientWidgetApi);
|
||||
|
||||
// End of setup
|
||||
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([room, roomWithCall]);
|
||||
await call.start();
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([roomWithCall, room]);
|
||||
await call.disconnect();
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([room, roomWithCall]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Room, 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/models";
|
||||
|
||||
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]);
|
||||
expect(algorithm.getLastTs(room, "@jane:matrix.org")).toBe(5);
|
||||
expect(algorithm.getLastTs(room, "@john:matrix.org")).toBe(5);
|
||||
|
||||
room.addLiveEvents([event2]);
|
||||
|
||||
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]);
|
||||
room2.addLiveEvents([evt2]);
|
||||
|
||||
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]);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
const { events: events2 } = mkThread({
|
||||
room: room2,
|
||||
client: cli,
|
||||
authorId: "@bob:matrix.org",
|
||||
participantUserIds: ["@bob:matrix.org"],
|
||||
ts: 14,
|
||||
length: 10,
|
||||
});
|
||||
room2.addLiveEvents(events2);
|
||||
|
||||
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]);
|
||||
|
||||
expect(algorithm.sortRooms([room1, room2], DefaultTagID.Untagged)).toEqual([room1, room2]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,425 @@
|
|||
/*
|
||||
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
|
||||
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, 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 },
|
||||
grey: { symbol: null, count: 1, level: NotificationLevel.Notification },
|
||||
none: { symbol: null, count: 0, level: NotificationLevel.None },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(RoomNotifs, "determineUnreadState").mockReturnValue({
|
||||
symbol: null,
|
||||
count: 0,
|
||||
level: NotificationLevel.None,
|
||||
});
|
||||
});
|
||||
|
||||
const setupAlgorithm = (sortAlgorithm: SortAlgorithm, rooms?: Room[]) => {
|
||||
const algorithm = new ImportanceAlgorithm(tagId, sortAlgorithm);
|
||||
algorithm.setRooms(rooms || [roomA, roomB, roomC]);
|
||||
return algorithm;
|
||||
};
|
||||
|
||||
describe("When sortAlgorithm is manual", () => {
|
||||
const sortAlgorithm = SortAlgorithm.Manual;
|
||||
it("orders rooms by tag order without categorizing", () => {
|
||||
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState");
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
// didn't check notif state
|
||||
expect(RoomNotificationStateStore.instance.getRoomState).not.toHaveBeenCalled();
|
||||
// sorted according to room tag order
|
||||
expect(algorithm.orderedRooms).toEqual([roomC, roomA, roomB]);
|
||||
});
|
||||
|
||||
describe("handleRoomUpdate", () => {
|
||||
// XXX: This doesn't work because manual ordered rooms dont get categoryindices
|
||||
// possibly related https://github.com/vector-im/element-web/issues/25099
|
||||
it.skip("removes a room", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.RoomRemoved);
|
||||
|
||||
expect(shouldTriggerUpdate).toBe(true);
|
||||
expect(algorithm.orderedRooms).toEqual([roomC, roomB]);
|
||||
});
|
||||
|
||||
// XXX: This doesn't work because manual ordered rooms dont get categoryindices
|
||||
it.skip("adds a new room", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.NewRoom);
|
||||
|
||||
expect(shouldTriggerUpdate).toBe(true);
|
||||
expect(algorithm.orderedRooms).toEqual([roomC, roomB, roomD, roomE]);
|
||||
});
|
||||
|
||||
it("does nothing and returns false for a timeline update", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
const beforeRooms = algorithm.orderedRooms;
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.Timeline);
|
||||
|
||||
expect(shouldTriggerUpdate).toBe(false);
|
||||
// strict equal
|
||||
expect(algorithm.orderedRooms).toBe(beforeRooms);
|
||||
});
|
||||
|
||||
it("does nothing and returns false for a read receipt update", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
const beforeRooms = algorithm.orderedRooms;
|
||||
const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.ReadReceipt);
|
||||
|
||||
expect(shouldTriggerUpdate).toBe(false);
|
||||
// strict equal
|
||||
expect(algorithm.orderedRooms).toBe(beforeRooms);
|
||||
});
|
||||
|
||||
it("throws for an unhandle update cause", () => {
|
||||
const algorithm = setupAlgorithm(sortAlgorithm);
|
||||
|
||||
expect(() =>
|
||||
algorithm.handleRoomUpdate(roomA, "something unexpected" as unknown as RoomUpdateCause),
|
||||
).toThrow("Unsupported update cause: something unexpected");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
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,
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,280 @@
|
|||
/*
|
||||
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
|
||||
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, 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,
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { 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, 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-var-requires
|
||||
const EventEmitter = require("events");
|
||||
class MockSpaceStore extends EventEmitter {
|
||||
isRoomInSpace = jest.fn();
|
||||
getSpaceFilteredUserIds = jest.fn().mockReturnValue(new Set<string>([]));
|
||||
getSpaceFilteredRoomIds = jest.fn().mockReturnValue(new Set<string>([]));
|
||||
}
|
||||
return { instance: new MockSpaceStore() };
|
||||
});
|
||||
|
||||
const SettingsStoreMock = mocked(SettingsStore);
|
||||
const SpaceStoreInstanceMock = mocked(SpaceStore.instance);
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("SpaceFilterCondition", () => {
|
||||
const space1 = "!space1:server";
|
||||
const space2 = "!space2:server";
|
||||
const room1Id = "!r1:server";
|
||||
const room2Id = "!r2:server";
|
||||
const room3Id = "!r3:server";
|
||||
const user1Id = "@u1:server";
|
||||
const user2Id = "@u2:server";
|
||||
const user3Id = "@u3:server";
|
||||
const makeMockGetValue =
|
||||
(settings: 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { Room, RoomType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { VisibilityProvider } from "../../../../../src/stores/room-list/filters/VisibilityProvider";
|
||||
import LegacyCallHandler from "../../../../../src/LegacyCallHandler";
|
||||
import VoipUserMapper from "../../../../../src/VoipUserMapper";
|
||||
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../../../src/models/LocalRoom";
|
||||
import { RoomListCustomisations } from "../../../../../src/customisations/RoomList";
|
||||
import { createTestClient } from "../../../../test-utils";
|
||||
|
||||
jest.mock("../../../../../src/VoipUserMapper", () => ({
|
||||
sharedInstance: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/LegacyCallHandler", () => ({
|
||||
instance: {
|
||||
getSupportsVirtualRooms: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/customisations/RoomList", () => ({
|
||||
RoomListCustomisations: {
|
||||
isRoomVisible: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const createRoom = (isSpaceRoom = false): Room => {
|
||||
return {
|
||||
isSpaceRoom: () => isSpaceRoom,
|
||||
getType: () => (isSpaceRoom ? RoomType.Space : undefined),
|
||||
} as unknown as Room;
|
||||
};
|
||||
|
||||
const createLocalRoom = (): LocalRoom => {
|
||||
const room = new LocalRoom(LOCAL_ROOM_ID_PREFIX + "test", createTestClient(), "@test:example.com");
|
||||
room.isSpaceRoom = () => false;
|
||||
return room;
|
||||
};
|
||||
|
||||
describe("VisibilityProvider", () => {
|
||||
let mockVoipUserMapper: VoipUserMapper;
|
||||
|
||||
beforeEach(() => {
|
||||
mockVoipUserMapper = {
|
||||
onNewInvitedRoom: jest.fn(),
|
||||
isVirtualRoom: jest.fn(),
|
||||
} as unknown as VoipUserMapper;
|
||||
mocked(VoipUserMapper.sharedInstance).mockReturnValue(mockVoipUserMapper);
|
||||
});
|
||||
|
||||
describe("instance", () => {
|
||||
it("should return an instance", () => {
|
||||
const visibilityProvider = VisibilityProvider.instance;
|
||||
expect(visibilityProvider).toBeInstanceOf(VisibilityProvider);
|
||||
expect(VisibilityProvider.instance).toBe(visibilityProvider);
|
||||
});
|
||||
});
|
||||
|
||||
describe("onNewInvitedRoom", () => {
|
||||
it("should call onNewInvitedRoom on VoipUserMapper.sharedInstance", async () => {
|
||||
const room = {} as unknown as Room;
|
||||
await VisibilityProvider.instance.onNewInvitedRoom(room);
|
||||
expect(mockVoipUserMapper.onNewInvitedRoom).toHaveBeenCalledWith(room);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isRoomVisible", () => {
|
||||
describe("for a virtual room", () => {
|
||||
beforeEach(() => {
|
||||
mocked(LegacyCallHandler.instance.getSupportsVirtualRooms).mockReturnValue(true);
|
||||
mocked(mockVoipUserMapper.isVirtualRoom).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("should return return false", () => {
|
||||
const room = createRoom();
|
||||
expect(VisibilityProvider.instance.isRoomVisible(room)).toBe(false);
|
||||
expect(mockVoipUserMapper.isVirtualRoom).toHaveBeenCalledWith(room);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false without room", () => {
|
||||
expect(VisibilityProvider.instance.isRoomVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for a space room", () => {
|
||||
const room = createRoom(true);
|
||||
expect(VisibilityProvider.instance.isRoomVisible(room)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for a local room", () => {
|
||||
const room = createLocalRoom();
|
||||
expect(VisibilityProvider.instance.isRoomVisible(room)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false if visibility customisation returns false", () => {
|
||||
mocked(RoomListCustomisations.isRoomVisible!).mockReturnValue(false);
|
||||
const room = createRoom();
|
||||
expect(VisibilityProvider.instance.isRoomVisible(room)).toBe(false);
|
||||
expect(RoomListCustomisations.isRoomVisible).toHaveBeenCalledWith(room);
|
||||
});
|
||||
|
||||
it("should return true if visibility customisation returns true", () => {
|
||||
mocked(RoomListCustomisations.isRoomVisible!).mockReturnValue(true);
|
||||
const room = createRoom();
|
||||
expect(VisibilityProvider.instance.isRoomVisible(room)).toBe(true);
|
||||
expect(RoomListCustomisations.isRoomVisible).toHaveBeenCalledWith(room);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { RelationType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MessageEventPreview } from "../../../../../src/stores/room-list/previews/MessageEventPreview";
|
||||
import { mkEvent, stubClient } from "../../../../test-utils";
|
||||
|
||||
describe("MessageEventPreview", () => {
|
||||
const preview = new MessageEventPreview();
|
||||
const userId = "@user:example.com";
|
||||
|
||||
beforeAll(() => {
|
||||
stubClient();
|
||||
});
|
||||
|
||||
describe("getTextFor", () => {
|
||||
it("when called with an event with empty content should return null", () => {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
content: {},
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
});
|
||||
expect(preview.getTextFor(event)).toBeNull();
|
||||
});
|
||||
|
||||
it("when called with an event with empty body should return null", () => {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
body: "",
|
||||
},
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
});
|
||||
expect(preview.getTextFor(event)).toBeNull();
|
||||
});
|
||||
|
||||
it("when called with an event with body should return »user: body«", () => {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
body: "test body",
|
||||
},
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
});
|
||||
expect(preview.getTextFor(event)).toBe(`${userId}: test body`);
|
||||
});
|
||||
|
||||
it("when called for a replaced event with new content should return the new content body", () => {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
["m.new_content"]: {
|
||||
body: "test new content body",
|
||||
},
|
||||
["m.relates_to"]: {
|
||||
rel_type: RelationType.Replace,
|
||||
event_id: "$asd123",
|
||||
},
|
||||
},
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
});
|
||||
expect(preview.getTextFor(event)).toBe(`${userId}: test new content body`);
|
||||
});
|
||||
|
||||
it("when called with a broadcast chunk event it should return null", () => {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
body: "test body",
|
||||
["io.element.voice_broadcast_chunk"]: {},
|
||||
},
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
});
|
||||
expect(preview.getTextFor(event)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { PollStartEventPreview } from "../../../../../src/stores/room-list/previews/PollStartEventPreview";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import { makePollStartEvent } from "../../../../test-utils";
|
||||
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue({
|
||||
getUserId: () => "@me:example.com",
|
||||
getSafeUserId: () => "@me:example.com",
|
||||
} as unknown as MatrixClient);
|
||||
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue({
|
||||
getUserId: () => "@me:example.com",
|
||||
getSafeUserId: () => "@me:example.com",
|
||||
} as unknown as MatrixClient);
|
||||
|
||||
describe("PollStartEventPreview", () => {
|
||||
it("shows the question for a poll I created", async () => {
|
||||
const pollStartEvent = makePollStartEvent("My Question", "@me:example.com");
|
||||
const preview = new PollStartEventPreview();
|
||||
expect(preview.getTextFor(pollStartEvent)).toBe("My Question");
|
||||
});
|
||||
|
||||
it("shows the sender and question for a poll created by someone else", async () => {
|
||||
const pollStartEvent = makePollStartEvent("Your Question", "@yo:example.com");
|
||||
const preview = new PollStartEventPreview();
|
||||
expect(preview.getTextFor(pollStartEvent)).toBe("@yo:example.com: Your Question");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { RelationType, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { mkEvent, stubClient } from "../../../../test-utils";
|
||||
import { ReactionEventPreview } from "../../../../../src/stores/room-list/previews/ReactionEventPreview";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
|
||||
describe("ReactionEventPreview", () => {
|
||||
const preview = new ReactionEventPreview();
|
||||
const userId = "@user:example.com";
|
||||
const roomId = "!room:example.com";
|
||||
|
||||
beforeAll(() => {
|
||||
stubClient();
|
||||
});
|
||||
|
||||
describe("getTextFor", () => {
|
||||
it("should return null for non-relations", () => {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
content: {},
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
room: roomId,
|
||||
});
|
||||
expect(preview.getTextFor(event)).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for non-reactions", () => {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
"body": "",
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Thread,
|
||||
event_id: "$foo:bar",
|
||||
},
|
||||
},
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
room: roomId,
|
||||
});
|
||||
expect(preview.getTextFor(event)).toBeNull();
|
||||
});
|
||||
|
||||
it("should use 'You' for your own reactions", () => {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const room = new Room(roomId, cli, userId);
|
||||
mocked(cli.getRoom).mockReturnValue(room);
|
||||
|
||||
const message = mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
"body": "duck duck goose",
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Thread,
|
||||
event_id: "$foo:bar",
|
||||
},
|
||||
},
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
room: roomId,
|
||||
});
|
||||
|
||||
room.getUnfilteredTimelineSet().addLiveEvent(message, {});
|
||||
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
key: "🪿",
|
||||
event_id: message.getId(),
|
||||
},
|
||||
},
|
||||
user: cli.getSafeUserId(),
|
||||
type: "m.reaction",
|
||||
room: roomId,
|
||||
});
|
||||
expect(preview.getTextFor(event)).toMatchInlineSnapshot(`"You reacted 🪿 to duck duck goose"`);
|
||||
});
|
||||
|
||||
it("should use display name for your others' reactions", () => {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const room = new Room(roomId, cli, userId);
|
||||
mocked(cli.getRoom).mockReturnValue(room);
|
||||
|
||||
const message = mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
"body": "duck duck goose",
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Thread,
|
||||
event_id: "$foo:bar",
|
||||
},
|
||||
},
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
room: roomId,
|
||||
});
|
||||
|
||||
room.getUnfilteredTimelineSet().addLiveEvent(message, {});
|
||||
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
key: "🪿",
|
||||
event_id: message.getId(),
|
||||
},
|
||||
},
|
||||
user: userId,
|
||||
type: "m.reaction",
|
||||
room: roomId,
|
||||
});
|
||||
event.sender = new RoomMember(roomId, userId);
|
||||
event.sender.name = "Bob";
|
||||
|
||||
expect(preview.getTextFor(event)).toMatchInlineSnapshot(`"Bob reacted 🪿 to duck duck goose"`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { VoiceBroadcastPreview } from "../../../../../src/stores/room-list/previews/VoiceBroadcastPreview";
|
||||
import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
|
||||
import { mkEvent, stubClient } from "../../../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
|
||||
|
||||
describe("VoiceBroadcastPreview.getTextFor", () => {
|
||||
const roomId = "!room:example.com";
|
||||
const userId = "@user:example.com";
|
||||
const deviceId = "d42";
|
||||
let preview: VoiceBroadcastPreview;
|
||||
|
||||
beforeAll(() => {
|
||||
preview = new VoiceBroadcastPreview();
|
||||
});
|
||||
|
||||
it("when passing an event with empty content, it should return null", () => {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
content: {},
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
});
|
||||
expect(preview.getTextFor(event)).toBeNull();
|
||||
});
|
||||
|
||||
it("when passing a broadcast started event, it should return null", () => {
|
||||
const event = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, userId, deviceId);
|
||||
expect(preview.getTextFor(event)).toBeNull();
|
||||
});
|
||||
|
||||
it("when passing a broadcast stopped event, it should return the expected text", () => {
|
||||
const event = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Stopped, userId, deviceId);
|
||||
expect(preview.getTextFor(event)).toBe("@user:example.com ended a voice broadcast");
|
||||
});
|
||||
|
||||
it("when passing a redacted broadcast stopped event, it should return null", () => {
|
||||
const event = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Stopped, userId, deviceId);
|
||||
event.makeRedacted(
|
||||
mkEvent({ event: true, content: {}, user: userId, type: "m.room.redaction" }),
|
||||
new Room(roomId, stubClient(), userId),
|
||||
);
|
||||
expect(preview.getTextFor(event)).toBeNull();
|
||||
});
|
||||
});
|
88
test/unit-tests/stores/room-list/utils/roomMute-test.ts
Normal file
88
test/unit-tests/stores/room-list/utils/roomMute-test.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { ConditionKind, EventType, IPushRule, MatrixEvent, PushRuleActionName } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { getChangedOverrideRoomMutePushRules } from "../../../../../src/stores/room-list/utils/roomMute";
|
||||
import { DEFAULT_PUSH_RULES, getDefaultRuleWithKind, makePushRule } from "../../../../test-utils/pushRules";
|
||||
|
||||
describe("getChangedOverrideRoomMutePushRules()", () => {
|
||||
const makePushRulesEvent = (overrideRules: IPushRule[] = []): MatrixEvent => {
|
||||
return new MatrixEvent({
|
||||
type: EventType.PushRules,
|
||||
content: {
|
||||
global: {
|
||||
...DEFAULT_PUSH_RULES.global,
|
||||
override: overrideRules,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it("returns undefined when dispatched action is not accountData", () => {
|
||||
const action = { action: "MatrixActions.Event.decrypted", event: new MatrixEvent({}) };
|
||||
expect(getChangedOverrideRoomMutePushRules(action)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when dispatched action is not pushrules", () => {
|
||||
const action = { action: "MatrixActions.accountData", event: new MatrixEvent({ type: "not-push-rules" }) };
|
||||
expect(getChangedOverrideRoomMutePushRules(action)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when actions event is falsy", () => {
|
||||
const action = { action: "MatrixActions.accountData" };
|
||||
expect(getChangedOverrideRoomMutePushRules(action)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when actions previousEvent is falsy", () => {
|
||||
const pushRulesEvent = makePushRulesEvent();
|
||||
const action = { action: "MatrixActions.accountData", event: pushRulesEvent };
|
||||
expect(getChangedOverrideRoomMutePushRules(action)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("filters out non-room specific rules", () => {
|
||||
// an override rule that exists in default rules
|
||||
const { rule } = getDefaultRuleWithKind(".m.rule.contains_display_name");
|
||||
const updatedRule = {
|
||||
...rule,
|
||||
actions: [PushRuleActionName.DontNotify],
|
||||
enabled: false,
|
||||
};
|
||||
const previousEvent = makePushRulesEvent([rule]);
|
||||
const pushRulesEvent = makePushRulesEvent([updatedRule]);
|
||||
const action = { action: "MatrixActions.accountData", event: pushRulesEvent, previousEvent: previousEvent };
|
||||
// contains_display_name changed, but is not room-specific
|
||||
expect(getChangedOverrideRoomMutePushRules(action)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns ruleIds for added room rules", () => {
|
||||
const roomId1 = "!room1:server.org";
|
||||
const rule = makePushRule(roomId1, {
|
||||
actions: [PushRuleActionName.DontNotify],
|
||||
conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: roomId1 }],
|
||||
});
|
||||
const previousEvent = makePushRulesEvent();
|
||||
const pushRulesEvent = makePushRulesEvent([rule]);
|
||||
const action = { action: "MatrixActions.accountData", event: pushRulesEvent, previousEvent: previousEvent };
|
||||
// contains_display_name changed, but is not room-specific
|
||||
expect(getChangedOverrideRoomMutePushRules(action)).toEqual([rule.rule_id]);
|
||||
});
|
||||
|
||||
it("returns ruleIds for removed room rules", () => {
|
||||
const roomId1 = "!room1:server.org";
|
||||
const rule = makePushRule(roomId1, {
|
||||
actions: [PushRuleActionName.DontNotify],
|
||||
conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: roomId1 }],
|
||||
});
|
||||
const previousEvent = makePushRulesEvent([rule]);
|
||||
const pushRulesEvent = makePushRulesEvent();
|
||||
const action = { action: "MatrixActions.accountData", event: pushRulesEvent, previousEvent: previousEvent };
|
||||
// contains_display_name changed, but is not room-specific
|
||||
expect(getChangedOverrideRoomMutePushRules(action)).toEqual([rule.rule_id]);
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue