@@ -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 = 20 ; // TODO tune
1818
1919/// A message, or one of its siblings shown in the message list.
2020///
@@ -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] .
@@ -269,6 +300,11 @@ mixin _MessageSequence {
269300 _fetchingOlder = false ;
270301 _fetchOlderCoolingDown = false ;
271302 _fetchOlderCooldownBackoffMachine = null ;
303+ _haveNewest = false ;
304+ _fetchingNewer = false ;
305+ _fetchNewerCoolingDown = false ;
306+ _fetchNewerCooldownBackoffMachine = null ;
307+ _firstUnreadMessageId = null ;
272308 contents.clear ();
273309 items.clear ();
274310 }
@@ -500,15 +536,16 @@ class MessageListView with ChangeNotifier, _MessageSequence {
500536 Future <void > fetchInitial () async {
501537 // TODO(#80): fetch from anchor firstUnread, instead of newest
502538 // TODO(#82): fetch from a given message ID as anchor
503- assert (! fetched && ! haveOldest && ! fetchingOlder && ! fetchOlderCoolingDown);
539+ assert (! fetched && ! haveOldest && ! fetchingOlder && ! fetchOlderCoolingDown
540+ && ! haveNewest && ! fetchingNewer && ! fetchNewerCoolingDown);
504541 assert (messages.isEmpty && contents.isEmpty);
505542 // TODO schedule all this in another isolate
506543 final generation = this .generation;
507544 final result = await getMessages (store.connection,
508545 narrow: narrow.apiEncode (),
509- anchor: AnchorCode .newest ,
546+ anchor: AnchorCode .firstUnread ,
510547 numBefore: kMessageListFetchBatchSize,
511- numAfter: 0 ,
548+ numAfter: kMessageListFetchBatchSize ,
512549 );
513550 if (this .generation > generation) return ;
514551 store.reconcileMessages (result.messages);
@@ -520,6 +557,8 @@ class MessageListView with ChangeNotifier, _MessageSequence {
520557 }
521558 _fetched = true ;
522559 _haveOldest = result.foundOldest;
560+ _haveNewest = result.foundNewest;
561+ _firstUnreadMessageId = result.anchor;
523562 _updateEndMarkers ();
524563 notifyListeners ();
525564 }
@@ -541,7 +580,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
541580 try {
542581 result = await getMessages (store.connection,
543582 narrow: narrow.apiEncode (),
544- anchor: NumericAnchor (messages[ 0 ] .id),
583+ anchor: NumericAnchor (messages.first .id),
545584 includeAnchor: false ,
546585 numBefore: kMessageListFetchBatchSize,
547586 numAfter: 0 ,
@@ -553,7 +592,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
553592 if (this .generation > generation) return ;
554593
555594 if (result.messages.isNotEmpty
556- && result.messages.last.id == messages[ 0 ] .id) {
595+ && result.messages.last.id == messages.first .id) {
557596 // TODO(server-6): includeAnchor should make this impossible
558597 result.messages.removeLast ();
559598 }
@@ -589,6 +628,69 @@ class MessageListView with ChangeNotifier, _MessageSequence {
589628 }
590629 }
591630
631+ /// Fetch the next batch of newer messages, if applicable.
632+ Future <void > fetchNewer () async {
633+ if (haveNewest) return ;
634+ if (fetchingNewer) return ;
635+ if (fetchNewerCoolingDown) return ;
636+ assert (fetched);
637+ assert (messages.isNotEmpty);
638+ _fetchingNewer = true ;
639+ // TODO handle markers
640+ _updateEndMarkers ();
641+ notifyListeners ();
642+ final generation = this .generation;
643+ bool hasFetchError = false ;
644+ try {
645+ final GetMessagesResult result;
646+ try {
647+ result = await getMessages (store.connection,
648+ narrow: narrow.apiEncode (),
649+ anchor: NumericAnchor (messages.last.id),
650+ includeAnchor: true ,
651+ numBefore: 0 ,
652+ numAfter: kMessageListFetchBatchSize,
653+ );
654+ } catch (e) {
655+ hasFetchError = true ;
656+ rethrow ;
657+ }
658+ if (this .generation > generation) return ;
659+
660+ // Remove the anchor.
661+ result.messages.removeAt (0 );
662+
663+ store.reconcileMessages (result.messages);
664+ store.recentSenders.handleMessages (result.messages); // TODO(#824)
665+
666+ final fetchedMessages = _allMessagesVisible
667+ ? result.messages // Avoid unnecessarily copying the list.
668+ : result.messages.where (_messageVisible);
669+
670+ _insertAllMessages (messages.length, fetchedMessages);
671+ _haveNewest = result.foundNewest;
672+ } finally {
673+ if (this .generation == generation) {
674+ _fetchingNewer = false ;
675+ if (hasFetchError) {
676+ assert (! fetchNewerCoolingDown);
677+ _fetchNewerCoolingDown = true ;
678+ unawaited ((_fetchNewerCooldownBackoffMachine ?? = BackoffMachine ())
679+ .wait ().then ((_) {
680+ if (this .generation != generation) return ;
681+ _fetchNewerCoolingDown = false ;
682+ _updateEndMarkers ();
683+ notifyListeners ();
684+ }));
685+ } else {
686+ _fetchNewerCooldownBackoffMachine = null ;
687+ }
688+ _updateEndMarkers ();
689+ notifyListeners ();
690+ }
691+ }
692+ }
693+
592694 void handleUserTopicEvent (UserTopicEvent event) {
593695 switch (_canAffectVisibility (event)) {
594696 case VisibilityEffect .none:
0 commit comments