Skip to content

Commit

Permalink
Update the store on action
Browse files Browse the repository at this point in the history
  • Loading branch information
MidhunSureshR committed Mar 2, 2025
1 parent f9754ea commit c6ff1e7
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 7 deletions.
94 changes: 92 additions & 2 deletions src/stores/room-list-v3/RoomListStoreV3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/

import type { EmptyObject, Room } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { EventType } from "matrix-js-sdk/src/matrix";

import type { EmptyObject, Room, RoomState } from "matrix-js-sdk/src/matrix";
import type { MatrixDispatcher } from "../../dispatcher/dispatcher";
import type { ActionPayload } from "../../dispatcher/payloads";
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
Expand All @@ -16,6 +19,8 @@ import { LISTS_UPDATE_EVENT } from "../room-list/RoomListStore";
import { RoomSkipList } from "./skip-list/RoomSkipList";
import { RecencySorter } from "./skip-list/sorters/RecencySorter";
import { AlphabeticSorter } from "./skip-list/sorters/AlphabeticSorter";
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
import { EffectiveMembership, getEffectiveMembership, getEffectiveMembershipTag } from "../../utils/membership";

/**
* This store allows for fast retrieval of the room list in a sorted and filtered manner.
Expand Down Expand Up @@ -78,7 +83,92 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
}

protected async onAction(payload: ActionPayload): Promise<void> {
return;
if (!this.matrixClient || !this.roomSkipList?.initialized) return;

switch (payload.action) {
case "MatrixActions.Room.receipt": {
if (readReceiptChangeIsFor(payload.event, this.matrixClient)) {
const room = payload.room;
if (!room) {
logger.warn(`Own read receipt was in unknown room ${room.roomId}`);
return;
}
this.addRoomAndEmit(room);
}
break;
}
case "MatrixActions.Room.tags": {
const room = payload.room;
this.addRoomAndEmit(room);
break;
}
case "MatrixActions.Event.decrypted": {
const roomId = payload.event.getRoomId();
if (!roomId) return;
const room = this.matrixClient.getRoom(roomId);
if (!room) {
logger.warn(`Event ${payload.event.getId()} was decrypted in an unknown room ${roomId}`);
return;
}
this.addRoomAndEmit(room);
break;
}
case "MatrixActions.accountData": {
if (payload.event_type !== EventType.Direct) return;
const dmMap = payload.event.getContent();
for (const userId of Object.keys(dmMap)) {
const roomIds = dmMap[userId];
for (const roomId of roomIds) {
const room = this.matrixClient.getRoom(roomId);
if (!room) {
logger.warn(`${roomId} was found in DMs but the room is not in the store`);
continue;
}
this.addRoomAndEmit(room);
}
}
break;
}
case "MatrixActions.Room.timeline": {
// Ignore non-live events (backfill) and notification timeline set events (without a room)
if (!payload.isLiveEvent || !payload.isLiveUnfilteredRoomTimelineEvent || !payload.room) return;

const roomId = payload.event.getRoomId();
const tryAdd = (): boolean => {
const room = this.matrixClient?.getRoom(roomId);
if (room) this.addRoomAndEmit(room);
return !!room;
};
if (!tryAdd()) setTimeout(tryAdd, 100);
break;
}
case "MatrixActions.Room.myMembership": {
const oldMembership = getEffectiveMembership(payload.oldMembership);
const newMembership = getEffectiveMembershipTag(payload.room, payload.membership);
if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) {
// If we're joining an upgraded room, we'll want to make sure we don't proliferate
// the dead room in the list.
const roomState: RoomState = payload.room.currentState;
const predecessor = roomState.findPredecessor(this.msc3946ProcessDynamicPredecessor);
if (predecessor) {
const prevRoom = this.matrixClient?.getRoom(predecessor.roomId);
if (prevRoom) {
this.roomSkipList.removeRoom(prevRoom);
} else {
logger.warn(`Unable to find predecessor room with id ${predecessor.roomId}`);
}
}
}
this.addRoomAndEmit(payload.room);
break;
}
}
}

private addRoomAndEmit(room: Room): void {
if (!this.roomSkipList) throw new Error("roomSkipList hasn't been created yet!");
this.roomSkipList.addRoom(room);
this.emit(LISTS_UPDATE_EVENT);
}
}

Expand Down
115 changes: 110 additions & 5 deletions test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,26 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/

import type { MatrixDispatcher } from "../../../../src/dispatcher/dispatcher";
import { EventType, KnownMembership, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";

import { RoomListStoreV3Class } from "../../../../src/stores/room-list-v3/RoomListStoreV3";
import { AsyncStoreWithClient } from "../../../../src/stores/AsyncStoreWithClient";
import { RecencySorter } from "../../../../src/stores/room-list-v3/skip-list/sorters/RecencySorter";
import { stubClient } from "../../../test-utils";
import { mkEvent, mkMessage, stubClient, upsertRoomStateEvents } from "../../../test-utils";
import { getMockedRooms } from "./skip-list/getMockedRooms";
import { AlphabeticSorter } from "../../../../src/stores/room-list-v3/skip-list/sorters/AlphabeticSorter";
import dispatcher from "../../../../src/dispatcher/dispatcher";
import { LISTS_UPDATE_EVENT } from "../../../../src/stores/room-list/RoomListStore";

describe("RoomListStoreV3", () => {
async function getRoomListStore() {
const client = stubClient();
const rooms = getMockedRooms(client);
client.getVisibleRooms = jest.fn().mockReturnValue(rooms);
jest.spyOn(AsyncStoreWithClient.prototype, "matrixClient", "get").mockReturnValue(client);
const fakeDispatcher = { register: jest.fn() } as unknown as MatrixDispatcher;
const store = new RoomListStoreV3Class(fakeDispatcher);
const store = new RoomListStoreV3Class(dispatcher);
store.start();
return { client, rooms, store };
return { client, rooms, store, dispatcher };
}

it("Provides an unsorted list of rooms", async () => {
Expand Down Expand Up @@ -50,4 +52,107 @@ describe("RoomListStoreV3", () => {
sortedRooms = new RecencySorter(client.getSafeUserId()).sort(rooms);
expect(store.getSortedRooms()).toEqual(sortedRooms);
});

describe("Updates", () => {
it("Room is re-inserted on timeline event", async () => {
const { store, rooms, dispatcher } = await getRoomListStore();

// Let's pretend like a new timeline event came on the room in 37th index.
const room = rooms[37];
const event = mkMessage({ room: room.roomId, user: `@foo${3}:matrix.org`, ts: 1000, event: true });
room.timeline.push(event);

const payload = {
action: "MatrixActions.Room.timeline",
event,
isLiveEvent: true,
isLiveUnfilteredRoomTimelineEvent: true,
room,
};

const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
dispatcher.dispatch(payload, true);

expect(fn).toHaveBeenCalled();
expect(store.getSortedRooms()[0].roomId).toEqual(room.roomId);
});

it("Predecessor room is removed on room upgrade", async () => {
const { store, rooms, client, dispatcher } = await getRoomListStore();
// Let's say that !foo32:matrix.org is being upgraded
const oldRoom = rooms[32];
// Create a new room with a predecessor event that points to oldRoom
const newRoom = new Room("!foonew:matrix.org", client, client.getSafeUserId(), {});
const createWithPredecessor = new MatrixEvent({
type: EventType.RoomCreate,
sender: "@foo:foo.org",
room_id: newRoom.roomId,
content: {
predecessor: { room_id: oldRoom.roomId, event_id: "tombstone_event_id" },
},
event_id: "$create",
state_key: "",
});
upsertRoomStateEvents(newRoom, [createWithPredecessor]);

const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
dispatcher.dispatch(
{
action: "MatrixActions.Room.myMembership",
oldMembership: KnownMembership.Invite,
membership: KnownMembership.Join,
room: newRoom,
},
true,
);

expect(fn).toHaveBeenCalled();
const roomIds = store.getSortedRooms().map((r) => r.roomId);
expect(roomIds).not.toContain(oldRoom.roomId);
expect(roomIds).toContain(newRoom.roomId);
});

it("Rooms are inserted on m.direct event", async () => {
const { store, dispatcher } = await getRoomListStore();

// Let's create a m.direct event that we can dispatch
const content = {
"@bar1:matrix.org": ["!newroom1:matrix.org", "!newroom2:matrix.org"],
"@bar2:matrix.org": ["!newroom3:matrix.org", "!newroom4:matrix.org"],
"@bar3:matrix.org": ["!newroom5:matrix.org"],
};
const event = mkEvent({
event: true,
content,
user: "@foo:matrix.org",
type: EventType.Direct,
});

const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
dispatcher.dispatch(
{
action: "MatrixActions.accountData",
event_type: EventType.Direct,
event,
},
true,
);

// Each of these rooms should now appear in the store
// We don't need to mock the rooms themselves since our mocked
// client will create the rooms on getRoom() call.
expect(fn).toHaveBeenCalledTimes(5);
const roomIds = store.getSortedRooms().map((r) => r.roomId);
[
"!newroom1:matrix.org",
"!newroom2:matrix.org",
"!newroom3:matrix.org",
"!newroom4:matrix.org",
"!newroom5:matrix.org",
].forEach((id) => expect(roomIds).toContain(id));
});
});
});

0 comments on commit c6ff1e7

Please sign in to comment.