@@ -64,6 +64,22 @@ class MessageListMessageItem extends MessageListMessageBaseItem {
64
64
});
65
65
}
66
66
67
+ /// An [OutboxMessage] to show in the message list.
68
+ class MessageListOutboxMessageItem extends MessageListMessageBaseItem {
69
+ @override
70
+ final OutboxMessage message;
71
+ @override
72
+ final ZulipContent content;
73
+
74
+ MessageListOutboxMessageItem (
75
+ this .message, {
76
+ required super .showSender,
77
+ required super .isLastInBlock,
78
+ }) : content = ZulipContent (nodes: [
79
+ ParagraphNode (links: [], nodes: [TextNode (message.content)]),
80
+ ]);
81
+ }
82
+
67
83
/// The sequence of messages in a message list, and how to display them.
68
84
///
69
85
/// This comprises much of the guts of [MessageListView] .
@@ -125,14 +141,25 @@ mixin _MessageSequence {
125
141
/// It exists as an optimization, to memoize the work of parsing.
126
142
final List <ZulipMessageContent > contents = [];
127
143
144
+ /// The messages sent by the self-user, retrieved from
145
+ /// [MessageStore.outboxMessages] .
146
+ ///
147
+ /// See also [items] .
148
+ ///
149
+ /// Usually this should not have that many items, so we do not anticipate
150
+ /// performance issues with unoptimized O(N) iterations through this list.
151
+ final List <OutboxMessage > outboxMessages = [];
152
+
128
153
/// The messages and their siblings in the UI, in order.
129
154
///
130
155
/// This has a [MessageListMessageItem] corresponding to each element
131
- /// of [messages] , in order. It may have additional items interspersed
132
- /// before, between, or after the messages.
156
+ /// of [messages] , then a [MessageListOutboxMessageItem] corresponding to each
157
+ /// element of [outboxMessages] , in order.
158
+ /// It may have additional items interspersed before, between, or after the
159
+ /// messages.
133
160
///
134
- /// This information is completely derived from [messages] and
135
- /// the flags [haveOldest] , [fetchingOlder] and [fetchOlderCoolingDown] .
161
+ /// This information is completely derived from [messages] , [outboxMessages]
162
+ /// and the flags [haveOldest] , [fetchingOlder] and [fetchOlderCoolingDown] .
136
163
/// It exists as an optimization, to memoize that computation.
137
164
final QueueList <MessageListItem > items = QueueList ();
138
165
@@ -149,9 +176,10 @@ mixin _MessageSequence {
149
176
switch (item) {
150
177
case MessageListRecipientHeaderItem (: var message):
151
178
case MessageListDateSeparatorItem (: var message):
152
- if (message.id == null ) return 1 ; // TODO(#1441): test
179
+ if (message.id == null ) return 1 ;
153
180
return message.id! <= messageId ? - 1 : 1 ;
154
181
case MessageListMessageItem (: var message): return message.id.compareTo (messageId);
182
+ case MessageListOutboxMessageItem (): return 1 ;
155
183
}
156
184
}
157
185
@@ -255,10 +283,46 @@ mixin _MessageSequence {
255
283
_reprocessAll ();
256
284
}
257
285
286
+ /// Append [outboxMessage] to [outboxMessages] , and update derived data
287
+ /// accordingly.
288
+ ///
289
+ /// The caller is responsible for ensuring this is an appropriate thing to do
290
+ /// given [narrow] and other concerns.
291
+ void _addOutboxMessage (OutboxMessage outboxMessage) {
292
+ assert (! outboxMessages.contains (outboxMessage));
293
+ outboxMessages.add (outboxMessage);
294
+ _processOutboxMessage (outboxMessages.length - 1 );
295
+ }
296
+
297
+ /// Remove the [outboxMessage] from the view.
298
+ ///
299
+ /// Returns true if the outbox message was removed, false otherwise.
300
+ bool _removeOutboxMessage (OutboxMessage outboxMessage) {
301
+ if (! outboxMessages.remove (outboxMessage)) {
302
+ return false ;
303
+ }
304
+ _reprocessOutboxMessages ();
305
+ return true ;
306
+ }
307
+
308
+ /// Remove all outbox messages that satisfy [test] from [outboxMessages] .
309
+ ///
310
+ /// Returns true if any outbox messages were removed, false otherwise.
311
+ bool _removeOutboxMessagesWhere (bool Function (OutboxMessage ) test) {
312
+ final count = outboxMessages.length;
313
+ outboxMessages.removeWhere (test);
314
+ if (outboxMessages.length == count) {
315
+ return false ;
316
+ }
317
+ _reprocessOutboxMessages ();
318
+ return true ;
319
+ }
320
+
258
321
/// Reset all [_MessageSequence] data, and cancel any active fetches.
259
322
void _reset () {
260
323
generation += 1 ;
261
324
messages.clear ();
325
+ outboxMessages.clear ();
262
326
_fetched = false ;
263
327
_haveOldest = false ;
264
328
_fetchingOlder = false ;
@@ -321,6 +385,7 @@ mixin _MessageSequence {
321
385
/// The previous messages in the list must already have been processed.
322
386
/// This message must already have been parsed and reflected in [contents] .
323
387
void _processMessage (int index) {
388
+ assert (items.lastOrNull is ! MessageListOutboxMessageItem );
324
389
final prevMessage = index == 0 ? null : messages[index - 1 ];
325
390
final message = messages[index];
326
391
final content = contents[index];
@@ -331,12 +396,64 @@ mixin _MessageSequence {
331
396
message, content, showSender: ! canShareSender, isLastInBlock: true ));
332
397
}
333
398
334
- /// Recompute [items] from scratch, based on [messages] , [contents] , and flags.
399
+ /// Append to [items] based on the index-th outbox message.
400
+ ///
401
+ /// All [messages] and previous messages in [outboxMessages] must already have
402
+ /// been processed.
403
+ void _processOutboxMessage (int index) {
404
+ final prevMessage = index == 0 ? messages.lastOrNull
405
+ : outboxMessages[index - 1 ];
406
+ final message = outboxMessages[index];
407
+
408
+ _addItemsForMessage (message,
409
+ prevMessage: prevMessage,
410
+ buildItem: (bool canShareSender) => MessageListOutboxMessageItem (
411
+ message, showSender: ! canShareSender, isLastInBlock: true ));
412
+ }
413
+
414
+ /// Remove items associated with [outboxMessages] from [items] .
415
+ ///
416
+ /// This is designed to be idempotent; repeated calls will not change the
417
+ /// content of [items] .
418
+ ///
419
+ /// This is efficient due to the expected small size of [outboxMessages] .
420
+ void _removeOutboxMessageItems () {
421
+ // This loop relies on the assumption that all items that follow
422
+ // the last [MessageListMessageItem] are derived from outbox messages.
423
+ // If there is no [MessageListMessageItem] at all,
424
+ // this will end up removing end markers.
425
+ while (items.isNotEmpty && items.last is ! MessageListMessageItem ) {
426
+ items.removeLast ();
427
+ }
428
+ assert (items.none ((e) => e is MessageListOutboxMessageItem ));
429
+
430
+ if (items.isNotEmpty) {
431
+ final lastItem = items.last as MessageListMessageItem ;
432
+ lastItem.isLastInBlock = true ;
433
+ }
434
+ }
435
+
436
+ /// Recompute the portion of [items] derived from outbox messages,
437
+ /// based on [outboxMessages] and [messages] .
438
+ ///
439
+ /// All [messages] should have been processed when this is called.
440
+ void _reprocessOutboxMessages () {
441
+ _removeOutboxMessageItems ();
442
+ for (var i = 0 ; i < outboxMessages.length; i++ ) {
443
+ _processOutboxMessage (i);
444
+ }
445
+ }
446
+
447
+ /// Recompute [items] from scratch, based on [messages] , [contents] ,
448
+ /// [outboxMessages] and flags.
335
449
void _reprocessAll () {
336
450
items.clear ();
337
451
for (var i = 0 ; i < messages.length; i++ ) {
338
452
_processMessage (i);
339
453
}
454
+ for (var i = 0 ; i < outboxMessages.length; i++ ) {
455
+ _processOutboxMessage (i);
456
+ }
340
457
}
341
458
}
342
459
@@ -380,7 +497,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
380
497
381
498
factory MessageListView .init (
382
499
{required PerAccountStore store, required Narrow narrow}) {
383
- final view = MessageListView ._(store: store, narrow: narrow);
500
+ final view = MessageListView ._(store: store, narrow: narrow)
501
+ .._syncOutboxMessages ()
502
+ .._reprocessOutboxMessages ();
384
503
store.registerMessageList (view);
385
504
return view;
386
505
}
@@ -479,11 +598,13 @@ class MessageListView with ChangeNotifier, _MessageSequence {
479
598
_adjustNarrowForTopicPermalink (result.messages.firstOrNull);
480
599
store.reconcileMessages (result.messages);
481
600
store.recentSenders.handleMessages (result.messages); // TODO(#824)
601
+ _removeOutboxMessageItems ();
482
602
for (final message in result.messages) {
483
603
if (_messageVisible (message)) {
484
604
_addMessage (message);
485
605
}
486
606
}
607
+ _reprocessOutboxMessages ();
487
608
_fetched = true ;
488
609
_haveOldest = result.foundOldest;
489
610
notifyListeners ();
@@ -587,9 +708,42 @@ class MessageListView with ChangeNotifier, _MessageSequence {
587
708
}
588
709
}
589
710
711
+ bool _shouldAddOutboxMessage (OutboxMessage outboxMessage, {
712
+ bool wasUnmuted = false ,
713
+ }) {
714
+ return ! outboxMessage.hidden
715
+ && narrow.containsMessage (outboxMessage)
716
+ && (_messageVisible (outboxMessage) || wasUnmuted);
717
+ }
718
+
719
+ /// Copy outbox messages from the store, keeping the ones belong to the view.
720
+ ///
721
+ /// This does not recompute [items] . The caller is expected to call
722
+ /// [_reprocessOutboxMessages] later to keep [items] up-to-date.
723
+ ///
724
+ /// This assumes that [outboxMessages] is empty.
725
+ void _syncOutboxMessages () {
726
+ assert (outboxMessages.isEmpty);
727
+ for (final outboxMessage in store.outboxMessages.values) {
728
+ if (_shouldAddOutboxMessage (outboxMessage)) {
729
+ outboxMessages.add (outboxMessage);
730
+ }
731
+ }
732
+ }
733
+
590
734
/// Add [outboxMessage] if it belongs to the view.
591
735
void addOutboxMessage (OutboxMessage outboxMessage) {
592
- // TODO(#1441) implement this
736
+ assert (outboxMessages.none (
737
+ (message) => message.localMessageId == outboxMessage.localMessageId));
738
+ if (_shouldAddOutboxMessage (outboxMessage)) {
739
+ _addOutboxMessage (outboxMessage);
740
+ if (fetched) {
741
+ // Only need to notify listeners when [fetched] is true, because
742
+ // otherwise the message list just shows a loading indicator with
743
+ // no other items.
744
+ notifyListeners ();
745
+ }
746
+ }
593
747
}
594
748
595
749
/// Remove the [outboxMessage] from the view.
@@ -598,7 +752,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
598
752
///
599
753
/// This should only be called from [MessageStore.takeOutboxMessage] .
600
754
void removeOutboxMessage (OutboxMessage outboxMessage) {
601
- // TODO(#1441) implement this
755
+ if (_removeOutboxMessage (outboxMessage)) {
756
+ notifyListeners ();
757
+ }
602
758
}
603
759
604
760
void handleUserTopicEvent (UserTopicEvent event) {
@@ -607,10 +763,17 @@ class MessageListView with ChangeNotifier, _MessageSequence {
607
763
return ;
608
764
609
765
case VisibilityEffect .muted:
610
- if (_removeMessagesWhere ((message) =>
611
- (message is StreamMessage
612
- && message.streamId == event.streamId
613
- && message.topic == event.topicName))) {
766
+ bool removed = _removeOutboxMessagesWhere ((message) =>
767
+ message is StreamOutboxMessage
768
+ && message.conversation.streamId == event.streamId
769
+ && message.conversation.topic == event.topicName);
770
+
771
+ removed | = _removeMessagesWhere ((message) =>
772
+ message is StreamMessage
773
+ && message.streamId == event.streamId
774
+ && message.topic == event.topicName);
775
+
776
+ if (removed) {
614
777
notifyListeners ();
615
778
}
616
779
@@ -623,6 +786,18 @@ class MessageListView with ChangeNotifier, _MessageSequence {
623
786
notifyListeners ();
624
787
fetchInitial ();
625
788
}
789
+
790
+ outboxMessages.clear ();
791
+ for (final outboxMessage in store.outboxMessages.values) {
792
+ if (_shouldAddOutboxMessage (
793
+ outboxMessage,
794
+ wasUnmuted: outboxMessage is StreamOutboxMessage
795
+ && outboxMessage.conversation.streamId == event.streamId
796
+ && outboxMessage.conversation.topic == event.topicName,
797
+ )) {
798
+ outboxMessages.add (outboxMessage);
799
+ }
800
+ }
626
801
}
627
802
}
628
803
@@ -636,14 +811,34 @@ class MessageListView with ChangeNotifier, _MessageSequence {
636
811
void handleMessageEvent (MessageEvent event) {
637
812
final message = event.message;
638
813
if (! narrow.containsMessage (message) || ! _messageVisible (message)) {
814
+ assert (event.localMessageId == null || outboxMessages.none ((message) =>
815
+ message.localMessageId == int .parse (event.localMessageId! , radix: 10 )));
639
816
return ;
640
817
}
641
818
if (! _fetched) {
642
819
// TODO mitigate this fetch/event race: save message to add to list later
643
820
return ;
644
821
}
822
+ if (outboxMessages.isEmpty) {
823
+ assert (items.none ((item) => item is MessageListOutboxMessageItem ));
824
+ _addMessage (message);
825
+ notifyListeners ();
826
+ return ;
827
+ }
828
+
829
+ // We always remove all outbox message items
830
+ // to ensure that message items come before them.
831
+ _removeOutboxMessageItems ();
645
832
// TODO insert in middle instead, when appropriate
646
833
_addMessage (message);
834
+ if (event.localMessageId != null ) {
835
+ final localMessageId = int .parse (event.localMessageId! );
836
+ // [outboxMessages] is epxected to be short, so removing the corresponding
837
+ // outbox message and reprocessing them all in linear time is efficient.
838
+ outboxMessages.removeWhere (
839
+ (message) => message.localMessageId == localMessageId);
840
+ }
841
+ _reprocessOutboxMessages ();
647
842
notifyListeners ();
648
843
}
649
844
@@ -675,6 +870,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
675
870
// TODO in cases where we do have data to do better, do better.
676
871
_reset ();
677
872
notifyListeners ();
873
+ _syncOutboxMessages ();
678
874
fetchInitial ();
679
875
}
680
876
@@ -690,6 +886,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
690
886
case PropagateMode .changeLater:
691
887
narrow = newNarrow;
692
888
_reset ();
889
+ _syncOutboxMessages ();
693
890
fetchInitial ();
694
891
case PropagateMode .changeOne:
695
892
}
@@ -764,7 +961,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
764
961
765
962
/// Notify listeners if the given outbox message is present in this view.
766
963
void notifyListenersIfOutboxMessagePresent (int localMessageId) {
767
- // TODO(#1441) implement this
964
+ final isAnyPresent =
965
+ outboxMessages.any ((message) => message.localMessageId == localMessageId);
966
+ if (isAnyPresent) {
967
+ notifyListeners ();
968
+ }
768
969
}
769
970
770
971
/// Called when the app is reassembled during debugging, e.g. for hot reload.
0 commit comments