@@ -66,6 +66,22 @@ class MessageListMessageItem extends MessageListMessageBaseItem {
66
66
});
67
67
}
68
68
69
+ /// An [OutboxMessage] to show in the message list.
70
+ class MessageListOutboxMessageItem extends MessageListMessageBaseItem {
71
+ @override
72
+ final OutboxMessage message;
73
+ @override
74
+ final ZulipContent content;
75
+
76
+ MessageListOutboxMessageItem (
77
+ this .message, {
78
+ required super .showSender,
79
+ required super .isLastInBlock,
80
+ }) : content = ZulipContent (nodes: [
81
+ ParagraphNode (links: null , nodes: [TextNode (message.contentMarkdown)]),
82
+ ]);
83
+ }
84
+
69
85
/// The status of outstanding or recent fetch requests from a [MessageListView] .
70
86
enum FetchingStatus {
71
87
/// The model has not made any fetch requests (since its last reset, if any).
@@ -158,14 +174,24 @@ mixin _MessageSequence {
158
174
/// It exists as an optimization, to memoize the work of parsing.
159
175
final List <ZulipMessageContent > contents = [];
160
176
177
+ /// The [OutboxMessage] s sent by the self-user, retrieved from
178
+ /// [MessageStore.outboxMessages] .
179
+ ///
180
+ /// See also [items] .
181
+ ///
182
+ /// O(N) iterations through this list are acceptable
183
+ /// because it won't normally have more than a few items.
184
+ final List <OutboxMessage > outboxMessages = [];
185
+
161
186
/// The messages and their siblings in the UI, in order.
162
187
///
163
188
/// This has a [MessageListMessageItem] corresponding to each element
164
189
/// of [messages] , in order. It may have additional items interspersed
165
- /// before, between, or after the messages.
190
+ /// before, between, or after the messages. Then, similarly,
191
+ /// [MessageListOutboxMessageItem] s corresponding to [outboxMessages] .
166
192
///
167
- /// This information is completely derived from [messages] and
168
- /// the flags [haveOldest] , [haveNewest] , and [busyFetchingMore] .
193
+ /// This information is completely derived from [messages] , [outboxMessages] ,
194
+ /// and the flags [haveOldest] , [haveNewest] , and [busyFetchingMore] .
169
195
/// It exists as an optimization, to memoize that computation.
170
196
///
171
197
/// See also [middleItem] , an index which divides this list
@@ -177,11 +203,14 @@ mixin _MessageSequence {
177
203
/// The indices 0 to before [middleItem] are the top slice of [items] ,
178
204
/// and the indices from [middleItem] to the end are the bottom slice.
179
205
///
180
- /// The top and bottom slices of [items] correspond to
181
- /// the top and bottom slices of [messages] respectively.
182
- /// Either the bottom slices of both [items] and [messages] are empty,
183
- /// or the first item in the bottom slice of [items] is a [MessageListMessageItem]
184
- /// for the first message in the bottom slice of [messages] .
206
+ /// The top slice of [items] corresponds to the top slice of [messages] .
207
+ /// The bottom slice of [items] corresponds to the bottom slice of [messages]
208
+ /// plus any [outboxMessages] .
209
+ ///
210
+ /// The bottom slice will either be empty
211
+ /// or start with a [MessageListMessageBaseItem] .
212
+ /// It will not start with a [MessageListDateSeparatorItem]
213
+ /// or a [MessageListRecipientHeaderItem] .
185
214
int middleItem = 0 ;
186
215
187
216
int _findMessageWithId (int messageId) {
@@ -197,9 +226,10 @@ mixin _MessageSequence {
197
226
switch (item) {
198
227
case MessageListRecipientHeaderItem (: var message):
199
228
case MessageListDateSeparatorItem (: var message):
200
- if (message.id == null ) return 1 ; // TODO(#1441): test
229
+ if (message.id == null ) return 1 ;
201
230
return message.id! <= messageId ? - 1 : 1 ;
202
231
case MessageListMessageItem (: var message): return message.id.compareTo (messageId);
232
+ case MessageListOutboxMessageItem (): return 1 ;
203
233
}
204
234
}
205
235
@@ -316,11 +346,48 @@ mixin _MessageSequence {
316
346
_reprocessAll ();
317
347
}
318
348
349
+ /// Append [outboxMessage] to [outboxMessages] and update derived data
350
+ /// accordingly.
351
+ ///
352
+ /// The caller is responsible for ensuring this is an appropriate thing to do
353
+ /// given [narrow] and other concerns.
354
+ void _addOutboxMessage (OutboxMessage outboxMessage) {
355
+ assert (haveNewest);
356
+ assert (! outboxMessages.contains (outboxMessage));
357
+ outboxMessages.add (outboxMessage);
358
+ _processOutboxMessage (outboxMessages.length - 1 );
359
+ }
360
+
361
+ /// Remove the [outboxMessage] from the view.
362
+ ///
363
+ /// Returns true if the outbox message was removed, false otherwise.
364
+ bool _removeOutboxMessage (OutboxMessage outboxMessage) {
365
+ if (! outboxMessages.remove (outboxMessage)) {
366
+ return false ;
367
+ }
368
+ _reprocessOutboxMessages ();
369
+ return true ;
370
+ }
371
+
372
+ /// Remove all outbox messages that satisfy [test] from [outboxMessages] .
373
+ ///
374
+ /// Returns true if any outbox messages were removed, false otherwise.
375
+ bool _removeOutboxMessagesWhere (bool Function (OutboxMessage ) test) {
376
+ final count = outboxMessages.length;
377
+ outboxMessages.removeWhere (test);
378
+ if (outboxMessages.length == count) {
379
+ return false ;
380
+ }
381
+ _reprocessOutboxMessages ();
382
+ return true ;
383
+ }
384
+
319
385
/// Reset all [_MessageSequence] data, and cancel any active fetches.
320
386
void _reset () {
321
387
generation += 1 ;
322
388
messages.clear ();
323
389
middleMessage = 0 ;
390
+ outboxMessages.clear ();
324
391
_haveOldest = false ;
325
392
_haveNewest = false ;
326
393
_status = FetchingStatus .unstarted;
@@ -379,7 +446,6 @@ mixin _MessageSequence {
379
446
assert (item.showSender == ! canShareSender);
380
447
assert (item.isLastInBlock);
381
448
if (shouldSetMiddleItem) {
382
- assert (item is MessageListMessageItem );
383
449
middleItem = items.length;
384
450
}
385
451
items.add (item);
@@ -390,6 +456,7 @@ mixin _MessageSequence {
390
456
/// The previous messages in the list must already have been processed.
391
457
/// This message must already have been parsed and reflected in [contents] .
392
458
void _processMessage (int index) {
459
+ assert (items.lastOrNull is ! MessageListOutboxMessageItem );
393
460
final prevMessage = index == 0 ? null : messages[index - 1 ];
394
461
final message = messages[index];
395
462
final content = contents[index];
@@ -401,13 +468,67 @@ mixin _MessageSequence {
401
468
message, content, showSender: ! canShareSender, isLastInBlock: true ));
402
469
}
403
470
404
- /// Recompute [items] from scratch, based on [messages] , [contents] , and flags.
471
+ /// Append to [items] based on the index-th message in [outboxMessages] .
472
+ ///
473
+ /// All [messages] and previous messages in [outboxMessages] must already have
474
+ /// been processed.
475
+ void _processOutboxMessage (int index) {
476
+ final prevMessage = index == 0 ? messages.lastOrNull
477
+ : outboxMessages[index - 1 ];
478
+ final message = outboxMessages[index];
479
+
480
+ _addItemsForMessage (message,
481
+ // The first outbox message item becomes the middle item
482
+ // when the bottom slice of [messages] is empty.
483
+ shouldSetMiddleItem: index == 0 && middleMessage == messages.length,
484
+ prevMessage: prevMessage,
485
+ buildItem: (bool canShareSender) => MessageListOutboxMessageItem (
486
+ message, showSender: ! canShareSender, isLastInBlock: true ));
487
+ }
488
+
489
+ /// Remove items associated with [outboxMessages] from [items] .
490
+ ///
491
+ /// This is designed to be idempotent; repeated calls will not change the
492
+ /// content of [items] .
493
+ ///
494
+ /// This is efficient due to the expected small size of [outboxMessages] .
495
+ void _removeOutboxMessageItems () {
496
+ // This loop relies on the assumption that all items that follow
497
+ // the last [MessageListMessageItem] are derived from outbox messages.
498
+ while (items.isNotEmpty && items.last is ! MessageListMessageItem ) {
499
+ items.removeLast ();
500
+ }
501
+
502
+ if (items.isNotEmpty) {
503
+ final lastItem = items.last as MessageListMessageItem ;
504
+ lastItem.isLastInBlock = true ;
505
+ }
506
+ if (middleMessage == messages.length) middleItem = items.length;
507
+ }
508
+
509
+ /// Recompute the portion of [items] derived from outbox messages,
510
+ /// based on [outboxMessages] and [messages] .
511
+ ///
512
+ /// All [messages] should have been processed when this is called.
513
+ void _reprocessOutboxMessages () {
514
+ assert (haveNewest);
515
+ _removeOutboxMessageItems ();
516
+ for (var i = 0 ; i < outboxMessages.length; i++ ) {
517
+ _processOutboxMessage (i);
518
+ }
519
+ }
520
+
521
+ /// Recompute [items] from scratch, based on [messages] , [contents] ,
522
+ /// [outboxMessages] and flags.
405
523
void _reprocessAll () {
406
524
items.clear ();
407
525
for (var i = 0 ; i < messages.length; i++ ) {
408
526
_processMessage (i);
409
527
}
410
528
if (middleMessage == messages.length) middleItem = items.length;
529
+ for (var i = 0 ; i < outboxMessages.length; i++ ) {
530
+ _processOutboxMessage (i);
531
+ }
411
532
}
412
533
}
413
534
@@ -602,6 +723,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
602
723
}
603
724
_haveOldest = result.foundOldest;
604
725
_haveNewest = result.foundNewest;
726
+
727
+ if (haveNewest) {
728
+ _syncOutboxMessagesFromStore ();
729
+ }
730
+
605
731
_setStatus (FetchingStatus .idle, was: FetchingStatus .fetchInitial);
606
732
}
607
733
@@ -706,6 +832,10 @@ class MessageListView with ChangeNotifier, _MessageSequence {
706
832
}
707
833
}
708
834
_haveNewest = result.foundNewest;
835
+
836
+ if (haveNewest) {
837
+ _syncOutboxMessagesFromStore ();
838
+ }
709
839
});
710
840
}
711
841
@@ -770,9 +900,42 @@ class MessageListView with ChangeNotifier, _MessageSequence {
770
900
fetchInitial ();
771
901
}
772
902
903
+ bool _shouldAddOutboxMessage (OutboxMessage outboxMessage) {
904
+ assert (haveNewest);
905
+ return ! outboxMessage.hidden
906
+ && narrow.containsMessage (outboxMessage)
907
+ && _messageVisible (outboxMessage);
908
+ }
909
+
910
+ /// Reads [MessageStore.outboxMessages] and copies to [outboxMessages]
911
+ /// the ones belonging to this view.
912
+ ///
913
+ /// This should only be called when [haveNewest] is true
914
+ /// because outbox messages are considered newer than regular messages.
915
+ ///
916
+ /// This does not call [notifyListeners] .
917
+ void _syncOutboxMessagesFromStore () {
918
+ assert (haveNewest);
919
+ assert (outboxMessages.isEmpty);
920
+ for (final outboxMessage in store.outboxMessages.values) {
921
+ if (_shouldAddOutboxMessage (outboxMessage)) {
922
+ _addOutboxMessage (outboxMessage);
923
+ }
924
+ }
925
+ }
926
+
773
927
/// Add [outboxMessage] if it belongs to the view.
774
928
void addOutboxMessage (OutboxMessage outboxMessage) {
775
- // TODO(#1441) implement this
929
+ // We don't have the newest messages;
930
+ // we shouldn't show any outbox messages until we do.
931
+ if (! haveNewest) return ;
932
+
933
+ assert (outboxMessages.none (
934
+ (message) => message.localMessageId == outboxMessage.localMessageId));
935
+ if (_shouldAddOutboxMessage (outboxMessage)) {
936
+ _addOutboxMessage (outboxMessage);
937
+ notifyListeners ();
938
+ }
776
939
}
777
940
778
941
/// Remove the [outboxMessage] from the view.
@@ -781,7 +944,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
781
944
///
782
945
/// This should only be called from [MessageStore.takeOutboxMessage] .
783
946
void removeOutboxMessage (OutboxMessage outboxMessage) {
784
- // TODO(#1441) implement this
947
+ if (_removeOutboxMessage (outboxMessage)) {
948
+ notifyListeners ();
949
+ }
785
950
}
786
951
787
952
void handleUserTopicEvent (UserTopicEvent event) {
@@ -790,10 +955,17 @@ class MessageListView with ChangeNotifier, _MessageSequence {
790
955
return ;
791
956
792
957
case VisibilityEffect .muted:
793
- if (_removeMessagesWhere ((message) =>
794
- (message is StreamMessage
795
- && message.streamId == event.streamId
796
- && message.topic == event.topicName))) {
958
+ bool removed = _removeMessagesWhere ((message) =>
959
+ message is StreamMessage
960
+ && message.streamId == event.streamId
961
+ && message.topic == event.topicName);
962
+
963
+ removed | = _removeOutboxMessagesWhere ((message) =>
964
+ message is StreamOutboxMessage
965
+ && message.conversation.streamId == event.streamId
966
+ && message.conversation.topic == event.topicName);
967
+
968
+ if (removed) {
797
969
notifyListeners ();
798
970
}
799
971
@@ -819,6 +991,8 @@ class MessageListView with ChangeNotifier, _MessageSequence {
819
991
void handleMessageEvent (MessageEvent event) {
820
992
final message = event.message;
821
993
if (! narrow.containsMessage (message) || ! _messageVisible (message)) {
994
+ assert (event.localMessageId == null || outboxMessages.none ((message) =>
995
+ message.localMessageId == int .parse (event.localMessageId! , radix: 10 )));
822
996
return ;
823
997
}
824
998
if (! haveNewest) {
@@ -833,8 +1007,20 @@ class MessageListView with ChangeNotifier, _MessageSequence {
833
1007
// didn't include this message.
834
1008
return ;
835
1009
}
836
- // TODO insert in middle instead, when appropriate
1010
+
1011
+ // Remove the outbox messages temporarily.
1012
+ // We'll add them back after the new message.
1013
+ _removeOutboxMessageItems ();
1014
+ // TODO insert in middle of [messages] instead, when appropriate
837
1015
_addMessage (message);
1016
+ if (event.localMessageId != null ) {
1017
+ final localMessageId = int .parse (event.localMessageId! , radix: 10 );
1018
+ // [outboxMessages] is expected to be short, so removing the corresponding
1019
+ // outbox message and reprocessing them all in linear time is efficient.
1020
+ outboxMessages.removeWhere (
1021
+ (message) => message.localMessageId == localMessageId);
1022
+ }
1023
+ _reprocessOutboxMessages ();
838
1024
notifyListeners ();
839
1025
}
840
1026
@@ -955,7 +1141,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
955
1141
956
1142
/// Notify listeners if the given outbox message is present in this view.
957
1143
void notifyListenersIfOutboxMessagePresent (int localMessageId) {
958
- // TODO(#1441) implement this
1144
+ final isAnyPresent =
1145
+ outboxMessages.any ((message) => message.localMessageId == localMessageId);
1146
+ if (isAnyPresent) {
1147
+ notifyListeners ();
1148
+ }
959
1149
}
960
1150
961
1151
/// Called when the app is reassembled during debugging, e.g. for hot reload.
0 commit comments