Skip to content

Commit 2451e10

Browse files
committed
Add getLatestLiveTimeline
A v2 of #2521
1 parent 4a33e58 commit 2451e10

File tree

4 files changed

+188
-58
lines changed

4 files changed

+188
-58
lines changed

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

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,12 @@ describe("MatrixClient event timelines", function() {
780780
});
781781
});
782782

783-
describe("getLatestTimeline", function() {
783+
describe("getLatestLiveTimeline", function() {
784+
beforeEach(() => {
785+
// @ts-ignore
786+
client.clientOpts.experimentalThreadSupport = true;
787+
});
788+
784789
it("timeline support must be enabled to work", async function() {
785790
await client.stopClient();
786791

@@ -797,7 +802,7 @@ describe("MatrixClient event timelines", function() {
797802

798803
const room = client.getRoom(roomId)!;
799804
const timelineSet = room.getTimelineSets()[0]!;
800-
await expect(client.getLatestTimeline(timelineSet)).rejects.toBeTruthy();
805+
await expect(client.getLatestLiveTimeline(timelineSet)).rejects.toBeTruthy();
801806
});
802807

803808
it("timeline support works when enabled", async function() {
@@ -816,7 +821,7 @@ describe("MatrixClient event timelines", function() {
816821
return startClient(httpBackend, client).then(() => {
817822
const room = client.getRoom(roomId)!;
818823
const timelineSet = room.getTimelineSets()[0];
819-
expect(client.getLatestTimeline(timelineSet)).rejects.toBeFalsy();
824+
expect(client.getLatestLiveTimeline(timelineSet)).rejects.toBeFalsy();
820825
});
821826
});
822827

@@ -835,14 +840,14 @@ describe("MatrixClient event timelines", function() {
835840
await startClient(httpBackend, client);
836841

837842
const timelineSet = new EventTimelineSet(undefined);
838-
await expect(client.getLatestTimeline(timelineSet)).rejects.toBeTruthy();
843+
await expect(client.getLatestLiveTimeline(timelineSet)).rejects.toBeTruthy();
839844
});
840845

841846
it("should create a new timeline for new events", function() {
842847
const room = client.getRoom(roomId)!;
843848
const timelineSet = room.getTimelineSets()[0];
844849

845-
const latestMessageId = 'event1:bar';
850+
const latestMessageId = EVENTS[2].event_id!;
846851

847852
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
848853
.respond(200, function() {
@@ -869,11 +874,11 @@ describe("MatrixClient event timelines", function() {
869874
});
870875

871876
return Promise.all([
872-
client.getLatestTimeline(timelineSet).then(function(tl) {
877+
client.getLatestLiveTimeline(timelineSet).then(function(tl) {
873878
// Instead of this assertion logic, we could just add a spy
874879
// for `getEventTimeline` and make sure it's called with the
875880
// correct parameters. This doesn't feel too bad to make sure
876-
// `getLatestTimeline` is doing the right thing though.
881+
// `getLatestLiveTimeline` is doing the right thing though.
877882
expect(tl!.getEvents().length).toEqual(4);
878883
for (let i = 0; i < 4; i++) {
879884
expect(tl!.getEvents()[i].event).toEqual(EVENTS[i]);
@@ -888,6 +893,64 @@ describe("MatrixClient event timelines", function() {
888893
]);
889894
});
890895

896+
it("should successfully create a new timeline even when the latest event is a threaded reply", function() {
897+
const room = client.getRoom(roomId);
898+
const timelineSet = room!.getTimelineSets()[0];
899+
expect(timelineSet.thread).toBeUndefined();
900+
901+
const latestMessageId = THREAD_REPLY.event_id;
902+
903+
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
904+
.respond(200, function() {
905+
return {
906+
chunk: [{
907+
event_id: latestMessageId,
908+
}],
909+
};
910+
});
911+
912+
httpBackend.when("GET", `/rooms/!foo%3Abar/context/${encodeURIComponent(latestMessageId)}`)
913+
.respond(200, function() {
914+
return {
915+
start: "start_token",
916+
events_before: [THREAD_ROOT, EVENTS[0]],
917+
event: THREAD_REPLY,
918+
events_after: [],
919+
state: [
920+
ROOM_NAME_EVENT,
921+
USER_MEMBERSHIP_EVENT,
922+
],
923+
end: "end_token",
924+
};
925+
});
926+
927+
// Make it easy to debug when there is a mismatch of events. We care
928+
// about the event ID for direct comparison and the content for a
929+
// human readable description.
930+
const eventPropertiesToCompare = (event) => {
931+
return {
932+
eventId: event.event_id || event.getId(),
933+
contentBody: event.content?.body || event.getContent()?.body,
934+
};
935+
};
936+
return Promise.all([
937+
client.getLatestLiveTimeline(timelineSet).then(function(tl) {
938+
const events = tl!.getEvents();
939+
const expectedEvents = [EVENTS[0], THREAD_ROOT];
940+
expect(events.map(event => eventPropertiesToCompare(event)))
941+
.toEqual(expectedEvents.map(event => eventPropertiesToCompare(event)));
942+
// Sanity check: The threaded reply should not be in the timeline
943+
expect(events.find(e => e.getId() === THREAD_REPLY.event_id)).toBeFalsy();
944+
945+
expect(tl!.getPaginationToken(EventTimeline.BACKWARDS))
946+
.toEqual("start_token");
947+
expect(tl!.getPaginationToken(EventTimeline.FORWARDS))
948+
.toEqual("end_token");
949+
}),
950+
httpBackend.flushAllExpected(),
951+
]);
952+
});
953+
891954
it("should throw error when /messages does not return a message", () => {
892955
const room = client.getRoom(roomId)!;
893956
const timelineSet = room.getTimelineSets()[0];
@@ -902,7 +965,7 @@ describe("MatrixClient event timelines", function() {
902965
});
903966

904967
return Promise.all([
905-
expect(client.getLatestTimeline(timelineSet)).rejects.toThrow(),
968+
expect(client.getLatestLiveTimeline(timelineSet)).rejects.toThrow(),
906969
httpBackend.flushAllExpected(),
907970
]);
908971
});
@@ -1122,7 +1185,7 @@ describe("MatrixClient event timelines", function() {
11221185
respondToContext();
11231186
await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!));
11241187
respondToThreads();
1125-
const timeline = await flushHttp(client.getLatestTimeline(timelineSet));
1188+
const timeline = await flushHttp(client.getLatestLiveTimeline(timelineSet));
11261189
expect(timeline).not.toBeNull();
11271190

11281191
respondToThreads();
@@ -1178,7 +1241,7 @@ describe("MatrixClient event timelines", function() {
11781241
await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!));
11791242

11801243
respondToMessagesRequest();
1181-
const timeline = await flushHttp(client.getLatestTimeline(timelineSet));
1244+
const timeline = await flushHttp(client.getLatestLiveTimeline(timelineSet));
11821245
expect(timeline).not.toBeNull();
11831246

11841247
respondToMessagesRequest();

spec/integ/matrix-client-room-timeline.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -829,7 +829,7 @@ describe("MatrixClient room timelines", function() {
829829
expect(room.timeline.length).toEqual(0);
830830

831831
// `/messages` request for `refreshLiveTimeline()` ->
832-
// `getLatestTimeline()` to construct a new timeline from.
832+
// `getLatestLiveTimeline()` to construct a new timeline from.
833833
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`)
834834
.respond(200, function() {
835835
return {
@@ -840,7 +840,7 @@ describe("MatrixClient room timelines", function() {
840840
};
841841
});
842842
// `/context` request for `refreshLiveTimeline()` ->
843-
// `getLatestTimeline()` -> `getEventTimeline()` to construct a new
843+
// `getLatestLiveTimeline()` -> `getEventTimeline()` to construct a new
844844
// timeline from.
845845
httpBackend!.when("GET", contextUrl)
846846
.respond(200, function() {

src/client.ts

Lines changed: 90 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5400,65 +5400,116 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
54005400
* Get an EventTimeline for the latest events in the room. This will just
54015401
* call `/messages` to get the latest message in the room, then use
54025402
* `client.getEventTimeline(...)` to construct a new timeline from it.
5403+
* Always returns timeline in the given `timelineSet`.
54035404
*
54045405
* @param {EventTimelineSet} timelineSet The timelineSet to find or add the timeline to
54055406
*
54065407
* @return {Promise} Resolves:
54075408
* {@link module:models/event-timeline~EventTimeline} timeline with the latest events in the room
54085409
*/
5409-
public async getLatestTimeline(timelineSet: EventTimelineSet): Promise<Optional<EventTimeline>> {
5410+
public async getLatestLiveTimeline(timelineSet: EventTimelineSet): Promise<EventTimeline> {
54105411
// don't allow any timeline support unless it's been enabled.
54115412
if (!this.timelineSupport) {
54125413
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
54135414
" parameter to true when creating MatrixClient to enable it.");
54145415
}
54155416

54165417
if (!timelineSet.room) {
5417-
throw new Error("getLatestTimeline only supports room timelines");
5418+
throw new Error("getLatestLiveTimeline only supports room timelines");
54185419
}
54195420

5420-
let event;
5421-
if (timelineSet.threadListType !== null) {
5422-
const res = await this.createThreadListMessagesRequest(
5423-
timelineSet.room.roomId,
5424-
null,
5425-
1,
5426-
Direction.Backward,
5427-
timelineSet.threadListType,
5428-
timelineSet.getFilter(),
5429-
);
5430-
event = res.chunk?.[0];
5431-
} else if (timelineSet.thread && Thread.hasServerSideSupport) {
5432-
const res = await this.fetchRelations(
5433-
timelineSet.room.roomId,
5434-
timelineSet.thread.id,
5435-
THREAD_RELATION_TYPE.name,
5436-
null,
5437-
{ dir: Direction.Backward, limit: 1 },
5438-
);
5439-
event = res.chunk?.[0];
5440-
} else {
5441-
const messagesPath = utils.encodeUri(
5442-
"/rooms/$roomId/messages", {
5443-
$roomId: timelineSet.room.roomId,
5444-
},
5445-
);
5421+
if (timelineSet.threadListType !== null || timelineSet.thread && Thread.hasServerSideSupport) {
5422+
throw new Error("getLatestLiveTimeline only supports live timelines");
5423+
}
54465424

5447-
const params: Record<string, string | string[]> = {
5448-
dir: 'b',
5449-
};
5450-
if (this.clientOpts?.lazyLoadMembers) {
5451-
params.filter = JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER);
5452-
}
5425+
const messagesPath = utils.encodeUri(
5426+
"/rooms/$roomId/messages", {
5427+
$roomId: timelineSet.room.roomId,
5428+
},
5429+
);
5430+
const messageRequestParams: Record<string, string | string[]> = {
5431+
dir: 'b',
5432+
// Since we only use the latest message in the response, we only need to
5433+
// fetch the one message here.
5434+
limit: "1",
5435+
};
5436+
if (this.clientOpts?.lazyLoadMembers) {
5437+
messageRequestParams.filter = JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER);
5438+
}
5439+
const messagesRes = await this.http.authedRequest<IMessagesResponse>(
5440+
Method.Get,
5441+
messagesPath,
5442+
messageRequestParams,
5443+
);
5444+
const latestEventInTimeline = messagesRes.chunk?.[0];
5445+
const latestEventIdInTimeline = latestEventInTimeline?.event_id;
5446+
if (!latestEventIdInTimeline) {
5447+
throw new Error("No message returned when trying to construct getLatestLiveTimeline");
5448+
}
54535449

5454-
const res = await this.http.authedRequest<IMessagesResponse>(Method.Get, messagesPath, params);
5455-
event = res.chunk?.[0];
5450+
const contextPath = utils.encodeUri(
5451+
"/rooms/$roomId/context/$eventId", {
5452+
$roomId: timelineSet.room.roomId,
5453+
$eventId: latestEventIdInTimeline,
5454+
},
5455+
);
5456+
let contextRequestParams: Record<string, string | string[]> | undefined = undefined;
5457+
if (this.clientOpts?.lazyLoadMembers) {
5458+
contextRequestParams = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) };
54565459
}
5457-
if (!event) {
5458-
throw new Error("No message returned when trying to construct getLatestTimeline");
5460+
const contextRes = await this.http.authedRequest<IContextResponse>(
5461+
Method.Get,
5462+
contextPath,
5463+
contextRequestParams,
5464+
);
5465+
if (!contextRes.event || contextRes.event.event_id !== latestEventIdInTimeline) {
5466+
throw new Error(
5467+
`getLatestLiveTimeline: \`/context\` response did not include latestEventIdInTimeline=` +
5468+
`${latestEventIdInTimeline} which we were asking about. This is probably a bug in the ` +
5469+
`homeserver since we just saw the event with the other request above and now the server ` +
5470+
`claims it does not exist.`,
5471+
);
5472+
}
5473+
5474+
// By the time the request completes, the event might have ended up in the timeline.
5475+
const shortcutTimelineForEvent = timelineSet.getTimelineForEvent(latestEventIdInTimeline);
5476+
if (shortcutTimelineForEvent) {
5477+
return shortcutTimelineForEvent;
5478+
}
5479+
5480+
const mapper = this.getEventMapper();
5481+
const latestMatrixEventInTimeline = mapper(contextRes.event);
5482+
const events = [
5483+
// Order events from most recent to oldest (reverse-chronological).
5484+
// We start with the last event, since that's the point at which we have known state.
5485+
// events_after is already backwards; events_before is forwards.
5486+
...contextRes.events_after.reverse().map(mapper),
5487+
latestMatrixEventInTimeline,
5488+
...contextRes.events_before.map(mapper),
5489+
];
5490+
5491+
// This function handles non-thread timelines only, but we still process any
5492+
// thread events to populate thread summaries.
5493+
let timeline = timelineSet.getTimelineForEvent(events[0].getId()!);
5494+
if (timeline) {
5495+
timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(contextRes.state.map(mapper));
5496+
} else {
5497+
// If the `latestEventIdInTimeline` does not belong to this `timelineSet`
5498+
// then it will be ignored and not added to the `timelineSet`. We'll instead
5499+
// just create a new blank timeline in the `timelineSet` with the proper
5500+
// pagination tokens setup to continue paginating.
5501+
timeline = timelineSet.addTimeline();
5502+
timeline.initialiseState(contextRes.state.map(mapper));
5503+
timeline.getState(EventTimeline.FORWARDS)!.paginationToken = contextRes.end;
54595504
}
54605505

5461-
return this.getEventTimeline(timelineSet, event.event_id);
5506+
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(events);
5507+
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, contextRes.start);
5508+
// The target event is not in a thread but process the contextual events, so we can show any threads around it.
5509+
this.processThreadEvents(timelineSet.room, threadedEvents, true);
5510+
this.processBeaconEvents(timelineSet.room, timelineEvents);
5511+
5512+
return timelineSet.getTimelineForEvent(latestEventIdInTimeline) ?? timeline;
54625513
}
54635514

54645515
/**

src/models/room.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -913,9 +913,10 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
913913
const backwardPaginationToken = liveTimelineBefore.getPaginationToken(EventTimeline.BACKWARDS);
914914
const eventsBefore = liveTimelineBefore.getEvents();
915915
const mostRecentEventInTimeline = eventsBefore[eventsBefore.length - 1];
916+
const mostRecentEventIdInTimeline = mostRecentEventInTimeline.getId();
916917
logger.log(
917918
`[refreshLiveTimeline for ${this.roomId}] at ` +
918-
`mostRecentEventInTimeline=${mostRecentEventInTimeline && mostRecentEventInTimeline.getId()} ` +
919+
`mostRecentEventIdInTimeline=${mostRecentEventIdInTimeline} ` +
919920
`liveTimelineBefore=${liveTimelineBefore.toString()} ` +
920921
`forwardPaginationToken=${forwardPaginationToken} ` +
921922
`backwardPaginationToken=${backwardPaginationToken}`,
@@ -924,15 +925,15 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
924925
// Get the main TimelineSet
925926
const timelineSet = this.getUnfilteredTimelineSet();
926927

927-
let newTimeline: Optional<EventTimeline>;
928+
let newTimeline: EventTimeline;
928929
// If there isn't any event in the timeline, let's go fetch the latest
929930
// event and construct a timeline from it.
930931
//
931932
// This should only really happen if the user ran into an error
932933
// with refreshing the timeline before which left them in a blank
933934
// timeline from `resetLiveTimeline`.
934-
if (!mostRecentEventInTimeline) {
935-
newTimeline = await this.client.getLatestTimeline(timelineSet);
935+
if (!mostRecentEventIdInTimeline) {
936+
newTimeline = await this.client.getLatestLiveTimeline(timelineSet);
936937
} else {
937938
// Empty out all of `this.timelineSets`. But we also need to keep the
938939
// same `timelineSet` references around so the React code updates
@@ -955,7 +956,22 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
955956
// we reset everything. The `timelineSet` we pass in needs to be empty
956957
// in order for this function to call `/context` and generate a new
957958
// timeline.
958-
newTimeline = await this.client.getEventTimeline(timelineSet, mostRecentEventInTimeline.getId()!);
959+
const timelineForMostRecentEvent = await this.client.getEventTimeline(
960+
timelineSet,
961+
mostRecentEventIdInTimeline,
962+
);
963+
964+
if (!timelineForMostRecentEvent) {
965+
throw new Error(
966+
`refreshLiveTimeline: No new timeline was returned by \`getEventTimeline(...)\`. ` +
967+
`This probably means that mostRecentEventIdInTimeline=${mostRecentEventIdInTimeline}` +
968+
`which was in the live timeline before, wasn't supposed to be there in the first place ` +
969+
`(maybe a problem with threads leaking into the main live timeline). ` +
970+
`This is a problem with Element, please report this error.`,
971+
);
972+
}
973+
974+
newTimeline = timelineForMostRecentEvent;
959975
}
960976

961977
// If a racing `/sync` beat us to creating a new timeline, use that
@@ -972,11 +988,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
972988
// of using the `/context` historical token (ex. `t12-13_0_0_0_0_0_0_0_0`)
973989
// so that it matches the next response from `/sync` and we can properly
974990
// continue the timeline.
975-
newTimeline!.setPaginationToken(forwardPaginationToken, EventTimeline.FORWARDS);
991+
newTimeline.setPaginationToken(forwardPaginationToken, EventTimeline.FORWARDS);
976992

977993
// Set our new fresh timeline as the live timeline to continue syncing
978994
// forwards and back paginating from.
979-
timelineSet.setLiveTimeline(newTimeline!);
995+
timelineSet.setLiveTimeline(newTimeline);
980996
// Fixup `this.oldstate` so that `scrollback` has the pagination tokens
981997
// available
982998
this.fixUpLegacyTimelineFields();

0 commit comments

Comments
 (0)