@@ -63,6 +63,21 @@ class MessageListMessageItem extends MessageListMessageBaseItem {
63
63
});
64
64
}
65
65
66
+ class MessageListOutboxMessageItem extends MessageListMessageBaseItem {
67
+ @override
68
+ final OutboxMessage message;
69
+ @override
70
+ final ZulipContent content;
71
+
72
+ MessageListOutboxMessageItem (
73
+ this .message, {
74
+ required super .showSender,
75
+ required super .isLastInBlock,
76
+ }) : content = ZulipContent (nodes: [
77
+ ParagraphNode (links: [], nodes: [TextNode (message.content)]),
78
+ ]);
79
+ }
80
+
66
81
/// Indicates the app is loading more messages at the top.
67
82
// TODO(#80): or loading at the bottom, by adding a [MessageListDirection.newer]
68
83
class MessageListLoadingItem extends MessageListItem {
@@ -90,7 +105,16 @@ mixin _MessageSequence {
90
105
/// See also [contents] and [items] .
91
106
final List <Message > messages = [];
92
107
93
- /// Whether [messages] and [items] represent the results of a fetch.
108
+ /// The messages sent by the self-user.
109
+ ///
110
+ /// See also [items] .
111
+ ///
112
+ /// Usually this should not have that many items, so we do not anticipate
113
+ /// performance issues with unoptimized O(N) iterations through this list.
114
+ final List <OutboxMessage > outboxMessages = [];
115
+
116
+ /// Whether [messages] , [outboxMessages] , and [items] represent the results
117
+ /// of a fetch.
94
118
///
95
119
/// This allows the UI to distinguish "still working on fetching messages"
96
120
/// from "there are in fact no messages here".
@@ -142,11 +166,12 @@ mixin _MessageSequence {
142
166
/// The messages and their siblings in the UI, in order.
143
167
///
144
168
/// This has a [MessageListMessageItem] corresponding to each element
145
- /// of [messages] , in order. It may have additional items interspersed
146
- /// before, between, or after the messages.
169
+ /// of [messages] , followed by each element in [outboxMessages] in order.
170
+ /// It may have additional items interspersed before, between, or after the
171
+ /// messages.
147
172
///
148
- /// This information is completely derived from [messages] and
149
- /// the flags [haveOldest] , [fetchingOlder] and [fetchOlderCoolingDown] .
173
+ /// This information is completely derived from [messages] , [outboxMessages]
174
+ /// and the flags [haveOldest] , [fetchingOlder] and [fetchOlderCoolingDown] .
150
175
/// It exists as an optimization, to memoize that computation.
151
176
final QueueList <MessageListItem > items = QueueList ();
152
177
@@ -168,9 +193,10 @@ mixin _MessageSequence {
168
193
}
169
194
case MessageListRecipientHeaderItem (: var message):
170
195
case MessageListDateSeparatorItem (: var message):
171
- if (message.id == null ) return 1 ; // TODO(#1441): test
196
+ if (message.id == null ) return 1 ;
172
197
return message.id! <= messageId ? - 1 : 1 ;
173
198
case MessageListMessageItem (: var message): return message.id.compareTo (messageId);
199
+ case MessageListOutboxMessageItem (): return 1 ;
174
200
}
175
201
}
176
202
@@ -274,10 +300,46 @@ mixin _MessageSequence {
274
300
_reprocessAll ();
275
301
}
276
302
303
+ /// Append [outboxMessage] to [outboxMessages] , and update derived data
304
+ /// accordingly.
305
+ ///
306
+ /// The caller is responsible for ensuring this is an appropriate thing to do
307
+ /// given [narrow] , our state of being caught up, and other concerns.
308
+ void _addOutboxMessage (OutboxMessage outboxMessage) {
309
+ assert (! outboxMessages.contains (outboxMessage));
310
+ outboxMessages.add (outboxMessage);
311
+ _processOutboxMessage (outboxMessages.length - 1 );
312
+ }
313
+
314
+ /// Remove the [outboxMessage] from the view.
315
+ ///
316
+ /// Returns true if the outbox message was removed, false otherwise.
317
+ bool _removeOutboxMessage (OutboxMessage outboxMessage) {
318
+ if (! outboxMessages.remove (outboxMessage)) {
319
+ return false ;
320
+ }
321
+ _reprocessOutboxMessages ();
322
+ return true ;
323
+ }
324
+
325
+ /// Remove all outbox messages that satisfy [test] from [outboxMessages] .
326
+ ///
327
+ /// Returns true if any outbox messages were removed, false otherwise.
328
+ bool _removeOutboxMessagesWhere (bool Function (OutboxMessage ) test) {
329
+ final count = outboxMessages.length;
330
+ outboxMessages.removeWhere (test);
331
+ if (outboxMessages.length == count) {
332
+ return false ;
333
+ }
334
+ _reprocessOutboxMessages ();
335
+ return true ;
336
+ }
337
+
277
338
/// Reset all [_MessageSequence] data, and cancel any active fetches.
278
339
void _reset () {
279
340
generation += 1 ;
280
341
messages.clear ();
342
+ outboxMessages.clear ();
281
343
_fetched = false ;
282
344
_haveOldest = false ;
283
345
_fetchingOlder = false ;
@@ -301,7 +363,8 @@ mixin _MessageSequence {
301
363
///
302
364
/// Returns whether an item has been appended or not.
303
365
///
304
- /// The caller must append a [MessageListMessageBaseItem] after this.
366
+ /// The caller must append a [MessageListMessageBaseItem] for [message]
367
+ /// after this.
305
368
bool _maybeAppendAuxillaryItem (MessageBase message, {
306
369
required MessageBase ? prevMessage,
307
370
}) {
@@ -328,6 +391,7 @@ mixin _MessageSequence {
328
391
/// The previous messages in the list must already have been processed.
329
392
/// This message must already have been parsed and reflected in [contents] .
330
393
void _processMessage (int index) {
394
+ assert (items.lastOrNull is ! MessageListOutboxMessageItem );
331
395
final prevMessage = index == 0 ? null : messages[index - 1 ];
332
396
final message = messages[index];
333
397
final content = contents[index];
@@ -338,6 +402,20 @@ mixin _MessageSequence {
338
402
isLastInBlock: true ));
339
403
}
340
404
405
+ /// Append to [items] based on the index-th outbox message.
406
+ ///
407
+ /// All [messages] and previous messages in [outboxMessages] must already have
408
+ /// been processed.
409
+ void _processOutboxMessage (int index) {
410
+ final prevMessage = index == 0 ? messages.lastOrNull : outboxMessages[index - 1 ];
411
+ final message = outboxMessages[index];
412
+
413
+ final appended = _maybeAppendAuxillaryItem (message, prevMessage: prevMessage);
414
+ items.add (MessageListOutboxMessageItem (message,
415
+ showSender: appended || prevMessage? .senderId != message.senderId,
416
+ isLastInBlock: true ));
417
+ }
418
+
341
419
/// Update [items] to include markers at start and end as appropriate.
342
420
void _updateEndMarkers () {
343
421
assert (fetched);
@@ -362,12 +440,55 @@ mixin _MessageSequence {
362
440
}
363
441
}
364
442
365
- /// Recompute [items] from scratch, based on [messages] , [contents] , and flags.
443
+ /// Remove items associated with [outboxMessages] from [items] .
444
+ ///
445
+ /// This is designed to be idempotent; repeated calls will not change the
446
+ /// content of [items] .
447
+ ///
448
+ /// This is efficient due to the expected small size of [outboxMessages] .
449
+ void _removeOutboxMessageItems () {
450
+ // This loop relies on the assumption that all items that follow
451
+ // the last [MessageListMessageItem] are derived from outbox messages.
452
+ // If there is no [MessageListMessageItem] at all,
453
+ // this will end up removing end markers.
454
+ while (items.isNotEmpty && items.last is ! MessageListMessageItem ) {
455
+ items.removeLast ();
456
+ }
457
+ assert (items.none ((e) => e is MessageListOutboxMessageItem ));
458
+
459
+ if (items.isNotEmpty) {
460
+ final lastItem = items.last as MessageListMessageItem ;
461
+ lastItem.isLastInBlock = true ;
462
+ }
463
+
464
+ if (fetched) {
465
+ // Restore the end markers in case they were removed; only do so when
466
+ // [fetched] is true, since the markers are not there otherwise.
467
+ _updateEndMarkers ();
468
+ }
469
+ }
470
+
471
+ /// Recompute the portion of [items] derived from outbox messages,
472
+ /// based on [outboxMessages] and [messages] .
473
+ ///
474
+ /// All [messages] should have been processed when this is called.
475
+ void _reprocessOutboxMessages () {
476
+ _removeOutboxMessageItems ();
477
+ for (var i = 0 ; i < outboxMessages.length; i++ ) {
478
+ _processOutboxMessage (i);
479
+ }
480
+ }
481
+
482
+ /// Recompute [items] from scratch, based on [messages] , [contents] ,
483
+ /// [outboxMessages] and flags.
366
484
void _reprocessAll () {
367
485
items.clear ();
368
486
for (var i = 0 ; i < messages.length; i++ ) {
369
487
_processMessage (i);
370
488
}
489
+ for (var i = 0 ; i < outboxMessages.length; i++ ) {
490
+ _processOutboxMessage (i);
491
+ }
371
492
_updateEndMarkers ();
372
493
}
373
494
}
@@ -412,7 +533,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
412
533
413
534
factory MessageListView .init (
414
535
{required PerAccountStore store, required Narrow narrow}) {
415
- final view = MessageListView ._(store: store, narrow: narrow);
536
+ final view = MessageListView ._(store: store, narrow: narrow)
537
+ .._syncOutboxMessages ()
538
+ .._reprocessOutboxMessages ();
416
539
store.registerMessageList (view);
417
540
return view;
418
541
}
@@ -510,11 +633,13 @@ class MessageListView with ChangeNotifier, _MessageSequence {
510
633
_adjustNarrowForTopicPermalink (result.messages.firstOrNull);
511
634
store.reconcileMessages (result.messages);
512
635
store.recentSenders.handleMessages (result.messages); // TODO(#824)
636
+ _removeOutboxMessageItems ();
513
637
for (final message in result.messages) {
514
638
if (_messageVisible (message)) {
515
639
_addMessage (message);
516
640
}
517
641
}
642
+ _reprocessOutboxMessages ();
518
643
_fetched = true ;
519
644
_haveOldest = result.foundOldest;
520
645
_updateEndMarkers ();
@@ -621,16 +746,51 @@ class MessageListView with ChangeNotifier, _MessageSequence {
621
746
}
622
747
}
623
748
749
+ bool _shouldAddOutboxMessage (OutboxMessage outboxMessage, {
750
+ bool wasUnmuted = false ,
751
+ }) {
752
+ return ! outboxMessage.hidden
753
+ && narrow.containsMessage (outboxMessage)
754
+ && (_messageVisible (outboxMessage) || wasUnmuted);
755
+ }
756
+
757
+ /// Copy outbox messages from the store, keeping the ones belong to the view.
758
+ ///
759
+ /// This does not recompute [items] . The caller is expected to call
760
+ /// [_reprocessOutboxMessages] later to keep [items] up-to-date.
761
+ ///
762
+ /// This assumes that [outboxMessages] is empty.
763
+ void _syncOutboxMessages () {
764
+ assert (outboxMessages.isEmpty);
765
+ for (final outboxMessage in store.outboxMessages.values) {
766
+ if (_shouldAddOutboxMessage (outboxMessage)) {
767
+ outboxMessages.add (outboxMessage);
768
+ }
769
+ }
770
+ }
771
+
624
772
/// Add [outboxMessage] if it belongs to the view.
625
773
void addOutboxMessage (OutboxMessage outboxMessage) {
626
- // TODO(#1441) implement this
774
+ assert (outboxMessages.none (
775
+ (message) => message.localMessageId == outboxMessage.localMessageId));
776
+ if (_shouldAddOutboxMessage (outboxMessage)) {
777
+ _addOutboxMessage (outboxMessage);
778
+ if (fetched) {
779
+ // Only need to notify listeners when [fetched] is true, because
780
+ // otherwise the message list just shows a loading indicator with
781
+ // no other items.
782
+ notifyListeners ();
783
+ }
784
+ }
627
785
}
628
786
629
787
/// Remove the [outboxMessage] from the view.
630
788
///
631
789
/// This is a no-op if the message is not found.
632
790
void removeOutboxMessage (OutboxMessage outboxMessage) {
633
- // TODO(#1441) implement this
791
+ if (_removeOutboxMessage (outboxMessage)) {
792
+ notifyListeners ();
793
+ }
634
794
}
635
795
636
796
void handleUserTopicEvent (UserTopicEvent event) {
@@ -639,10 +799,17 @@ class MessageListView with ChangeNotifier, _MessageSequence {
639
799
return ;
640
800
641
801
case VisibilityEffect .muted:
642
- if (_removeMessagesWhere ((message) =>
643
- (message is StreamMessage
644
- && message.streamId == event.streamId
645
- && message.topic == event.topicName))) {
802
+ bool removed = _removeOutboxMessagesWhere ((message) =>
803
+ message is StreamOutboxMessage
804
+ && message.conversation.streamId == event.streamId
805
+ && message.conversation.topic == event.topicName);
806
+
807
+ removed | = _removeMessagesWhere ((message) =>
808
+ message is StreamMessage
809
+ && message.streamId == event.streamId
810
+ && message.topic == event.topicName);
811
+
812
+ if (removed) {
646
813
notifyListeners ();
647
814
}
648
815
@@ -655,6 +822,18 @@ class MessageListView with ChangeNotifier, _MessageSequence {
655
822
notifyListeners ();
656
823
fetchInitial ();
657
824
}
825
+
826
+ outboxMessages.clear ();
827
+ for (final outboxMessage in store.outboxMessages.values) {
828
+ if (_shouldAddOutboxMessage (
829
+ outboxMessage,
830
+ wasUnmuted: outboxMessage is StreamOutboxMessage
831
+ && outboxMessage.conversation.streamId == event.streamId
832
+ && outboxMessage.conversation.topic == event.topicName,
833
+ )) {
834
+ outboxMessages.add (outboxMessage);
835
+ }
836
+ }
658
837
}
659
838
}
660
839
@@ -668,14 +847,27 @@ class MessageListView with ChangeNotifier, _MessageSequence {
668
847
void handleMessageEvent (MessageEvent event) {
669
848
final message = event.message;
670
849
if (! narrow.containsMessage (message) || ! _messageVisible (message)) {
850
+ assert (event.localMessageId == null || outboxMessages.none ((message) =>
851
+ message.localMessageId == int .parse (event.localMessageId! , radix: 10 )));
671
852
return ;
672
853
}
673
854
if (! _fetched) {
674
855
// TODO mitigate this fetch/event race: save message to add to list later
675
856
return ;
676
857
}
858
+ // We always remove all outbox message items
859
+ // to ensure that message items come before them.
860
+ _removeOutboxMessageItems ();
677
861
// TODO insert in middle instead, when appropriate
678
862
_addMessage (message);
863
+ if (event.localMessageId != null ) {
864
+ final localMessageId = int .parse (event.localMessageId! );
865
+ // [outboxMessages] is epxected to be short, so removing the corresponding
866
+ // outbox message and reprocessing them all in linear time is efficient.
867
+ outboxMessages.removeWhere (
868
+ (message) => message.localMessageId == localMessageId);
869
+ }
870
+ _reprocessOutboxMessages ();
679
871
notifyListeners ();
680
872
}
681
873
@@ -707,6 +899,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
707
899
// TODO in cases where we do have data to do better, do better.
708
900
_reset ();
709
901
notifyListeners ();
902
+ _syncOutboxMessages ();
710
903
fetchInitial ();
711
904
}
712
905
@@ -722,6 +915,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
722
915
case PropagateMode .changeLater:
723
916
narrow = newNarrow;
724
917
_reset ();
918
+ _syncOutboxMessages ();
725
919
fetchInitial ();
726
920
case PropagateMode .changeOne:
727
921
}
@@ -796,7 +990,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
796
990
797
991
/// Notify listeners if the given outbox message is present in this view.
798
992
void notifyListenersIfOutboxMessagePresent (int localMessageId) {
799
- // TODO(#1441) implement this
993
+ final isAnyPresent =
994
+ outboxMessages.any ((message) => message.localMessageId == localMessageId);
995
+ if (isAnyPresent) {
996
+ notifyListeners ();
997
+ }
800
998
}
801
999
802
1000
/// Called when the app is reassembled during debugging, e.g. for hot reload.
0 commit comments