Skip to content

Commit 8e896c4

Browse files
authored
Fix issues with getEventTimeline and thread roots (#2444)
* Add additional tests for thread timelines * Fix issues around mixing up event timeline sets with /context/ API * Increase coverage * Increase coverage * Better scope assertions * Iterate PR
1 parent 2c2686c commit 8e896c4

File tree

5 files changed

+130
-33
lines changed

5 files changed

+130
-33
lines changed

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

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as utils from "../test-utils/test-utils";
2-
import { EventTimeline } from "../../src/matrix";
2+
import { EventTimeline, Filter, MatrixEvent } from "../../src/matrix";
33
import { logger } from "../../src/logger";
44
import { TestClient } from "../TestClient";
55
import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
@@ -500,7 +500,8 @@ describe("MatrixClient event timelines", function() {
500500
Thread.setServerSideSupport(true);
501501
client.stopClient(); // we don't need the client to be syncing at this time
502502
const room = client.getRoom(roomId);
503-
const timelineSet = room.getTimelineSets()[0];
503+
const thread = room.createThread(THREAD_ROOT.event_id, undefined, [], false);
504+
const timelineSet = thread.timelineSet;
504505

505506
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id))
506507
.respond(200, function() {
@@ -538,6 +539,92 @@ describe("MatrixClient event timelines", function() {
538539
expect(timeline.getEvents().find(e => e.getId() === THREAD_ROOT.event_id)).toBeTruthy();
539540
expect(timeline.getEvents().find(e => e.getId() === THREAD_REPLY.event_id)).toBeTruthy();
540541
});
542+
543+
it("should return undefined when event is not within a thread but timelineSet is", async () => {
544+
client.clientOpts.experimentalThreadSupport = true;
545+
Thread.setServerSideSupport(true);
546+
client.stopClient(); // we don't need the client to be syncing at this time
547+
const room = client.getRoom(roomId);
548+
const threadRoot = new MatrixEvent(THREAD_ROOT);
549+
const thread = room.createThread(THREAD_ROOT.event_id, threadRoot, [threadRoot], false);
550+
const timelineSet = thread.timelineSet;
551+
552+
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id))
553+
.respond(200, function() {
554+
return THREAD_ROOT;
555+
});
556+
557+
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id))
558+
.respond(200, function() {
559+
return {
560+
start: "start_token0",
561+
events_before: [],
562+
event: EVENTS[0],
563+
events_after: [],
564+
end: "end_token0",
565+
state: [],
566+
};
567+
});
568+
569+
const timelinePromise = client.getEventTimeline(timelineSet, EVENTS[0].event_id);
570+
await httpBackend.flushAllExpected();
571+
572+
const timeline = await timelinePromise;
573+
expect(timeline).toBeUndefined();
574+
});
575+
576+
it("should return undefined when event is within a thread but timelineSet is not", async () => {
577+
client.clientOpts.experimentalThreadSupport = true;
578+
Thread.setServerSideSupport(true);
579+
client.stopClient(); // we don't need the client to be syncing at this time
580+
const room = client.getRoom(roomId);
581+
const timelineSet = room.getTimelineSets()[0];
582+
583+
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id))
584+
.respond(200, function() {
585+
return {
586+
start: "start_token0",
587+
events_before: [],
588+
event: THREAD_REPLY,
589+
events_after: [],
590+
end: "end_token0",
591+
state: [],
592+
};
593+
});
594+
595+
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id);
596+
await httpBackend.flushAllExpected();
597+
598+
const timeline = await timelinePromise;
599+
expect(timeline).toBeUndefined();
600+
});
601+
602+
it("should should add lazy loading filter when requested", async () => {
603+
client.clientOpts.lazyLoadMembers = true;
604+
client.stopClient(); // we don't need the client to be syncing at this time
605+
const room = client.getRoom(roomId);
606+
const timelineSet = room.getTimelineSets()[0];
607+
608+
const req = httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id));
609+
req.respond(200, function() {
610+
return {
611+
start: "start_token0",
612+
events_before: [],
613+
event: EVENTS[0],
614+
events_after: [],
615+
end: "end_token0",
616+
state: [],
617+
};
618+
});
619+
req.check((request) => {
620+
expect(request.opts.qs.filter).toEqual(JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER));
621+
});
622+
623+
await Promise.all([
624+
client.getEventTimeline(timelineSet, EVENTS[0].event_id),
625+
httpBackend.flushAllExpected(),
626+
]);
627+
});
541628
});
542629

543630
describe("getLatestTimeline", function() {

src/client.ts

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5245,14 +5245,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
52455245
* <p>If the EventTimelineSet object already has the given event in its store, the
52465246
* corresponding timeline will be returned. Otherwise, a /context request is
52475247
* made, and used to construct an EventTimeline.
5248+
* If the event does not belong to this EventTimelineSet then undefined will be returned.
52485249
*
5249-
* @param {EventTimelineSet} timelineSet The timelineSet to look for the event in
5250+
* @param {EventTimelineSet} timelineSet The timelineSet to look for the event in, must be bound to a room
52505251
* @param {string} eventId The ID of the event to look for
52515252
*
52525253
* @return {Promise} Resolves:
52535254
* {@link module:models/event-timeline~EventTimeline} including the given event
52545255
*/
5255-
public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline> {
5256+
public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline | undefined> {
52565257
// don't allow any timeline support unless it's been enabled.
52575258
if (!this.timelineSupport) {
52585259
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
@@ -5297,38 +5298,44 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
52975298
...res.events_before.map(mapper),
52985299
];
52995300

5300-
// Where the event is a thread reply (not a root) and running in MSC-enabled mode the Thread timeline only
5301-
// functions contiguously, so we have to jump through some hoops to get our target event in it.
5302-
// XXX: workaround for https://github.com/vector-im/element-meta/issues/150
5303-
if (Thread.hasServerSideSupport &&
5304-
this.supportsExperimentalThreads() &&
5305-
event.isRelation(THREAD_RELATION_TYPE.name)
5306-
) {
5307-
const [, threadedEvents] = timelineSet.room.partitionThreadedEvents(events);
5308-
let thread = timelineSet.room.getThread(event.threadRootId);
5309-
if (!thread) {
5310-
thread = timelineSet.room.createThread(event.threadRootId, undefined, threadedEvents, true);
5301+
if (this.supportsExperimentalThreads()) {
5302+
const { threadId, shouldLiveInRoom } = timelineSet.room.eventShouldLiveIn(event);
5303+
5304+
if (!timelineSet.thread && !shouldLiveInRoom) {
5305+
// Thread response does not belong in this timelineSet
5306+
return undefined;
53115307
}
53125308

5313-
const opts: IRelationsRequestOpts = {
5314-
direction: Direction.Backward,
5315-
limit: 50,
5316-
};
5309+
if (timelineSet.thread?.id !== threadId) {
5310+
// Event does not belong in this timelineSet
5311+
return undefined;
5312+
}
53175313

5318-
await thread.fetchInitialEvents();
5319-
let nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward);
5314+
// Where the event is a thread reply (not a root) and running in MSC-enabled mode the Thread timeline only
5315+
// functions contiguously, so we have to jump through some hoops to get our target event in it.
5316+
// XXX: workaround for https://github.com/vector-im/element-meta/issues/150
5317+
if (Thread.hasServerSideSupport && timelineSet.thread) {
5318+
const thread = timelineSet.thread;
5319+
const opts: IRelationsRequestOpts = {
5320+
direction: Direction.Backward,
5321+
limit: 50,
5322+
};
53205323

5321-
// Fetch events until we find the one we were asked for, or we run out of pages
5322-
while (!thread.findEventById(eventId)) {
5323-
if (nextBatch) {
5324-
opts.from = nextBatch;
5324+
await thread.fetchInitialEvents();
5325+
let nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward);
5326+
5327+
// Fetch events until we find the one we were asked for, or we run out of pages
5328+
while (!thread.findEventById(eventId)) {
5329+
if (nextBatch) {
5330+
opts.from = nextBatch;
5331+
}
5332+
5333+
({ nextBatch } = await thread.fetchEvents(opts));
5334+
if (!nextBatch) break;
53255335
}
53265336

5327-
({ nextBatch } = await thread.fetchEvents(opts));
5328-
if (!nextBatch) break;
5337+
return thread.liveTimeline;
53295338
}
5330-
5331-
return thread.liveTimeline;
53325339
}
53335340

53345341
// Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.

src/models/event-timeline-set.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { RoomState } from "./room-state";
2727
import { TypedEventEmitter } from "./typed-event-emitter";
2828
import { RelationsContainer } from "./relations-container";
2929
import { MatrixClient } from "../client";
30+
import { Thread } from "./thread";
3031

3132
const DEBUG = true;
3233

@@ -110,7 +111,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
110111
* map from event_id to timeline and index.
111112
*
112113
* @constructor
113-
* @param {?Room} room
114+
* @param {Room=} room
114115
* Room for this timelineSet. May be null for non-room cases, such as the
115116
* notification timeline.
116117
* @param {Object} opts Options inherited from Room.
@@ -119,13 +120,15 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
119120
* Set to true to enable improved timeline support.
120121
* @param {Object} [opts.filter = null]
121122
* The filter object, if any, for this timelineSet.
122-
* @param {MatrixClient} client the Matrix client which owns this EventTimelineSet,
123+
* @param {MatrixClient=} client the Matrix client which owns this EventTimelineSet,
123124
* can be omitted if room is specified.
125+
* @param {Thread=} thread the thread to which this timeline set relates.
124126
*/
125127
constructor(
126128
public readonly room: Room | undefined,
127129
opts: IOpts = {},
128130
client?: MatrixClient,
131+
public readonly thread?: Thread,
129132
) {
130133
super();
131134

src/models/event.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
537537
return mRelatesTo?.['m.in_reply_to']?.event_id;
538538
}
539539

540-
public get relationEventId(): string {
540+
public get relationEventId(): string | undefined {
541541
return this.getWireContent()
542542
?.["m.relates_to"]
543543
?.event_id;

src/models/thread.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
9090
this.timelineSet = new EventTimelineSet(this.room, {
9191
timelineSupport: true,
9292
pendingEvents: true,
93-
});
93+
}, this.client, this);
9494
this.reEmitter = new TypedReEmitter(this);
9595

9696
this.reEmitter.reEmit(this.timelineSet, [

0 commit comments

Comments
 (0)