Skip to content

Commit 8e29f8e

Browse files
Germainclokeprichvdh
authored
Improve hasUserReadEvent and getUserReadUpTo realibility with threads (#3031)
Co-authored-by: Patrick Cloke <[email protected]> Co-authored-by: Richard van der Hoff <[email protected]>
1 parent 185ded4 commit 8e29f8e

File tree

5 files changed

+332
-24
lines changed

5 files changed

+332
-24
lines changed

spec/unit/models/thread.spec.ts

Lines changed: 178 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2022 The Matrix.org Foundation C.I.C.
2+
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -19,8 +19,12 @@ import { Room } from "../../../src/models/room";
1919
import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread";
2020
import { mkThread } from "../../test-utils/thread";
2121
import { TestClient } from "../../TestClient";
22-
import { emitPromise, mkMessage } from "../../test-utils/test-utils";
23-
import { EventStatus } from "../../../src";
22+
import { emitPromise, mkMessage, mock } from "../../test-utils/test-utils";
23+
import { EventStatus, MatrixEvent } from "../../../src";
24+
import { ReceiptType } from "../../../src/@types/read_receipts";
25+
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils/client";
26+
import { ReEmitter } from "../../../src/ReEmitter";
27+
import { Feature, ServerSupport } from "../../../src/feature";
2428

2529
describe("Thread", () => {
2630
describe("constructor", () => {
@@ -71,17 +75,54 @@ describe("Thread", () => {
7175
});
7276

7377
describe("hasUserReadEvent", () => {
74-
const myUserId = "@bob:example.org";
78+
let myUserId: string;
7579
let client: MatrixClient;
7680
let room: Room;
7781

7882
beforeEach(() => {
79-
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, {
80-
timelineSupport: false,
83+
client = getMockClientWithEventEmitter({
84+
...mockClientMethodsUser(),
85+
getRoom: jest.fn().mockImplementation(() => room),
86+
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
87+
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
88+
});
89+
client.reEmitter = mock(ReEmitter, "ReEmitter");
90+
client.canSupport = new Map();
91+
Object.keys(Feature).forEach((feature) => {
92+
client.canSupport.set(feature as Feature, ServerSupport.Stable);
8193
});
82-
client = testClient.client;
94+
95+
myUserId = client.getUserId()!;
96+
8397
room = new Room("123", client, myUserId);
8498

99+
const receipt = new MatrixEvent({
100+
type: "m.receipt",
101+
room_id: "!foo:bar",
102+
content: {
103+
// first threaded receipt
104+
"$event0:localhost": {
105+
[ReceiptType.Read]: {
106+
[client.getUserId()!]: { ts: 100, thread_id: "$threadId:localhost" },
107+
},
108+
},
109+
// last unthreaded receipt
110+
"$event1:localhost": {
111+
[ReceiptType.Read]: {
112+
[client.getUserId()!]: { ts: 200 },
113+
["@alice:example.org"]: { ts: 200 },
114+
},
115+
},
116+
// last threaded receipt
117+
"$event2:localhost": {
118+
[ReceiptType.Read]: {
119+
[client.getUserId()!]: { ts: 300, thread_id: "$threadId" },
120+
},
121+
},
122+
},
123+
});
124+
room.addReceipt(receipt);
125+
85126
jest.spyOn(client, "getRoom").mockReturnValue(room);
86127
});
87128

@@ -98,19 +139,148 @@ describe("Thread", () => {
98139
length: 2,
99140
});
100141

142+
// The event is automatically considered read as the current user is the sender
101143
expect(thread.hasUserReadEvent(myUserId, events.at(-1)!.getId() ?? "")).toBeTruthy();
102144
});
103145

104146
it("considers other events with no RR as unread", () => {
147+
const { thread, events } = mkThread({
148+
room,
149+
client,
150+
authorId: myUserId,
151+
participantUserIds: [myUserId],
152+
length: 25,
153+
ts: 190,
154+
});
155+
156+
// Before alice's last unthreaded receipt
157+
expect(thread.hasUserReadEvent("@alice:example.org", events.at(1)!.getId() ?? "")).toBeTruthy();
158+
159+
// After alice's last unthreaded receipt
160+
expect(thread.hasUserReadEvent("@alice:example.org", events.at(-1)!.getId() ?? "")).toBeFalsy();
161+
});
162+
163+
it("considers event as read if there's a more recent unthreaded receipt", () => {
105164
const { thread, events } = mkThread({
106165
room,
107166
client,
108167
authorId: myUserId,
109168
participantUserIds: ["@alice:example.org"],
110169
length: 2,
170+
ts: 150, // before the latest unthreaded receipt
111171
});
172+
expect(thread.hasUserReadEvent(client.getUserId()!, events.at(-1)!.getId() ?? "")).toBe(true);
173+
});
112174

113-
expect(thread.hasUserReadEvent("@alice:example.org", events.at(-1)!.getId() ?? "")).toBeFalsy();
175+
it("considers event as unread if there's no more recent unthreaded receipt", () => {
176+
const { thread, events } = mkThread({
177+
room,
178+
client,
179+
authorId: myUserId,
180+
participantUserIds: ["@alice:example.org"],
181+
length: 2,
182+
ts: 1000,
183+
});
184+
expect(thread.hasUserReadEvent(client.getUserId()!, events.at(-1)!.getId() ?? "")).toBe(false);
185+
});
186+
});
187+
188+
describe("getEventReadUpTo", () => {
189+
let myUserId: string;
190+
let client: MatrixClient;
191+
let room: Room;
192+
193+
beforeEach(() => {
194+
client = getMockClientWithEventEmitter({
195+
...mockClientMethodsUser(),
196+
getRoom: jest.fn().mockImplementation(() => room),
197+
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
198+
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
199+
});
200+
client.reEmitter = mock(ReEmitter, "ReEmitter");
201+
client.canSupport = new Map();
202+
Object.keys(Feature).forEach((feature) => {
203+
client.canSupport.set(feature as Feature, ServerSupport.Stable);
204+
});
205+
206+
myUserId = client.getUserId()!;
207+
208+
room = new Room("123", client, myUserId);
209+
210+
jest.spyOn(client, "getRoom").mockReturnValue(room);
211+
});
212+
213+
afterAll(() => {
214+
jest.resetAllMocks();
215+
});
216+
217+
it("uses unthreaded receipt to figure out read up to", () => {
218+
const receipt = new MatrixEvent({
219+
type: "m.receipt",
220+
room_id: "!foo:bar",
221+
content: {
222+
// last unthreaded receipt
223+
"$event1:localhost": {
224+
[ReceiptType.Read]: {
225+
["@alice:example.org"]: { ts: 200 },
226+
},
227+
},
228+
},
229+
});
230+
room.addReceipt(receipt);
231+
232+
const { thread, events } = mkThread({
233+
room,
234+
client,
235+
authorId: myUserId,
236+
participantUserIds: [myUserId],
237+
length: 25,
238+
ts: 190,
239+
});
240+
241+
// The 10th event has been read, as alice's last unthreaded receipt is at ts 200
242+
// and `mkThread` increment every thread response by 1ms.
243+
expect(thread.getEventReadUpTo("@alice:example.org")).toBe(events.at(9)!.getId());
244+
});
245+
246+
it("considers thread created before the first threaded receipt to be read", () => {
247+
const receipt = new MatrixEvent({
248+
type: "m.receipt",
249+
room_id: "!foo:bar",
250+
content: {
251+
// last unthreaded receipt
252+
"$event1:localhost": {
253+
[ReceiptType.Read]: {
254+
[myUserId]: { ts: 200, thread_id: "$threadId" },
255+
},
256+
},
257+
},
258+
});
259+
room.addReceipt(receipt);
260+
261+
const { thread, events } = mkThread({
262+
room,
263+
client,
264+
authorId: "@alice:example.org",
265+
participantUserIds: ["@alice:example.org"],
266+
length: 2,
267+
ts: 10,
268+
});
269+
270+
// This is marked as read as it is before alice's first threaded receipt...
271+
expect(thread.getEventReadUpTo(myUserId)).toBe(events.at(-1)!.getId());
272+
273+
const { thread: thread2 } = mkThread({
274+
room,
275+
client,
276+
authorId: "@alice:example.org",
277+
participantUserIds: ["@alice:example.org"],
278+
length: 2,
279+
ts: 1000,
280+
});
281+
282+
// Nothing has been read, this thread is after the first threaded receipt...
283+
expect(thread2.getEventReadUpTo(myUserId)).toBe(null);
114284
});
115285
});
116286
});

spec/unit/notifications.spec.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2022 The Matrix.org Foundation C.I.C.
2+
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import { ReceiptType } from "../../src/@types/read_receipts";
1718
import { Feature, ServerSupport } from "../../src/feature";
1819
import {
1920
EventType,
@@ -64,6 +65,30 @@ describe("fixNotificationCountOnDecryption", () => {
6465
});
6566

6667
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "");
68+
69+
const receipt = new MatrixEvent({
70+
type: "m.receipt",
71+
room_id: "!foo:bar",
72+
content: {
73+
"$event0:localhost": {
74+
[ReceiptType.Read]: {
75+
[mockClient.getUserId()!]: { ts: 123 },
76+
},
77+
},
78+
"$event1:localhost": {
79+
[ReceiptType.Read]: {
80+
[mockClient.getUserId()!]: { ts: 666, thread_id: THREAD_ID },
81+
},
82+
},
83+
"$otherevent:localhost": {
84+
[ReceiptType.Read]: {
85+
[mockClient.getUserId()!]: { ts: 999, thread_id: "$otherthread:localhost" },
86+
},
87+
},
88+
},
89+
});
90+
room.addReceipt(receipt);
91+
6792
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
6893
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
6994

@@ -75,6 +100,7 @@ describe("fixNotificationCountOnDecryption", () => {
75100
body: "Hello world!",
76101
},
77102
event: true,
103+
ts: 1234,
78104
},
79105
mockClient,
80106
);
@@ -90,6 +116,7 @@ describe("fixNotificationCountOnDecryption", () => {
90116
"msgtype": MsgType.Text,
91117
"body": "Thread reply",
92118
},
119+
ts: 5678,
93120
event: true,
94121
});
95122
room.createThread(THREAD_ID, event, [threadEvent], false);
@@ -155,6 +182,7 @@ describe("fixNotificationCountOnDecryption", () => {
155182
"msgtype": MsgType.Text,
156183
"body": "Thread reply",
157184
},
185+
ts: 8901,
158186
event: true,
159187
});
160188

spec/unit/room.spec.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { ReceiptType, WrappedReceipt } from "../../src/@types/read_receipts";
5050
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../src/models/thread";
5151
import { Crypto } from "../../src/crypto";
5252
import { mkThread } from "../test-utils/thread";
53+
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client";
5354

5455
describe("Room", function () {
5556
const roomId = "!foo:bar";
@@ -3228,6 +3229,14 @@ describe("Room", function () {
32283229
});
32293230

32303231
describe("findPredecessorRoomId", () => {
3232+
let client: MatrixClient | null = null;
3233+
beforeEach(() => {
3234+
client = getMockClientWithEventEmitter({
3235+
...mockClientMethodsUser(),
3236+
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
3237+
});
3238+
});
3239+
32313240
function roomCreateEvent(newRoomId: string, predecessorRoomId: string | null): MatrixEvent {
32323241
const content: {
32333242
creator: string;
@@ -3258,18 +3267,18 @@ describe("Room", function () {
32583267
}
32593268

32603269
it("Returns null if there is no create event", () => {
3261-
const room = new Room("roomid", null as unknown as MatrixClient, "@u:example.com");
3270+
const room = new Room("roomid", client!, "@u:example.com");
32623271
expect(room.findPredecessorRoomId()).toBeNull();
32633272
});
32643273

32653274
it("Returns null if the create event has no predecessor", () => {
3266-
const room = new Room("roomid", null as unknown as MatrixClient, "@u:example.com");
3275+
const room = new Room("roomid", client!, "@u:example.com");
32673276
room.addLiveEvents([roomCreateEvent("roomid", null)]);
32683277
expect(room.findPredecessorRoomId()).toBeNull();
32693278
});
32703279

32713280
it("Returns the predecessor ID if one is provided via create event", () => {
3272-
const room = new Room("roomid", null as unknown as MatrixClient, "@u:example.com");
3281+
const room = new Room("roomid", client!, "@u:example.com");
32733282
room.addLiveEvents([roomCreateEvent("roomid", "replacedroomid")]);
32743283
expect(room.findPredecessorRoomId()).toBe("replacedroomid");
32753284
});

0 commit comments

Comments
 (0)