Skip to content

Commit 66492e7

Browse files
authored
Fix edge cases around non-thread relations to thread roots and read receipts (#3607)
* Ensure non-thread relations to a thread root are actually in both timelines * Make thread in sendReceipt & sendReadReceipt explicit rather than guessing it * Apply suggestions from code review * Fix Room::eventShouldLiveIn to better match Synapse to diverging ideas of notifications * Update read receipt sending behaviour to align with Synapse * Fix tests * Fix thread rel type
1 parent 43b2404 commit 66492e7

File tree

7 files changed

+178
-101
lines changed

7 files changed

+178
-101
lines changed

spec/integ/matrix-client-event-timeline.spec.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1284,7 +1284,6 @@ describe("MatrixClient event timelines", function () {
12841284
THREAD_ROOT.event_id,
12851285
THREAD_REPLY.event_id,
12861286
THREAD_REPLY2.getId(),
1287-
THREAD_ROOT_REACTION.getId(),
12881287
THREAD_REPLY3.getId(),
12891288
]);
12901289
});

spec/integ/matrix-client-methods.spec.ts

+11-27
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,7 @@ describe("MatrixClient", function () {
656656
expect(threaded).toEqual([]);
657657
});
658658

659-
it("copies pre-thread in-timeline vote events onto both timelines", function () {
659+
it("should not copy pre-thread in-timeline vote events onto both timelines", function () {
660660
// @ts-ignore setting private property
661661
client.clientOpts = {
662662
...defaultClientOpts,
@@ -684,10 +684,10 @@ describe("MatrixClient", function () {
684684
const eventRefWithThreadId = withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()!);
685685
expect(eventRefWithThreadId.threadRootId).toBeTruthy();
686686

687-
expect(threaded).toEqual([eventPollStartThreadRoot, eventMessageInThread, eventRefWithThreadId]);
687+
expect(threaded).toEqual([eventPollStartThreadRoot, eventMessageInThread]);
688688
});
689689

690-
it("copies pre-thread in-timeline reactions onto both timelines", function () {
690+
it("should not copy pre-thread in-timeline reactions onto both timelines", function () {
691691
// @ts-ignore setting private property
692692
client.clientOpts = {
693693
...defaultClientOpts,
@@ -704,14 +704,10 @@ describe("MatrixClient", function () {
704704

705705
expect(timeline).toEqual([eventPollStartThreadRoot, eventReaction]);
706706

707-
expect(threaded).toEqual([
708-
eventPollStartThreadRoot,
709-
eventMessageInThread,
710-
withThreadId(eventReaction, eventPollStartThreadRoot.getId()!),
711-
]);
707+
expect(threaded).toEqual([eventPollStartThreadRoot, eventMessageInThread]);
712708
});
713709

714-
it("copies post-thread in-timeline vote events onto both timelines", function () {
710+
it("should not copy post-thread in-timeline vote events onto both timelines", function () {
715711
// @ts-ignore setting private property
716712
client.clientOpts = {
717713
...defaultClientOpts,
@@ -728,14 +724,10 @@ describe("MatrixClient", function () {
728724

729725
expect(timeline).toEqual([eventPollStartThreadRoot, eventPollResponseReference]);
730726

731-
expect(threaded).toEqual([
732-
eventPollStartThreadRoot,
733-
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()!),
734-
eventMessageInThread,
735-
]);
727+
expect(threaded).toEqual([eventPollStartThreadRoot, eventMessageInThread]);
736728
});
737729

738-
it("copies post-thread in-timeline reactions onto both timelines", function () {
730+
it("should not copy post-thread in-timeline reactions onto both timelines", function () {
739731
// @ts-ignore setting private property
740732
client.clientOpts = {
741733
...defaultClientOpts,
@@ -752,11 +744,7 @@ describe("MatrixClient", function () {
752744

753745
expect(timeline).toEqual([eventPollStartThreadRoot, eventReaction]);
754746

755-
expect(threaded).toEqual([
756-
eventPollStartThreadRoot,
757-
eventMessageInThread,
758-
withThreadId(eventReaction, eventPollStartThreadRoot.getId()!),
759-
]);
747+
expect(threaded).toEqual([eventPollStartThreadRoot, eventMessageInThread]);
760748
});
761749

762750
it("sends room state events to the main timeline only", function () {
@@ -809,11 +797,7 @@ describe("MatrixClient", function () {
809797
]);
810798

811799
// Thread should contain only stuff that happened in the thread - no room state events
812-
expect(threaded).toEqual([
813-
eventPollStartThreadRoot,
814-
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()!),
815-
eventMessageInThread,
816-
]);
800+
expect(threaded).toEqual([eventPollStartThreadRoot, eventMessageInThread]);
817801
});
818802

819803
it("sends redactions of reactions to thread responses to thread timeline only", () => {
@@ -878,9 +862,9 @@ describe("MatrixClient", function () {
878862

879863
const [timeline, threaded] = room.partitionThreadedEvents(events);
880864

881-
expect(timeline).toEqual([threadRootEvent, replyToThreadResponse]);
865+
expect(timeline).toEqual([threadRootEvent]);
882866

883-
expect(threaded).toEqual([threadRootEvent, eventMessageInThread]);
867+
expect(threaded).toEqual([threadRootEvent, eventMessageInThread, replyToThreadResponse]);
884868
});
885869
});
886870

spec/test-utils/thread.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { RelationType } from "../../src/@types/event";
1818
import { MatrixClient } from "../../src/client";
1919
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
2020
import { Room } from "../../src/models/room";
21-
import { Thread } from "../../src/models/thread";
21+
import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
2222
import { mkMessage } from "./test-utils";
2323

2424
export const makeThreadEvent = ({
@@ -34,7 +34,7 @@ export const makeThreadEvent = ({
3434
...props,
3535
relatesTo: {
3636
event_id: rootEventId,
37-
rel_type: "m.thread",
37+
rel_type: THREAD_RELATION_TYPE.name,
3838
["m.in_reply_to"]: {
3939
event_id: replyToEventId,
4040
},

spec/unit/read-receipt.spec.ts

+67-31
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import MockHttpBackend from "matrix-mock-request";
1818

1919
import { MAIN_ROOM_TIMELINE, ReceiptType } from "../../src/@types/read_receipts";
2020
import { MatrixClient } from "../../src/client";
21-
import { EventType } from "../../src/matrix";
21+
import { EventType, MatrixEvent, Room } from "../../src/matrix";
2222
import { synthesizeReceipt } from "../../src/models/read-receipt";
2323
import { encodeUri } from "../../src/utils";
2424
import * as utils from "../test-utils/test-utils";
@@ -42,42 +42,45 @@ let httpBackend: MockHttpBackend;
4242
const THREAD_ID = "$thread_event_id";
4343
const ROOM_ID = "!123:matrix.org";
4444

45-
const threadEvent = utils.mkEvent({
46-
event: true,
47-
type: EventType.RoomMessage,
48-
user: "@bob:matrix.org",
49-
room: ROOM_ID,
50-
content: {
51-
"body": "Hello from a thread",
52-
"m.relates_to": {
53-
"event_id": THREAD_ID,
54-
"m.in_reply_to": {
55-
event_id: THREAD_ID,
56-
},
57-
"rel_type": "m.thread",
58-
},
59-
},
60-
});
61-
62-
const roomEvent = utils.mkEvent({
63-
event: true,
64-
type: EventType.RoomMessage,
65-
user: "@bob:matrix.org",
66-
room: ROOM_ID,
67-
content: {
68-
body: "Hello from a room",
69-
},
70-
});
71-
7245
describe("Read receipt", () => {
46+
let threadEvent: MatrixEvent;
47+
let roomEvent: MatrixEvent;
48+
7349
beforeEach(() => {
7450
httpBackend = new MockHttpBackend();
7551
client = new MatrixClient({
52+
userId: "@user:server",
7653
baseUrl: "https://my.home.server",
7754
accessToken: "my.access.token",
7855
fetchFn: httpBackend.fetchFn as typeof global.fetch,
7956
});
8057
client.isGuest = () => false;
58+
59+
threadEvent = utils.mkEvent({
60+
event: true,
61+
type: EventType.RoomMessage,
62+
user: "@bob:matrix.org",
63+
room: ROOM_ID,
64+
content: {
65+
"body": "Hello from a thread",
66+
"m.relates_to": {
67+
"event_id": THREAD_ID,
68+
"m.in_reply_to": {
69+
event_id: THREAD_ID,
70+
},
71+
"rel_type": "m.thread",
72+
},
73+
},
74+
});
75+
roomEvent = utils.mkEvent({
76+
event: true,
77+
type: EventType.RoomMessage,
78+
user: "@bob:matrix.org",
79+
room: ROOM_ID,
80+
content: {
81+
body: "Hello from a room",
82+
},
83+
});
8184
});
8285

8386
describe("sendReceipt", () => {
@@ -143,13 +146,46 @@ describe("Read receipt", () => {
143146
await httpBackend.flushAllExpected();
144147
await flushPromises();
145148
});
149+
150+
it("should send a main timeline read receipt for a reaction to a thread root", async () => {
151+
roomEvent.event.event_id = THREAD_ID;
152+
const reaction = utils.mkReaction(roomEvent, client, client.getSafeUserId(), ROOM_ID);
153+
const thread = new Room(ROOM_ID, client, client.getSafeUserId()).createThread(
154+
THREAD_ID,
155+
roomEvent,
156+
[threadEvent],
157+
false,
158+
);
159+
threadEvent.setThread(thread);
160+
reaction.setThread(thread);
161+
162+
httpBackend
163+
.when(
164+
"POST",
165+
encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
166+
$roomId: ROOM_ID,
167+
$receiptType: ReceiptType.Read,
168+
$eventId: reaction.getId()!,
169+
}),
170+
)
171+
.check((request) => {
172+
expect(request.data.thread_id).toEqual(MAIN_ROOM_TIMELINE);
173+
})
174+
.respond(200, {});
175+
176+
client.sendReceipt(reaction, ReceiptType.Read, {});
177+
178+
await httpBackend.flushAllExpected();
179+
await flushPromises();
180+
});
146181
});
147182

148183
describe("synthesizeReceipt", () => {
149184
it.each([
150-
{ event: roomEvent, destinationId: MAIN_ROOM_TIMELINE },
151-
{ event: threadEvent, destinationId: threadEvent.threadRootId! },
152-
])("adds the receipt to $destinationId", ({ event, destinationId }) => {
185+
{ getEvent: () => roomEvent, destinationId: MAIN_ROOM_TIMELINE },
186+
{ getEvent: () => threadEvent, destinationId: THREAD_ID },
187+
])("adds the receipt to $destinationId", ({ getEvent, destinationId }) => {
188+
const event = getEvent();
153189
const userId = "@bob:example.org";
154190
const receiptType = ReceiptType.Read;
155191

spec/unit/room.spec.ts

+64-22
Original file line numberDiff line numberDiff line change
@@ -2849,7 +2849,7 @@ describe("Room", function () {
28492849
Thread.setServerSideSupport(FeatureSupport.Stable);
28502850
const room = new Room(roomId, client, userA);
28512851

2852-
it("thread root and its relations&redactions should be in both", () => {
2852+
it("thread root and its relations&redactions should be in main timeline", () => {
28532853
const randomMessage = mkMessage();
28542854
const threadRoot = mkMessage();
28552855
const threadResponse1 = mkThreadResponse(threadRoot);
@@ -2867,6 +2867,9 @@ describe("Room", function () {
28672867
threadReaction2Redaction,
28682868
];
28692869

2870+
const thread = room.createThread(threadRoot.getId()!, threadRoot, [], false);
2871+
events.slice(1).forEach((ev) => ev.setThread(thread));
2872+
28702873
expect(room.eventShouldLiveIn(randomMessage, events, roots).shouldLiveInRoom).toBeTruthy();
28712874
expect(room.eventShouldLiveIn(randomMessage, events, roots).shouldLiveInThread).toBeFalsy();
28722875

@@ -2878,14 +2881,11 @@ describe("Room", function () {
28782881
expect(room.eventShouldLiveIn(threadResponse1, events, roots).threadId).toBe(threadRoot.getId());
28792882

28802883
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInRoom).toBeTruthy();
2881-
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInThread).toBeTruthy();
2882-
expect(room.eventShouldLiveIn(threadReaction1, events, roots).threadId).toBe(threadRoot.getId());
2884+
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInThread).toBeFalsy();
28832885
expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInRoom).toBeTruthy();
2884-
expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInThread).toBeTruthy();
2885-
expect(room.eventShouldLiveIn(threadReaction2, events, roots).threadId).toBe(threadRoot.getId());
2886+
expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInThread).toBeFalsy();
28862887
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInRoom).toBeTruthy();
2887-
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInThread).toBeTruthy();
2888-
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).threadId).toBe(threadRoot.getId());
2888+
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInThread).toBeFalsy();
28892889
});
28902890

28912891
it("thread response and its relations&redactions should be only in thread timeline", () => {
@@ -2909,25 +2909,39 @@ describe("Room", function () {
29092909
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).threadId).toBe(threadRoot.getId());
29102910
});
29112911

2912-
it("reply to thread response and its relations&redactions should be only in main timeline", () => {
2912+
it("reply to thread response and its relations&redactions should be only in thread timeline", () => {
29132913
const threadRoot = mkMessage();
2914-
const threadResponse1 = mkThreadResponse(threadRoot);
2915-
const reply1 = mkReply(threadResponse1);
2916-
const reaction1 = utils.mkReaction(reply1, room.client, userA, roomId);
2917-
const reaction2 = utils.mkReaction(reply1, room.client, userA, roomId);
2918-
const reaction2Redaction = mkRedaction(reply1);
2914+
const threadResp1 = mkThreadResponse(threadRoot);
2915+
const threadResp1Reply1 = mkReply(threadResp1);
2916+
const threadResp1Reply1Reaction1 = utils.mkReaction(threadResp1Reply1, room.client, userA, roomId);
2917+
const threadResp1Reply1Reaction2 = utils.mkReaction(threadResp1Reply1, room.client, userA, roomId);
2918+
const thResp1Rep1React2Redaction = mkRedaction(threadResp1Reply1);
29192919

29202920
const roots = new Set([threadRoot.getId()!]);
2921-
const events = [threadRoot, threadResponse1, reply1, reaction1, reaction2, reaction2Redaction];
2921+
const events = [
2922+
threadRoot,
2923+
threadResp1,
2924+
threadResp1Reply1,
2925+
threadResp1Reply1Reaction1,
2926+
threadResp1Reply1Reaction2,
2927+
thResp1Rep1React2Redaction,
2928+
];
29222929

2923-
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInRoom).toBeTruthy();
2924-
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInThread).toBeFalsy();
2925-
expect(room.eventShouldLiveIn(reaction1, events, roots).shouldLiveInRoom).toBeTruthy();
2926-
expect(room.eventShouldLiveIn(reaction1, events, roots).shouldLiveInThread).toBeFalsy();
2927-
expect(room.eventShouldLiveIn(reaction2, events, roots).shouldLiveInRoom).toBeTruthy();
2928-
expect(room.eventShouldLiveIn(reaction2, events, roots).shouldLiveInThread).toBeFalsy();
2929-
expect(room.eventShouldLiveIn(reaction2Redaction, events, roots).shouldLiveInRoom).toBeTruthy();
2930-
expect(room.eventShouldLiveIn(reaction2Redaction, events, roots).shouldLiveInThread).toBeFalsy();
2930+
const thread = room.createThread(threadRoot.getId()!, threadRoot, [], false);
2931+
events.forEach((ev) => ev.setThread(thread));
2932+
2933+
expect(room.eventShouldLiveIn(threadResp1Reply1, events, roots).shouldLiveInRoom).toBeFalsy();
2934+
expect(room.eventShouldLiveIn(threadResp1Reply1, events, roots).shouldLiveInThread).toBeTruthy();
2935+
expect(room.eventShouldLiveIn(threadResp1Reply1, events, roots).threadId).toBe(thread.id);
2936+
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction1, events, roots).shouldLiveInRoom).toBeFalsy();
2937+
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction1, events, roots).shouldLiveInThread).toBeTruthy();
2938+
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction1, events, roots).threadId).toBe(thread.id);
2939+
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction2, events, roots).shouldLiveInRoom).toBeFalsy();
2940+
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction2, events, roots).shouldLiveInThread).toBeTruthy();
2941+
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction2, events, roots).threadId).toBe(thread.id);
2942+
expect(room.eventShouldLiveIn(thResp1Rep1React2Redaction, events, roots).shouldLiveInRoom).toBeFalsy();
2943+
expect(room.eventShouldLiveIn(thResp1Rep1React2Redaction, events, roots).shouldLiveInThread).toBeTruthy();
2944+
expect(room.eventShouldLiveIn(thResp1Rep1React2Redaction, events, roots).threadId).toBe(thread.id);
29312945
});
29322946

29332947
it("reply to reply to thread root should only be in the main timeline", () => {
@@ -2939,12 +2953,40 @@ describe("Room", function () {
29392953
const roots = new Set([threadRoot.getId()!]);
29402954
const events = [threadRoot, threadResponse1, reply1, reply2];
29412955

2956+
const thread = room.createThread(threadRoot.getId()!, threadRoot, [], false);
2957+
threadResponse1.setThread(thread);
2958+
29422959
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInRoom).toBeTruthy();
29432960
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInThread).toBeFalsy();
29442961
expect(room.eventShouldLiveIn(reply2, events, roots).shouldLiveInRoom).toBeTruthy();
29452962
expect(room.eventShouldLiveIn(reply2, events, roots).shouldLiveInThread).toBeFalsy();
29462963
});
29472964

2965+
it("edit to thread root should live in main timeline only", () => {
2966+
const threadRoot = mkMessage();
2967+
const threadResponse1 = mkThreadResponse(threadRoot);
2968+
const threadRootEdit = mkEdit(threadRoot);
2969+
threadRoot.makeReplaced(threadRootEdit);
2970+
2971+
const thread = room.createThread(threadRoot.getId()!, threadRoot, [threadResponse1], false);
2972+
threadResponse1.setThread(thread);
2973+
threadRootEdit.setThread(thread);
2974+
2975+
const roots = new Set([threadRoot.getId()!]);
2976+
const events = [threadRoot, threadResponse1, threadRootEdit];
2977+
2978+
expect(room.eventShouldLiveIn(threadRoot, events, roots).shouldLiveInRoom).toBeTruthy();
2979+
expect(room.eventShouldLiveIn(threadRoot, events, roots).shouldLiveInThread).toBeTruthy();
2980+
expect(room.eventShouldLiveIn(threadRoot, events, roots).threadId).toBe(threadRoot.getId());
2981+
2982+
expect(room.eventShouldLiveIn(threadResponse1, events, roots).shouldLiveInRoom).toBeFalsy();
2983+
expect(room.eventShouldLiveIn(threadResponse1, events, roots).shouldLiveInThread).toBeTruthy();
2984+
expect(room.eventShouldLiveIn(threadResponse1, events, roots).threadId).toBe(threadRoot.getId());
2985+
2986+
expect(room.eventShouldLiveIn(threadRootEdit, events, roots).shouldLiveInRoom).toBeTruthy();
2987+
expect(room.eventShouldLiveIn(threadRootEdit, events, roots).shouldLiveInThread).toBeFalsy();
2988+
});
2989+
29482990
it("should aggregate relations in thread event timeline set", async () => {
29492991
Thread.setServerSideSupport(FeatureSupport.Stable);
29502992
const threadRoot = mkMessage();

0 commit comments

Comments
 (0)