@@ -27,6 +27,15 @@ class MessageListMessageItem extends MessageListItem {
27
27
MessageListMessageItem (this .message, this .content);
28
28
}
29
29
30
+ /// Indicates the app is loading more messages at the top or bottom.
31
+ class MessageListLoadingItem extends MessageListItem {
32
+ final MessageListDirection direction;
33
+
34
+ const MessageListLoadingItem (this .direction);
35
+ }
36
+
37
+ enum MessageListDirection { older, newer }
38
+
30
39
/// Indicates we've reached the oldest message in the narrow.
31
40
class MessageListHistoryStartItem extends MessageListItem {
32
41
const MessageListHistoryStartItem ();
@@ -55,6 +64,10 @@ mixin _MessageSequence {
55
64
bool get haveOldest => _haveOldest;
56
65
bool _haveOldest = false ;
57
66
67
+ /// Whether we are currently fetching the next batch of older messages.
68
+ bool get fetchingOlder => _fetchingOlder;
69
+ bool _fetchingOlder = false ;
70
+
58
71
/// The parsed message contents, as a list parallel to [messages] .
59
72
///
60
73
/// The i'th element is the result of parsing the i'th element of [messages] .
@@ -70,7 +83,7 @@ mixin _MessageSequence {
70
83
/// before, between, or after the messages.
71
84
///
72
85
/// This information is completely derived from [messages] and
73
- /// the flag [haveOldest] .
86
+ /// the flags [haveOldest] and [fetchingOlder ] .
74
87
/// It exists as an optimization, to memoize that computation.
75
88
final QueueList <MessageListItem > items = QueueList ();
76
89
@@ -86,6 +99,11 @@ mixin _MessageSequence {
86
99
static int _compareItemToMessageId (MessageListItem item, int messageId) {
87
100
switch (item) {
88
101
case MessageListHistoryStartItem (): return - 1 ;
102
+ case MessageListLoadingItem ():
103
+ switch (item.direction) {
104
+ case MessageListDirection .older: return - 1 ;
105
+ case MessageListDirection .newer: return 1 ;
106
+ }
89
107
case MessageListMessageItem (: var message): return message.id.compareTo (messageId);
90
108
}
91
109
}
@@ -115,6 +133,19 @@ mixin _MessageSequence {
115
133
_processMessage (messages.length - 1 );
116
134
}
117
135
136
+ void _insertAllMessages (int index, Iterable <Message > toInsert) {
137
+ // TODO parse/process messages in smaller batches, to not drop frames.
138
+ // On a Pixel 5, a batch of 100 messages takes ~15-20ms in _insertAllMessages.
139
+ // (Before that, ~2-5ms in jsonDecode and 0ms in fromJson,
140
+ // so skip worrying about those steps.)
141
+ assert (contents.length == messages.length);
142
+ messages.insertAll (index, toInsert);
143
+ contents.insertAll (index, toInsert.map (
144
+ (message) => parseContent (message.content)));
145
+ assert (contents.length == messages.length);
146
+ _reprocessAll ();
147
+ }
148
+
118
149
/// Redo all computations from scratch, based on [messages] .
119
150
void _recompute () {
120
151
assert (contents.length == messages.length);
@@ -137,12 +168,22 @@ mixin _MessageSequence {
137
168
138
169
/// Update [items] to include markers at start and end as appropriate.
139
170
void _updateEndMarkers () {
140
- switch ((items.firstOrNull, haveOldest)) {
141
- case (MessageListHistoryStartItem (), true ): break ;
142
- case (MessageListHistoryStartItem (), _ ): items.removeFirst ();
143
-
144
- case (_, true ): items.addFirst (const MessageListHistoryStartItem ());
145
- case (_, _): break ;
171
+ assert (! (haveOldest && fetchingOlder));
172
+ final startMarker = switch ((fetchingOlder, haveOldest)) {
173
+ (true , _) => const MessageListLoadingItem (MessageListDirection .older),
174
+ (_, true ) => const MessageListHistoryStartItem (),
175
+ (_, _) => null ,
176
+ };
177
+ final hasStartMarker = switch (items.firstOrNull) {
178
+ MessageListLoadingItem () => true ,
179
+ MessageListHistoryStartItem () => true ,
180
+ _ => false ,
181
+ };
182
+ switch ((startMarker != null , hasStartMarker)) {
183
+ case (true , true ): items[0 ] = startMarker! ;
184
+ case (true , _ ): items.addFirst (startMarker! );
185
+ case (_, true ): items.removeFirst ();
186
+ case (_, _ ): break ;
146
187
}
147
188
}
148
189
@@ -164,13 +205,12 @@ mixin _MessageSequence {
164
205
/// Lifecycle:
165
206
/// * Create with [init] .
166
207
/// * Add listeners with [addListener] .
167
- /// * Fetch messages with [fetch ] . When the fetch completes, this object
208
+ /// * Fetch messages with [fetchInitial ] . When the fetch completes, this object
168
209
/// will notify its listeners (as it will any other time the data changes.)
210
+ /// * Fetch more messages as needed with [fetchOlder] .
169
211
/// * On reassemble, call [reassemble] .
170
212
/// * When the object will no longer be used, call [dispose] to free
171
213
/// resources on the [PerAccountStore].
172
- ///
173
- /// TODO support fetching another batch
174
214
class MessageListView with ChangeNotifier , _MessageSequence {
175
215
MessageListView ._({required this .store, required this .narrow});
176
216
@@ -190,10 +230,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
190
230
final PerAccountStore store;
191
231
final Narrow narrow;
192
232
193
- Future <void > fetch () async {
233
+ /// Fetch messages, starting from scratch.
234
+ Future <void > fetchInitial () async {
194
235
// TODO(#80): fetch from anchor firstUnread, instead of newest
195
236
// TODO(#82): fetch from a given message ID as anchor
196
- assert (! fetched && ! haveOldest);
237
+ assert (! fetched && ! haveOldest && ! fetchingOlder );
197
238
assert (messages.isEmpty && contents.isEmpty);
198
239
// TODO schedule all this in another isolate
199
240
final result = await getMessages (store.connection,
@@ -211,6 +252,39 @@ class MessageListView with ChangeNotifier, _MessageSequence {
211
252
notifyListeners ();
212
253
}
213
254
255
+ /// Fetch the next batch of older messages, if applicable.
256
+ Future <void > fetchOlder () async {
257
+ if (haveOldest) return ;
258
+ if (fetchingOlder) return ;
259
+ assert (fetched);
260
+ assert (messages.isNotEmpty);
261
+ _fetchingOlder = true ;
262
+ _updateEndMarkers ();
263
+ notifyListeners ();
264
+ try {
265
+ final result = await getMessages (store.connection,
266
+ narrow: narrow.apiEncode (),
267
+ anchor: NumericAnchor (messages[0 ].id),
268
+ includeAnchor: false ,
269
+ numBefore: kMessageListFetchBatchSize,
270
+ numAfter: 0 ,
271
+ );
272
+
273
+ if (result.messages.isNotEmpty
274
+ && result.messages.last.id == messages[0 ].id) {
275
+ // TODO(server-6): includeAnchor should make this impossible
276
+ result.messages.removeLast ();
277
+ }
278
+
279
+ _insertAllMessages (0 , result.messages);
280
+ _haveOldest = result.foundOldest;
281
+ } finally {
282
+ _fetchingOlder = false ;
283
+ _updateEndMarkers ();
284
+ notifyListeners ();
285
+ }
286
+ }
287
+
214
288
/// Add [message] to this view, if it belongs here.
215
289
///
216
290
/// Called in particular when we get a [MessageEvent] .
0 commit comments