Skip to content

Commit 127f690

Browse files
committed
msglist: Track three fetch request statuses separately
This change is necessary when there is a need to fetch more messages in both directions, older and newer, and when fetching in one direction avoids fetching in another direction at the same time, because of the `if (busyFetchingMore) return` line in both `fetchOlder` and `fetchNewer`. This scenario happens when a conversation is opened in its first unread, such that the number of messages in the initial batch is so low (because they're muted in one way or another) that it's already past the certain point where the scroll metrics listener in the widget code triggers both `fetchOlder` and `fetchNewer`. In 2025-11, that code first calls `fetchOlder` then `fetchNewer`, and for the reason mentioned above, `fetchNewer` will not fetch any new messages. But that's fine, because as soon as older messages from `fetchOlder` arrives, there will be a change in the scroll metrics, so `fetchNewer` will be called again, fetching new messages. But imagine if the number of messages in the initial batch occupies less than a screenful, and then `fetchOlder` returns no messages or a few messages that combined with the initial batch messages are still less than a screenful; in that case, there will be no change in the scroll metrics to call `fetchNewer`. With the three fetch request types being separated, especially the two request types for older and newer messages, each direction can fetch more messages independently without interfering with one another. Another benefit of this change is that if there's a backoff in one direction, it will not affect the other one. Fixes: #1256
1 parent 03d61ca commit 127f690

File tree

5 files changed

+322
-146
lines changed

5 files changed

+322
-146
lines changed

lib/model/message_list.dart

Lines changed: 94 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -83,22 +83,33 @@ class MessageListOutboxMessageItem extends MessageListMessageBaseItem {
8383
]);
8484
}
8585

86-
/// The status of outstanding or recent fetch requests from a [MessageListView].
87-
enum FetchingStatus {
88-
/// The model has not made any fetch requests (since its last reset, if any).
86+
/// The status of outstanding or recent `fetchInitial` request from a [MessageListView].
87+
enum FetchingInitialStatus {
88+
/// The model has not made a `fetchInitial` request (since its last reset, if any).
8989
unstarted,
9090

9191
/// The model has made a `fetchInitial` request, which hasn't succeeded.
92-
fetchInitial,
92+
fetching,
9393

94-
/// The model made a successful `fetchInitial` request,
95-
/// and has no outstanding requests or backoff.
94+
/// The model made a successful `fetchInitial` request.
9695
idle,
96+
}
97+
98+
/// The status of outstanding or recent "fetch more" request from a [MessageListView].
99+
///
100+
/// By a "fetch more" request we mean a `fetchOlder` or a `fetchNewer` request.
101+
enum FetchingMoreStatus {
102+
/// The model has not made the "fetch more" request (since its last reset, if any).
103+
unstarted,
104+
105+
/// The model has made the "fetch more" request, which hasn't succeeded.
106+
fetching,
97107

98-
/// The model has an active `fetchOlder` or `fetchNewer` request.
99-
fetchingMore,
108+
/// The model made a successful "fetch more" request,
109+
/// and has no outstanding request of the same kind or backoff.
110+
idle,
100111

101-
/// The model is in a backoff period from a failed request.
112+
/// The model is in a backoff period from the failed "fetch more" request.
102113
backoff,
103114
}
104115

@@ -125,7 +136,7 @@ mixin _MessageSequence {
125136
///
126137
/// This may or may not represent all the message history that
127138
/// conceptually belongs in this message list.
128-
/// That information is expressed in [fetched], [haveOldest], [haveNewest].
139+
/// That information is expressed in [initialFetched], [haveOldest], [haveNewest].
129140
///
130141
/// This also may or may not represent all the message history that
131142
/// conceptually belongs in this narrow because some messages might be
@@ -165,12 +176,15 @@ mixin _MessageSequence {
165176
int? get newMessageId => _newMessageId;
166177
int? _newMessageId;
167178

168-
/// Whether [messages] and [items] represent the results of a fetch.
179+
/// Whether the first batch of messages for this narrow is fetched yet.
169180
///
170-
/// This allows the UI to distinguish "still working on fetching messages"
171-
/// from "there are in fact no messages here".
172-
bool get fetched => switch (_status) {
173-
FetchingStatus.unstarted || FetchingStatus.fetchInitial => false,
181+
/// Some or all of the fetched messages may not make it to [messages]
182+
/// and [items] if they're muted in one way or another.
183+
///
184+
/// This allows the UI to distinguish "still working on fetching first batch
185+
/// of messages" from "there are in fact no messages here".
186+
bool get initialFetched => switch (_fetchInitialStatus) {
187+
.unstarted || .fetching => false,
174188
_ => true,
175189
};
176190

@@ -187,19 +201,34 @@ mixin _MessageSequence {
187201
bool _haveNewest = false;
188202

189203
/// Whether this message list is currently busy when it comes to
190-
/// fetching more messages.
204+
/// fetching older messages.
205+
///
206+
/// Here "busy" means a new call to fetch older messages would do nothing,
207+
/// rather than make any request to the server,
208+
/// as a result of an existing recent request.
209+
/// This is true both when the recent request is still outstanding,
210+
/// and when it failed and the backoff from that is still in progress.
211+
bool get busyFetchingOlder => switch(_fetchOlderStatus) {
212+
.fetching || .backoff => true,
213+
_ => false,
214+
};
215+
216+
/// Whether this message list is currently busy when it comes to
217+
/// fetching newer messages.
191218
///
192-
/// Here "busy" means a new call to fetch more messages would do nothing,
219+
/// Here "busy" means a new call to fetch older messages would do nothing,
193220
/// rather than make any request to the server,
194221
/// as a result of an existing recent request.
195222
/// This is true both when the recent request is still outstanding,
196223
/// and when it failed and the backoff from that is still in progress.
197-
bool get busyFetchingMore => switch (_status) {
198-
FetchingStatus.fetchingMore || FetchingStatus.backoff => true,
224+
bool get busyFetchingNewer => switch(_fetchNewerStatus) {
225+
.fetching || .backoff => true,
199226
_ => false,
200227
};
201228

202-
FetchingStatus _status = FetchingStatus.unstarted;
229+
FetchingInitialStatus _fetchInitialStatus = .unstarted;
230+
FetchingMoreStatus _fetchOlderStatus = .unstarted;
231+
FetchingMoreStatus _fetchNewerStatus = .unstarted;
203232

204233
BackoffMachine? _fetchBackoffMachine;
205234

@@ -228,7 +257,7 @@ mixin _MessageSequence {
228257
/// [MessageListOutboxMessageItem]s corresponding to [outboxMessages].
229258
///
230259
/// This information is completely derived from [messages], [outboxMessages],
231-
/// and the flags [haveOldest], [haveNewest], and [busyFetchingMore].
260+
/// and the flags [haveOldest], [haveNewest], [busyFetchingNewer], and [busyFetchingOlder].
232261
/// It exists as an optimization, to memoize that computation.
233262
///
234263
/// See also [middleItem], an index which divides this list
@@ -434,7 +463,9 @@ mixin _MessageSequence {
434463
outboxMessages.clear();
435464
_haveOldest = false;
436465
_haveNewest = false;
437-
_status = FetchingStatus.unstarted;
466+
_fetchInitialStatus = .unstarted;
467+
_fetchOlderStatus = .unstarted;
468+
_fetchNewerStatus = .unstarted;
438469
_fetchBackoffMachine = null;
439470
contents.clear();
440471
items.clear();
@@ -786,16 +817,29 @@ class MessageListView with ChangeNotifier, _MessageSequence {
786817
}
787818
}
788819

789-
void _setStatus(FetchingStatus value, {FetchingStatus? was}) {
790-
assert(was == null || _status == was);
791-
_status = value;
792-
if (!fetched) return;
820+
void _setInitialStatus(FetchingInitialStatus value, {FetchingInitialStatus? was}) {
821+
assert(was == null || _fetchInitialStatus == was);
822+
_fetchInitialStatus = value;
823+
if (!initialFetched) return;
824+
notifyListeners();
825+
}
826+
827+
void _setOlderStatus(FetchingMoreStatus value, {FetchingMoreStatus? was}) {
828+
assert(was == null || _fetchOlderStatus == was);
829+
_fetchOlderStatus = value;
830+
notifyListeners();
831+
}
832+
833+
void _setNewerStatus(FetchingMoreStatus value, {FetchingMoreStatus? was}) {
834+
assert(was == null || _fetchNewerStatus == was);
835+
_fetchNewerStatus = value;
793836
notifyListeners();
794837
}
795838

796839
/// Fetch messages, starting from scratch.
797840
Future<void> fetchInitial() async {
798-
assert(!fetched && !haveOldest && !haveNewest && !busyFetchingMore);
841+
assert(!initialFetched && !haveOldest && !haveNewest
842+
&& !busyFetchingOlder && !busyFetchingNewer);
799843
assert(messages.isEmpty && contents.isEmpty);
800844
assert(oldMessageId == null && newMessageId == null);
801845

@@ -805,11 +849,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
805849
// probably better if the UI code doesn't take it to this point.
806850
_haveOldest = true;
807851
_haveNewest = true;
808-
_setStatus(FetchingStatus.idle, was: FetchingStatus.unstarted);
852+
_setInitialStatus(.idle, was: .unstarted);
809853
return;
810854
}
811855

812-
_setStatus(FetchingStatus.fetchInitial, was: FetchingStatus.unstarted);
856+
_setInitialStatus(.fetching, was: .unstarted);
813857
// TODO schedule all this in another isolate
814858
final generation = this.generation;
815859
final result = await getMessages(store.connection,
@@ -851,7 +895,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
851895
_syncOutboxMessagesFromStore();
852896
}
853897

854-
_setStatus(FetchingStatus.idle, was: FetchingStatus.fetchInitial);
898+
_setInitialStatus(.idle, was: .fetching);
855899
}
856900

857901
/// Update [narrow] for the result of a "with" narrow (topic permalink) fetch.
@@ -889,23 +933,24 @@ class MessageListView with ChangeNotifier, _MessageSequence {
889933
/// Fetch the next batch of older messages, if applicable.
890934
///
891935
/// If there are no older messages to fetch (i.e. if [haveOldest]),
892-
/// or if this message list is already busy fetching more messages
893-
/// (i.e. if [busyFetchingMore], which includes backoff from failed requests),
936+
/// or if this message list is already busy fetching older messages
937+
/// (i.e. if [busyFetchingOlder], which includes backoff from failed requests),
894938
/// then this method does nothing and immediately returns.
895939
/// That makes this method suitable to call frequently, e.g. every frame,
896-
/// whenever it looks likely to be useful to have more messages.
940+
/// whenever it looks likely to be useful to have older messages.
897941
Future<void> fetchOlder() async {
898942
final generation = this.generation;
899943
int visibleMessageCount = 0;
900944
do {
901945
if (haveOldest) return;
902-
if (busyFetchingMore) return;
903-
assert(fetched);
946+
if (busyFetchingOlder) return;
947+
assert(initialFetched);
904948
assert(oldMessageId != null);
905949
await _fetchMore(
906950
anchor: NumericAnchor(oldMessageId!),
907951
numBefore: kMessageListFetchBatchSize,
908952
numAfter: 0,
953+
setStatus: _setOlderStatus,
909954
processResult: (result) {
910955
_oldMessageId = result.messages.firstOrNull?.id ?? oldMessageId;
911956
store.reconcileMessages(result.messages);
@@ -926,23 +971,24 @@ class MessageListView with ChangeNotifier, _MessageSequence {
926971
/// Fetch the next batch of newer messages, if applicable.
927972
///
928973
/// If there are no newer messages to fetch (i.e. if [haveNewest]),
929-
/// or if this message list is already busy fetching more messages
930-
/// (i.e. if [busyFetchingMore], which includes backoff from failed requests),
974+
/// or if this message list is already busy fetching newer messages
975+
/// (i.e. if [busyFetchingNewer], which includes backoff from failed requests),
931976
/// then this method does nothing and immediately returns.
932977
/// That makes this method suitable to call frequently, e.g. every frame,
933-
/// whenever it looks likely to be useful to have more messages.
978+
/// whenever it looks likely to be useful to have newer messages.
934979
Future<void> fetchNewer() async {
935980
final generation = this.generation;
936981
int visibleMessageCount = 0;
937982
do {
938983
if (haveNewest) return;
939-
if (busyFetchingMore) return;
940-
assert(fetched);
984+
if (busyFetchingNewer) return;
985+
assert(initialFetched);
941986
assert(newMessageId != null);
942987
await _fetchMore(
943988
anchor: NumericAnchor(newMessageId!),
944989
numBefore: 0,
945990
numAfter: kMessageListFetchBatchSize,
991+
setStatus: _setNewerStatus,
946992
processResult: (result) {
947993
_newMessageId = result.messages.lastOrNull?.id ?? newMessageId;
948994
store.reconcileMessages(result.messages);
@@ -968,12 +1014,13 @@ class MessageListView with ChangeNotifier, _MessageSequence {
9681014
required Anchor anchor,
9691015
required int numBefore,
9701016
required int numAfter,
1017+
required void Function(FetchingMoreStatus value, {FetchingMoreStatus? was}) setStatus,
9711018
required void Function(GetMessagesResult) processResult,
9721019
}) async {
9731020
assert(narrow is! TopicNarrow
9741021
// We only intend to send "with" in [fetchInitial]; see there.
9751022
|| (narrow as TopicNarrow).with_ == null);
976-
_setStatus(FetchingStatus.fetchingMore, was: FetchingStatus.idle);
1023+
setStatus(.fetching);
9771024
final generation = this.generation;
9781025
bool hasFetchError = false;
9791026
try {
@@ -994,14 +1041,14 @@ class MessageListView with ChangeNotifier, _MessageSequence {
9941041
} finally {
9951042
if (this.generation == generation) {
9961043
if (hasFetchError) {
997-
_setStatus(FetchingStatus.backoff, was: FetchingStatus.fetchingMore);
1044+
setStatus(.backoff, was: .fetching);
9981045
unawaited((_fetchBackoffMachine ??= BackoffMachine())
9991046
.wait().then((_) {
10001047
if (this.generation != generation) return;
1001-
_setStatus(FetchingStatus.idle, was: FetchingStatus.backoff);
1048+
setStatus(.idle, was: .backoff);
10021049
}));
10031050
} else {
1004-
_setStatus(FetchingStatus.idle, was: FetchingStatus.fetchingMore);
1051+
setStatus(.idle, was: .fetching);
10051052
_fetchBackoffMachine = null;
10061053
}
10071054
}
@@ -1013,7 +1060,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
10131060
/// This will set [anchor] to [AnchorCode.newest],
10141061
/// and cause messages to be re-fetched from scratch.
10151062
void jumpToEnd() {
1016-
assert(fetched);
1063+
assert(initialFetched);
10171064
assert(!haveNewest);
10181065
assert(anchor != AnchorCode.newest);
10191066
_anchor = AnchorCode.newest;
@@ -1095,7 +1142,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
10951142
// TODO get the newly-unmuted messages from the message store
10961143
// For now, we simplify the task by just refetching this message list
10971144
// from scratch.
1098-
if (fetched) {
1145+
if (initialFetched) {
10991146
_reset();
11001147
notifyListeners();
11011148
fetchInitial();
@@ -1123,7 +1170,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
11231170
// TODO get the newly-unmuted messages from the message store
11241171
// For now, we simplify the task by just refetching this message list
11251172
// from scratch.
1126-
if (fetched) {
1173+
if (initialFetched) {
11271174
_reset();
11281175
notifyListeners();
11291176
fetchInitial();

lib/widgets/message_list.dart

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,7 +1026,7 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
10261026
Widget build(BuildContext context) {
10271027
final zulipLocalizations = ZulipLocalizations.of(context);
10281028

1029-
if (!model.fetched) return const Center(child: CircularProgressIndicator());
1029+
if (!model.initialFetched) return const Center(child: CircularProgressIndicator());
10301030

10311031
if (model.items.isEmpty && model.haveNewest && model.haveOldest) {
10321032
final String header;
@@ -1206,11 +1206,9 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
12061206
// Else if we're busy with fetching, then show a loading indicator.
12071207
//
12081208
// This applies even if the fetch is over, but failed, and we're still
1209-
// in backoff from it; and even if the fetch is/was for the other direction.
1210-
// The loading indicator really means "busy, working on it"; and that's the
1211-
// right summary even if the fetch is internally queued behind other work.
1209+
// in backoff from it.
12121210
return model.haveOldest ? const _MessageListHistoryStart()
1213-
: model.busyFetchingMore ? const _MessageListLoadingMore()
1211+
: model.busyFetchingOlder ? const _MessageListLoadingMore()
12141212
: const SizedBox.shrink();
12151213
}
12161214

@@ -1225,7 +1223,7 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
12251223
// https://chat.zulip.org/#narrow/channel/48-mobile/topic/space.20at.20end.20of.20thread/near/2203391
12261224
const SizedBox(height: 12),
12271225
]);
1228-
} else if (model.busyFetchingMore) {
1226+
} else if (model.busyFetchingNewer) {
12291227
// See [_buildStartCap] for why this condition shows a loading indicator.
12301228
return const _MessageListLoadingMore();
12311229
} else {

0 commit comments

Comments
 (0)