Skip to content

Commit b8d502b

Browse files
authored
Support dynamic room predecessors in RoomNotificationStateStore (matrix-org#10297)
* Tests for RoomNotificationStateStore emitting events * Support dynamic room predecessors in RoomNotificationStateStore * Remove unused arguments from emit call. UPDATE_STATUS_INDICATOR is used in: * SpacePanel * MatrixChat * RoomHeaderButtons but these arguments are not used in any of those places. Remove them so when I refactor I don't have to make up values for them. * Fix broken test (wrong expected args to emit) UPDATE_STATUS_INDICATOR is used in: * SpacePanel * MatrixChat * RoomHeaderButtons but these arguments are not used in any of those places. Remove them so when I refactor I don't have to make up values for them. * Update the RoomNotificationStore whenever the predecessor labs flag changes * Fix type errors * Fix other tests that trigger our new watcher
1 parent 80fc099 commit b8d502b

File tree

4 files changed

+186
-15
lines changed

4 files changed

+186
-15
lines changed

src/stores/notifications/RoomNotificationStateStore.ts

+34-8
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,19 @@ limitations under the License.
1515
*/
1616

1717
import { Room } from "matrix-js-sdk/src/models/room";
18-
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
18+
import { SyncState } from "matrix-js-sdk/src/sync";
1919
import { ClientEvent } from "matrix-js-sdk/src/client";
2020

2121
import { ActionPayload } from "../../dispatcher/payloads";
2222
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
23-
import defaultDispatcher from "../../dispatcher/dispatcher";
23+
import defaultDispatcher, { MatrixDispatcher } from "../../dispatcher/dispatcher";
2424
import { DefaultTagID, TagID } from "../room-list/models";
2525
import { FetchRoomFn, ListNotificationState } from "./ListNotificationState";
2626
import { RoomNotificationState } from "./RoomNotificationState";
2727
import { SummarizedNotificationState } from "./SummarizedNotificationState";
2828
import { VisibilityProvider } from "../room-list/filters/VisibilityProvider";
2929
import { PosthogAnalytics } from "../../PosthogAnalytics";
30+
import SettingsStore from "../../settings/SettingsStore";
3031

3132
interface IState {}
3233

@@ -43,8 +44,22 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
4344
private listMap = new Map<TagID, ListNotificationState>();
4445
private _globalState = new SummarizedNotificationState();
4546

46-
private constructor() {
47-
super(defaultDispatcher, {});
47+
private constructor(dispatcher = defaultDispatcher) {
48+
super(dispatcher, {});
49+
SettingsStore.watchSetting("feature_dynamic_room_predecessors", null, () => {
50+
// We pass SyncState.Syncing here to "simulate" a sync happening.
51+
// The code that receives these events actually doesn't care
52+
// what state we pass, except that it behaves differently if we
53+
// pass SyncState.Error.
54+
this.emitUpdateIfStateChanged(SyncState.Syncing, false);
55+
});
56+
}
57+
58+
/**
59+
* @internal Public for test only
60+
*/
61+
public static testInstance(dispatcher: MatrixDispatcher): RoomNotificationStateStore {
62+
return new RoomNotificationStateStore();
4863
}
4964

5065
/**
@@ -93,11 +108,22 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
93108
return RoomNotificationStateStore.internalInstance;
94109
}
95110

96-
private onSync = (state: SyncState, prevState: SyncState | null, data?: ISyncStateData): void => {
111+
private onSync = (state: SyncState, prevState: SyncState | null): void => {
112+
this.emitUpdateIfStateChanged(state, state !== prevState);
113+
};
114+
115+
/**
116+
* If the SummarizedNotificationState of this room has changed, or forceEmit
117+
* is true, emit an UPDATE_STATUS_INDICATOR event.
118+
*
119+
* @internal public for test
120+
*/
121+
public emitUpdateIfStateChanged = (state: SyncState, forceEmit: boolean): void => {
97122
// Only count visible rooms to not torment the user with notification counts in rooms they can't see.
98123
// This will include highlights from the previous version of the room internally
124+
const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors");
99125
const globalState = new SummarizedNotificationState();
100-
const visibleRooms = this.matrixClient.getVisibleRooms();
126+
const visibleRooms = this.matrixClient.getVisibleRooms(msc3946ProcessDynamicPredecessor);
101127

102128
let numFavourites = 0;
103129
for (const room of visibleRooms) {
@@ -115,10 +141,10 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
115141
this.globalState.count !== globalState.count ||
116142
this.globalState.color !== globalState.color ||
117143
this.globalState.numUnreadStates !== globalState.numUnreadStates ||
118-
state !== prevState
144+
forceEmit
119145
) {
120146
this._globalState = globalState;
121-
this.emit(UPDATE_STATUS_INDICATOR, globalState, state, prevState, data);
147+
this.emit(UPDATE_STATUS_INDICATOR, globalState, state);
122148
}
123149
};
124150

test/components/structures/RoomView-test.tsx

+13-5
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,19 @@ describe("RoomView", () => {
203203
expect(instance.getHiddenHighlightCount()).toBe(23);
204204
});
205205

206-
it("and feature_dynamic_room_predecessors is enabled it should pass the setting to findPredecessor", async () => {
207-
SettingsStore.setValue("feature_dynamic_room_predecessors", null, SettingLevel.DEVICE, true);
208-
expect(instance.getHiddenHighlightCount()).toBe(0);
209-
expect(room.findPredecessor).toHaveBeenCalledWith(true);
210-
SettingsStore.setValue("feature_dynamic_room_predecessors", null, SettingLevel.DEVICE, null);
206+
describe("and feature_dynamic_room_predecessors is enabled", () => {
207+
beforeEach(() => {
208+
instance.setState({ msc3946ProcessDynamicPredecessor: true });
209+
});
210+
211+
afterEach(() => {
212+
instance.setState({ msc3946ProcessDynamicPredecessor: false });
213+
});
214+
215+
it("should pass the setting to findPredecessor", async () => {
216+
expect(instance.getHiddenHighlightCount()).toBe(0);
217+
expect(room.findPredecessor).toHaveBeenCalledWith(true);
218+
});
211219
});
212220
});
213221

test/components/views/dialogs/SpotlightDialog-test.tsx

+8-2
Original file line numberDiff line numberDiff line change
@@ -198,11 +198,17 @@ describe("Spotlight Dialog", () => {
198198

199199
describe("when MSC3946 dynamic room predecessors is enabled", () => {
200200
beforeEach(() => {
201-
SettingsStore.setValue("feature_dynamic_room_predecessors", null, SettingLevel.DEVICE, true);
201+
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId, excludeDefault) => {
202+
if (settingName === "feature_dynamic_room_predecessors") {
203+
return true;
204+
} else {
205+
return []; // SpotlightSearch.recentSearches
206+
}
207+
});
202208
});
203209

204210
afterEach(() => {
205-
SettingsStore.setValue("feature_dynamic_room_predecessors", null, SettingLevel.DEVICE, null);
211+
jest.restoreAllMocks();
206212
});
207213

208214
it("should call getVisibleRooms with MSC3946 dynamic room predecessors", async () => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { mocked } from "jest-mock";
18+
import { ClientEvent, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
19+
import { SyncState } from "matrix-js-sdk/src/sync";
20+
21+
import { createTestClient, setupAsyncStoreWithClient } from "../test-utils";
22+
import {
23+
RoomNotificationStateStore,
24+
UPDATE_STATUS_INDICATOR,
25+
} from "../../src/stores/notifications/RoomNotificationStateStore";
26+
import SettingsStore from "../../src/settings/SettingsStore";
27+
import { MatrixDispatcher } from "../../src/dispatcher/dispatcher";
28+
29+
describe("RoomNotificationStateStore", function () {
30+
let store: RoomNotificationStateStore;
31+
let client: MatrixClient;
32+
let dis: MatrixDispatcher;
33+
34+
beforeEach(() => {
35+
client = createTestClient();
36+
dis = new MatrixDispatcher();
37+
jest.resetAllMocks();
38+
store = RoomNotificationStateStore.testInstance(dis);
39+
store.emit = jest.fn();
40+
setupAsyncStoreWithClient(store, client);
41+
});
42+
43+
it("Emits no event when a room has no unreads", async () => {
44+
// Given a room with 0 unread messages
45+
const room = fakeRoom(0);
46+
47+
// When we sync and the room is visible
48+
mocked(client.getVisibleRooms).mockReturnValue([room]);
49+
client.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing);
50+
51+
// Then we emit an event from the store
52+
expect(store.emit).not.toHaveBeenCalled();
53+
});
54+
55+
it("Emits an event when a room has unreads", async () => {
56+
// Given a room with 2 unread messages
57+
const room = fakeRoom(2);
58+
59+
// When we sync and the room is visible
60+
mocked(client.getVisibleRooms).mockReturnValue([room]);
61+
client.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing);
62+
63+
// Then we emit an event from the store
64+
expect(store.emit).toHaveBeenCalledWith(UPDATE_STATUS_INDICATOR, expect.anything(), "SYNCING");
65+
});
66+
67+
it("Emits an event when a feature flag changes notification state", async () => {
68+
// Given we have synced already
69+
let room = fakeRoom(0);
70+
mocked(store.emit).mockReset();
71+
mocked(client.getVisibleRooms).mockReturnValue([room]);
72+
client.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing);
73+
expect(store.emit).not.toHaveBeenCalled();
74+
75+
// When we update the feature flag and it makes us have a notification
76+
room = fakeRoom(2);
77+
mocked(client.getVisibleRooms).mockReturnValue([room]);
78+
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
79+
store.emitUpdateIfStateChanged(SyncState.Syncing, false);
80+
81+
// Then we get notified
82+
expect(store.emit).toHaveBeenCalledWith(UPDATE_STATUS_INDICATOR, expect.anything(), "SYNCING");
83+
});
84+
85+
describe("If the feature_dynamic_room_predecessors is not enabled", () => {
86+
beforeEach(() => {
87+
// Turn off feature_dynamic_room_predecessors setting
88+
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
89+
});
90+
91+
it("Passes the dynamic predecessor flag to getVisibleRooms", async () => {
92+
// When we sync
93+
mocked(client.getVisibleRooms).mockReturnValue([]);
94+
client.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing);
95+
96+
// Then we check visible rooms, using the dynamic predecessor flag
97+
expect(client.getVisibleRooms).toHaveBeenCalledWith(false);
98+
expect(client.getVisibleRooms).not.toHaveBeenCalledWith(true);
99+
});
100+
});
101+
102+
describe("If the feature_dynamic_room_predecessors is enabled", () => {
103+
beforeEach(() => {
104+
// Turn on feature_dynamic_room_predecessors setting
105+
jest.spyOn(SettingsStore, "getValue").mockImplementation(
106+
(settingName) => settingName === "feature_dynamic_room_predecessors",
107+
);
108+
});
109+
110+
it("Passes the dynamic predecessor flag to getVisibleRooms", async () => {
111+
// When we sync
112+
mocked(client.getVisibleRooms).mockReturnValue([]);
113+
client.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing);
114+
115+
// Then we check visible rooms, using the dynamic predecessor flag
116+
expect(client.getVisibleRooms).toHaveBeenCalledWith(true);
117+
expect(client.getVisibleRooms).not.toHaveBeenCalledWith(false);
118+
});
119+
});
120+
121+
let roomIdx = 0;
122+
123+
function fakeRoom(numUnreads: number): Room {
124+
roomIdx++;
125+
const ret = new Room(`room${roomIdx}`, client, "@user:example.com");
126+
ret.getPendingEvents = jest.fn().mockReturnValue([]);
127+
ret.isSpaceRoom = jest.fn().mockReturnValue(false);
128+
ret.getUnreadNotificationCount = jest.fn().mockReturnValue(numUnreads);
129+
return ret;
130+
}
131+
});

0 commit comments

Comments
 (0)