Skip to content

Commit a9c49b5

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 (busyFetchingNewer) 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` are received, there will be a change in the scroll metrics, so this time `fetchNewer` will be called normally. 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 15f7b87 commit a9c49b5

File tree

5 files changed

+141
-93
lines changed

5 files changed

+141
-93
lines changed

lib/model/message_list.dart

Lines changed: 95 additions & 45 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

@@ -188,18 +202,37 @@ mixin _MessageSequence {
188202

189203
/// Whether this message list is currently busy when it comes to
190204
/// fetching more messages.
205+
bool get busyFetchingMore => busyFetchingOlder || busyFetchingNewer;
206+
207+
/// Whether this message list is currently busy when it comes to
208+
/// fetching older messages.
209+
///
210+
/// Here "busy" means a new call to fetch older messages would do nothing,
211+
/// rather than make any request to the server,
212+
/// as a result of an existing recent request.
213+
/// This is true both when the recent request is still outstanding,
214+
/// and when it failed and the backoff from that is still in progress.
215+
bool get busyFetchingOlder => switch(_fetchOlderStatus) {
216+
.fetching || .backoff => true,
217+
_ => false,
218+
};
219+
220+
/// Whether this message list is currently busy when it comes to
221+
/// fetching newer messages.
191222
///
192-
/// Here "busy" means a new call to fetch more messages would do nothing,
223+
/// Here "busy" means a new call to fetch older messages would do nothing,
193224
/// rather than make any request to the server,
194225
/// as a result of an existing recent request.
195226
/// This is true both when the recent request is still outstanding,
196227
/// 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,
228+
bool get busyFetchingNewer => switch(_fetchNewerStatus) {
229+
.fetching || .backoff => true,
199230
_ => false,
200231
};
201232

202-
FetchingStatus _status = FetchingStatus.unstarted;
233+
FetchingInitialStatus _fetchInitialStatus = .unstarted;
234+
FetchingMoreStatus _fetchOlderStatus = .unstarted;
235+
FetchingMoreStatus _fetchNewerStatus = .unstarted;
203236

204237
BackoffMachine? _fetchBackoffMachine;
205238

@@ -434,7 +467,9 @@ mixin _MessageSequence {
434467
outboxMessages.clear();
435468
_haveOldest = false;
436469
_haveNewest = false;
437-
_status = FetchingStatus.unstarted;
470+
_fetchInitialStatus = .unstarted;
471+
_fetchOlderStatus = .unstarted;
472+
_fetchNewerStatus = .unstarted;
438473
_fetchBackoffMachine = null;
439474
contents.clear();
440475
items.clear();
@@ -786,16 +821,28 @@ class MessageListView with ChangeNotifier, _MessageSequence {
786821
}
787822
}
788823

789-
void _setStatus(FetchingStatus value, {FetchingStatus? was}) {
790-
assert(was == null || _status == was);
791-
_status = value;
792-
if (!fetched) return;
824+
void _setInitialStatus(FetchingInitialStatus value, {FetchingInitialStatus? was}) {
825+
assert(was == null || _fetchInitialStatus == was);
826+
_fetchInitialStatus = value;
827+
if (!initialFetched) return;
828+
notifyListeners();
829+
}
830+
831+
void _setOlderStatus(FetchingMoreStatus value, {FetchingMoreStatus? was}) {
832+
assert(was == null || _fetchOlderStatus == was);
833+
_fetchOlderStatus = value;
834+
notifyListeners();
835+
}
836+
837+
void _setNewerStatus(FetchingMoreStatus value, {FetchingMoreStatus? was}) {
838+
assert(was == null || _fetchNewerStatus == was);
839+
_fetchNewerStatus = value;
793840
notifyListeners();
794841
}
795842

796843
/// Fetch messages, starting from scratch.
797844
Future<void> fetchInitial() async {
798-
assert(!fetched && !haveOldest && !haveNewest && !busyFetchingMore);
845+
assert(!initialFetched && !haveOldest && !haveNewest && !busyFetchingMore);
799846
assert(messages.isEmpty && contents.isEmpty);
800847
assert(oldMessageId == null && newMessageId == null);
801848

@@ -805,11 +852,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
805852
// probably better if the UI code doesn't take it to this point.
806853
_haveOldest = true;
807854
_haveNewest = true;
808-
_setStatus(FetchingStatus.idle, was: FetchingStatus.unstarted);
855+
_setInitialStatus(.idle, was: .unstarted);
809856
return;
810857
}
811858

812-
_setStatus(FetchingStatus.fetchInitial, was: FetchingStatus.unstarted);
859+
_setInitialStatus(.fetching, was: .unstarted);
813860
// TODO schedule all this in another isolate
814861
final generation = this.generation;
815862
final result = await getMessages(store.connection,
@@ -850,7 +897,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
850897
_syncOutboxMessagesFromStore();
851898
}
852899

853-
_setStatus(FetchingStatus.idle, was: FetchingStatus.fetchInitial);
900+
_setInitialStatus(.idle, was: .fetching);
854901
}
855902

856903
/// Update [narrow] for the result of a "with" narrow (topic permalink) fetch.
@@ -888,22 +935,23 @@ class MessageListView with ChangeNotifier, _MessageSequence {
888935
/// Fetch the next batch of older messages, if applicable.
889936
///
890937
/// If there are no older messages to fetch (i.e. if [haveOldest]),
891-
/// or if this message list is already busy fetching more messages
892-
/// (i.e. if [busyFetchingMore], which includes backoff from failed requests),
938+
/// or if this message list is already busy fetching older messages
939+
/// (i.e. if [busyFetchingOlder], which includes backoff from failed requests),
893940
/// then this method does nothing and immediately returns.
894941
/// That makes this method suitable to call frequently, e.g. every frame,
895-
/// whenever it looks likely to be useful to have more messages.
942+
/// whenever it looks likely to be useful to have older messages.
896943
Future<void> fetchOlder() async {
897944
int visibleMessageCount = 0;
898945
do {
899946
if (haveOldest) return;
900-
if (busyFetchingMore) return;
901-
assert(fetched);
947+
if (busyFetchingOlder) return;
948+
assert(initialFetched);
902949
assert(oldMessageId != null);
903950
await _fetchMore(
904951
anchor: NumericAnchor(oldMessageId!),
905952
numBefore: kMessageListFetchBatchSize,
906953
numAfter: 0,
954+
setStatus: _setOlderStatus,
907955
processResult: (result) {
908956
_oldMessageId = result.messages.firstOrNull?.id ?? oldMessageId;
909957
store.reconcileMessages(result.messages);
@@ -923,22 +971,23 @@ class MessageListView with ChangeNotifier, _MessageSequence {
923971
/// Fetch the next batch of newer messages, if applicable.
924972
///
925973
/// If there are no newer messages to fetch (i.e. if [haveNewest]),
926-
/// or if this message list is already busy fetching more messages
927-
/// (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),
928976
/// then this method does nothing and immediately returns.
929977
/// That makes this method suitable to call frequently, e.g. every frame,
930-
/// whenever it looks likely to be useful to have more messages.
978+
/// whenever it looks likely to be useful to have newer messages.
931979
Future<void> fetchNewer() async {
932980
int visibleMessageCount = 0;
933981
do {
934982
if (haveNewest) return;
935-
if (busyFetchingMore) return;
936-
assert(fetched);
983+
if (busyFetchingNewer) return;
984+
assert(initialFetched);
937985
assert(newMessageId != null);
938986
await _fetchMore(
939987
anchor: NumericAnchor(newMessageId!),
940988
numBefore: 0,
941989
numAfter: kMessageListFetchBatchSize,
990+
setStatus: _setNewerStatus,
942991
processResult: (result) {
943992
_newMessageId = result.messages.lastOrNull?.id ?? newMessageId;
944993
store.reconcileMessages(result.messages);
@@ -963,12 +1012,13 @@ class MessageListView with ChangeNotifier, _MessageSequence {
9631012
required Anchor anchor,
9641013
required int numBefore,
9651014
required int numAfter,
1015+
required void Function(FetchingMoreStatus value, {FetchingMoreStatus? was}) setStatus,
9661016
required void Function(GetMessagesResult) processResult,
9671017
}) async {
9681018
assert(narrow is! TopicNarrow
9691019
// We only intend to send "with" in [fetchInitial]; see there.
9701020
|| (narrow as TopicNarrow).with_ == null);
971-
_setStatus(FetchingStatus.fetchingMore, was: FetchingStatus.idle);
1021+
setStatus(.fetching);
9721022
final generation = this.generation;
9731023
bool hasFetchError = false;
9741024
try {
@@ -989,14 +1039,14 @@ class MessageListView with ChangeNotifier, _MessageSequence {
9891039
} finally {
9901040
if (this.generation == generation) {
9911041
if (hasFetchError) {
992-
_setStatus(FetchingStatus.backoff, was: FetchingStatus.fetchingMore);
1042+
setStatus(.backoff, was: .fetching);
9931043
unawaited((_fetchBackoffMachine ??= BackoffMachine())
9941044
.wait().then((_) {
9951045
if (this.generation != generation) return;
996-
_setStatus(FetchingStatus.idle, was: FetchingStatus.backoff);
1046+
setStatus(.idle, was: .backoff);
9971047
}));
9981048
} else {
999-
_setStatus(FetchingStatus.idle, was: FetchingStatus.fetchingMore);
1049+
setStatus(.idle, was: .fetching);
10001050
_fetchBackoffMachine = null;
10011051
}
10021052
}
@@ -1008,7 +1058,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
10081058
/// This will set [anchor] to [AnchorCode.newest],
10091059
/// and cause messages to be re-fetched from scratch.
10101060
void jumpToEnd() {
1011-
assert(fetched);
1061+
assert(initialFetched);
10121062
assert(!haveNewest);
10131063
assert(anchor != AnchorCode.newest);
10141064
_anchor = AnchorCode.newest;
@@ -1090,7 +1140,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
10901140
// TODO get the newly-unmuted messages from the message store
10911141
// For now, we simplify the task by just refetching this message list
10921142
// from scratch.
1093-
if (fetched) {
1143+
if (initialFetched) {
10941144
_reset();
10951145
notifyListeners();
10961146
fetchInitial();
@@ -1118,7 +1168,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
11181168
// TODO get the newly-unmuted messages from the message store
11191169
// For now, we simplify the task by just refetching this message list
11201170
// from scratch.
1121-
if (fetched) {
1171+
if (initialFetched) {
11221172
_reset();
11231173
notifyListeners();
11241174
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)