@@ -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
@@ -261,6 +287,23 @@ mixin _MessageSequence {
261
287
return true ;
262
288
}
263
289
290
+ /// Remove all outbox messages that satisfy [test] .
291
+ ///
292
+ /// Returns true if any outbox messages were removed, false otherwise.
293
+ bool _removeOutboxMessagesWhere (bool Function (OutboxMessage ) test) {
294
+ final count = outboxMessages.length;
295
+ outboxMessages.removeWhere (test);
296
+ if (outboxMessages.length == count) {
297
+ return false ;
298
+ }
299
+ _removeOutboxMessageItems ();
300
+ for (int i = 0 ; i < outboxMessages.length; i++ ) {
301
+ _processOutboxMessage (i);
302
+ }
303
+ _updateEndMarkers ();
304
+ return true ;
305
+ }
306
+
264
307
void _insertAllMessages (int index, Iterable <Message > toInsert) {
265
308
// TODO parse/process messages in smaller batches, to not drop frames.
266
309
// On a Pixel 5, a batch of 100 messages takes ~15-20ms in _insertAllMessages.
@@ -278,6 +321,7 @@ mixin _MessageSequence {
278
321
void _reset () {
279
322
generation += 1 ;
280
323
messages.clear ();
324
+ outboxMessages.clear ();
281
325
_fetched = false ;
282
326
_haveOldest = false ;
283
327
_fetchingOlder = false ;
@@ -301,7 +345,8 @@ mixin _MessageSequence {
301
345
///
302
346
/// Returns whether an item has been appended or not.
303
347
///
304
- /// The caller must append a [MessageListMessageBaseItem] after this.
348
+ /// The caller must append a [MessageListMessageBaseItem] for [message]
349
+ /// after this.
305
350
bool _maybeAppendAuxillaryItem (MessageBase message, {
306
351
required MessageBase ? prevMessage,
307
352
}) {
@@ -338,6 +383,40 @@ mixin _MessageSequence {
338
383
isLastInBlock: true ));
339
384
}
340
385
386
+ /// Append to [items] based on the index-th outbox message.
387
+ ///
388
+ /// All [messages] and previous messages in [outboxMessages] must already have
389
+ /// been processed.
390
+ void _processOutboxMessage (int index) {
391
+ final prevMessage = index == 0 ? messages.lastOrNull : outboxMessages[index - 1 ];
392
+ final message = outboxMessages[index];
393
+
394
+ final appended = _maybeAppendAuxillaryItem (message, prevMessage: prevMessage);
395
+ items.add (MessageListOutboxMessageItem (message,
396
+ showSender: appended || prevMessage? .senderId != message.senderId,
397
+ isLastInBlock: true ));
398
+ }
399
+
400
+ /// Remove items associated with [outboxMessages] from [items] .
401
+ ///
402
+ /// This is efficient due to the expected small size of [outboxMessages] .
403
+ void _removeOutboxMessageItems () {
404
+ // This loop relies on the assumption that all [MessageListMessageItem]
405
+ // items comes before those associated with outbox messages. If there
406
+ // is no [MessageListMessageItem] at all, this will end up removing
407
+ // end markers as well.
408
+ while (items.isNotEmpty && items.last is ! MessageListMessageItem ) {
409
+ items.removeLast ();
410
+ }
411
+ assert (items.none ((e) => e is MessageListOutboxMessageItem ));
412
+
413
+ if (items.isNotEmpty) {
414
+ final lastItem = items.last as MessageListMessageItem ;
415
+ lastItem.isLastInBlock = true ;
416
+ }
417
+ _updateEndMarkers ();
418
+ }
419
+
341
420
/// Update [items] to include markers at start and end as appropriate.
342
421
void _updateEndMarkers () {
343
422
assert (fetched);
@@ -362,12 +441,16 @@ mixin _MessageSequence {
362
441
}
363
442
}
364
443
365
- /// Recompute [items] from scratch, based on [messages] , [contents] , and flags.
444
+ /// Recompute [items] from scratch, based on [messages] , [contents] ,
445
+ /// [outboxMessages] and flags.
366
446
void _reprocessAll () {
367
447
items.clear ();
368
448
for (var i = 0 ; i < messages.length; i++ ) {
369
449
_processMessage (i);
370
450
}
451
+ for (var i = 0 ; i < outboxMessages.length; i++ ) {
452
+ _processOutboxMessage (i);
453
+ }
371
454
_updateEndMarkers ();
372
455
}
373
456
}
@@ -497,7 +580,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
497
580
// TODO(#80): fetch from anchor firstUnread, instead of newest
498
581
// TODO(#82): fetch from a given message ID as anchor
499
582
assert (! fetched && ! haveOldest && ! fetchingOlder && ! fetchOlderCoolingDown);
500
- assert (messages.isEmpty && contents.isEmpty);
583
+ assert (messages.isEmpty && contents.isEmpty && outboxMessages.isEmpty );
501
584
// TODO schedule all this in another isolate
502
585
final generation = this .generation;
503
586
final result = await getMessages (store.connection,
@@ -515,6 +598,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
515
598
_addMessage (message);
516
599
}
517
600
}
601
+ for (final outboxMessage in store.outboxMessages.values) {
602
+ _maybeAddOutboxMessage (outboxMessage);
603
+ }
518
604
_fetched = true ;
519
605
_haveOldest = result.foundOldest;
520
606
_updateEndMarkers ();
@@ -621,15 +707,43 @@ class MessageListView with ChangeNotifier, _MessageSequence {
621
707
}
622
708
}
623
709
710
+ /// Add [outboxMessage] if it belongs to the view.
711
+ ///
712
+ /// Returns true if the message was added, false otherwise.
713
+ bool _maybeAddOutboxMessage (OutboxMessage outboxMessage) {
714
+ assert (outboxMessages.none (
715
+ (message) => message.localMessageId == outboxMessage.localMessageId));
716
+ if (! outboxMessage.hidden
717
+ && narrow.containsMessage (outboxMessage)
718
+ && _messageVisible (outboxMessage)) {
719
+ outboxMessages.add (outboxMessage);
720
+ _processOutboxMessage (outboxMessages.length - 1 );
721
+ return true ;
722
+ }
723
+ return false ;
724
+ }
725
+
624
726
void addOutboxMessage (OutboxMessage outboxMessage) {
625
- // TODO(#1441) implement this
727
+ if (! fetched) return ;
728
+ if (_maybeAddOutboxMessage (outboxMessage)) {
729
+ notifyListeners ();
730
+ }
626
731
}
627
732
628
733
/// Remove the [outboxMessage] from the view.
629
734
///
630
735
/// This is a no-op if the message is not found.
631
736
void removeOutboxMessage (OutboxMessage outboxMessage) {
632
- // TODO(#1441) implement this
737
+ final removed = outboxMessages.remove (outboxMessage);
738
+ if (! removed) {
739
+ return ;
740
+ }
741
+
742
+ _removeOutboxMessageItems ();
743
+ for (int i = 0 ; i < outboxMessages.length; i++ ) {
744
+ _processOutboxMessage (i);
745
+ }
746
+ notifyListeners ();
633
747
}
634
748
635
749
void handleUserTopicEvent (UserTopicEvent event) {
@@ -638,10 +752,17 @@ class MessageListView with ChangeNotifier, _MessageSequence {
638
752
return ;
639
753
640
754
case VisibilityEffect .muted:
641
- if (_removeMessagesWhere ((message) =>
642
- (message is StreamMessage
643
- && message.streamId == event.streamId
644
- && message.topic == event.topicName))) {
755
+ bool removed = _removeOutboxMessagesWhere ((message) =>
756
+ message is StreamOutboxMessage
757
+ && message.conversation.streamId == event.streamId
758
+ && message.conversation.topic == event.topicName);
759
+
760
+ removed | = _removeMessagesWhere ((message) =>
761
+ message is StreamMessage
762
+ && message.streamId == event.streamId
763
+ && message.topic == event.topicName);
764
+
765
+ if (removed) {
645
766
notifyListeners ();
646
767
}
647
768
@@ -667,14 +788,29 @@ class MessageListView with ChangeNotifier, _MessageSequence {
667
788
void handleMessageEvent (MessageEvent event) {
668
789
final message = event.message;
669
790
if (! narrow.containsMessage (message) || ! _messageVisible (message)) {
791
+ assert (event.localMessageId == null || outboxMessages.none ((message) =>
792
+ message.localMessageId == int .parse (event.localMessageId! , radix: 10 )));
670
793
return ;
671
794
}
672
795
if (! _fetched) {
673
796
// TODO mitigate this fetch/event race: save message to add to list later
674
797
return ;
675
798
}
799
+ // We always remove all outbox message items
800
+ // to ensure that message items come before them.
801
+ _removeOutboxMessageItems ();
676
802
// TODO insert in middle instead, when appropriate
677
803
_addMessage (message);
804
+ if (event.localMessageId != null ) {
805
+ final localMessageId = int .parse (event.localMessageId! );
806
+ // [outboxMessages] is epxected to be short, so removing the corresponding
807
+ // outbox message and reprocessing them all in linear time is efficient.
808
+ outboxMessages.removeWhere (
809
+ (message) => message.localMessageId == localMessageId);
810
+ }
811
+ for (int i = 0 ; i < outboxMessages.length; i++ ) {
812
+ _processOutboxMessage (i);
813
+ }
678
814
notifyListeners ();
679
815
}
680
816
@@ -795,7 +931,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
795
931
796
932
/// Notify listeners if the given outbox message is present in this view.
797
933
void notifyListenersIfOutboxMessagePresent (int localMessageId) {
798
- // TODO(#1441) implement this
934
+ final isAnyPresent =
935
+ outboxMessages.any ((message) => message.localMessageId == localMessageId);
936
+ if (isAnyPresent) {
937
+ notifyListeners ();
938
+ }
799
939
}
800
940
801
941
/// Called when the app is reassembled during debugging, e.g. for hot reload.
0 commit comments