Skip to content

Commit f6b697f

Browse files
wip
1 parent c02d947 commit f6b697f

File tree

4 files changed

+387
-44
lines changed

4 files changed

+387
-44
lines changed

lib/model/message_list.dart

Lines changed: 131 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import 'narrow.dart';
1414
import '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
///
@@ -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.
6464
class 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) || !(fetchingNewer && fetchNewerCoolingDown));
358+
321359
final effectiveFetchingOlder = fetchingOlder || fetchOlderCoolingDown;
322360
assert(!(effectiveFetchingOlder && haveOldest));
323361
final startMarker = switch ((effectiveFetchingOlder, haveOldest)) {
@@ -336,6 +374,22 @@ 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.last) {
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 (true, true): break;
391+
case (false, false): break;
392+
}
339393
}
340394

341395
/// Recompute [items] from scratch, based on [messages], [contents], and flags.
@@ -500,15 +554,16 @@ class MessageListView with ChangeNotifier, _MessageSequence {
500554
Future<void> fetchInitial() async {
501555
// TODO(#80): fetch from anchor firstUnread, instead of newest
502556
// TODO(#82): fetch from a given message ID as anchor
503-
assert(!fetched && !haveOldest && !fetchingOlder && !fetchOlderCoolingDown);
557+
assert(!fetched && !haveOldest && !fetchingOlder && !fetchOlderCoolingDown
558+
&& !haveNewest && !fetchingNewer && !fetchNewerCoolingDown);
504559
assert(messages.isEmpty && contents.isEmpty);
505560
// TODO schedule all this in another isolate
506561
final generation = this.generation;
507562
final result = await getMessages(store.connection,
508563
narrow: narrow.apiEncode(),
509-
anchor: AnchorCode.newest,
564+
anchor: AnchorCode.firstUnread,
510565
numBefore: kMessageListFetchBatchSize,
511-
numAfter: 0,
566+
numAfter: kMessageListFetchBatchSize,
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,69 @@ 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+
// 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: true,
669+
numBefore: 0,
670+
numAfter: kMessageListFetchBatchSize,
671+
);
672+
} catch (e) {
673+
hasFetchError = true;
674+
rethrow;
675+
}
676+
if (this.generation > generation) return;
677+
678+
// Remove the anchor.
679+
result.messages.removeAt(0);
680+
681+
store.reconcileMessages(result.messages);
682+
store.recentSenders.handleMessages(result.messages); // TODO(#824)
683+
684+
final fetchedMessages = _allMessagesVisible
685+
? result.messages // Avoid unnecessarily copying the list.
686+
: result.messages.where(_messageVisible);
687+
688+
_insertAllMessages(messages.length, fetchedMessages);
689+
_haveNewest = result.foundNewest;
690+
} finally {
691+
if (this.generation == generation) {
692+
_fetchingNewer = false;
693+
if (hasFetchError) {
694+
assert(!fetchNewerCoolingDown);
695+
_fetchNewerCoolingDown = true;
696+
unawaited((_fetchNewerCooldownBackoffMachine ??= BackoffMachine())
697+
.wait().then((_) {
698+
if (this.generation != generation) return;
699+
_fetchNewerCoolingDown = false;
700+
_updateEndMarkers();
701+
notifyListeners();
702+
}));
703+
} else {
704+
_fetchNewerCooldownBackoffMachine = null;
705+
}
706+
_updateEndMarkers();
707+
notifyListeners();
708+
}
709+
}
710+
}
711+
592712
void handleUserTopicEvent(UserTopicEvent event) {
593713
switch (_canAffectVisibility(event)) {
594714
case VisibilityEffect.none:

0 commit comments

Comments
 (0)