Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 203c15f

Browse files
authored
Update the TAC indicator on event decryption (#12243)
* Update the TAC indicator on event decryption But throttled so we don't kill the client Fixes element-hq/element-web#26990 * Just pass the function
1 parent 36c07be commit 203c15f

File tree

2 files changed

+148
-7
lines changed

2 files changed

+148
-7
lines changed

src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
* /
1717
*/
1818

19-
import { useEffect, useState } from "react";
20-
import { ClientEvent, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
19+
import { useCallback, useEffect, useState } from "react";
20+
import { ClientEvent, MatrixClient, MatrixEventEvent, Room } from "matrix-js-sdk/src/matrix";
21+
import { throttle } from "lodash";
2122

2223
import { doesRoomHaveUnreadThreads } from "../../../../Unread";
2324
import { NotificationLevel } from "../../../../stores/notifications/NotificationLevel";
@@ -27,6 +28,8 @@ import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext
2728
import { useEventEmitter } from "../../../../hooks/useEventEmitter";
2829
import { VisibilityProvider } from "../../../../stores/room-list/filters/VisibilityProvider";
2930

31+
const MIN_UPDATE_INTERVAL_MS = 500;
32+
3033
type Result = {
3134
greatestNotificationLevel: NotificationLevel;
3235
rooms: Array<{ room: Room; notificationLevel: NotificationLevel }>;
@@ -44,17 +47,33 @@ export function useUnreadThreadRooms(forceComputation: boolean): Result {
4447

4548
const [result, setResult] = useState<Result>({ greatestNotificationLevel: NotificationLevel.None, rooms: [] });
4649

47-
// Listen to sync events to update the result
48-
useEventEmitter(mxClient, ClientEvent.Sync, () => {
50+
const doUpdate = useCallback(() => {
4951
setResult(computeUnreadThreadRooms(mxClient, msc3946ProcessDynamicPredecessor));
50-
});
52+
}, [mxClient, msc3946ProcessDynamicPredecessor]);
53+
54+
// The exhautive deps lint rule can't compute dependencies here since it's not a plain inline func.
55+
// We make this as simple as possible so its only dep is doUpdate itself.
56+
// eslint-disable-next-line react-hooks/exhaustive-deps
57+
const scheduleUpdate = useCallback(
58+
throttle(doUpdate, MIN_UPDATE_INTERVAL_MS, {
59+
leading: false,
60+
trailing: true,
61+
}),
62+
[doUpdate],
63+
);
64+
65+
// Listen to sync events to update the result
66+
useEventEmitter(mxClient, ClientEvent.Sync, scheduleUpdate);
67+
// and also when events get decrypted, since this will often happen after the sync
68+
// event and may change notifications.
69+
useEventEmitter(mxClient, MatrixEventEvent.Decrypted, scheduleUpdate);
5170

5271
// Force the list computation
5372
useEffect(() => {
5473
if (forceComputation) {
55-
setResult(computeUnreadThreadRooms(mxClient, msc3946ProcessDynamicPredecessor));
74+
doUpdate();
5675
}
57-
}, [mxClient, msc3946ProcessDynamicPredecessor, forceComputation]);
76+
}, [doUpdate, forceComputation]);
5877

5978
return result;
6079
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright 2024 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 React from "react";
18+
import { renderHook } from "@testing-library/react-hooks";
19+
import {
20+
MatrixClient,
21+
MatrixEventEvent,
22+
NotificationCountType,
23+
PendingEventOrdering,
24+
Room,
25+
} from "matrix-js-sdk/src/matrix";
26+
import { act } from "@testing-library/react";
27+
28+
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
29+
import { stubClient } from "../../../test-utils";
30+
import { populateThread } from "../../../test-utils/threads";
31+
import { NotificationLevel } from "../../../../src/stores/notifications/NotificationLevel";
32+
import { useUnreadThreadRooms } from "../../../../src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms";
33+
34+
describe("useUnreadThreadRooms", () => {
35+
let client: MatrixClient;
36+
let room: Room;
37+
38+
beforeEach(() => {
39+
client = stubClient();
40+
client.supportsThreads = () => true;
41+
room = new Room("!room1:example.org", client, "@fee:bar", {
42+
pendingEventOrdering: PendingEventOrdering.Detached,
43+
});
44+
});
45+
46+
it("has no notifications with no rooms", async () => {
47+
const { result } = renderHook(() => useUnreadThreadRooms(false));
48+
const { greatestNotificationLevel, rooms } = result.current;
49+
50+
expect(greatestNotificationLevel).toBe(NotificationLevel.None);
51+
expect(rooms.length).toEqual(0);
52+
});
53+
54+
it("a notification and a highlight summarise to a highlight", async () => {
55+
const notifThreadInfo = await populateThread({
56+
room: room,
57+
client: client,
58+
authorId: "@foo:bar",
59+
participantUserIds: ["@fee:bar"],
60+
});
61+
room.setThreadUnreadNotificationCount(notifThreadInfo.thread.id, NotificationCountType.Total, 1);
62+
63+
const highlightThreadInfo = await populateThread({
64+
room: room,
65+
client: client,
66+
authorId: "@foo:bar",
67+
participantUserIds: ["@fee:bar"],
68+
});
69+
room.setThreadUnreadNotificationCount(highlightThreadInfo.thread.id, NotificationCountType.Highlight, 1);
70+
71+
client.getVisibleRooms = jest.fn().mockReturnValue([room]);
72+
73+
const wrapper = ({ children }: { children: React.ReactNode }) => (
74+
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
75+
);
76+
77+
const { result } = renderHook(() => useUnreadThreadRooms(true), { wrapper });
78+
const { greatestNotificationLevel, rooms } = result.current;
79+
80+
expect(greatestNotificationLevel).toBe(NotificationLevel.Highlight);
81+
expect(rooms.length).toEqual(1);
82+
});
83+
84+
describe("updates", () => {
85+
beforeEach(() => {
86+
jest.useFakeTimers();
87+
});
88+
89+
afterEach(() => {
90+
jest.useRealTimers();
91+
});
92+
93+
it("updates on decryption within 1s", async () => {
94+
const notifThreadInfo = await populateThread({
95+
room: room,
96+
client: client,
97+
authorId: "@foo:bar",
98+
participantUserIds: ["@fee:bar"],
99+
});
100+
room.setThreadUnreadNotificationCount(notifThreadInfo.thread.id, NotificationCountType.Total, 0);
101+
102+
client.getVisibleRooms = jest.fn().mockReturnValue([room]);
103+
104+
const wrapper = ({ children }: { children: React.ReactNode }) => (
105+
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
106+
);
107+
108+
const { result } = renderHook(() => useUnreadThreadRooms(true), { wrapper });
109+
110+
expect(result.current.greatestNotificationLevel).toBe(NotificationLevel.Activity);
111+
112+
act(() => {
113+
room.setThreadUnreadNotificationCount(notifThreadInfo.thread.id, NotificationCountType.Highlight, 1);
114+
client.emit(MatrixEventEvent.Decrypted, notifThreadInfo.thread.events[0]);
115+
116+
jest.advanceTimersByTime(1000);
117+
});
118+
119+
expect(result.current.greatestNotificationLevel).toBe(NotificationLevel.Highlight);
120+
});
121+
});
122+
});

0 commit comments

Comments
 (0)