Skip to content

Commit

Permalink
Apply edits discovered from sync after thread is initialised
Browse files Browse the repository at this point in the history
  • Loading branch information
germain-gg committed Dec 21, 2022
1 parent b83c372 commit 54731b3
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 6 deletions.
68 changes: 67 additions & 1 deletion spec/unit/event-timeline-set.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ import {
MatrixClient,
MatrixEvent,
MatrixEventEvent,
RelationType,
Room,
RoomEvent,
} from "../../src";
import { Thread } from "../../src/models/thread";
import { FeatureSupport, Thread } from "../../src/models/thread";
import { ReEmitter } from "../../src/ReEmitter";

describe("EventTimelineSet", () => {
Expand Down Expand Up @@ -202,6 +204,70 @@ describe("EventTimelineSet", () => {
expect(liveTimeline.getEvents().length).toStrictEqual(0);
});

it("should allow edits to be added to thread timeline", async () => {
jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true);
Thread.hasServerSideSupport = FeatureSupport.Stable;

const sender = "@alice:matrix.org";

const root = utils.mkEvent({
event: true,
content: {
body: "Thread root",
},
type: EventType.RoomMessage,
sender,
});
room.addLiveEvents([root]);

const threadReply = utils.mkEvent({
event: true,
content: {
"body": "Thread reply",
"m.relates_to": {
event_id: root.getId()!,
rel_type: RelationType.Thread,
},
},
type: EventType.RoomMessage,
sender,
});

const editToThreadReply = utils.mkEvent({
event: true,
content: {
"body": " * edit",
"m.new_content": {
"body": "edit",
"msgtype": "m.text",
"org.matrix.msc1767.text": "edit",
},
"m.relates_to": {
event_id: threadReply.getId()!,
rel_type: RelationType.Replace,
},
},
type: EventType.RoomMessage,
sender,
});

const thread = room.createThread(root.getId()!, root, [threadReply, editToThreadReply], false);

jest.spyOn(thread, "processEvent").mockResolvedValue();
jest.spyOn(client, "paginateEventTimeline").mockImplementation(async () => {
thread.timelineSet.getLiveTimeline().addEvent(threadReply, { toStartOfTimeline: true });
return true;
});
jest.spyOn(client, "relations").mockResolvedValue({
events: [],
});

thread.once(RoomEvent.TimelineReset, () => {
const lastEvent = thread.timeline.at(-1)!;
expect(lastEvent.getContent().body).toBe(" * edit");
});
});

describe("non-room timeline", () => {
it("Adds event to timeline", () => {
const nonRoomEventTimelineSet = new EventTimelineSet(
Expand Down
33 changes: 28 additions & 5 deletions src/models/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
private readonly pendingEventOrdering: PendingEventOrdering;

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

public constructor(public readonly id: string, public rootEvent: MatrixEvent | undefined, opts: IThreadOpts) {
super();
Expand Down Expand Up @@ -136,7 +141,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
this.setEventMetadata(this.rootEvent);
}

private async fetchRootEvent(): Promise<void> {
public async fetchRootEvent(): Promise<void> {
this.rootEvent = this.room.findEventById(this.id);
// If the rootEvent does not exist in the local stores, then fetch it from the server.
try {
Expand Down Expand Up @@ -266,9 +271,23 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
this.addEventToTimeline(event, false);
this.fetchEditsWhereNeeded(event);
} else if (event.isRelation(RelationType.Annotation) || event.isRelation(RelationType.Replace)) {
// Apply annotations and replace relations to the relations of the timeline only
this.timelineSet.relations?.aggregateParentEvent(event);
this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet);
if (!this.initialEventsFetched) {
/**
* A thread can be fully discovered via a single sync response
* And when that's the case we still ask the server to do an initialisation
* as it's the safest to ensure we have everything.
* However when we are in that scenario we might loose annotation or edits
*
* This fix keeps a reference to those events and replay them once the thread
* has been initialised properly.
*/
this.replayEvents?.push(event);
} else {
this.addEventToTimeline(event, toStartOfTimeline);
// Apply annotations and replace relations to the relations of the timeline only
this.timelineSet.relations?.aggregateParentEvent(event);
this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet);
}
return;
}

Expand Down Expand Up @@ -316,7 +335,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
return rootEvent?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
}

private async processRootEvent(): Promise<void> {
public async processRootEvent(): Promise<void> {
const bundledRelationship = this.getRootEventBundledRelationship();
if (Thread.hasServerSideSupport && bundledRelationship) {
this.replyCount = bundledRelationship.count;
Expand Down Expand Up @@ -375,6 +394,10 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
limit: Math.max(1, this.length),
});
}
for (const event of this.replayEvents!) {
this.addEvent(event, false);
}
this.replayEvents = null;
// just to make sure that, if we've created a timeline window for this thread before the thread itself
// existed (e.g. when creating a new thread), we'll make sure the panel is force refreshed correctly.
this.emit(RoomEvent.TimelineReset, this.room, this.timelineSet, true);
Expand Down

0 comments on commit 54731b3

Please sign in to comment.