Skip to content

Commit 813ee69

Browse files
committed
msglist: Fetch newer messages despite previous muted batch
1 parent 75ae045 commit 813ee69

File tree

3 files changed

+110
-65
lines changed

3 files changed

+110
-65
lines changed

lib/model/message_list.dart

Lines changed: 79 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,26 @@ mixin _MessageSequence {
145145
/// The corresponding item index is [middleItem].
146146
int middleMessage = 0;
147147

148+
/// The ID of the oldest known message so far in this narrow.
149+
///
150+
/// This will be `null` if no messages of this narrow are fetched yet.
151+
/// Having a non-null value for this doesn't always mean [haveOldest] is `true`.
152+
///
153+
/// The related message may not appear in [messages] because it
154+
/// is muted in one way or another.
155+
int? get oldMessageId => _oldMessageId;
156+
int? _oldMessageId;
157+
158+
/// The ID of the newest known message so far in this narrow.
159+
///
160+
/// This will be `null` if no messages of this narrow are fetched yet.
161+
/// Having a non-null value for this doesn't always mean [haveNewest] is `true`.
162+
///
163+
/// The related message may not appear in [messages] because it
164+
/// is muted in one way or another.
165+
int? get newMessageId => _newMessageId;
166+
int? _newMessageId;
167+
148168
/// Whether [messages] and [items] represent the results of a fetch.
149169
///
150170
/// This allows the UI to distinguish "still working on fetching messages"
@@ -409,6 +429,8 @@ mixin _MessageSequence {
409429
generation += 1;
410430
messages.clear();
411431
middleMessage = 0;
432+
_oldMessageId = null;
433+
_newMessageId = null;
412434
outboxMessages.clear();
413435
_haveOldest = false;
414436
_haveNewest = false;
@@ -775,6 +797,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
775797
Future<void> fetchInitial() async {
776798
assert(!fetched && !haveOldest && !haveNewest && !busyFetchingMore);
777799
assert(messages.isEmpty && contents.isEmpty);
800+
assert(oldMessageId == null && newMessageId == null);
778801

779802
if (narrow case KeywordSearchNarrow(keyword: '')) {
780803
// The server would reject an empty keyword search; skip the request.
@@ -796,6 +819,8 @@ class MessageListView with ChangeNotifier, _MessageSequence {
796819
numAfter: kMessageListFetchBatchSize,
797820
allowEmptyTopicName: true,
798821
);
822+
_oldMessageId = result.messages.firstOrNull?.id;
823+
_newMessageId = result.messages.lastOrNull?.id;
799824
if (this.generation > generation) return;
800825

801826
_adjustNarrowForTopicPermalink(result.messages.firstOrNull);
@@ -869,25 +894,32 @@ class MessageListView with ChangeNotifier, _MessageSequence {
869894
/// That makes this method suitable to call frequently, e.g. every frame,
870895
/// whenever it looks likely to be useful to have more messages.
871896
Future<void> fetchOlder() async {
872-
if (haveOldest) return;
873-
if (busyFetchingMore) return;
874-
assert(fetched);
875-
assert(messages.isNotEmpty);
876-
await _fetchMore(
877-
anchor: NumericAnchor(messages[0].id),
878-
numBefore: kMessageListFetchBatchSize,
879-
numAfter: 0,
880-
processResult: (result) {
881-
store.reconcileMessages(result.messages);
882-
store.recentSenders.handleMessages(result.messages); // TODO(#824)
883-
884-
final fetchedMessages = _allMessagesVisible
885-
? result.messages // Avoid unnecessarily copying the list.
886-
: result.messages.where(_messageVisible);
887-
888-
_insertAllMessages(0, fetchedMessages);
889-
_haveOldest = result.foundOldest;
890-
});
897+
final generation = this.generation;
898+
int visibleMessageCount = 0;
899+
do {
900+
if (haveOldest) return;
901+
if (busyFetchingMore) return;
902+
assert(fetched);
903+
assert(oldMessageId != null);
904+
await _fetchMore(
905+
anchor: NumericAnchor(oldMessageId!),
906+
numBefore: kMessageListFetchBatchSize,
907+
numAfter: 0,
908+
processResult: (result) {
909+
_oldMessageId = result.messages.firstOrNull?.id ?? oldMessageId;
910+
store.reconcileMessages(result.messages);
911+
store.recentSenders.handleMessages(result.messages); // TODO(#824)
912+
913+
final fetchedMessages = _allMessagesVisible
914+
? result.messages // Avoid unnecessarily copying the list.
915+
: result.messages.where(_messageVisible);
916+
917+
_insertAllMessages(0, fetchedMessages);
918+
_haveOldest = result.foundOldest;
919+
visibleMessageCount += fetchedMessages.length;
920+
});
921+
} while (visibleMessageCount < kMessageListFetchBatchSize / 2
922+
&& this.generation == generation);
891923
}
892924

893925
/// Fetch the next batch of newer messages, if applicable.
@@ -899,29 +931,36 @@ class MessageListView with ChangeNotifier, _MessageSequence {
899931
/// That makes this method suitable to call frequently, e.g. every frame,
900932
/// whenever it looks likely to be useful to have more messages.
901933
Future<void> fetchNewer() async {
902-
if (haveNewest) return;
903-
if (busyFetchingMore) return;
904-
assert(fetched);
905-
assert(messages.isNotEmpty);
906-
await _fetchMore(
907-
anchor: NumericAnchor(messages.last.id),
908-
numBefore: 0,
909-
numAfter: kMessageListFetchBatchSize,
910-
processResult: (result) {
911-
store.reconcileMessages(result.messages);
912-
store.recentSenders.handleMessages(result.messages); // TODO(#824)
913-
914-
for (final message in result.messages) {
915-
if (_messageVisible(message)) {
916-
_addMessage(message);
934+
final generation = this.generation;
935+
int visibleMessageCount = 0;
936+
do {
937+
if (haveNewest) return;
938+
if (busyFetchingMore) return;
939+
assert(fetched);
940+
assert(newMessageId != null);
941+
await _fetchMore(
942+
anchor: NumericAnchor(newMessageId!),
943+
numBefore: 0,
944+
numAfter: kMessageListFetchBatchSize,
945+
processResult: (result) {
946+
_newMessageId = result.messages.lastOrNull?.id ?? newMessageId;
947+
store.reconcileMessages(result.messages);
948+
store.recentSenders.handleMessages(result.messages); // TODO(#824)
949+
950+
for (final message in result.messages) {
951+
if (_messageVisible(message)) {
952+
_addMessage(message);
953+
visibleMessageCount++;
954+
}
917955
}
918-
}
919-
_haveNewest = result.foundNewest;
956+
_haveNewest = result.foundNewest;
920957

921-
if (haveNewest) {
922-
_syncOutboxMessagesFromStore();
923-
}
924-
});
958+
if (haveNewest) {
959+
_syncOutboxMessagesFromStore();
960+
}
961+
});
962+
} while (visibleMessageCount < kMessageListFetchBatchSize / 2
963+
&& this.generation == generation);
925964
}
926965

927966
Future<void> _fetchMore({

lib/widgets/message_list.dart

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -838,8 +838,6 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
838838
model.fetchInitial();
839839
}
840840

841-
bool _prevFetched = false;
842-
843841
void _modelChanged() {
844842
// When you're scrolling quickly, our mark-as-read requests include the
845843
// messages *between* _messagesRecentlyInViewport and the messages currently
@@ -866,14 +864,13 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
866864
// This method was called because that just changed.
867865
});
868866

869-
if (!_prevFetched && model.fetched && model.messages.isEmpty) {
867+
if (model.messages.isEmpty && model.haveOldest && model.haveNewest) {
870868
// If the fetch came up empty, there's nothing to read,
871869
// so opening the keyboard won't be bothersome and could be helpful.
872870
// It's definitely helpful if we got here from the new-DM page.
873871
MessageListPage.ancestorOf(context)
874872
.composeBoxState?.controller.requestFocusIfUnfocused();
875873
}
876-
_prevFetched = model.fetched;
877874
}
878875

879876
/// Find the range of message IDs on screen, as a (first, last) tuple,
@@ -1222,7 +1219,8 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
12221219
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
12231220
TypingStatusWidget(narrow: widget.narrow),
12241221
// TODO perhaps offer mark-as-read even when not done fetching?
1225-
MarkAsReadWidget(narrow: widget.narrow),
1222+
if (model.messages.isNotEmpty)
1223+
MarkAsReadWidget(narrow: widget.narrow),
12261224
// To reinforce that the end of the feed has been reached:
12271225
// https://chat.zulip.org/#narrow/channel/48-mobile/topic/space.20at.20end.20of.20thread/near/2203391
12281226
const SizedBox(height: 12),

test/model/message_list_test.dart

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -640,10 +640,10 @@ void main() {
640640
});
641641

642642
test('nop during backoff', () => awaitFakeAsync((async) async {
643-
final olderMessages = List.generate(5, (i) => eg.streamMessage());
644-
final initialMessages = List.generate(5, (i) => eg.streamMessage());
645-
final newerMessages = List.generate(5, (i) => eg.streamMessage());
646-
await prepare(anchor: NumericAnchor(initialMessages[2].id));
643+
final olderMessages = List.generate(50, (i) => eg.streamMessage());
644+
final initialMessages = List.generate(50, (i) => eg.streamMessage());
645+
final newerMessages = List.generate(50, (i) => eg.streamMessage());
646+
await prepare(anchor: NumericAnchor(initialMessages[25].id));
647647
await prepareMessages(foundOldest: false, foundNewest: false,
648648
messages: initialMessages);
649649
check(connection.takeRequests()).single;
@@ -687,10 +687,10 @@ void main() {
687687
// TODO(#824): move this test
688688
test('fetchOlder recent senders track all the messages', () async {
689689
await prepare();
690-
final initialMessages = List.generate(10, (i) => eg.streamMessage(id: 100 + i));
690+
final initialMessages = List.generate(50, (i) => eg.streamMessage(id: 100 + i));
691691
await prepareMessages(foundOldest: false, messages: initialMessages);
692692

693-
final oldMessages = List.generate(10, (i) => eg.streamMessage(id: 89 + i))
693+
final oldMessages = List.generate(50, (i) => eg.streamMessage(id: 49 + i))
694694
// Not subscribed to the stream with id 10.
695695
..add(eg.streamMessage(id: 99, stream: eg.stream(streamId: 10)));
696696
connection.prepare(json: olderResult(
@@ -699,28 +699,28 @@ void main() {
699699
).toJson());
700700
await model.fetchOlder();
701701

702-
check(model).messages.length.equals(20);
702+
check(model).messages.length.equals(100);
703703
recent_senders_test.checkMatchesMessages(store.recentSenders,
704704
[...initialMessages, ...oldMessages]);
705705
});
706706

707707
// TODO(#824): move this test
708708
test('TODO fetchNewer recent senders track all the messages', () async {
709709
await prepare(anchor: NumericAnchor(100));
710-
final initialMessages = List.generate(10, (i) => eg.streamMessage(id: 100 + i));
710+
final initialMessages = List.generate(50, (i) => eg.streamMessage(id: 100 + i));
711711
await prepareMessages(foundOldest: true, foundNewest: false,
712712
messages: initialMessages);
713713

714-
final newMessages = List.generate(10, (i) => eg.streamMessage(id: 110 + i))
714+
final newMessages = List.generate(50, (i) => eg.streamMessage(id: 150 + i))
715715
// Not subscribed to the stream with id 10.
716-
..add(eg.streamMessage(id: 120, stream: eg.stream(streamId: 10)));
716+
..add(eg.streamMessage(id: 200, stream: eg.stream(streamId: 10)));
717717
connection.prepare(json: newerResult(
718718
anchor: 100, foundNewest: false,
719719
messages: newMessages,
720720
).toJson());
721721
await model.fetchNewer();
722722

723-
check(model).messages.length.equals(20);
723+
check(model).messages.length.equals(100);
724724
recent_senders_test.checkMatchesMessages(store.recentSenders,
725725
[...initialMessages, ...newMessages]);
726726
});
@@ -2822,11 +2822,19 @@ void main() {
28222822
Message dmMessage(int id) =>
28232823
eg.dmMessage(id: id, from: eg.selfUser, to: [], timestamp: timestamp);
28242824

2825+
List<Message> streamMessages({required int fromId, required int toId}) {
2826+
assert(fromId > 0 && fromId <= toId);
2827+
return [
2828+
for (int id = fromId; id <= toId; id++)
2829+
streamMessage(id)
2830+
];
2831+
}
2832+
28252833
// First, test fetchInitial, where some headers are needed and others not.
28262834
await prepare(narrow: CombinedFeedNarrow());
28272835
connection.prepare(json: newestResult(
28282836
foundOldest: false,
2829-
messages: [streamMessage(10), streamMessage(11), dmMessage(12)],
2837+
messages: [streamMessage(200), streamMessage(201), dmMessage(202)],
28302838
).toJson());
28312839
await model.fetchInitial();
28322840
checkNotifiedOnce();
@@ -2835,7 +2843,7 @@ void main() {
28352843
connection.prepare(json: olderResult(
28362844
anchor: model.messages[0].id,
28372845
foundOldest: false,
2838-
messages: [streamMessage(7), streamMessage(8), dmMessage(9)],
2846+
messages: [...streamMessages(fromId: 150, toId: 198), dmMessage(199)],
28392847
).toJson());
28402848
await model.fetchOlder();
28412849
checkNotified(count: 2);
@@ -2844,17 +2852,17 @@ void main() {
28442852
connection.prepare(json: olderResult(
28452853
anchor: model.messages[0].id,
28462854
foundOldest: false,
2847-
messages: [streamMessage(6)],
2855+
messages: streamMessages(fromId: 100, toId: 149),
28482856
).toJson());
28492857
await model.fetchOlder();
28502858
checkNotified(count: 2);
28512859

28522860
// Then test MessageEvent, where a new header is needed…
2853-
await store.addMessage(streamMessage(13));
2861+
await store.addMessage(streamMessage(203));
28542862
checkNotifiedOnce();
28552863

28562864
// … and where it's not.
2857-
await store.addMessage(streamMessage(14));
2865+
await store.addMessage(streamMessage(204));
28582866
checkNotifiedOnce();
28592867

28602868
// Then test UpdateMessageEvent edits, where a header is and remains needed…
@@ -2892,7 +2900,7 @@ void main() {
28922900
connection.prepare(json: olderResult(
28932901
anchor: model.messages[0].id,
28942902
foundOldest: true,
2895-
messages: [streamMessage(5)],
2903+
messages: [streamMessage(99)],
28962904
).toJson());
28972905
await model.fetchOlder();
28982906
checkNotified(count: 2);
@@ -2904,16 +2912,16 @@ void main() {
29042912
final outboxMessageIds = store.outboxMessages.keys.toList();
29052913
// Then test removing the first outbox message…
29062914
await store.handleEvent(eg.messageEvent(
2907-
dmMessage(15), localMessageId: outboxMessageIds.first));
2915+
dmMessage(205), localMessageId: outboxMessageIds.first));
29082916
checkNotifiedOnce();
29092917

29102918
// … and handling a new non-outbox message…
2911-
await store.handleEvent(eg.messageEvent(streamMessage(16)));
2919+
await store.handleEvent(eg.messageEvent(streamMessage(206)));
29122920
checkNotifiedOnce();
29132921

29142922
// … and removing the second outbox message.
29152923
await store.handleEvent(eg.messageEvent(
2916-
dmMessage(17), localMessageId: outboxMessageIds.last));
2924+
dmMessage(207), localMessageId: outboxMessageIds.last));
29172925
checkNotifiedOnce();
29182926
}));
29192927

0 commit comments

Comments
 (0)