-
-
Notifications
You must be signed in to change notification settings - Fork 591
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix refreshLiveTimeline(...)
not working when the latest event in the room is a threaded message
#2852
Conversation
A v2 of #2521
@@ -1178,7 +1241,7 @@ describe("MatrixClient event timelines", function() { | |||
await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!)); | |||
|
|||
respondToMessagesRequest(); | |||
const timeline = await flushHttp(client.getLatestTimeline(timelineSet)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like some non-refresh-timeline tests got infected with getLatestTimeline
usage in the months while these PR's have sat.
But I don't really want to support getLatestTimeline
for threads which these tests rely on.
Either we have to maintain the old getLatestTimeline(...)
alongside getLatestLiveTimeline(...)
forever or we can refactor these tests away from it.
Do we care about any external consumers that also may have started using it? Maybe should have marked it experimental to match the sole usage when it was introduced.
src/client.ts
Outdated
const messagesPath = utils.encodeUri( | ||
"/rooms/$roomId/messages", { | ||
$roomId: timelineSet.room.roomId, | ||
}, | ||
); | ||
const messageRequestParams: Record<string, string | string[]> = { | ||
dir: 'b', | ||
// Since we only use the latest message in the response, we only need to | ||
// fetch the one message here. | ||
limit: "1", | ||
}; | ||
if (this.clientOpts?.lazyLoadMembers) { | ||
messageRequestParams.filter = JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER); | ||
} | ||
const messagesRes = await this.http.authedRequest<IMessagesResponse>( | ||
Method.Get, | ||
messagesPath, | ||
messageRequestParams, | ||
); | ||
const latestEventInTimeline = messagesRes.chunk?.[0]; | ||
const latestEventIdInTimeline = latestEventInTimeline?.event_id; | ||
if (!latestEventIdInTimeline) { | ||
throw new Error("No message returned when trying to construct getLatestLiveTimeline"); | ||
} | ||
|
||
const res = await this.http.authedRequest<IMessagesResponse>(Method.Get, messagesPath, params); | ||
event = res.chunk?.[0]; | ||
const contextPath = utils.encodeUri( | ||
"/rooms/$roomId/context/$eventId", { | ||
$roomId: timelineSet.room.roomId, | ||
$eventId: latestEventIdInTimeline, | ||
}, | ||
); | ||
let contextRequestParams: Record<string, string | string[]> | undefined = undefined; | ||
if (this.clientOpts?.lazyLoadMembers) { | ||
contextRequestParams = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) }; | ||
} | ||
if (!event) { | ||
throw new Error("No message returned when trying to construct getLatestTimeline"); | ||
const contextRes = await this.http.authedRequest<IContextResponse>( | ||
Method.Get, | ||
contextPath, | ||
contextRequestParams, | ||
); | ||
if (!contextRes.event || contextRes.event.event_id !== latestEventIdInTimeline) { | ||
throw new Error( | ||
`getLatestLiveTimeline: \`/context\` response did not include latestEventIdInTimeline=` + | ||
`${latestEventIdInTimeline} which we were asking about. This is probably a bug in the ` + | ||
`homeserver since we just saw the event with the other request above and now the server ` + | ||
`claims it does not exist.`, | ||
); | ||
} | ||
|
||
// By the time the request completes, the event might have ended up in the timeline. | ||
const shortcutTimelineForEvent = timelineSet.getTimelineForEvent(latestEventIdInTimeline); | ||
if (shortcutTimelineForEvent) { | ||
return shortcutTimelineForEvent; | ||
} | ||
|
||
const mapper = this.getEventMapper(); | ||
const latestMatrixEventInTimeline = mapper(contextRes.event); | ||
const events = [ | ||
// Order events from most recent to oldest (reverse-chronological). | ||
// We start with the last event, since that's the point at which we have known state. | ||
// events_after is already backwards; events_before is forwards. | ||
...contextRes.events_after.reverse().map(mapper), | ||
latestMatrixEventInTimeline, | ||
...contextRes.events_before.map(mapper), | ||
]; | ||
|
||
// This function handles non-thread timelines only, but we still process any | ||
// thread events to populate thread summaries. | ||
let timeline = timelineSet.getTimelineForEvent(events[0].getId()!); | ||
if (timeline) { | ||
timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(contextRes.state.map(mapper)); | ||
} else { | ||
// If the `latestEventIdInTimeline` does not belong to this `timelineSet` | ||
// then it will be ignored and not added to the `timelineSet`. We'll instead | ||
// just create a new blank timeline in the `timelineSet` with the proper | ||
// pagination tokens setup to continue paginating. | ||
timeline = timelineSet.addTimeline(); | ||
timeline.initialiseState(contextRes.state.map(mapper)); | ||
timeline.getState(EventTimeline.FORWARDS)!.paginationToken = contextRes.end; | ||
} | ||
|
||
return this.getEventTimeline(timelineSet, event.event_id); | ||
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(events); | ||
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, contextRes.start); | ||
// The target event is not in a thread but process the contextual events, so we can show any threads around it. | ||
this.processThreadEvents(timelineSet.room, threadedEvents, true); | ||
this.processBeaconEvents(timelineSet.room, timelineEvents); | ||
|
||
return timelineSet.getTimelineForEvent(latestEventIdInTimeline) ?? timeline; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opted for a new function as concluded in #2521 (comment)
This is very similar to the existing logic in getEventTimeline(...)
but changed so that
// If the `latestEventIdInTimeline` does not belong to this `timelineSet` // then it will be ignored and not added to the `timelineSet`. We'll instead // just create a new blank timeline in the `timelineSet` with the proper // pagination tokens setup to continue paginating.
In getEventTimeline(...)
, it will return undefined
which isn't useful when we want the new empty timeline. Any good way to de-dupe this logic or untangle the mess?
const mostRecentEventInTimeline = eventsBefore[eventsBefore.length - 1]; | ||
const mostRecentEventIdInTimeline = mostRecentEventInTimeline?.getId(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am surprised that TypeScript didn't catch this before. I would have assumed that TypeScript would infer undefined
as a possibility from an array lookup but I guess not.
Related https://stackoverflow.com/questions/59479171/typescript-does-not-infer-undefined-as-a-return-type-from-array-prototype-find which mentions that strictNullChecks
needs to be enabled for it to infer undefined
there.
Is strictNullChecks
separate from strict
? We added strict
recently in #2835
Related to element-hq/element-web#21967 for adding strictNullChecks
} else { | ||
throw new Error( | ||
`fetchLatestLiveTimeline: While updating backwards state of the existing ` + | ||
` timeline=${timeline.toString()}, it unexpectedly did not have any backwards ` + | ||
`state. Something probably broke with the timeline earlier for this to happen.`, | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume SonarCloud is complaining that we don't have coverage for these two conditionals (this one and the one below like it).
These are just meant as catches for unexpected state and giving a better error message than the havoc the NPE and undefined behavior would cause the down the line. Seems better than assuming the reference is there with a !
which is what getEventTimeline
is opting to use. I can also update it do a similar thing (just poke)
Lines 5330 to 5338 in 0fbd0b3
// Here we handle non-thread timelines only, but still process any thread events to populate thread summaries. | |
let timeline = timelineSet.getTimelineForEvent(events[0].getId()); | |
if (timeline) { | |
timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(res.state.map(mapper)); | |
} else { | |
timeline = timelineSet.addTimeline(); | |
timeline.initialiseState(res.state.map(mapper)); | |
timeline.getState(EventTimeline.FORWARDS)!.paginationToken = res.end; | |
} |
To be clear, I haven't run into these unexpected states before and I haven't looked into why a timeline wouldn't have these set. I'm guessing non-room timelines won't have them set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to matrix-org/matrix-react-sdk#9558 (comment) about not stressing over coverage for these safety checks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clearing element-call-reviewers review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR duplicates a lot of code. I think it would be worth extracting and sharing the bulk of the getEventTimeline
/fetchLatestLiveTimeline
functions that is afaict identical
const messagesPath = utils.encodeUri( | ||
"/rooms/$roomId/messages", { | ||
$roomId: timelineSet.room.roomId, | ||
}, | ||
); | ||
const messageRequestParams: Record<string, string | string[]> = { | ||
dir: 'b', | ||
// Since we only use the latest message in the response, we only need to | ||
// fetch the one message here. | ||
limit: "1", | ||
}; | ||
if (this.clientOpts?.lazyLoadMembers) { | ||
messageRequestParams.filter = JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER); | ||
} | ||
const messagesRes = await this.http.authedRequest<IMessagesResponse>( | ||
Method.Get, | ||
messagesPath, | ||
messageRequestParams, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this reuse createMessagesRequest
instead?
const contextPath = utils.encodeUri( | ||
"/rooms/$roomId/context/$eventId", { | ||
$roomId: timelineSet.room.roomId, | ||
$eventId: latestEventIdInTimeline, | ||
}, | ||
); | ||
let contextRequestParams: Record<string, string | string[]> | undefined = undefined; | ||
if (this.clientOpts?.lazyLoadMembers) { | ||
contextRequestParams = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) }; | ||
} | ||
const contextRes = await this.http.authedRequest<IContextResponse>( | ||
Method.Get, | ||
contextPath, | ||
contextRequestParams, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be extracted into a function and reused in the other identical call sites?
@@ -411,10 +411,11 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap | |||
this.invitee = opts.invitee; | |||
this.client = opts.client; | |||
|
|||
if (!this.client.deviceId) throw new Error("Client must have a device ID to start calls"); | |||
const deviceId = this.client.getDeviceId(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
change seems unrelated? can you remove?
}, | ||
); | ||
const messageRequestParams: Record<string, string | string[]> = { | ||
dir: 'b', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Direction.Backward
await client.stopClient(); | ||
|
||
const testClient = new TestClient( | ||
userId, | ||
"DEVICE", | ||
accessToken, | ||
undefined, | ||
{ timelineSupport: false }, | ||
); | ||
client = testClient.client; | ||
httpBackend = testClient.httpBackend; | ||
await startClient(httpBackend, client); | ||
|
||
const room = client.getRoom(roomId)!; | ||
const timelineSet = room.getTimelineSets()[0]!; | ||
await expect(client.fetchLatestLiveTimeline(timelineSet)).rejects.toBeTruthy(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's a little room for deduplicating code in these first 3 tests
Hi! Are you still working on it? |
Fix
refreshLiveTimeline(...)
not working when the latest event in the room is a threaded message.matrix-react-sdk
PR: Ask to refresh timeline when historical messages are imported (MSC2716) matrix-react-sdk#8354getLatestTimeline
not working when the latest event in the room is a threaded message #2521This works because
fetchLatestLiveTimeline()
will get anEventTimeline
for the latest events in the room. It first calls/messages?dir=b
to find the latest message,/context
to get the proper pagination tokens, sets those pagination tokens on the timeline so it can paginate appropriately. It doesn't matter whether the latest event in the room is a thread because if it is, then we will just create a newEventTimeline
in theEventTimelineSet
, otherwise will get the existing timeline.See matrix-org/matrix-react-sdk#8354 (comment)
We also have to keep in mind that we don't want to mix messages in the wrong timelines (main vs threaded timeline):
getEventTimeline
returning undefined for thread roots in main timeline #2454Checklist
Sign-off given on the changes (see CONTRIBUTING.md)Here's what your changelog entry will look like:
🐛 Bug Fixes
refreshLiveTimeline(...)
not working when the latest event in the room is a threaded message (#2852). Contributed by @MadLittleMods.