@@ -56,9 +56,13 @@ class MessageListLoadingItem extends MessageListItem {
56
56
final MessageListDirection direction;
57
57
58
58
const MessageListLoadingItem (this .direction);
59
+
59
60
}
60
61
61
- enum MessageListDirection { older }
62
+ enum MessageListDirection {
63
+ older,
64
+ newer
65
+ }
62
66
63
67
/// Indicates we've reached the oldest message in the narrow.
64
68
class MessageListHistoryStartItem extends MessageListItem {
@@ -85,9 +89,7 @@ mixin _MessageSequence {
85
89
bool _fetched = false ;
86
90
87
91
/// 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.)
92
+
91
93
bool get haveOldest => _haveOldest;
92
94
bool _haveOldest = false ;
93
95
@@ -118,6 +120,38 @@ mixin _MessageSequence {
118
120
119
121
BackoffMachine ? _fetchOlderCooldownBackoffMachine;
120
122
123
+
124
+ /// Whether we are currently fetching the next batch of newer messages.
125
+ ///
126
+ /// When this is true, [fetchNewer] is a no-op.
127
+ /// That method is called frequently by Flutter's scrolling logic,
128
+ /// and this field helps us avoid spamming the same request just to get
129
+ /// the same response each time.
130
+ ///
131
+ /// See also [fetchNewerCoolingDown] .
132
+ bool get fetchingNewer => _fetchingNewer;
133
+ bool _fetchingNewer = false ;
134
+
135
+ /// Whether [fetchNewer] had a request error recently.
136
+ ///
137
+ /// When this is true, [fetchNewer] is a no-op.
138
+ /// That method is called frequently by Flutter's scrolling logic,
139
+ /// and this field helps us avoid spamming the same request and getting
140
+ /// the same error each time.
141
+ ///
142
+ /// "Recently" is decided by a [BackoffMachine] that resets
143
+ /// when a [fetchNewer] request succeeds.
144
+ ///
145
+ /// See also [fetchingNewer] .
146
+ bool get fetchNewerCoolingDown => _fetchNewerCoolingDown;
147
+ bool _fetchNewerCoolingDown = false ;
148
+
149
+ BackoffMachine ? _fetchNewerCooldownBackoffMachine;
150
+
151
+ /// Whether we know we have the newest messages for this narrow.
152
+ bool get haveNewest => _haveNewest;
153
+ bool _haveNewest = false ;
154
+
121
155
/// The parsed message contents, as a list parallel to [messages] .
122
156
///
123
157
/// The i'th element is the result of parsing the i'th element of [messages] .
@@ -133,7 +167,8 @@ mixin _MessageSequence {
133
167
/// before, between, or after the messages.
134
168
///
135
169
/// This information is completely derived from [messages] and
136
- /// the flags [haveOldest] , [fetchingOlder] and [fetchOlderCoolingDown] .
170
+ /// the flags [haveOldest] , [fetchingOlder] and [fetchOlderCoolingDown]
171
+ /// and [haveNewest] , [fetchingNewer] and [fetchNewerCoolingDown] .
137
172
/// It exists as an optimization, to memoize that computation.
138
173
final QueueList <MessageListItem > items = QueueList ();
139
174
@@ -152,6 +187,7 @@ mixin _MessageSequence {
152
187
case MessageListLoadingItem ():
153
188
switch (item.direction) {
154
189
case MessageListDirection .older: return - 1 ;
190
+ case MessageListDirection .newer: return 1 ;
155
191
}
156
192
case MessageListRecipientHeaderItem (: var message):
157
193
case MessageListDateSeparatorItem (: var message):
@@ -271,6 +307,10 @@ mixin _MessageSequence {
271
307
_fetchOlderCooldownBackoffMachine = null ;
272
308
contents.clear ();
273
309
items.clear ();
310
+ _fetchingNewer = false ;
311
+ _fetchNewerCoolingDown = false ;
312
+ _fetchNewerCooldownBackoffMachine = null ;
313
+ _haveNewest = false ;
274
314
}
275
315
276
316
/// Redo all computations from scratch, based on [messages] .
@@ -318,24 +358,53 @@ mixin _MessageSequence {
318
358
void _updateEndMarkers () {
319
359
assert (fetched);
320
360
assert (! (fetchingOlder && fetchOlderCoolingDown));
361
+ assert (! (fetchingNewer && fetchNewerCoolingDown));
362
+
321
363
final effectiveFetchingOlder = fetchingOlder || fetchOlderCoolingDown;
364
+ final effectiveFetchingNewer = fetchingNewer || fetchNewerCoolingDown;
365
+
322
366
assert (! (effectiveFetchingOlder && haveOldest));
367
+ assert (! (effectiveFetchingNewer && haveNewest));
368
+
369
+ // Handle start marker (older messages)
323
370
final startMarker = switch ((effectiveFetchingOlder, haveOldest)) {
324
371
(true , _) => const MessageListLoadingItem (MessageListDirection .older),
325
372
(_, true ) => const MessageListHistoryStartItem (),
326
373
(_, _) => null ,
327
374
};
375
+
376
+ // Handle end marker (newer messages)
377
+ final endMarker = switch ((effectiveFetchingNewer, haveNewest)) {
378
+ (true , _) => const MessageListLoadingItem (MessageListDirection .newer),
379
+ (_, _) => null , // No "history end" marker needed since we start from newest
380
+ };
381
+
328
382
final hasStartMarker = switch (items.firstOrNull) {
329
383
MessageListLoadingItem () => true ,
330
384
MessageListHistoryStartItem () => true ,
331
385
_ => false ,
332
386
};
387
+
388
+ final hasEndMarker = switch (items.lastOrNull) {
389
+ MessageListLoadingItem () => true ,
390
+ _ => false ,
391
+ };
392
+
393
+ // Update start marker
333
394
switch ((startMarker != null , hasStartMarker)) {
334
395
case (true , true ): items[0 ] = startMarker! ;
335
396
case (true , _ ): items.addFirst (startMarker! );
336
397
case (_, true ): items.removeFirst ();
337
398
case (_, _ ): break ;
338
399
}
400
+
401
+ // Update end marker
402
+ switch ((endMarker != null , hasEndMarker)) {
403
+ case (true , true ): items[items.length - 1 ] = endMarker! ;
404
+ case (true , _ ): items.add (endMarker! );
405
+ case (_, true ): items.removeLast ();
406
+ case (_, _ ): break ;
407
+ }
339
408
}
340
409
341
410
/// Recompute [items] from scratch, based on [messages] , [contents] , and flags.
@@ -408,16 +477,20 @@ bool _sameDay(DateTime date1, DateTime date2) {
408
477
/// * Add listeners with [addListener] .
409
478
/// * Fetch messages with [fetchInitial] . When the fetch completes, this object
410
479
/// will notify its listeners (as it will any other time the data changes.)
411
- /// * Fetch more messages as needed with [fetchOlder] .
480
+ /// * Fetch more messages as needed with [fetchOlder] or [fetchNewer] .
412
481
/// * On reassemble, call [reassemble] .
413
482
/// * When the object will no longer be used, call [dispose] to free
414
483
/// resources on the [PerAccountStore].
415
484
class MessageListView with ChangeNotifier , _MessageSequence {
416
- MessageListView ._({required this .store, required this .narrow});
485
+ MessageListView ._({required this .store, required this .narrow, this .anchorMessageId });
417
486
487
+ // Anchor message ID is used to fetch messages from a specific point in the list.
488
+ // It is set when the user navigates to a message list page with a specific anchor message.
489
+ int ? anchorMessageId;
490
+ int ? get anchorIndex => anchorMessageId != null ? findItemWithMessageId (anchorMessageId! ) : null ;
418
491
factory MessageListView .init (
419
- {required PerAccountStore store, required Narrow narrow}) {
420
- final view = MessageListView ._(store: store, narrow: narrow);
492
+ {required PerAccountStore store, required Narrow narrow, int ? anchorMessageId }) {
493
+ final view = MessageListView ._(store: store, narrow: narrow, anchorMessageId : anchorMessageId );
421
494
store.registerMessageList (view);
422
495
return view;
423
496
}
@@ -496,20 +569,30 @@ class MessageListView with ChangeNotifier, _MessageSequence {
496
569
}
497
570
}
498
571
572
+
573
+
499
574
/// Fetch messages, starting from scratch.
500
575
Future <void > fetchInitial () async {
501
576
// TODO(#80): fetch from anchor firstUnread, instead of newest
502
- // TODO(#82): fetch from a given message ID as anchor
503
- assert (! fetched && ! haveOldest && ! fetchingOlder && ! fetchOlderCoolingDown);
577
+
578
+ assert (! fetched && ! haveOldest && ! fetchingOlder && ! fetchOlderCoolingDown && ! fetchingNewer && ! fetchNewerCoolingDown && ! haveNewest );
504
579
assert (messages.isEmpty && contents.isEmpty);
505
580
// TODO schedule all this in another isolate
506
581
final generation = this .generation;
507
582
final result = await getMessages (store.connection,
508
583
narrow: narrow.apiEncode (),
509
- anchor: AnchorCode .newest,
510
- numBefore: kMessageListFetchBatchSize,
511
- numAfter: 0 ,
584
+ anchor: anchorMessageId != null
585
+ ? NumericAnchor (anchorMessageId! )
586
+ : AnchorCode .newest,
587
+ numBefore: anchorMessageId != null
588
+ ? kMessageListFetchBatchSize ~ / 2 // Fetch messages before and after anchor
589
+ : kMessageListFetchBatchSize, // Fetch only older messages when no anchor
590
+ numAfter: anchorMessageId != null
591
+ ? kMessageListFetchBatchSize ~ / 2 // Fetch messages before and after anchor
592
+ : 0 , // Don't fetch newer messages when no anchor
512
593
);
594
+ anchorMessageId ?? = result.messages.last.id;
595
+
513
596
if (this .generation > generation) return ;
514
597
store.reconcileMessages (result.messages);
515
598
store.recentSenders.handleMessages (result.messages); // TODO(#824)
@@ -520,10 +603,12 @@ class MessageListView with ChangeNotifier, _MessageSequence {
520
603
}
521
604
_fetched = true ;
522
605
_haveOldest = result.foundOldest;
606
+ _haveNewest = result.foundNewest;
523
607
_updateEndMarkers ();
524
608
notifyListeners ();
525
609
}
526
610
611
+
527
612
/// Fetch the next batch of older messages, if applicable.
528
613
Future <void > fetchOlder () async {
529
614
if (haveOldest) return ;
@@ -589,6 +674,76 @@ class MessageListView with ChangeNotifier, _MessageSequence {
589
674
}
590
675
}
591
676
677
+ /// Fetch the next batch of newer messages, if applicable.
678
+ Future <void > fetchNewer () async {
679
+ if (haveNewest) return ;
680
+ if (fetchingNewer) return ;
681
+ if (fetchNewerCoolingDown) return ;
682
+ assert (fetched);
683
+ assert (messages.isNotEmpty);
684
+
685
+ _fetchingNewer = true ;
686
+ _updateEndMarkers ();
687
+ notifyListeners ();
688
+
689
+ final generation = this .generation;
690
+ bool hasFetchError = false ;
691
+
692
+ try {
693
+ final GetMessagesResult result;
694
+ try {
695
+ result = await getMessages (store.connection,
696
+ narrow: narrow.apiEncode (),
697
+ anchor: NumericAnchor (messages.last.id),
698
+ includeAnchor: false ,
699
+ numBefore: 0 ,
700
+ numAfter: kMessageListFetchBatchSize,
701
+ );
702
+ } catch (e) {
703
+ hasFetchError = true ;
704
+ rethrow ;
705
+ }
706
+ if (this .generation > generation) return ;
707
+
708
+ if (result.messages.isNotEmpty
709
+ && result.messages.first.id == messages.last.id) {
710
+ // TODO(server-6): includeAnchor should make this impossible
711
+ result.messages.removeAt (0 );
712
+ }
713
+
714
+ store.reconcileMessages (result.messages);
715
+ store.recentSenders.handleMessages (result.messages);
716
+
717
+ final fetchedMessages = _allMessagesVisible
718
+ ? result.messages
719
+ : result.messages.where (_messageVisible);
720
+
721
+ _insertAllMessages (messages.length, fetchedMessages);
722
+
723
+ _haveNewest = result.foundNewest;
724
+
725
+ } finally {
726
+ if (this .generation == generation) {
727
+ _fetchingNewer = false ;
728
+ if (hasFetchError) {
729
+ assert (! fetchNewerCoolingDown);
730
+ _fetchNewerCoolingDown = true ;
731
+ unawaited ((_fetchNewerCooldownBackoffMachine ?? = BackoffMachine ())
732
+ .wait ().then ((_) {
733
+ if (this .generation != generation) return ;
734
+ _fetchNewerCoolingDown = false ;
735
+ _updateEndMarkers ();
736
+ notifyListeners ();
737
+ }));
738
+ } else {
739
+ _fetchNewerCooldownBackoffMachine = null ;
740
+ }
741
+ _updateEndMarkers ();
742
+ notifyListeners ();
743
+ }
744
+ }
745
+ }
746
+
592
747
void handleUserTopicEvent (UserTopicEvent event) {
593
748
switch (_canAffectVisibility (event)) {
594
749
case VisibilityEffect .none:
0 commit comments