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

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
This commit is contained in:
Michael Telatynski
2022-06-08 23:11:22 +01:00
committed by GitHub
parent 2c2686c910
commit 8e896c4da3
5 changed files with 130 additions and 33 deletions

View File

@@ -1,5 +1,5 @@
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
import { EventTimeline } from "../../src/matrix"; import { EventTimeline, Filter, MatrixEvent } from "../../src/matrix";
import { logger } from "../../src/logger"; import { logger } from "../../src/logger";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread"; import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
@@ -500,7 +500,8 @@ describe("MatrixClient event timelines", function() {
Thread.setServerSideSupport(true); Thread.setServerSideSupport(true);
client.stopClient(); // we don't need the client to be syncing at this time client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId); const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0]; const thread = room.createThread(THREAD_ROOT.event_id, undefined, [], false);
const timelineSet = thread.timelineSet;
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id)) httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id))
.respond(200, function() { .respond(200, function() {
@@ -538,6 +539,92 @@ describe("MatrixClient event timelines", function() {
expect(timeline.getEvents().find(e => e.getId() === THREAD_ROOT.event_id)).toBeTruthy(); expect(timeline.getEvents().find(e => e.getId() === THREAD_ROOT.event_id)).toBeTruthy();
expect(timeline.getEvents().find(e => e.getId() === THREAD_REPLY.event_id)).toBeTruthy(); expect(timeline.getEvents().find(e => e.getId() === THREAD_REPLY.event_id)).toBeTruthy();
}); });
it("should return undefined when event is not within a thread but timelineSet is", async () => {
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(true);
client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId);
const threadRoot = new MatrixEvent(THREAD_ROOT);
const thread = room.createThread(THREAD_ROOT.event_id, threadRoot, [threadRoot], false);
const timelineSet = thread.timelineSet;
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id))
.respond(200, function() {
return THREAD_ROOT;
});
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: EVENTS[0],
events_after: [],
end: "end_token0",
state: [],
};
});
const timelinePromise = client.getEventTimeline(timelineSet, EVENTS[0].event_id);
await httpBackend.flushAllExpected();
const timeline = await timelinePromise;
expect(timeline).toBeUndefined();
});
it("should return undefined when event is within a thread but timelineSet is not", async () => {
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(true);
client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: THREAD_REPLY,
events_after: [],
end: "end_token0",
state: [],
};
});
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id);
await httpBackend.flushAllExpected();
const timeline = await timelinePromise;
expect(timeline).toBeUndefined();
});
it("should should add lazy loading filter when requested", async () => {
client.clientOpts.lazyLoadMembers = true;
client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
const req = httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id));
req.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: EVENTS[0],
events_after: [],
end: "end_token0",
state: [],
};
});
req.check((request) => {
expect(request.opts.qs.filter).toEqual(JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER));
});
await Promise.all([
client.getEventTimeline(timelineSet, EVENTS[0].event_id),
httpBackend.flushAllExpected(),
]);
});
}); });
describe("getLatestTimeline", function() { describe("getLatestTimeline", function() {

View File

@@ -5245,14 +5245,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* <p>If the EventTimelineSet object already has the given event in its store, the * <p>If the EventTimelineSet object already has the given event in its store, the
* corresponding timeline will be returned. Otherwise, a /context request is * corresponding timeline will be returned. Otherwise, a /context request is
* made, and used to construct an EventTimeline. * made, and used to construct an EventTimeline.
* If the event does not belong to this EventTimelineSet then undefined will be returned.
* *
* @param {EventTimelineSet} timelineSet The timelineSet to look for the event in * @param {EventTimelineSet} timelineSet The timelineSet to look for the event in, must be bound to a room
* @param {string} eventId The ID of the event to look for * @param {string} eventId The ID of the event to look for
* *
* @return {Promise} Resolves: * @return {Promise} Resolves:
* {@link module:models/event-timeline~EventTimeline} including the given event * {@link module:models/event-timeline~EventTimeline} including the given event
*/ */
public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline> { public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline | undefined> {
// don't allow any timeline support unless it's been enabled. // don't allow any timeline support unless it's been enabled.
if (!this.timelineSupport) { if (!this.timelineSupport) {
throw new Error("timeline support is disabled. Set the 'timelineSupport'" + throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
@@ -5297,19 +5298,24 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
...res.events_before.map(mapper), ...res.events_before.map(mapper),
]; ];
if (this.supportsExperimentalThreads()) {
const { threadId, shouldLiveInRoom } = timelineSet.room.eventShouldLiveIn(event);
if (!timelineSet.thread && !shouldLiveInRoom) {
// Thread response does not belong in this timelineSet
return undefined;
}
if (timelineSet.thread?.id !== threadId) {
// Event does not belong in this timelineSet
return undefined;
}
// Where the event is a thread reply (not a root) and running in MSC-enabled mode the Thread timeline only // Where the event is a thread reply (not a root) and running in MSC-enabled mode the Thread timeline only
// functions contiguously, so we have to jump through some hoops to get our target event in it. // functions contiguously, so we have to jump through some hoops to get our target event in it.
// XXX: workaround for https://github.com/vector-im/element-meta/issues/150 // XXX: workaround for https://github.com/vector-im/element-meta/issues/150
if (Thread.hasServerSideSupport && if (Thread.hasServerSideSupport && timelineSet.thread) {
this.supportsExperimentalThreads() && const thread = timelineSet.thread;
event.isRelation(THREAD_RELATION_TYPE.name)
) {
const [, threadedEvents] = timelineSet.room.partitionThreadedEvents(events);
let thread = timelineSet.room.getThread(event.threadRootId);
if (!thread) {
thread = timelineSet.room.createThread(event.threadRootId, undefined, threadedEvents, true);
}
const opts: IRelationsRequestOpts = { const opts: IRelationsRequestOpts = {
direction: Direction.Backward, direction: Direction.Backward,
limit: 50, limit: 50,
@@ -5330,6 +5336,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return thread.liveTimeline; return thread.liveTimeline;
} }
}
// Here we handle non-thread timelines only, but still process any thread events to populate thread summaries. // Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.
let timeline = timelineSet.getTimelineForEvent(events[0].getId()); let timeline = timelineSet.getTimelineForEvent(events[0].getId());

View File

@@ -27,6 +27,7 @@ import { RoomState } from "./room-state";
import { TypedEventEmitter } from "./typed-event-emitter"; import { TypedEventEmitter } from "./typed-event-emitter";
import { RelationsContainer } from "./relations-container"; import { RelationsContainer } from "./relations-container";
import { MatrixClient } from "../client"; import { MatrixClient } from "../client";
import { Thread } from "./thread";
const DEBUG = true; const DEBUG = true;
@@ -110,7 +111,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
* map from event_id to timeline and index. * map from event_id to timeline and index.
* *
* @constructor * @constructor
* @param {?Room} room * @param {Room=} room
* Room for this timelineSet. May be null for non-room cases, such as the * Room for this timelineSet. May be null for non-room cases, such as the
* notification timeline. * notification timeline.
* @param {Object} opts Options inherited from Room. * @param {Object} opts Options inherited from Room.
@@ -119,13 +120,15 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
* Set to true to enable improved timeline support. * Set to true to enable improved timeline support.
* @param {Object} [opts.filter = null] * @param {Object} [opts.filter = null]
* The filter object, if any, for this timelineSet. * The filter object, if any, for this timelineSet.
* @param {MatrixClient} client the Matrix client which owns this EventTimelineSet, * @param {MatrixClient=} client the Matrix client which owns this EventTimelineSet,
* can be omitted if room is specified. * can be omitted if room is specified.
* @param {Thread=} thread the thread to which this timeline set relates.
*/ */
constructor( constructor(
public readonly room: Room | undefined, public readonly room: Room | undefined,
opts: IOpts = {}, opts: IOpts = {},
client?: MatrixClient, client?: MatrixClient,
public readonly thread?: Thread,
) { ) {
super(); super();

View File

@@ -537,7 +537,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
return mRelatesTo?.['m.in_reply_to']?.event_id; return mRelatesTo?.['m.in_reply_to']?.event_id;
} }
public get relationEventId(): string { public get relationEventId(): string | undefined {
return this.getWireContent() return this.getWireContent()
?.["m.relates_to"] ?.["m.relates_to"]
?.event_id; ?.event_id;

View File

@@ -90,7 +90,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
this.timelineSet = new EventTimelineSet(this.room, { this.timelineSet = new EventTimelineSet(this.room, {
timelineSupport: true, timelineSupport: true,
pendingEvents: true, pendingEvents: true,
}); }, this.client, this);
this.reEmitter = new TypedReEmitter(this); this.reEmitter = new TypedReEmitter(this);
this.reEmitter.reEmit(this.timelineSet, [ this.reEmitter.reEmit(this.timelineSet, [