356 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			356 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
/*
 | 
						|
Copyright 2022 The Matrix.org Foundation C.I.C.
 | 
						|
 | 
						|
Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
you may not use this file except in compliance with the License.
 | 
						|
You may obtain a copy of the License at
 | 
						|
 | 
						|
    http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
Unless required by applicable law or agreed to in writing, software
 | 
						|
distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
See the License for the specific language governing permissions and
 | 
						|
limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
import {
 | 
						|
    ConditionKind,
 | 
						|
    EventType,
 | 
						|
    IPushRule,
 | 
						|
    MatrixEvent,
 | 
						|
    PendingEventOrdering,
 | 
						|
    PushRuleActionName,
 | 
						|
    Room,
 | 
						|
} from "matrix-js-sdk/src/matrix";
 | 
						|
 | 
						|
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 { OrderedDefaultTagIDs, RoomUpdateCause } from "../../../src/stores/room-list/models";
 | 
						|
import RoomListStore, { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore";
 | 
						|
import DMRoomMap from "../../../src/utils/DMRoomMap";
 | 
						|
import { flushPromises, stubClient, upsertRoomStateEvents } from "../../test-utils";
 | 
						|
import { DEFAULT_PUSH_RULES, makePushRule } from "../../test-utils/pushRules";
 | 
						|
 | 
						|
describe("RoomListStore", () => {
 | 
						|
    const client = stubClient();
 | 
						|
    const newRoomId = "!roomid:example.com";
 | 
						|
    const roomNoPredecessorId = "!roomnopreid:example.com";
 | 
						|
    const oldRoomId = "!oldroomid:example.com";
 | 
						|
    const userId = "@user:example.com";
 | 
						|
    const createWithPredecessor = new MatrixEvent({
 | 
						|
        type: EventType.RoomCreate,
 | 
						|
        sender: userId,
 | 
						|
        room_id: newRoomId,
 | 
						|
        content: {
 | 
						|
            predecessor: { room_id: oldRoomId, event_id: "tombstone_event_id" },
 | 
						|
        },
 | 
						|
        event_id: "$create",
 | 
						|
        state_key: "",
 | 
						|
    });
 | 
						|
    const createNoPredecessor = new MatrixEvent({
 | 
						|
        type: EventType.RoomCreate,
 | 
						|
        sender: userId,
 | 
						|
        room_id: newRoomId,
 | 
						|
        content: {},
 | 
						|
        event_id: "$create",
 | 
						|
        state_key: "",
 | 
						|
    });
 | 
						|
    const predecessor = new MatrixEvent({
 | 
						|
        type: EventType.RoomPredecessor,
 | 
						|
        sender: userId,
 | 
						|
        room_id: newRoomId,
 | 
						|
        content: {
 | 
						|
            predecessor_room_id: oldRoomId,
 | 
						|
            last_known_event_id: "tombstone_event_id",
 | 
						|
        },
 | 
						|
        event_id: "$pred",
 | 
						|
        state_key: "",
 | 
						|
    });
 | 
						|
    const roomWithPredecessorEvent = new Room(newRoomId, client, userId, {});
 | 
						|
    upsertRoomStateEvents(roomWithPredecessorEvent, [predecessor]);
 | 
						|
    const roomWithCreatePredecessor = new Room(newRoomId, client, userId, {});
 | 
						|
    upsertRoomStateEvents(roomWithCreatePredecessor, [createWithPredecessor]);
 | 
						|
    const roomNoPredecessor = new Room(roomNoPredecessorId, client, userId, {
 | 
						|
        pendingEventOrdering: PendingEventOrdering.Detached,
 | 
						|
    });
 | 
						|
    upsertRoomStateEvents(roomNoPredecessor, [createNoPredecessor]);
 | 
						|
    const oldRoom = new Room(oldRoomId, client, userId, {});
 | 
						|
    const normalRoom = new Room("!normal:server.org", client, userId);
 | 
						|
    client.getRoom = jest.fn().mockImplementation((roomId) => {
 | 
						|
        switch (roomId) {
 | 
						|
            case newRoomId:
 | 
						|
                return roomWithCreatePredecessor;
 | 
						|
            case oldRoomId:
 | 
						|
                return oldRoom;
 | 
						|
            case normalRoom.roomId:
 | 
						|
                return normalRoom;
 | 
						|
            default:
 | 
						|
                return null;
 | 
						|
        }
 | 
						|
    });
 | 
						|
 | 
						|
    beforeAll(async () => {
 | 
						|
        await (RoomListStore.instance as RoomListStoreClass).makeReady(client);
 | 
						|
    });
 | 
						|
 | 
						|
    it.each(OrderedDefaultTagIDs)("defaults to importance ordering for %s=", (tagId) => {
 | 
						|
        expect(RoomListStore.instance.getTagSorting(tagId)).toBe(SortAlgorithm.Recent);
 | 
						|
    });
 | 
						|
 | 
						|
    it.each(OrderedDefaultTagIDs)("defaults to activity ordering for %s=", (tagId) => {
 | 
						|
        expect(RoomListStore.instance.getListOrder(tagId)).toBe(ListAlgorithm.Natural);
 | 
						|
    });
 | 
						|
 | 
						|
    function createStore(): { store: RoomListStoreClass; handleRoomUpdate: jest.Mock<any, any> } {
 | 
						|
        const fakeDispatcher = { register: jest.fn() } as unknown as MatrixDispatcher;
 | 
						|
        const store = new RoomListStoreClass(fakeDispatcher);
 | 
						|
        // @ts-ignore accessing private member to set client
 | 
						|
        store.readyStore.matrixClient = client;
 | 
						|
        const handleRoomUpdate = jest.fn();
 | 
						|
        // @ts-ignore accessing private member to mock it
 | 
						|
        store.algorithm.handleRoomUpdate = handleRoomUpdate;
 | 
						|
 | 
						|
        return { store, handleRoomUpdate };
 | 
						|
    }
 | 
						|
 | 
						|
    it("Removes old room if it finds a predecessor in the create event", () => {
 | 
						|
        // Given a store we can spy on
 | 
						|
        const { store, handleRoomUpdate } = createStore();
 | 
						|
 | 
						|
        // When we tell it we joined a new room that has an old room as
 | 
						|
        // predecessor in the create event
 | 
						|
        const payload = {
 | 
						|
            oldMembership: "invite",
 | 
						|
            membership: "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: "invite",
 | 
						|
            membership: "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("join");
 | 
						|
        room2.updateMyMembership("join");
 | 
						|
        room3.updateMyMembership("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: "invite",
 | 
						|
                membership: "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 });
 | 
						|
                // flush setImmediate
 | 
						|
                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 });
 | 
						|
                // flush setImmediate
 | 
						|
                await flushPromises();
 | 
						|
 | 
						|
                // only one call to update made for normalRoom
 | 
						|
                expect(algorithmSpy).toHaveBeenCalledTimes(1);
 | 
						|
                expect(algorithmSpy).toHaveBeenCalledWith(normalRoom, RoomUpdateCause.PossibleMuteChange);
 | 
						|
            });
 | 
						|
        });
 | 
						|
    });
 | 
						|
});
 |