@@ -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,15 @@ 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
+ // Usually this should not have that many items, so we do not anticipate
112
+ // performance issues with unoptimized O(N) iterations through this list.
113
+ final List <OutboxMessage > outboxMessages = [];
114
+
115
+ /// Whether [messages] , [outboxMessages] , and [items] represent the results
116
+ /// of a fetch.
94
117
///
95
118
/// This allows the UI to distinguish "still working on fetching messages"
96
119
/// from "there are in fact no messages here".
@@ -142,11 +165,12 @@ mixin _MessageSequence {
142
165
/// The messages and their siblings in the UI, in order.
143
166
///
144
167
/// 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.
168
+ /// of [messages] , followed by each element in [outboxMessages] in order.
169
+ /// It may have additional items interspersed before, between, or after the
170
+ /// messages.
147
171
///
148
- /// This information is completely derived from [messages] and
149
- /// the flags [haveOldest] , [fetchingOlder] and [fetchOlderCoolingDown] .
172
+ /// This information is completely derived from [messages] , [outboxMessages]
173
+ /// and the flags [haveOldest] , [fetchingOlder] and [fetchOlderCoolingDown] .
150
174
/// It exists as an optimization, to memoize that computation.
151
175
final QueueList <MessageListItem > items = QueueList ();
152
176
@@ -170,6 +194,7 @@ mixin _MessageSequence {
170
194
case MessageListDateSeparatorItem (: var message):
171
195
return message.id != null && message.id! <= messageId ? - 1 : 1 ;
172
196
case MessageListMessageItem (: var message): return message.id.compareTo (messageId);
197
+ case MessageListOutboxMessageItem (): return 1 ;
173
198
}
174
199
}
175
200
@@ -277,6 +302,7 @@ mixin _MessageSequence {
277
302
void _reset () {
278
303
generation += 1 ;
279
304
messages.clear ();
305
+ outboxMessages.clear ();
280
306
_fetched = false ;
281
307
_haveOldest = false ;
282
308
_fetchingOlder = false ;
@@ -300,7 +326,8 @@ mixin _MessageSequence {
300
326
///
301
327
/// Returns whether an item has been appended or not.
302
328
///
303
- /// The caller must append a [MessageListMessageBaseItem] after this.
329
+ /// The caller must append a [MessageListMessageBaseItem] for [message]
330
+ /// after this.
304
331
bool _maybeAppendAuxillaryItem (MessageBase message, {
305
332
required MessageBase ? prevMessage,
306
333
}) {
@@ -337,6 +364,40 @@ mixin _MessageSequence {
337
364
isLastInBlock: true ));
338
365
}
339
366
367
+ /// Append to [items] based on the index-th outbox message.
368
+ ///
369
+ /// All [messages] and previous messages in [outboxMessages] must already have
370
+ /// been processed.
371
+ void _processOutboxMessage (int index) {
372
+ final prevMessage = index == 0 ? messages.lastOrNull : outboxMessages[index - 1 ];
373
+ final message = outboxMessages[index];
374
+
375
+ final appended = _maybeAppendAuxillaryItem (message, prevMessage: prevMessage);
376
+ items.add (MessageListOutboxMessageItem (message,
377
+ showSender: appended || prevMessage? .senderId != message.senderId,
378
+ isLastInBlock: true ));
379
+ }
380
+
381
+ /// Remove items associated with [outboxMessages] from [items] .
382
+ ///
383
+ /// This is efficient due to the expected small size of [outboxMessages] .
384
+ void _removeOutboxMessageItems () {
385
+ // This loop relies on the assumption that all [MessageListMessageItem]
386
+ // items comes before those associated with outbox messages. If there
387
+ // is no [MessageListMessageItem] at all, this will end up removing
388
+ // end markers as well.
389
+ while (items.isNotEmpty && items.last is ! MessageListMessageItem ) {
390
+ items.removeLast ();
391
+ }
392
+ assert (items.none ((e) => e is MessageListOutboxMessageItem ));
393
+
394
+ if (items.isNotEmpty) {
395
+ final lastItem = items.last as MessageListMessageItem ;
396
+ lastItem.isLastInBlock = true ;
397
+ }
398
+ _updateEndMarkers ();
399
+ }
400
+
340
401
/// Update [items] to include markers at start and end as appropriate.
341
402
void _updateEndMarkers () {
342
403
assert (fetched);
@@ -361,12 +422,16 @@ mixin _MessageSequence {
361
422
}
362
423
}
363
424
364
- /// Recompute [items] from scratch, based on [messages] , [contents] , and flags.
425
+ /// Recompute [items] from scratch, based on [messages] , [contents] ,
426
+ /// [outboxMessages] and flags.
365
427
void _reprocessAll () {
366
428
items.clear ();
367
429
for (var i = 0 ; i < messages.length; i++ ) {
368
430
_processMessage (i);
369
431
}
432
+ for (var i = 0 ; i < outboxMessages.length; i++ ) {
433
+ _processOutboxMessage (i);
434
+ }
370
435
_updateEndMarkers ();
371
436
}
372
437
}
@@ -527,7 +592,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
527
592
// TODO(#80): fetch from anchor firstUnread, instead of newest
528
593
// TODO(#82): fetch from a given message ID as anchor
529
594
assert (! fetched && ! haveOldest && ! fetchingOlder && ! fetchOlderCoolingDown);
530
- assert (messages.isEmpty && contents.isEmpty);
595
+ assert (messages.isEmpty && contents.isEmpty && outboxMessages.isEmpty );
531
596
// TODO schedule all this in another isolate
532
597
final generation = this .generation;
533
598
final result = await getMessages (store.connection,
@@ -545,6 +610,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
545
610
_addMessage (message);
546
611
}
547
612
}
613
+ for (final outboxMessage in store.outboxMessages.values) {
614
+ _maybeAddOutboxMessage (outboxMessage);
615
+ }
548
616
_fetched = true ;
549
617
_haveOldest = result.foundOldest;
550
618
_updateEndMarkers ();
@@ -651,15 +719,43 @@ class MessageListView with ChangeNotifier, _MessageSequence {
651
719
}
652
720
}
653
721
722
+ /// Add [outboxMessage] if it belongs to the view.
723
+ ///
724
+ /// Returns true if the message was added, false otherwise.
725
+ bool _maybeAddOutboxMessage (OutboxMessage outboxMessage) {
726
+ assert (outboxMessages.none (
727
+ (message) => message.localMessageId == outboxMessage.localMessageId));
728
+ if (! outboxMessage.hidden
729
+ && narrow.containsMessage (outboxMessage)
730
+ && _messageVisible (outboxMessage)) {
731
+ outboxMessages.add (outboxMessage);
732
+ _processOutboxMessage (outboxMessages.length - 1 );
733
+ return true ;
734
+ }
735
+ return false ;
736
+ }
737
+
654
738
void handleOutboxMessage (OutboxMessage outboxMessage) {
655
- // TODO: implement this
739
+ if (! fetched) return ;
740
+ if (_maybeAddOutboxMessage (outboxMessage)) {
741
+ notifyListeners ();
742
+ }
656
743
}
657
744
658
745
/// Remove the [outboxMessage] from the view.
659
746
///
660
747
/// This is a no-op if the message is not found.
661
748
void removeOutboxMessageIfExists (OutboxMessage outboxMessage) {
662
- // TODO: implement this
749
+ final removed = outboxMessages.remove (outboxMessage);
750
+ if (! removed) {
751
+ return ;
752
+ }
753
+
754
+ _removeOutboxMessageItems ();
755
+ for (int i = 0 ; i < outboxMessages.length; i++ ) {
756
+ _processOutboxMessage (i);
757
+ }
758
+ notifyListeners ();
663
759
}
664
760
665
761
void handleUserTopicEvent (UserTopicEvent event) {
@@ -697,14 +793,29 @@ class MessageListView with ChangeNotifier, _MessageSequence {
697
793
void handleMessageEvent (MessageEvent event) {
698
794
final message = event.message;
699
795
if (! narrow.containsMessage (message) || ! _messageVisible (message)) {
796
+ assert (event.localMessageId == null || outboxMessages.none ((message) =>
797
+ message.localMessageId == int .parse (event.localMessageId! , radix: 10 )));
700
798
return ;
701
799
}
702
800
if (! _fetched) {
703
801
// TODO mitigate this fetch/event race: save message to add to list later
704
802
return ;
705
803
}
804
+ // We always remove all outbox message items
805
+ // to ensure that message items come before them.
806
+ _removeOutboxMessageItems ();
706
807
// TODO insert in middle instead, when appropriate
707
808
_addMessage (message);
809
+ if (event.localMessageId != null ) {
810
+ final localMessageId = int .parse (event.localMessageId! );
811
+ // [outboxMessages] is epxected to be short, so removing the corresponding
812
+ // outbox message and reprocessing them all in linear time is efficient.
813
+ outboxMessages.removeWhere (
814
+ (message) => message.localMessageId == localMessageId);
815
+ }
816
+ for (int i = 0 ; i < outboxMessages.length; i++ ) {
817
+ _processOutboxMessage (i);
818
+ }
708
819
notifyListeners ();
709
820
}
710
821
@@ -825,7 +936,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
825
936
826
937
/// Notify listeners if the given outbox message is present in this view.
827
938
void notifyListenersIfOutboxMessagePresent (int localMessageId) {
828
- // TODO: implement this
939
+ final isAnyPresent =
940
+ outboxMessages.any ((message) => message.localMessageId == localMessageId);
941
+ if (isAnyPresent) {
942
+ notifyListeners ();
943
+ }
829
944
}
830
945
831
946
/// Called when the app is reassembled during debugging, e.g. for hot reload.
0 commit comments