Skip to content

Commit aead401

Browse files
author
Germain
authored
Apply edits discovered from sync after thread is initialised (#3002)
1 parent af9525e commit aead401

File tree

2 files changed

+109
-1
lines changed

2 files changed

+109
-1
lines changed

spec/unit/event-timeline-set.spec.ts

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@ import {
2424
MatrixClient,
2525
MatrixEvent,
2626
MatrixEventEvent,
27+
RelationType,
2728
Room,
29+
RoomEvent,
2830
} from "../../src";
29-
import { Thread } from "../../src/models/thread";
31+
import { FeatureSupport, Thread } from "../../src/models/thread";
3032
import { ReEmitter } from "../../src/ReEmitter";
33+
import { eventMapperFor } from "../../src/event-mapper";
3134

3235
describe("EventTimelineSet", () => {
3336
const roomId = "!foo:bar";
@@ -202,6 +205,88 @@ describe("EventTimelineSet", () => {
202205
expect(liveTimeline.getEvents().length).toStrictEqual(0);
203206
});
204207

208+
it("should allow edits to be added to thread timeline", async () => {
209+
jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true);
210+
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
211+
Thread.hasServerSideSupport = FeatureSupport.Stable;
212+
213+
const sender = "@alice:matrix.org";
214+
215+
const root = utils.mkEvent({
216+
event: true,
217+
content: {
218+
body: "Thread root",
219+
},
220+
type: EventType.RoomMessage,
221+
sender,
222+
});
223+
room.addLiveEvents([root]);
224+
225+
const threadReply = utils.mkEvent({
226+
event: true,
227+
content: {
228+
"body": "Thread reply",
229+
"m.relates_to": {
230+
event_id: root.getId()!,
231+
rel_type: RelationType.Thread,
232+
},
233+
},
234+
type: EventType.RoomMessage,
235+
sender,
236+
});
237+
238+
root.setUnsigned({
239+
"m.relations": {
240+
[RelationType.Thread]: {
241+
count: 1,
242+
latest_event: {
243+
content: threadReply.getContent(),
244+
origin_server_ts: 5,
245+
room_id: room.roomId,
246+
sender,
247+
type: EventType.RoomMessage,
248+
event_id: threadReply.getId()!,
249+
user_id: sender,
250+
age: 1,
251+
},
252+
current_user_participated: true,
253+
},
254+
},
255+
});
256+
257+
const editToThreadReply = utils.mkEvent({
258+
event: true,
259+
content: {
260+
"body": " * edit",
261+
"m.new_content": {
262+
"body": "edit",
263+
"msgtype": "m.text",
264+
"org.matrix.msc1767.text": "edit",
265+
},
266+
"m.relates_to": {
267+
event_id: threadReply.getId()!,
268+
rel_type: RelationType.Replace,
269+
},
270+
},
271+
type: EventType.RoomMessage,
272+
sender,
273+
});
274+
275+
jest.spyOn(client, "paginateEventTimeline").mockImplementation(async () => {
276+
thread.timelineSet.getLiveTimeline().addEvent(threadReply, { toStartOfTimeline: true });
277+
return true;
278+
});
279+
jest.spyOn(client, "relations").mockResolvedValue({
280+
events: [],
281+
});
282+
283+
const thread = room.createThread(root.getId()!, root, [threadReply, editToThreadReply], false);
284+
thread.once(RoomEvent.TimelineReset, () => {
285+
const lastEvent = thread.timeline.at(-1)!;
286+
expect(lastEvent.getContent().body).toBe(" * edit");
287+
});
288+
});
289+
205290
describe("non-room timeline", () => {
206291
it("Adds event to timeline", () => {
207292
const nonRoomEventTimelineSet = new EventTimelineSet(

src/models/thread.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
9797
private readonly pendingEventOrdering: PendingEventOrdering;
9898

9999
public initialEventsFetched = !Thread.hasServerSideSupport;
100+
/**
101+
* An array of events to add to the timeline once the thread has been initialised
102+
* with server suppport.
103+
*/
104+
public replayEvents: MatrixEvent[] | null = [];
100105

101106
public constructor(public readonly id: string, public rootEvent: MatrixEvent | undefined, opts: IThreadOpts) {
102107
super();
@@ -266,6 +271,20 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
266271
this.addEventToTimeline(event, false);
267272
this.fetchEditsWhereNeeded(event);
268273
} else if (event.isRelation(RelationType.Annotation) || event.isRelation(RelationType.Replace)) {
274+
if (!this.initialEventsFetched) {
275+
/**
276+
* A thread can be fully discovered via a single sync response
277+
* And when that's the case we still ask the server to do an initialisation
278+
* as it's the safest to ensure we have everything.
279+
* However when we are in that scenario we might loose annotation or edits
280+
*
281+
* This fix keeps a reference to those events and replay them once the thread
282+
* has been initialised properly.
283+
*/
284+
this.replayEvents?.push(event);
285+
} else {
286+
this.addEventToTimeline(event, toStartOfTimeline);
287+
}
269288
// Apply annotations and replace relations to the relations of the timeline only
270289
this.timelineSet.relations?.aggregateParentEvent(event);
271290
this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet);
@@ -375,6 +394,10 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
375394
limit: Math.max(1, this.length),
376395
});
377396
}
397+
for (const event of this.replayEvents!) {
398+
this.addEvent(event, false);
399+
}
400+
this.replayEvents = null;
378401
// just to make sure that, if we've created a timeline window for this thread before the thread itself
379402
// existed (e.g. when creating a new thread), we'll make sure the panel is force refreshed correctly.
380403
this.emit(RoomEvent.TimelineReset, this.room, this.timelineSet, true);

0 commit comments

Comments
 (0)