@@ -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 = 5 ; // TODO tune
18
18
19
19
/// A message, or one of its siblings shown in the message list.
20
20
///
@@ -58,7 +58,7 @@ class MessageListLoadingItem extends MessageListItem {
58
58
const MessageListLoadingItem (this .direction);
59
59
}
60
60
61
- enum MessageListDirection { older }
61
+ enum MessageListDirection { older, newer }
62
62
63
63
/// Indicates we've reached the oldest message in the narrow.
64
64
class MessageListHistoryStartItem extends MessageListItem {
@@ -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] .
@@ -151,6 +182,7 @@ mixin _MessageSequence {
151
182
case MessageListHistoryStartItem (): return - 1 ;
152
183
case MessageListLoadingItem ():
153
184
switch (item.direction) {
185
+ case MessageListDirection .newer: return 1 ;
154
186
case MessageListDirection .older: return - 1 ;
155
187
}
156
188
case MessageListRecipientHeaderItem (: var message):
@@ -269,6 +301,11 @@ mixin _MessageSequence {
269
301
_fetchingOlder = false ;
270
302
_fetchOlderCoolingDown = false ;
271
303
_fetchOlderCooldownBackoffMachine = null ;
304
+ _haveNewest = false ;
305
+ _fetchingNewer = false ;
306
+ _fetchNewerCoolingDown = false ;
307
+ _fetchNewerCooldownBackoffMachine = null ;
308
+ _firstUnreadMessageId = null ;
272
309
contents.clear ();
273
310
items.clear ();
274
311
}
@@ -317,7 +354,8 @@ mixin _MessageSequence {
317
354
/// Update [items] to include markers at start and end as appropriate.
318
355
void _updateEndMarkers () {
319
356
assert (fetched);
320
- assert (! (fetchingOlder && fetchOlderCoolingDown));
357
+ assert (! (fetchingOlder && fetchOlderCoolingDown)
358
+ || ! (fetchingNewer && fetchNewerCoolingDown));
321
359
final effectiveFetchingOlder = fetchingOlder || fetchOlderCoolingDown;
322
360
assert (! (effectiveFetchingOlder && haveOldest));
323
361
final startMarker = switch ((effectiveFetchingOlder, haveOldest)) {
@@ -336,6 +374,21 @@ mixin _MessageSequence {
336
374
case (_, true ): items.removeFirst ();
337
375
case (_, _ ): break ;
338
376
}
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
+ }
339
392
}
340
393
341
394
/// Recompute [items] from scratch, based on [messages] , [contents] , and flags.
@@ -500,15 +553,17 @@ class MessageListView with ChangeNotifier, _MessageSequence {
500
553
Future <void > fetchInitial () async {
501
554
// TODO(#80): fetch from anchor firstUnread, instead of newest
502
555
// 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);
504
558
assert (messages.isEmpty && contents.isEmpty);
505
559
// TODO schedule all this in another isolate
506
560
final generation = this .generation;
507
561
final result = await getMessages (store.connection,
508
562
narrow: narrow.apiEncode (),
509
- anchor: AnchorCode .newest ,
563
+ anchor: AnchorCode .firstUnread ,
510
564
numBefore: kMessageListFetchBatchSize,
511
- numAfter: 0 ,
565
+ // Results will include the anchor message, so fetch one less.
566
+ numAfter: kMessageListFetchBatchSize - 1 ,
512
567
);
513
568
if (this .generation > generation) return ;
514
569
store.reconcileMessages (result.messages);
@@ -520,6 +575,8 @@ class MessageListView with ChangeNotifier, _MessageSequence {
520
575
}
521
576
_fetched = true ;
522
577
_haveOldest = result.foundOldest;
578
+ _haveNewest = result.foundNewest;
579
+ _firstUnreadMessageId = result.anchor;
523
580
_updateEndMarkers ();
524
581
notifyListeners ();
525
582
}
@@ -541,7 +598,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
541
598
try {
542
599
result = await getMessages (store.connection,
543
600
narrow: narrow.apiEncode (),
544
- anchor: NumericAnchor (messages[ 0 ] .id),
601
+ anchor: NumericAnchor (messages.first .id),
545
602
includeAnchor: false ,
546
603
numBefore: kMessageListFetchBatchSize,
547
604
numAfter: 0 ,
@@ -553,7 +610,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
553
610
if (this .generation > generation) return ;
554
611
555
612
if (result.messages.isNotEmpty
556
- && result.messages.last.id == messages[ 0 ] .id) {
613
+ && result.messages.last.id == messages.first .id) {
557
614
// TODO(server-6): includeAnchor should make this impossible
558
615
result.messages.removeLast ();
559
616
}
@@ -589,6 +646,72 @@ class MessageListView with ChangeNotifier, _MessageSequence {
589
646
}
590
647
}
591
648
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
+ // TODO handle markers
658
+ _updateEndMarkers ();
659
+ notifyListeners ();
660
+ final generation = this .generation;
661
+ bool hasFetchError = false ;
662
+ try {
663
+ final GetMessagesResult result;
664
+ try {
665
+ result = await getMessages (store.connection,
666
+ narrow: narrow.apiEncode (),
667
+ anchor: NumericAnchor (messages.last.id),
668
+ includeAnchor: false ,
669
+ numBefore: 0 ,
670
+ numAfter: kMessageListFetchBatchSize,
671
+ );
672
+ } catch (e) {
673
+ hasFetchError = true ;
674
+ rethrow ;
675
+ }
676
+ if (this .generation > generation) return ;
677
+
678
+ if (result.messages.isNotEmpty
679
+ && result.messages.first.id == messages.last.id) {
680
+ // TODO(server-6): includeAnchor should make this impossible
681
+ result.messages.removeAt (0 );
682
+ }
683
+
684
+ store.reconcileMessages (result.messages);
685
+ store.recentSenders.handleMessages (result.messages); // TODO(#824)
686
+
687
+ final fetchedMessages = _allMessagesVisible
688
+ ? result.messages // Avoid unnecessarily copying the list.
689
+ : result.messages.where (_messageVisible);
690
+
691
+ _insertAllMessages (messages.length, fetchedMessages);
692
+ _haveNewest = result.foundNewest;
693
+ } finally {
694
+ if (this .generation == generation) {
695
+ _fetchingNewer = false ;
696
+ if (hasFetchError) {
697
+ assert (! fetchNewerCoolingDown);
698
+ _fetchNewerCoolingDown = true ;
699
+ final machine = (_fetchNewerCooldownBackoffMachine ?? = BackoffMachine ());
700
+ unawaited (machine.wait ().then ((_) {
701
+ if (this .generation != generation) return ;
702
+ _fetchNewerCoolingDown = false ;
703
+ _updateEndMarkers ();
704
+ notifyListeners ();
705
+ }));
706
+ } else {
707
+ _fetchNewerCooldownBackoffMachine = null ;
708
+ }
709
+ _updateEndMarkers ();
710
+ notifyListeners ();
711
+ }
712
+ }
713
+ }
714
+
592
715
void handleUserTopicEvent (UserTopicEvent event) {
593
716
switch (_canAffectVisibility (event)) {
594
717
case VisibilityEffect .none:
0 commit comments