-
Notifications
You must be signed in to change notification settings - Fork 454
msglist: Fetch more messages despite previous muted batch #1989
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
base: main
Are you sure you want to change the base?
Changes from all commits
6dce47b
9b5ea9a
ca30293
8007e46
bbac187
a92bb30
a3065ec
7145e19
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -895,7 +895,7 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat | |
| model.fetchInitial(); | ||
| } | ||
|
|
||
| bool _prevFetched = false; | ||
| bool _hasAutofocused = false; | ||
|
|
||
| void _modelChanged() { | ||
| // When you're scrolling quickly, our mark-as-read requests include the | ||
|
|
@@ -923,14 +923,36 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat | |
| // This method was called because that just changed. | ||
| }); | ||
|
|
||
| if (!_prevFetched && model.fetched && model.messages.isEmpty) { | ||
| // If the fetch came up empty, there's nothing to read, | ||
| // so opening the keyboard won't be bothersome and could be helpful. | ||
| SchedulerBinding.instance.addPostFrameCallback((_) { | ||
| if (!model.fetched || !scrollController.hasClients) { | ||
| return; | ||
| } | ||
|
|
||
| // If fetchInitial or fetchOlder/fetchNewer | ||
| // haven't filled model.messages with any visible (i.e. unmuted) messages, | ||
| // or anyway not enough to fill the screen, fetch again. | ||
| // If we're in a long run of muted messages, this has the effect of | ||
| // fetching in a loop until we've either fetched the narrow's whole history | ||
| // or we've filled the screen with visible messages, | ||
| // without needing user scroll input between iterations. | ||
| // | ||
| // The right time for the "if-needed" check is | ||
| // after the current model change has been laid out | ||
| // and the scroll metrics have been updated, | ||
| // so that when we receive and lay out a screenful of messages, | ||
| // we don't fetch again unnecessarily. | ||
| // That's why we do it in a post-frame callback. | ||
| _fetchMoreIfNeeded(scrollController.position); | ||
| }); | ||
|
|
||
| if (model.messages.isEmpty && model.haveNewest && model.haveOldest && !_hasAutofocused) { | ||
| // If there are no messages to show in the whole history, | ||
| // opening the keyboard won't be bothersome and could be helpful. | ||
| // It's definitely helpful if we got here from the new-DM page. | ||
| MessageListPage.ancestorOf(context) | ||
| .composeBoxState?.controller.requestFocusIfUnfocused(); | ||
| _hasAutofocused = true; | ||
| } | ||
| _prevFetched = model.fetched; | ||
| } | ||
|
|
||
| /// Find the range of message IDs on screen, as a (first, last) tuple, | ||
|
|
@@ -1042,6 +1064,15 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat | |
| ?? GlobalStoreWidget.settingsOf(context).markReadOnScrollForNarrow(widget.narrow); | ||
| } | ||
|
|
||
| void _fetchMoreIfNeeded(ScrollMetrics scrollMetrics) { | ||
| if (scrollMetrics.extentBefore < kFetchMessagesBufferPixels) { | ||
| model.fetchOlder(); | ||
| } | ||
| if (scrollMetrics.extentAfter < kFetchMessagesBufferPixels) { | ||
| model.fetchNewer(); | ||
| } | ||
| } | ||
|
|
||
| void _handleScrollMetrics(ScrollMetrics scrollMetrics) { | ||
| if (_effectiveMarkReadOnScroll()) { | ||
| _markReadFromScroll(); | ||
|
|
@@ -1053,17 +1084,17 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat | |
| _scrollToBottomVisible.value = true; | ||
| } | ||
|
|
||
| if (scrollMetrics.extentBefore < kFetchMessagesBufferPixels) { | ||
| // TODO: This ends up firing a second time shortly after we fetch a batch. | ||
| // The result is that each time we decide to fetch a batch, we end up | ||
| // fetching two batches in quick succession. This is basically harmless | ||
| // but makes things a bit more complicated to reason about. | ||
| // The cause seems to be that this gets called again with maxScrollExtent | ||
| // still not yet updated to account for the newly-added messages. | ||
| model.fetchOlder(); | ||
| } | ||
| if (scrollMetrics.extentAfter < kFetchMessagesBufferPixels) { | ||
| model.fetchNewer(); | ||
| if (SchedulerBinding.instance.schedulerPhase == .transientCallbacks) { | ||
| SchedulerBinding.instance.addPostFrameCallback((_) { | ||
| if (scrollController.hasClients) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reasoning for this condition?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case I think it doesn't really make any difference, but it's a safety check. Maybe there is a possibility that |
||
| // From the `transientCallbacks` phase to `postFrameCallbacks` phase, | ||
| // `scrollMetrics` can become stale; so we use the fresh value | ||
| // from `scrollController`. | ||
| _fetchMoreIfNeeded(scrollController.position); | ||
| } | ||
| }); | ||
| } else { | ||
| _fetchMoreIfNeeded(scrollMetrics); | ||
| } | ||
| } | ||
|
|
||
|
|
||
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 you explain why this solves the problem (the double-fetch glitch)? And why
.transientCallbacks; are there potentially any other phases where we should do the same thing?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.
Yeah, so as explained in #2104 description, double-fetch was used to happen whenever the response for a fetch-older request arrived while there was a fling going on in the message list. The fling (which is driven by an animation) causes the scroll position to change, but that change happens in
.transientCallbacksphase (the phase where the animations tick, in this case, the animation for the fling). The change in the scroll position eventually calls_handleScrollMetrics(in thetransientCallbacksphase) with old scroll metrics (specificallyminScrollExtent), which causes a double fetch glitch. Then in the.persistentCallbacksphase, where the message list view is laid out with the newly arrived messages,minScrollExtentwill be updated to account for the new messages. The updatedminScrollExtentvalue will then be available in.postFrameCallbacksphase, so that's why we wrap_fetchMoreIfNeededinaddPostFrameCallback. And the check for.transientCallbacksis there because in this specific case, the double-fetch glitch is caused by_handleScrollMetricsbeing called in.transientCallbacksphase.Right now,
_handleScrollMetricsis called by_scrollChangedand_handleScrollMetricsNotification._scrollChangedis called throughScrollPosition.setPixels. Its dartdoc says:"This should only be called by the current [ScrollActivity], either during the transient callback phase or in response to user input." So it's called either during
.transientCallbacksor.idle(user input handlers are executed here) phase._handleScrollMetricsNotificationis called throughNotificationListener.onNotification. Its dartdoc says: "Notifications vary in terms of when they are dispatched. There are two main possibilities: dispatch between frames, and dispatch during layout." So it's called either during.idleor.persistentCallbacksphase._handleScrollMetricsNotificationis specifically called onScrollMetricsNotification, which in turn is only dispatched in.idlephase — seeScrollPosition.didUpdateScrollMetricsand its call site.So besides
.transientCallbacks,.idleis the only other phase where_handleScrollMetricscould be called. Should we also check for the.idlephase, given that we still haven't experienced any double-fetch glitch caused by it?