@@ -14,7 +14,7 @@ import 'narrow.dart';
14
14
import 'store.dart' ;
15
15
16
16
/// The number of messages to fetch in each request.
17
- const kMessageListFetchBatchSize = 100 ; // TODO tune
17
+ const kMessageListFetchBatchSize = 20 ; // TODO tune
18
18
19
19
/// A message, or one of its siblings shown in the message list.
20
20
///
@@ -85,9 +85,6 @@ mixin _MessageSequence {
85
85
bool _fetched = false ;
86
86
87
87
/// 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.)
91
88
bool get haveOldest => _haveOldest;
92
89
bool _haveOldest = false ;
93
90
@@ -118,6 +115,40 @@ mixin _MessageSequence {
118
115
119
116
BackoffMachine ? _fetchOlderCooldownBackoffMachine;
120
117
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
+
121
152
/// The parsed message contents, as a list parallel to [messages] .
122
153
///
123
154
/// The i'th element is the result of parsing the i'th element of [messages] .
@@ -269,6 +300,11 @@ mixin _MessageSequence {
269
300
_fetchingOlder = false ;
270
301
_fetchOlderCoolingDown = false ;
271
302
_fetchOlderCooldownBackoffMachine = null ;
303
+ _haveNewest = false ;
304
+ _fetchingNewer = false ;
305
+ _fetchNewerCoolingDown = false ;
306
+ _fetchNewerCooldownBackoffMachine = null ;
307
+ _firstUnreadMessageId = null ;
272
308
contents.clear ();
273
309
items.clear ();
274
310
}
@@ -500,15 +536,16 @@ class MessageListView with ChangeNotifier, _MessageSequence {
500
536
Future <void > fetchInitial () async {
501
537
// TODO(#80): fetch from anchor firstUnread, instead of newest
502
538
// 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);
504
541
assert (messages.isEmpty && contents.isEmpty);
505
542
// TODO schedule all this in another isolate
506
543
final generation = this .generation;
507
544
final result = await getMessages (store.connection,
508
545
narrow: narrow.apiEncode (),
509
- anchor: AnchorCode .newest ,
546
+ anchor: AnchorCode .firstUnread ,
510
547
numBefore: kMessageListFetchBatchSize,
511
- numAfter: 0 ,
548
+ numAfter: kMessageListFetchBatchSize ,
512
549
);
513
550
if (this .generation > generation) return ;
514
551
store.reconcileMessages (result.messages);
@@ -520,6 +557,8 @@ class MessageListView with ChangeNotifier, _MessageSequence {
520
557
}
521
558
_fetched = true ;
522
559
_haveOldest = result.foundOldest;
560
+ _haveNewest = result.foundNewest;
561
+ _firstUnreadMessageId = result.anchor;
523
562
_updateEndMarkers ();
524
563
notifyListeners ();
525
564
}
@@ -541,7 +580,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
541
580
try {
542
581
result = await getMessages (store.connection,
543
582
narrow: narrow.apiEncode (),
544
- anchor: NumericAnchor (messages[ 0 ] .id),
583
+ anchor: NumericAnchor (messages.first .id),
545
584
includeAnchor: false ,
546
585
numBefore: kMessageListFetchBatchSize,
547
586
numAfter: 0 ,
@@ -553,7 +592,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
553
592
if (this .generation > generation) return ;
554
593
555
594
if (result.messages.isNotEmpty
556
- && result.messages.last.id == messages[ 0 ] .id) {
595
+ && result.messages.last.id == messages.first .id) {
557
596
// TODO(server-6): includeAnchor should make this impossible
558
597
result.messages.removeLast ();
559
598
}
@@ -589,6 +628,69 @@ class MessageListView with ChangeNotifier, _MessageSequence {
589
628
}
590
629
}
591
630
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
+
592
694
void handleUserTopicEvent (UserTopicEvent event) {
593
695
switch (_canAffectVisibility (event)) {
594
696
case VisibilityEffect .none:
0 commit comments