Skip to content

Commit c43eaad

Browse files
committed
msglist: Fetch more messages despite previous muted batch
Fixes: #1256
1 parent b3a4f5e commit c43eaad

File tree

5 files changed

+435
-65
lines changed

5 files changed

+435
-65
lines changed

lib/model/message_list.dart

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,32 @@ mixin _MessageSequence {
145145
/// The corresponding item index is [middleItem].
146146
int middleMessage = 0;
147147

148+
/// The ID of the oldest known message so far in this narrow.
149+
///
150+
/// This is used as the anchor for fetching the next batch of older messages
151+
/// and will be `null` if no messages of this narrow have been fetched yet.
152+
/// A non-null value for this doesn't always mean [haveOldest] is `true`.
153+
///
154+
/// The related message may not appear in [messages] because it
155+
/// is muted in one way or another. Also, the related message may not stay
156+
/// in the narrow for reasons such as message moves or deletions,
157+
/// but that's fine for what this is used for.
158+
int? get oldMessageId => _oldMessageId;
159+
int? _oldMessageId;
160+
161+
/// The ID of the newest known message so far in this narrow.
162+
///
163+
/// This is used as the anchor for fetching the next batch of newer messages
164+
/// and will be `null` if no messages of this narrow have been fetched yet.
165+
/// A non-null value for this doesn't always mean [haveNewest] is `true`.
166+
///
167+
/// The related message may not appear in [messages] because it
168+
/// is muted in one way or another. Also, the related message may not stay
169+
/// in the narrow for reasons such as message moves or deletions,
170+
/// but that's fine for what this is used for.
171+
int? get newMessageId => _newMessageId;
172+
int? _newMessageId;
173+
148174
/// Whether [messages] and [items] represent the results of a fetch.
149175
///
150176
/// This allows the UI to distinguish "still working on fetching messages"
@@ -409,6 +435,8 @@ mixin _MessageSequence {
409435
generation += 1;
410436
messages.clear();
411437
middleMessage = 0;
438+
_oldMessageId = null;
439+
_newMessageId = null;
412440
outboxMessages.clear();
413441
_haveOldest = false;
414442
_haveNewest = false;
@@ -818,6 +846,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
818846
Future<void> fetchInitial() async {
819847
assert(!fetched && !haveOldest && !haveNewest && !busyFetchingMore);
820848
assert(messages.isEmpty && contents.isEmpty);
849+
assert(oldMessageId == null && newMessageId == null);
821850

822851
if (narrow case KeywordSearchNarrow(keyword: '')) {
823852
// The server would reject an empty keyword search; skip the request.
@@ -841,6 +870,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
841870
);
842871
if (this.generation > generation) return;
843872

873+
_oldMessageId = result.messages.firstOrNull?.id;
874+
_newMessageId = result.messages.lastOrNull?.id;
875+
844876
_adjustNarrowForTopicPermalink(result.messages.firstOrNull);
845877

846878
store.reconcileMessages(result.messages);
@@ -868,6 +900,12 @@ class MessageListView with ChangeNotifier, _MessageSequence {
868900
_syncOutboxMessagesFromStore();
869901
}
870902

903+
while (messages.isEmpty && !(haveOldest && haveNewest)) {
904+
await fetchOlder(partOfInitialFetch: true);
905+
if (messages.isNotEmpty) break;
906+
await fetchNewer(partOfInitialFetch: true);
907+
}
908+
871909
_setStatus(FetchingStatus.idle, was: FetchingStatus.fetchInitial);
872910
}
873911

@@ -911,16 +949,18 @@ class MessageListView with ChangeNotifier, _MessageSequence {
911949
/// then this method does nothing and immediately returns.
912950
/// That makes this method suitable to call frequently, e.g. every frame,
913951
/// whenever it looks likely to be useful to have more messages.
914-
Future<void> fetchOlder() async {
952+
Future<void> fetchOlder({bool partOfInitialFetch = false}) async {
915953
if (haveOldest) return;
916954
if (busyFetchingMore) return;
917-
assert(fetched);
918-
assert(messages.isNotEmpty);
955+
assert(partOfInitialFetch || fetched);
956+
assert(oldMessageId != null);
919957
await _fetchMore(
920-
anchor: NumericAnchor(messages[0].id),
958+
anchor: NumericAnchor(oldMessageId!),
921959
numBefore: kMessageListFetchBatchSize,
922960
numAfter: 0,
961+
partOfInitialFetch: partOfInitialFetch,
923962
processResult: (result) {
963+
_oldMessageId = result.messages.firstOrNull?.id ?? oldMessageId;
924964
store.reconcileMessages(result.messages);
925965
store.recentSenders.handleMessages(result.messages); // TODO(#824)
926966

@@ -941,16 +981,18 @@ class MessageListView with ChangeNotifier, _MessageSequence {
941981
/// then this method does nothing and immediately returns.
942982
/// That makes this method suitable to call frequently, e.g. every frame,
943983
/// whenever it looks likely to be useful to have more messages.
944-
Future<void> fetchNewer() async {
984+
Future<void> fetchNewer({bool partOfInitialFetch = false}) async {
945985
if (haveNewest) return;
946986
if (busyFetchingMore) return;
947-
assert(fetched);
948-
assert(messages.isNotEmpty);
987+
assert(partOfInitialFetch || fetched);
988+
assert(newMessageId != null);
949989
await _fetchMore(
950-
anchor: NumericAnchor(messages.last.id),
990+
anchor: NumericAnchor(newMessageId!),
951991
numBefore: 0,
952992
numAfter: kMessageListFetchBatchSize,
993+
partOfInitialFetch: partOfInitialFetch,
953994
processResult: (result) {
995+
_newMessageId = result.messages.lastOrNull?.id ?? newMessageId;
954996
store.reconcileMessages(result.messages);
955997
store.recentSenders.handleMessages(result.messages); // TODO(#824)
956998

@@ -971,12 +1013,15 @@ class MessageListView with ChangeNotifier, _MessageSequence {
9711013
required Anchor anchor,
9721014
required int numBefore,
9731015
required int numAfter,
1016+
required bool partOfInitialFetch,
9741017
required void Function(GetMessagesResult) processResult,
9751018
}) async {
9761019
assert(narrow is! TopicNarrow
9771020
// We only intend to send "with" in [fetchInitial]; see there.
9781021
|| (narrow as TopicNarrow).with_ == null);
979-
_setStatus(FetchingStatus.fetchingMore, was: FetchingStatus.idle);
1022+
if (!partOfInitialFetch) {
1023+
_setStatus(FetchingStatus.fetchingMore, was: FetchingStatus.idle);
1024+
}
9801025
final generation = this.generation;
9811026
bool hasFetchError = false;
9821027
try {
@@ -998,7 +1043,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
9981043

9991044
processResult(result);
10001045
} finally {
1001-
if (this.generation == generation) {
1046+
if (this.generation == generation && !partOfInitialFetch) {
10021047
if (hasFetchError) {
10031048
_setStatus(FetchingStatus.backoff, was: FetchingStatus.fetchingMore);
10041049
unawaited((_fetchBackoffMachine ??= BackoffMachine())

lib/widgets/message_list.dart

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -874,8 +874,6 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
874874
model.fetchInitial();
875875
}
876876

877-
bool _prevFetched = false;
878-
879877
void _modelChanged() {
880878
// When you're scrolling quickly, our mark-as-read requests include the
881879
// messages *between* _messagesRecentlyInViewport and the messages currently
@@ -902,14 +900,17 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
902900
// This method was called because that just changed.
903901
});
904902

905-
if (!_prevFetched && model.fetched && model.messages.isEmpty) {
903+
if (scrollController.hasClients && model.fetched) {
904+
_fetchMoreIfNeeded(scrollController.position);
905+
}
906+
907+
if (model.messages.isEmpty && model.haveOldest && model.haveNewest) {
906908
// If the fetch came up empty, there's nothing to read,
907909
// so opening the keyboard won't be bothersome and could be helpful.
908910
// It's definitely helpful if we got here from the new-DM page.
909911
MessageListPage.ancestorOf(context)
910912
.composeBoxState?.controller.requestFocusIfUnfocused();
911913
}
912-
_prevFetched = model.fetched;
913914
}
914915

915916
/// Find the range of message IDs on screen, as a (first, last) tuple,
@@ -1032,26 +1033,21 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
10321033
_scrollToBottomVisible.value = true;
10331034
}
10341035

1036+
_fetchMoreIfNeeded(scrollMetrics);
1037+
}
1038+
1039+
void _fetchMoreIfNeeded(ScrollMetrics scrollMetrics) {
10351040
if (scrollMetrics.extentBefore < kFetchMessagesBufferPixels) {
1036-
// This ends up firing a second time shortly after we fetch a batch while
1037-
// there is a fling going on.
1041+
// This ends up firing a second time shortly after we fetch a batch.
10381042
// The result is that each time we decide to fetch a batch, we end up
10391043
// fetching two batches in quick succession. This is basically harmless
10401044
// but makes things a bit more complicated to reason about.
10411045
// The cause is that this gets called again with minScrollExtent
10421046
// still not yet updated to account for the newly-added messages.
1043-
// This relates to how [SchedulerBinding] executes different tasks when
1044-
// producing a new frame, like first executing transient callbacks
1045-
// (typically ticking animations) followed by persistent callbacks
1046-
// (typically the build/layout/paint pipeline), and so on.
1047-
// So when there is a new message batch received, the related widgets are
1048-
// marked dirty for the next frame. With the ongoing fling, the underlying
1049-
// animation registers transient callback(s) for [ScrollPosition.setPixels]
1050-
// to be executed in the transient callbacks phase at the start of
1051-
// the frame. It will then notify its listeners, eventually calling
1052-
// `_scrollChanged` and in turn current method with old minScrollExtent,
1053-
// causing the second batch fetch. Then in the persistent callbacks phase,
1054-
// minScrollExtent will be updated, effective in the next frame.
1047+
// This relates to `_modelChanged` (and in turn the current method) being
1048+
// called right away as a listener of the model, before the next frame
1049+
// being drawn with the newly-added messages where minScrollExtent
1050+
// is be updated.
10551051
model.fetchOlder();
10561052
}
10571053
if (scrollMetrics.extentAfter < kFetchMessagesBufferPixels) {

0 commit comments

Comments
 (0)