1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-06 12:02:40 +03:00

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
This commit is contained in:
Michael Telatynski
2023-07-19 12:21:50 +01:00
committed by GitHub
parent 43b2404865
commit 66492e7ba8
7 changed files with 185 additions and 108 deletions

View File

@@ -1284,7 +1284,6 @@ describe("MatrixClient event timelines", function () {
THREAD_ROOT.event_id, THREAD_ROOT.event_id,
THREAD_REPLY.event_id, THREAD_REPLY.event_id,
THREAD_REPLY2.getId(), THREAD_REPLY2.getId(),
THREAD_ROOT_REACTION.getId(),
THREAD_REPLY3.getId(), THREAD_REPLY3.getId(),
]); ]);
}); });

View File

@@ -656,7 +656,7 @@ describe("MatrixClient", function () {
expect(threaded).toEqual([]); expect(threaded).toEqual([]);
}); });
it("copies pre-thread in-timeline vote events onto both timelines", function () { it("should not copy pre-thread in-timeline vote events onto both timelines", function () {
// @ts-ignore setting private property // @ts-ignore setting private property
client.clientOpts = { client.clientOpts = {
...defaultClientOpts, ...defaultClientOpts,
@@ -684,10 +684,10 @@ describe("MatrixClient", function () {
const eventRefWithThreadId = withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()!); const eventRefWithThreadId = withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()!);
expect(eventRefWithThreadId.threadRootId).toBeTruthy(); expect(eventRefWithThreadId.threadRootId).toBeTruthy();
expect(threaded).toEqual([eventPollStartThreadRoot, eventMessageInThread, eventRefWithThreadId]); expect(threaded).toEqual([eventPollStartThreadRoot, eventMessageInThread]);
}); });
it("copies pre-thread in-timeline reactions onto both timelines", function () { it("should not copy pre-thread in-timeline reactions onto both timelines", function () {
// @ts-ignore setting private property // @ts-ignore setting private property
client.clientOpts = { client.clientOpts = {
...defaultClientOpts, ...defaultClientOpts,
@@ -704,14 +704,10 @@ describe("MatrixClient", function () {
expect(timeline).toEqual([eventPollStartThreadRoot, eventReaction]); expect(timeline).toEqual([eventPollStartThreadRoot, eventReaction]);
expect(threaded).toEqual([ expect(threaded).toEqual([eventPollStartThreadRoot, eventMessageInThread]);
eventPollStartThreadRoot,
eventMessageInThread,
withThreadId(eventReaction, eventPollStartThreadRoot.getId()!),
]);
}); });
it("copies post-thread in-timeline vote events onto both timelines", function () { it("should not copy post-thread in-timeline vote events onto both timelines", function () {
// @ts-ignore setting private property // @ts-ignore setting private property
client.clientOpts = { client.clientOpts = {
...defaultClientOpts, ...defaultClientOpts,
@@ -728,14 +724,10 @@ describe("MatrixClient", function () {
expect(timeline).toEqual([eventPollStartThreadRoot, eventPollResponseReference]); expect(timeline).toEqual([eventPollStartThreadRoot, eventPollResponseReference]);
expect(threaded).toEqual([ expect(threaded).toEqual([eventPollStartThreadRoot, eventMessageInThread]);
eventPollStartThreadRoot,
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()!),
eventMessageInThread,
]);
}); });
it("copies post-thread in-timeline reactions onto both timelines", function () { it("should not copy post-thread in-timeline reactions onto both timelines", function () {
// @ts-ignore setting private property // @ts-ignore setting private property
client.clientOpts = { client.clientOpts = {
...defaultClientOpts, ...defaultClientOpts,
@@ -752,11 +744,7 @@ describe("MatrixClient", function () {
expect(timeline).toEqual([eventPollStartThreadRoot, eventReaction]); expect(timeline).toEqual([eventPollStartThreadRoot, eventReaction]);
expect(threaded).toEqual([ expect(threaded).toEqual([eventPollStartThreadRoot, eventMessageInThread]);
eventPollStartThreadRoot,
eventMessageInThread,
withThreadId(eventReaction, eventPollStartThreadRoot.getId()!),
]);
}); });
it("sends room state events to the main timeline only", function () { it("sends room state events to the main timeline only", function () {
@@ -809,11 +797,7 @@ describe("MatrixClient", function () {
]); ]);
// Thread should contain only stuff that happened in the thread - no room state events // Thread should contain only stuff that happened in the thread - no room state events
expect(threaded).toEqual([ expect(threaded).toEqual([eventPollStartThreadRoot, eventMessageInThread]);
eventPollStartThreadRoot,
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()!),
eventMessageInThread,
]);
}); });
it("sends redactions of reactions to thread responses to thread timeline only", () => { it("sends redactions of reactions to thread responses to thread timeline only", () => {
@@ -878,9 +862,9 @@ describe("MatrixClient", function () {
const [timeline, threaded] = room.partitionThreadedEvents(events); const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([threadRootEvent, replyToThreadResponse]); expect(timeline).toEqual([threadRootEvent]);
expect(threaded).toEqual([threadRootEvent, eventMessageInThread]); expect(threaded).toEqual([threadRootEvent, eventMessageInThread, replyToThreadResponse]);
}); });
}); });

View File

@@ -18,7 +18,7 @@ import { RelationType } from "../../src/@types/event";
import { MatrixClient } from "../../src/client"; import { MatrixClient } from "../../src/client";
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event"; import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { Room } from "../../src/models/room"; import { Room } from "../../src/models/room";
import { Thread } from "../../src/models/thread"; import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
import { mkMessage } from "./test-utils"; import { mkMessage } from "./test-utils";
export const makeThreadEvent = ({ export const makeThreadEvent = ({
@@ -34,7 +34,7 @@ export const makeThreadEvent = ({
...props, ...props,
relatesTo: { relatesTo: {
event_id: rootEventId, event_id: rootEventId,
rel_type: "m.thread", rel_type: THREAD_RELATION_TYPE.name,
["m.in_reply_to"]: { ["m.in_reply_to"]: {
event_id: replyToEventId, event_id: replyToEventId,
}, },

View File

@@ -18,7 +18,7 @@ import MockHttpBackend from "matrix-mock-request";
import { MAIN_ROOM_TIMELINE, ReceiptType } from "../../src/@types/read_receipts"; import { MAIN_ROOM_TIMELINE, ReceiptType } from "../../src/@types/read_receipts";
import { MatrixClient } from "../../src/client"; import { MatrixClient } from "../../src/client";
import { EventType } from "../../src/matrix"; import { EventType, MatrixEvent, Room } from "../../src/matrix";
import { synthesizeReceipt } from "../../src/models/read-receipt"; import { synthesizeReceipt } from "../../src/models/read-receipt";
import { encodeUri } from "../../src/utils"; import { encodeUri } from "../../src/utils";
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
@@ -42,42 +42,45 @@ let httpBackend: MockHttpBackend;
const THREAD_ID = "$thread_event_id"; const THREAD_ID = "$thread_event_id";
const ROOM_ID = "!123:matrix.org"; const ROOM_ID = "!123:matrix.org";
const threadEvent = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: ROOM_ID,
content: {
"body": "Hello from a thread",
"m.relates_to": {
"event_id": THREAD_ID,
"m.in_reply_to": {
event_id: THREAD_ID,
},
"rel_type": "m.thread",
},
},
});
const roomEvent = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: ROOM_ID,
content: {
body: "Hello from a room",
},
});
describe("Read receipt", () => { describe("Read receipt", () => {
let threadEvent: MatrixEvent;
let roomEvent: MatrixEvent;
beforeEach(() => { beforeEach(() => {
httpBackend = new MockHttpBackend(); httpBackend = new MockHttpBackend();
client = new MatrixClient({ client = new MatrixClient({
userId: "@user:server",
baseUrl: "https://my.home.server", baseUrl: "https://my.home.server",
accessToken: "my.access.token", accessToken: "my.access.token",
fetchFn: httpBackend.fetchFn as typeof global.fetch, fetchFn: httpBackend.fetchFn as typeof global.fetch,
}); });
client.isGuest = () => false; client.isGuest = () => false;
threadEvent = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: ROOM_ID,
content: {
"body": "Hello from a thread",
"m.relates_to": {
"event_id": THREAD_ID,
"m.in_reply_to": {
event_id: THREAD_ID,
},
"rel_type": "m.thread",
},
},
});
roomEvent = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: ROOM_ID,
content: {
body: "Hello from a room",
},
});
}); });
describe("sendReceipt", () => { describe("sendReceipt", () => {
@@ -143,13 +146,46 @@ describe("Read receipt", () => {
await httpBackend.flushAllExpected(); await httpBackend.flushAllExpected();
await flushPromises(); await flushPromises();
}); });
it("should send a main timeline read receipt for a reaction to a thread root", async () => {
roomEvent.event.event_id = THREAD_ID;
const reaction = utils.mkReaction(roomEvent, client, client.getSafeUserId(), ROOM_ID);
const thread = new Room(ROOM_ID, client, client.getSafeUserId()).createThread(
THREAD_ID,
roomEvent,
[threadEvent],
false,
);
threadEvent.setThread(thread);
reaction.setThread(thread);
httpBackend
.when(
"POST",
encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: reaction.getId()!,
}),
)
.check((request) => {
expect(request.data.thread_id).toEqual(MAIN_ROOM_TIMELINE);
})
.respond(200, {});
client.sendReceipt(reaction, ReceiptType.Read, {});
await httpBackend.flushAllExpected();
await flushPromises();
});
}); });
describe("synthesizeReceipt", () => { describe("synthesizeReceipt", () => {
it.each([ it.each([
{ event: roomEvent, destinationId: MAIN_ROOM_TIMELINE }, { getEvent: () => roomEvent, destinationId: MAIN_ROOM_TIMELINE },
{ event: threadEvent, destinationId: threadEvent.threadRootId! }, { getEvent: () => threadEvent, destinationId: THREAD_ID },
])("adds the receipt to $destinationId", ({ event, destinationId }) => { ])("adds the receipt to $destinationId", ({ getEvent, destinationId }) => {
const event = getEvent();
const userId = "@bob:example.org"; const userId = "@bob:example.org";
const receiptType = ReceiptType.Read; const receiptType = ReceiptType.Read;

View File

@@ -2849,7 +2849,7 @@ describe("Room", function () {
Thread.setServerSideSupport(FeatureSupport.Stable); Thread.setServerSideSupport(FeatureSupport.Stable);
const room = new Room(roomId, client, userA); const room = new Room(roomId, client, userA);
it("thread root and its relations&redactions should be in both", () => { it("thread root and its relations&redactions should be in main timeline", () => {
const randomMessage = mkMessage(); const randomMessage = mkMessage();
const threadRoot = mkMessage(); const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot); const threadResponse1 = mkThreadResponse(threadRoot);
@@ -2867,6 +2867,9 @@ describe("Room", function () {
threadReaction2Redaction, threadReaction2Redaction,
]; ];
const thread = room.createThread(threadRoot.getId()!, threadRoot, [], false);
events.slice(1).forEach((ev) => ev.setThread(thread));
expect(room.eventShouldLiveIn(randomMessage, events, roots).shouldLiveInRoom).toBeTruthy(); expect(room.eventShouldLiveIn(randomMessage, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(randomMessage, events, roots).shouldLiveInThread).toBeFalsy(); expect(room.eventShouldLiveIn(randomMessage, events, roots).shouldLiveInThread).toBeFalsy();
@@ -2878,14 +2881,11 @@ describe("Room", function () {
expect(room.eventShouldLiveIn(threadResponse1, events, roots).threadId).toBe(threadRoot.getId()); expect(room.eventShouldLiveIn(threadResponse1, events, roots).threadId).toBe(threadRoot.getId());
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInRoom).toBeTruthy(); expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInThread).toBeTruthy(); expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInThread).toBeFalsy();
expect(room.eventShouldLiveIn(threadReaction1, events, roots).threadId).toBe(threadRoot.getId());
expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInRoom).toBeTruthy(); expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInThread).toBeTruthy(); expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInThread).toBeFalsy();
expect(room.eventShouldLiveIn(threadReaction2, events, roots).threadId).toBe(threadRoot.getId());
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInRoom).toBeTruthy(); expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInThread).toBeTruthy(); expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInThread).toBeFalsy();
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).threadId).toBe(threadRoot.getId());
}); });
it("thread response and its relations&redactions should be only in thread timeline", () => { it("thread response and its relations&redactions should be only in thread timeline", () => {
@@ -2909,25 +2909,39 @@ describe("Room", function () {
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).threadId).toBe(threadRoot.getId()); expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).threadId).toBe(threadRoot.getId());
}); });
it("reply to thread response and its relations&redactions should be only in main timeline", () => { it("reply to thread response and its relations&redactions should be only in thread timeline", () => {
const threadRoot = mkMessage(); const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot); const threadResp1 = mkThreadResponse(threadRoot);
const reply1 = mkReply(threadResponse1); const threadResp1Reply1 = mkReply(threadResp1);
const reaction1 = utils.mkReaction(reply1, room.client, userA, roomId); const threadResp1Reply1Reaction1 = utils.mkReaction(threadResp1Reply1, room.client, userA, roomId);
const reaction2 = utils.mkReaction(reply1, room.client, userA, roomId); const threadResp1Reply1Reaction2 = utils.mkReaction(threadResp1Reply1, room.client, userA, roomId);
const reaction2Redaction = mkRedaction(reply1); const thResp1Rep1React2Redaction = mkRedaction(threadResp1Reply1);
const roots = new Set([threadRoot.getId()!]); const roots = new Set([threadRoot.getId()!]);
const events = [threadRoot, threadResponse1, reply1, reaction1, reaction2, reaction2Redaction]; const events = [
threadRoot,
threadResp1,
threadResp1Reply1,
threadResp1Reply1Reaction1,
threadResp1Reply1Reaction2,
thResp1Rep1React2Redaction,
];
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInRoom).toBeTruthy(); const thread = room.createThread(threadRoot.getId()!, threadRoot, [], false);
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInThread).toBeFalsy(); events.forEach((ev) => ev.setThread(thread));
expect(room.eventShouldLiveIn(reaction1, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(reaction1, events, roots).shouldLiveInThread).toBeFalsy(); expect(room.eventShouldLiveIn(threadResp1Reply1, events, roots).shouldLiveInRoom).toBeFalsy();
expect(room.eventShouldLiveIn(reaction2, events, roots).shouldLiveInRoom).toBeTruthy(); expect(room.eventShouldLiveIn(threadResp1Reply1, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(reaction2, events, roots).shouldLiveInThread).toBeFalsy(); expect(room.eventShouldLiveIn(threadResp1Reply1, events, roots).threadId).toBe(thread.id);
expect(room.eventShouldLiveIn(reaction2Redaction, events, roots).shouldLiveInRoom).toBeTruthy(); expect(room.eventShouldLiveIn(threadResp1Reply1Reaction1, events, roots).shouldLiveInRoom).toBeFalsy();
expect(room.eventShouldLiveIn(reaction2Redaction, events, roots).shouldLiveInThread).toBeFalsy(); expect(room.eventShouldLiveIn(threadResp1Reply1Reaction1, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction1, events, roots).threadId).toBe(thread.id);
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction2, events, roots).shouldLiveInRoom).toBeFalsy();
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction2, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction2, events, roots).threadId).toBe(thread.id);
expect(room.eventShouldLiveIn(thResp1Rep1React2Redaction, events, roots).shouldLiveInRoom).toBeFalsy();
expect(room.eventShouldLiveIn(thResp1Rep1React2Redaction, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(thResp1Rep1React2Redaction, events, roots).threadId).toBe(thread.id);
}); });
it("reply to reply to thread root should only be in the main timeline", () => { it("reply to reply to thread root should only be in the main timeline", () => {
@@ -2939,12 +2953,40 @@ describe("Room", function () {
const roots = new Set([threadRoot.getId()!]); const roots = new Set([threadRoot.getId()!]);
const events = [threadRoot, threadResponse1, reply1, reply2]; const events = [threadRoot, threadResponse1, reply1, reply2];
const thread = room.createThread(threadRoot.getId()!, threadRoot, [], false);
threadResponse1.setThread(thread);
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInRoom).toBeTruthy(); expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInThread).toBeFalsy(); expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInThread).toBeFalsy();
expect(room.eventShouldLiveIn(reply2, events, roots).shouldLiveInRoom).toBeTruthy(); expect(room.eventShouldLiveIn(reply2, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(reply2, events, roots).shouldLiveInThread).toBeFalsy(); expect(room.eventShouldLiveIn(reply2, events, roots).shouldLiveInThread).toBeFalsy();
}); });
it("edit to thread root should live in main timeline only", () => {
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
const threadRootEdit = mkEdit(threadRoot);
threadRoot.makeReplaced(threadRootEdit);
const thread = room.createThread(threadRoot.getId()!, threadRoot, [threadResponse1], false);
threadResponse1.setThread(thread);
threadRootEdit.setThread(thread);
const roots = new Set([threadRoot.getId()!]);
const events = [threadRoot, threadResponse1, threadRootEdit];
expect(room.eventShouldLiveIn(threadRoot, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(threadRoot, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(threadRoot, events, roots).threadId).toBe(threadRoot.getId());
expect(room.eventShouldLiveIn(threadResponse1, events, roots).shouldLiveInRoom).toBeFalsy();
expect(room.eventShouldLiveIn(threadResponse1, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(threadResponse1, events, roots).threadId).toBe(threadRoot.getId());
expect(room.eventShouldLiveIn(threadRootEdit, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(threadRootEdit, events, roots).shouldLiveInThread).toBeFalsy();
});
it("should aggregate relations in thread event timeline set", async () => { it("should aggregate relations in thread event timeline set", async () => {
Thread.setServerSideSupport(FeatureSupport.Stable); Thread.setServerSideSupport(FeatureSupport.Stable);
const threadRoot = mkMessage(); const threadRoot = mkMessage();

View File

@@ -5000,8 +5000,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}); });
if (!unthreaded) { if (!unthreaded) {
// A thread cannot be just a thread root and a thread root can only be read in the main timeline // XXX: the spec currently says a threaded read receipt can be sent for the root of a thread,
const isThread = !!event.threadRootId && !event.isThreadRoot; // but in practice this isn't possible and the spec needs updating.
const isThread =
!!event.threadRootId &&
// A thread cannot be just a thread root and a thread root can only be read in the main timeline
!event.isThreadRoot &&
// Similarly non-thread relations upon the thread root (reactions, edits) should also be for the main timeline.
event.isRelation() &&
(event.isRelation(THREAD_RELATION_TYPE.name) || event.relationEventId !== event.threadRootId);
body = { body = {
...body, ...body,
// Only thread replies should define a specific thread. Thread roots can only be read in the main timeline. // Only thread replies should define a specific thread. Thread roots can only be read in the main timeline.

View File

@@ -2093,6 +2093,14 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
} }
} }
/**
* Determine which timeline(s) a given event should live in
* Thread roots live in both the main timeline and their corresponding thread timeline
* Relations, redactions, replies to thread relation events live only in the thread timeline
* Relations (other than m.thread), redactions, replies to a thread root live only in the main timeline
* Relations, redactions, replies where the parent cannot be found live in no timelines but should be aggregated regardless.
* Otherwise, the event lives in the main timeline only.
*/
public eventShouldLiveIn( public eventShouldLiveIn(
event: MatrixEvent, event: MatrixEvent,
events?: MatrixEvent[], events?: MatrixEvent[],
@@ -2109,7 +2117,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
}; };
} }
// A thread root is always shown in both timelines // A thread root is the only event shown in both timelines
if (event.isThreadRoot || roots?.has(event.getId()!)) { if (event.isThreadRoot || roots?.has(event.getId()!)) {
return { return {
shouldLiveInRoom: true, shouldLiveInRoom: true,
@@ -2118,8 +2126,29 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
}; };
} }
// A thread relation (1st and 2nd order) is always only shown in a thread const isThreadRelation = event.isRelation(THREAD_RELATION_TYPE.name);
const parentEventId = event.getAssociatedId();
const threadRootId = event.threadRootId; const threadRootId = event.threadRootId;
// Where the parent is the thread root and this is a non-thread relation this should live only in the main timeline
if (!!parentEventId && !isThreadRelation && (threadRootId === parentEventId || roots?.has(parentEventId!))) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: false,
};
}
let parentEvent: MatrixEvent | undefined;
if (parentEventId) {
parentEvent = this.findEventById(parentEventId) ?? events?.find((e) => e.getId() === parentEventId);
}
// Treat non-thread-relations, redactions, and replies as extensions of their parents so evaluate parentEvent instead
if (parentEvent && !isThreadRelation) {
return this.eventShouldLiveIn(parentEvent, events, roots);
}
// A thread relation (1st and 2nd order) is always only shown in a thread
if (threadRootId != undefined) { if (threadRootId != undefined) {
return { return {
shouldLiveInRoom: false, shouldLiveInRoom: false,
@@ -2128,33 +2157,13 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
}; };
} }
const parentEventId = event.getAssociatedId(); if (!parentEventId) {
let parentEvent: MatrixEvent | undefined;
if (parentEventId) {
parentEvent = this.findEventById(parentEventId) ?? events?.find((e) => e.getId() === parentEventId);
}
// Treat relations and redactions as extensions of their parents so evaluate parentEvent instead
if (parentEvent && (event.isRelation() || event.isRedaction())) {
return this.eventShouldLiveIn(parentEvent, events, roots);
}
if (!event.isRelation()) {
return { return {
shouldLiveInRoom: true, shouldLiveInRoom: true,
shouldLiveInThread: false, shouldLiveInThread: false,
}; };
} }
// Edge case where we know the event is a relation but don't have the parentEvent
if (roots?.has(event.relationEventId!)) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: true,
threadId: event.relationEventId,
};
}
// We've exhausted all scenarios, // We've exhausted all scenarios,
// we cannot assume that it lives in the main timeline as this may be a relation for an unknown thread // we cannot assume that it lives in the main timeline as this may be a relation for an unknown thread
// adding the event in the wrong timeline causes stuck notifications and can break ability to send read receipts // adding the event in the wrong timeline causes stuck notifications and can break ability to send read receipts