@@ -66,6 +66,22 @@ class MessageListMessageItem extends MessageListMessageBaseItem {
6666 });
6767}
6868
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+
6985/// The status of outstanding or recent fetch requests from a [MessageListView] .
7086enum FetchingStatus {
7187 /// The model has not made any fetch requests (since its last reset, if any).
@@ -158,14 +174,24 @@ mixin _MessageSequence {
158174 /// It exists as an optimization, to memoize the work of parsing.
159175 final List <ZulipMessageContent > contents = [];
160176
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+
161186 /// The messages and their siblings in the UI, in order.
162187 ///
163188 /// This has a [MessageListMessageItem] corresponding to each element
164189 /// 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] .
166192 ///
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] .
169195 /// It exists as an optimization, to memoize that computation.
170196 ///
171197 /// See also [middleItem] , an index which divides this list
@@ -177,11 +203,14 @@ mixin _MessageSequence {
177203 /// The indices 0 to before [middleItem] are the top slice of [items] ,
178204 /// and the indices from [middleItem] to the end are the bottom slice.
179205 ///
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] .
185214 int middleItem = 0 ;
186215
187216 int _findMessageWithId (int messageId) {
@@ -197,9 +226,10 @@ mixin _MessageSequence {
197226 switch (item) {
198227 case MessageListRecipientHeaderItem (: var message):
199228 case MessageListDateSeparatorItem (: var message):
200- if (message.id == null ) return 1 ; // TODO(#1441): test
229+ if (message.id == null ) return 1 ;
201230 return message.id! <= messageId ? - 1 : 1 ;
202231 case MessageListMessageItem (: var message): return message.id.compareTo (messageId);
232+ case MessageListOutboxMessageItem (): return 1 ;
203233 }
204234 }
205235
@@ -316,11 +346,48 @@ mixin _MessageSequence {
316346 _reprocessAll ();
317347 }
318348
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+
319385 /// Reset all [_MessageSequence] data, and cancel any active fetches.
320386 void _reset () {
321387 generation += 1 ;
322388 messages.clear ();
323389 middleMessage = 0 ;
390+ outboxMessages.clear ();
324391 _haveOldest = false ;
325392 _haveNewest = false ;
326393 _status = FetchingStatus .unstarted;
@@ -379,7 +446,6 @@ mixin _MessageSequence {
379446 assert (item.showSender == ! canShareSender);
380447 assert (item.isLastInBlock);
381448 if (shouldSetMiddleItem) {
382- assert (item is MessageListMessageItem );
383449 middleItem = items.length;
384450 }
385451 items.add (item);
@@ -390,6 +456,7 @@ mixin _MessageSequence {
390456 /// The previous messages in the list must already have been processed.
391457 /// This message must already have been parsed and reflected in [contents] .
392458 void _processMessage (int index) {
459+ assert (items.lastOrNull is ! MessageListOutboxMessageItem );
393460 final prevMessage = index == 0 ? null : messages[index - 1 ];
394461 final message = messages[index];
395462 final content = contents[index];
@@ -401,13 +468,67 @@ mixin _MessageSequence {
401468 message, content, showSender: ! canShareSender, isLastInBlock: true ));
402469 }
403470
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.
405523 void _reprocessAll () {
406524 items.clear ();
407525 for (var i = 0 ; i < messages.length; i++ ) {
408526 _processMessage (i);
409527 }
410528 if (middleMessage == messages.length) middleItem = items.length;
529+ for (var i = 0 ; i < outboxMessages.length; i++ ) {
530+ _processOutboxMessage (i);
531+ }
411532 }
412533}
413534
@@ -602,6 +723,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
602723 }
603724 _haveOldest = result.foundOldest;
604725 _haveNewest = result.foundNewest;
726+
727+ if (haveNewest) {
728+ _syncOutboxMessagesFromStore ();
729+ }
730+
605731 _setStatus (FetchingStatus .idle, was: FetchingStatus .fetchInitial);
606732 }
607733
@@ -706,6 +832,10 @@ class MessageListView with ChangeNotifier, _MessageSequence {
706832 }
707833 }
708834 _haveNewest = result.foundNewest;
835+
836+ if (haveNewest) {
837+ _syncOutboxMessagesFromStore ();
838+ }
709839 });
710840 }
711841
@@ -756,9 +886,42 @@ class MessageListView with ChangeNotifier, _MessageSequence {
756886 }
757887 }
758888
889+ bool _shouldAddOutboxMessage (OutboxMessage outboxMessage) {
890+ assert (haveNewest);
891+ return ! outboxMessage.hidden
892+ && narrow.containsMessage (outboxMessage)
893+ && _messageVisible (outboxMessage);
894+ }
895+
896+ /// Reads [MessageStore.outboxMessages] and copies to [outboxMessages]
897+ /// the ones belonging to this view.
898+ ///
899+ /// This should only be called when [haveNewest] is true
900+ /// because outbox messages are considered newer than regular messages.
901+ ///
902+ /// This does not call [notifyListeners] .
903+ void _syncOutboxMessagesFromStore () {
904+ assert (haveNewest);
905+ assert (outboxMessages.isEmpty);
906+ for (final outboxMessage in store.outboxMessages.values) {
907+ if (_shouldAddOutboxMessage (outboxMessage)) {
908+ _addOutboxMessage (outboxMessage);
909+ }
910+ }
911+ }
912+
759913 /// Add [outboxMessage] if it belongs to the view.
760914 void addOutboxMessage (OutboxMessage outboxMessage) {
761- // TODO(#1441) implement this
915+ // We don't have the newest messages;
916+ // we shouldn't show any outbox messages until we do.
917+ if (! haveNewest) return ;
918+
919+ assert (outboxMessages.none (
920+ (message) => message.localMessageId == outboxMessage.localMessageId));
921+ if (_shouldAddOutboxMessage (outboxMessage)) {
922+ _addOutboxMessage (outboxMessage);
923+ notifyListeners ();
924+ }
762925 }
763926
764927 /// Remove the [outboxMessage] from the view.
@@ -767,7 +930,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
767930 ///
768931 /// This should only be called from [MessageStore.takeOutboxMessage] .
769932 void removeOutboxMessage (OutboxMessage outboxMessage) {
770- // TODO(#1441) implement this
933+ if (_removeOutboxMessage (outboxMessage)) {
934+ notifyListeners ();
935+ }
771936 }
772937
773938 void handleUserTopicEvent (UserTopicEvent event) {
@@ -776,10 +941,17 @@ class MessageListView with ChangeNotifier, _MessageSequence {
776941 return ;
777942
778943 case VisibilityEffect .muted:
779- if (_removeMessagesWhere ((message) =>
780- (message is StreamMessage
781- && message.streamId == event.streamId
782- && message.topic == event.topicName))) {
944+ bool removed = _removeMessagesWhere ((message) =>
945+ message is StreamMessage
946+ && message.streamId == event.streamId
947+ && message.topic == event.topicName);
948+
949+ removed | = _removeOutboxMessagesWhere ((message) =>
950+ message is StreamOutboxMessage
951+ && message.conversation.streamId == event.streamId
952+ && message.conversation.topic == event.topicName);
953+
954+ if (removed) {
783955 notifyListeners ();
784956 }
785957
@@ -805,6 +977,8 @@ class MessageListView with ChangeNotifier, _MessageSequence {
805977 void handleMessageEvent (MessageEvent event) {
806978 final message = event.message;
807979 if (! narrow.containsMessage (message) || ! _messageVisible (message)) {
980+ assert (event.localMessageId == null || outboxMessages.none ((message) =>
981+ message.localMessageId == int .parse (event.localMessageId! , radix: 10 )));
808982 return ;
809983 }
810984 if (! haveNewest) {
@@ -819,8 +993,20 @@ class MessageListView with ChangeNotifier, _MessageSequence {
819993 // didn't include this message.
820994 return ;
821995 }
822- // TODO insert in middle instead, when appropriate
996+
997+ // Remove the outbox messages temporarily.
998+ // We'll add them back after the new message.
999+ _removeOutboxMessageItems ();
1000+ // TODO insert in middle of [messages] instead, when appropriate
8231001 _addMessage (message);
1002+ if (event.localMessageId != null ) {
1003+ final localMessageId = int .parse (event.localMessageId! , radix: 10 );
1004+ // [outboxMessages] is expected to be short, so removing the corresponding
1005+ // outbox message and reprocessing them all in linear time is efficient.
1006+ outboxMessages.removeWhere (
1007+ (message) => message.localMessageId == localMessageId);
1008+ }
1009+ _reprocessOutboxMessages ();
8241010 notifyListeners ();
8251011 }
8261012
@@ -941,7 +1127,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
9411127
9421128 /// Notify listeners if the given outbox message is present in this view.
9431129 void notifyListenersIfOutboxMessagePresent (int localMessageId) {
944- // TODO(#1441) implement this
1130+ final isAnyPresent =
1131+ outboxMessages.any ((message) => message.localMessageId == localMessageId);
1132+ if (isAnyPresent) {
1133+ notifyListeners ();
1134+ }
9451135 }
9461136
9471137 /// Called when the app is reassembled during debugging, e.g. for hot reload.
0 commit comments