@@ -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. 
159175final  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] . 
185214int  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. 
320386void  _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] . 
392458void  _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. 
405523void  _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. 
760914void  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] . 
769932void  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 {
805977void  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. 
9431129void  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