-
-
Notifications
You must be signed in to change notification settings - Fork 830
Ask to refresh timeline when historical messages are imported (MSC2716) #8303
Ask to refresh timeline when historical messages are imported (MSC2716) #8303
Conversation
Associated matrix-js-sdk PR: matrix-org/matrix-js-sdk#2282
// TODO: What's the best way to refresh the timeline? Something like | ||
// `room.resetLiveTimeline(null, null);` although this just seems to | ||
// clear the timeline. I also tried to split out | ||
// `scrollbackFromPaginationToken` from the `scrollback` method in to | ||
// paginate from the beginning of the room but it's just not right. |
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.
What's the best way to refresh the timeline? Something like room.resetLiveTimeline(null, null);
although this just seems to clear the timeline.
I also tried to split out the logic from scrollback
into a new scrollbackFromPaginationToken
method in order to paginate from the latest in the room (instead of from the current paginationToken
) and fill in the timeline again after it goes blank but the room just stays blank.
scrollbackFromPaginationToken refactor
/**
* Retrieve older messages from the given room and put them in the timeline.
*
* If this is called multiple times whilst a request is ongoing, the <i>same</i>
* Promise will be returned. If there was a problem requesting scrollback, there
* will be a small delay before another request can be made (to prevent tight-looping
* when there is no connection).
*
* @param {Room} room The room to get older messages in.
* @param {Integer} limit Optional. The maximum number of previous events to
* pull in. Default: 30.
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: Room. If you are at the beginning
* of the timeline, <code>Room.oldState.paginationToken</code> will be
* <code>null</code>.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public scrollback(room: Room, limit = 30, callback?: Callback): Promise<Room> {
if (utils.isFunction(limit)) {
callback = limit as any as Callback; // legacy
limit = undefined;
}
let timeToWaitMs = 0;
let info = this.ongoingScrollbacks[room.roomId] || {};
if (info.promise) {
return info.promise;
} else if (info.errorTs) {
const timeWaitedMs = Date.now() - info.errorTs;
timeToWaitMs = Math.max(SCROLLBACK_DELAY_MS - timeWaitedMs, 0);
}
if (room.oldState.paginationToken === null) {
return Promise.resolve(room); // already at the start.
}
// attempt to grab more events from the store first
const numAdded = this.store.scrollback(room, limit).length;
if (numAdded === limit) {
// store contained everything we needed.
return Promise.resolve(room);
}
// reduce the required number of events appropriately
limit = limit - numAdded;
const prom = Promise.resolve().then(async () => {
try {
// wait for a time before doing this request
// (which may be 0 in order not to special case the code paths)
await sleep(timeToWaitMs)
await this.scrollbackFromPaginationToken({
room,
fromToken: room.oldState.paginationToken,
direction: Direction.Backward,
limit,
})
this.ongoingScrollbacks[room.roomId] = null;
callback?.(null, room);
return room;
} catch(err) {
this.ongoingScrollbacks[room.roomId] = {
errorTs: Date.now(),
};
callback?.(err);
throw err;
}
});
info = {
promise: prom,
errorTs: null,
};
this.ongoingScrollbacks[room.roomId] = info;
return prom;
}
public async scrollbackFromPaginationToken({
room,
fromToken,
direction,
limit,
}: {
room: Room,
fromToken: string | null,
direction: Direction,
limit?: number,
}) {
const res: IMessagesResponse = await this.createMessagesRequest(
room.roomId,
fromToken,
limit,
direction,
);
const matrixEvents = res.chunk.map(this.getEventMapper());
if (res.state) {
const stateEvents = res.state.map(this.getEventMapper());
room.currentState.setUnknownStateEvents(stateEvents);
}
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(room, matrixEvents);
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
await this.processThreadEvents(room, threadedEvents, true);
room.oldState.paginationToken = res.end;
if (res.chunk.length === 0) {
room.oldState.paginationToken = null;
}
this.store.storeEvents(room, matrixEvents, res.end, true);
}
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.
Assigned review to get some advice for this question ^
Otherwise just need to add tests for this PR
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 code review - will need to re-request when not-draft.
Codecov Report
@@ Coverage Diff @@
## develop #8303 +/- ##
===========================================
- Coverage 29.79% 29.78% -0.01%
===========================================
Files 872 872
Lines 50006 50019 +13
Branches 12723 12728 +5
===========================================
Hits 14897 14897
- Misses 35109 35122 +13
|
const client = this.context; | ||
client.on("sync", this.onSyncStateChange); | ||
client.on("Room.localEchoUpdated", this.onRoomLocalEchoUpdated); | ||
client.on("Room.historyImportedWithinTimeline", this.onRoomHistoryImportedWithinTimeline); |
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.
Please add the context type definition around line L87 as string emitter keys are no longer allowed in TypedEventEmitter, TS thinks this.context
is any
here though
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 we also not listen on the room directly, re-emitters suck
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 we also not listen on the room directly, re-emitters suck
So I don't guess, what should we do? 🙇
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.props.room.on(...);
but with componentDidUpdate comparing this.props.room with prevProps.room to remove old listener and setup a new one. This is all a lot nicer in hooks where its a one-liner.
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.props.room.on(...);
but with componentDidUpdate comparing this.props.room with prevProps.room to remove old listener and setup a new one. This is all a lot nicer in hooks where its a one-liner.
Updated 👍
Please add the context type definition around line L87 as string emitter keys are no longer allowed in TypedEventEmitter
I'm not sure what to do here exactly. I've stopped using string keys. Is there anything further?
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.
Conversation continued at #8354 (comment)
this.setState({ | ||
timelineNeedsRefresh: false, | ||
}); | ||
}; |
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 the behavior of all of this would be best served by an end-to-end test. It looks like we just added Cypress recently but it's missing all of the utility necessary to complete something like this.
- Needs a command to create a user behind the scenes and visit Element as the user already signed in
- Needs commands to setup rooms and send events
- Needs a way to enable
experimental_features
in thehomeserver.yaml
template - Needs a way to add an application service registration in the Synapse instance. The MSC2716
/batch_send
endpoint is only accessible from a AS token. No extra AS server needed, just the AS token configured. - Needs a way to interact with the homserver directly from the application service token to call
/batch_send
(probably viamatrix-js-sdk
)
I can't find an overall issue describing the need/want to use Cypress so it's unclear how much we want to move forward with it. I've had many troubles using Cypress with Gitter.
It seems like we have other e2e tests using Puppeteer but I'm guessing we want to move all of these to Cypress. Better place I should be adding some e2e tests or approaching this?
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.
Conversation continued at #8354 (comment)
Closing in favor of #8354 which has the same branch name as matrix-org/matrix-js-sdk#2299 |
if (timelineSet !== this.props.timelineSet) return; | ||
|
||
if (this.messagePanel.current && this.messagePanel.current.isAtBottom()) { | ||
if (this.canResetTimeline()) { |
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 seems like a missed refactor. canResetTimeline
by name seems aplicable here and the logic appears pretty equivalent.
matrix-react-sdk/src/components/structures/RoomView.tsx
Lines 1004 to 1009 in 27118a9
public canResetTimeline = () => { | |
if (!this.messagePanel) { | |
return true; | |
} | |
return this.messagePanel.canResetTimeline(); | |
}; |
public canResetTimeline = () => this.messagePanel?.current.isAtBottom(); |
Ask to refresh timeline when historical messages are imported (MSC2716)
Associated
matrix-js-sdk
PR: matrix-org/matrix-js-sdk#2299Beware: edge case
There is one edge case where the "History import detected" banner can re-appear after you have refreshed the timeline.
matrix-js-sdk
only persists/sync
data to IndexDB every 5 minutes which means it can have a stalenext_batch
pagination token compared to the latest events you might be seeing on screen. So if you receive amarker
event, it will show the banner to refresh the timeline. You can then use the button to refresh your timeline and the banner will disappear as expected. Then if you restart your entire Element client (like refreshing the entire browser), Element willsyncFromCache
with the/sync
response from 5 minutes ago and query/sync
with that old pagination token which will fetch themarker
event again and show the banner again.Dev notes
Todo
matrix-js-sdk
to actually refresh the timeline, Ask to refresh timeline when historical messages are imported (MSC2716) #8303 (comment)Here's what your changelog entry will look like:
✨ Features
Preview: https://pr8303--matrix-react-sdk.netlify.app
⚠️ Do you trust the author of this PR? Maybe this build will steal your keys or give you malware. Exercise caution. Use test accounts.