@@ -14,7 +14,7 @@ import 'narrow.dart';
1414import 'store.dart' ;
1515
1616/// The number of messages to fetch in each request.
17- const kMessageListFetchBatchSize = 100 ; // TODO tune
17+ const kMessageListFetchBatchSize = 5 ; // TODO tune
1818
1919/// A message, or one of its siblings shown in the message list.
2020///
@@ -58,7 +58,7 @@ class MessageListLoadingItem extends MessageListItem {
5858 const MessageListLoadingItem (this .direction);
5959}
6060
61- enum MessageListDirection { older }
61+ enum MessageListDirection { older, newer }
6262
6363/// Indicates we've reached the oldest message in the narrow.
6464class MessageListHistoryStartItem extends MessageListItem {
@@ -85,9 +85,6 @@ mixin _MessageSequence {
8585 bool _fetched = false ;
8686
8787 /// Whether we know we have the oldest messages for this narrow.
88- ///
89- /// (Currently we always have the newest messages for the narrow,
90- /// once [fetched] is true, because we start from the newest.)
9188 bool get haveOldest => _haveOldest;
9289 bool _haveOldest = false ;
9390
@@ -118,6 +115,40 @@ mixin _MessageSequence {
118115
119116 BackoffMachine ? _fetchOlderCooldownBackoffMachine;
120117
118+ /// Whether we know we have the newest messages for this narrow.
119+ bool get haveNewest => _haveNewest;
120+ bool _haveNewest = false ;
121+
122+ /// Whether we are currently fetching the next batch of newer messages.
123+ ///
124+ /// When this is true, [fetchNewer] is a no-op.
125+ /// That method is called frequently by Flutter's scrolling logic,
126+ /// and this field helps us avoid spamming the same request just to get
127+ /// the same response each time.
128+ ///
129+ /// See also [fetchNewerCoolingDown] .
130+ bool get fetchingNewer => _fetchingNewer;
131+ bool _fetchingNewer = false ;
132+
133+ /// Whether [fetchNewer] had a request error recently.
134+ ///
135+ /// When this is true, [fetchNewer] is a no-op.
136+ /// That method is called frequently by Flutter's scrolling logic,
137+ /// and this field mitigates spamming the same request and getting
138+ /// the same error each time.
139+ ///
140+ /// "Recently" is decided by a [BackoffMachine] that resets
141+ /// when a [fetchNewer] request succeeds.
142+ ///
143+ /// See also [fetchingNewer] .
144+ bool get fetchNewerCoolingDown => _fetchNewerCoolingDown;
145+ bool _fetchNewerCoolingDown = false ;
146+
147+ BackoffMachine ? _fetchNewerCooldownBackoffMachine;
148+
149+ int ? get firstUnreadMessageId => _firstUnreadMessageId;
150+ int ? _firstUnreadMessageId;
151+
121152 /// The parsed message contents, as a list parallel to [messages] .
122153 ///
123154 /// The i'th element is the result of parsing the i'th element of [messages] .
@@ -151,6 +182,7 @@ mixin _MessageSequence {
151182 case MessageListHistoryStartItem (): return - 1 ;
152183 case MessageListLoadingItem ():
153184 switch (item.direction) {
185+ case MessageListDirection .newer: return 1 ;
154186 case MessageListDirection .older: return - 1 ;
155187 }
156188 case MessageListRecipientHeaderItem (: var message):
@@ -269,6 +301,11 @@ mixin _MessageSequence {
269301 _fetchingOlder = false ;
270302 _fetchOlderCoolingDown = false ;
271303 _fetchOlderCooldownBackoffMachine = null ;
304+ _haveNewest = false ;
305+ _fetchingNewer = false ;
306+ _fetchNewerCoolingDown = false ;
307+ _fetchNewerCooldownBackoffMachine = null ;
308+ _firstUnreadMessageId = null ;
272309 contents.clear ();
273310 items.clear ();
274311 }
@@ -317,7 +354,8 @@ mixin _MessageSequence {
317354 /// Update [items] to include markers at start and end as appropriate.
318355 void _updateEndMarkers () {
319356 assert (fetched);
320- assert (! (fetchingOlder && fetchOlderCoolingDown));
357+ assert (! (fetchingOlder && fetchOlderCoolingDown)
358+ || ! (fetchingNewer && fetchNewerCoolingDown));
321359 final effectiveFetchingOlder = fetchingOlder || fetchOlderCoolingDown;
322360 assert (! (effectiveFetchingOlder && haveOldest));
323361 final startMarker = switch ((effectiveFetchingOlder, haveOldest)) {
@@ -336,6 +374,21 @@ mixin _MessageSequence {
336374 case (_, true ): items.removeFirst ();
337375 case (_, _ ): break ;
338376 }
377+
378+ final effectiveFetchingNewer = fetchingNewer || fetchNewerCoolingDown;
379+ final endMarker = switch (effectiveFetchingNewer) {
380+ true => const MessageListLoadingItem (MessageListDirection .newer),
381+ false => null ,
382+ };
383+ final hasEndMarker = switch (items.lastOrNull) {
384+ MessageListLoadingItem () => true ,
385+ _ => false ,
386+ };
387+ switch ((endMarker != null , hasEndMarker)) {
388+ case (true , false ): items.add (endMarker! );
389+ case (false , true ): items.removeLast ();
390+ case (_, _ ): break ;
391+ }
339392 }
340393
341394 /// Recompute [items] from scratch, based on [messages] , [contents] , and flags.
@@ -500,15 +553,17 @@ class MessageListView with ChangeNotifier, _MessageSequence {
500553 Future <void > fetchInitial () async {
501554 // TODO(#80): fetch from anchor firstUnread, instead of newest
502555 // TODO(#82): fetch from a given message ID as anchor
503- assert (! fetched && ! haveOldest && ! fetchingOlder && ! fetchOlderCoolingDown);
556+ assert (! fetched && ! haveOldest && ! fetchingOlder && ! fetchOlderCoolingDown
557+ && ! haveNewest && ! fetchingNewer && ! fetchNewerCoolingDown);
504558 assert (messages.isEmpty && contents.isEmpty);
505559 // TODO schedule all this in another isolate
506560 final generation = this .generation;
507561 final result = await getMessages (store.connection,
508562 narrow: narrow.apiEncode (),
509- anchor: AnchorCode .newest ,
563+ anchor: AnchorCode .firstUnread ,
510564 numBefore: kMessageListFetchBatchSize,
511- numAfter: 0 ,
565+ // Results will include the anchor message, so fetch one less.
566+ numAfter: kMessageListFetchBatchSize - 1 ,
512567 );
513568 if (this .generation > generation) return ;
514569 store.reconcileMessages (result.messages);
@@ -520,6 +575,8 @@ class MessageListView with ChangeNotifier, _MessageSequence {
520575 }
521576 _fetched = true ;
522577 _haveOldest = result.foundOldest;
578+ _haveNewest = result.foundNewest;
579+ _firstUnreadMessageId = result.anchor;
523580 _updateEndMarkers ();
524581 notifyListeners ();
525582 }
@@ -541,7 +598,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
541598 try {
542599 result = await getMessages (store.connection,
543600 narrow: narrow.apiEncode (),
544- anchor: NumericAnchor (messages[ 0 ] .id),
601+ anchor: NumericAnchor (messages.first .id),
545602 includeAnchor: false ,
546603 numBefore: kMessageListFetchBatchSize,
547604 numAfter: 0 ,
@@ -553,7 +610,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
553610 if (this .generation > generation) return ;
554611
555612 if (result.messages.isNotEmpty
556- && result.messages.last.id == messages[ 0 ] .id) {
613+ && result.messages.last.id == messages.first .id) {
557614 // TODO(server-6): includeAnchor should make this impossible
558615 result.messages.removeLast ();
559616 }
@@ -589,6 +646,71 @@ class MessageListView with ChangeNotifier, _MessageSequence {
589646 }
590647 }
591648
649+ /// Fetch the next batch of newer messages, if applicable.
650+ Future <void > fetchNewer () async {
651+ if (haveNewest) return ;
652+ if (fetchingNewer) return ;
653+ if (fetchNewerCoolingDown) return ;
654+ assert (fetched);
655+ assert (messages.isNotEmpty);
656+ _fetchingNewer = true ;
657+ _updateEndMarkers ();
658+ notifyListeners ();
659+ final generation = this .generation;
660+ bool hasFetchError = false ;
661+ try {
662+ final GetMessagesResult result;
663+ try {
664+ result = await getMessages (store.connection,
665+ narrow: narrow.apiEncode (),
666+ anchor: NumericAnchor (messages.last.id),
667+ includeAnchor: false ,
668+ numBefore: 0 ,
669+ numAfter: kMessageListFetchBatchSize,
670+ );
671+ } catch (e) {
672+ hasFetchError = true ;
673+ rethrow ;
674+ }
675+ if (this .generation > generation) return ;
676+
677+ if (result.messages.isNotEmpty
678+ && result.messages.first.id == messages.last.id) {
679+ // TODO(server-6): includeAnchor should make this impossible
680+ result.messages.removeAt (0 );
681+ }
682+
683+ store.reconcileMessages (result.messages);
684+ store.recentSenders.handleMessages (result.messages); // TODO(#824)
685+
686+ final fetchedMessages = _allMessagesVisible
687+ ? result.messages // Avoid unnecessarily copying the list.
688+ : result.messages.where (_messageVisible);
689+
690+ _insertAllMessages (messages.length, fetchedMessages);
691+ _haveNewest = result.foundNewest;
692+ } finally {
693+ if (this .generation == generation) {
694+ _fetchingNewer = false ;
695+ if (hasFetchError) {
696+ assert (! fetchNewerCoolingDown);
697+ _fetchNewerCoolingDown = true ;
698+ unawaited ((_fetchNewerCooldownBackoffMachine ?? = BackoffMachine ())
699+ .wait ().then ((_) {
700+ if (this .generation != generation) return ;
701+ _fetchNewerCoolingDown = false ;
702+ _updateEndMarkers ();
703+ notifyListeners ();
704+ }));
705+ } else {
706+ _fetchNewerCooldownBackoffMachine = null ;
707+ }
708+ _updateEndMarkers ();
709+ notifyListeners ();
710+ }
711+ }
712+ }
713+
592714 void handleUserTopicEvent (UserTopicEvent event) {
593715 switch (_canAffectVisibility (event)) {
594716 case VisibilityEffect .none:
0 commit comments