@@ -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
@@ -770,9 +900,42 @@ class MessageListView with ChangeNotifier, _MessageSequence {
770900 fetchInitial ();
771901 }
772902
903+ bool _shouldAddOutboxMessage (OutboxMessage outboxMessage) {
904+ assert (haveNewest);
905+ return ! outboxMessage.hidden
906+ && narrow.containsMessage (outboxMessage)
907+ && _messageVisible (outboxMessage);
908+ }
909+
910+ /// Reads [MessageStore.outboxMessages] and copies to [outboxMessages]
911+ /// the ones belonging to this view.
912+ ///
913+ /// This should only be called when [haveNewest] is true
914+ /// because outbox messages are considered newer than regular messages.
915+ ///
916+ /// This does not call [notifyListeners] .
917+ void _syncOutboxMessagesFromStore () {
918+ assert (haveNewest);
919+ assert (outboxMessages.isEmpty);
920+ for (final outboxMessage in store.outboxMessages.values) {
921+ if (_shouldAddOutboxMessage (outboxMessage)) {
922+ _addOutboxMessage (outboxMessage);
923+ }
924+ }
925+ }
926+
773927 /// Add [outboxMessage] if it belongs to the view.
774928 void addOutboxMessage (OutboxMessage outboxMessage) {
775- // TODO(#1441) implement this
929+ // We don't have the newest messages;
930+ // we shouldn't show any outbox messages until we do.
931+ if (! haveNewest) return ;
932+
933+ assert (outboxMessages.none (
934+ (message) => message.localMessageId == outboxMessage.localMessageId));
935+ if (_shouldAddOutboxMessage (outboxMessage)) {
936+ _addOutboxMessage (outboxMessage);
937+ notifyListeners ();
938+ }
776939 }
777940
778941 /// Remove the [outboxMessage] from the view.
@@ -781,7 +944,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
781944 ///
782945 /// This should only be called from [MessageStore.takeOutboxMessage] .
783946 void removeOutboxMessage (OutboxMessage outboxMessage) {
784- // TODO(#1441) implement this
947+ if (_removeOutboxMessage (outboxMessage)) {
948+ notifyListeners ();
949+ }
785950 }
786951
787952 void handleUserTopicEvent (UserTopicEvent event) {
@@ -790,10 +955,17 @@ class MessageListView with ChangeNotifier, _MessageSequence {
790955 return ;
791956
792957 case VisibilityEffect .muted:
793- if (_removeMessagesWhere ((message) =>
794- (message is StreamMessage
795- && message.streamId == event.streamId
796- && message.topic == event.topicName))) {
958+ bool removed = _removeMessagesWhere ((message) =>
959+ message is StreamMessage
960+ && message.streamId == event.streamId
961+ && message.topic == event.topicName);
962+
963+ removed | = _removeOutboxMessagesWhere ((message) =>
964+ message is StreamOutboxMessage
965+ && message.conversation.streamId == event.streamId
966+ && message.conversation.topic == event.topicName);
967+
968+ if (removed) {
797969 notifyListeners ();
798970 }
799971
@@ -819,6 +991,8 @@ class MessageListView with ChangeNotifier, _MessageSequence {
819991 void handleMessageEvent (MessageEvent event) {
820992 final message = event.message;
821993 if (! narrow.containsMessage (message) || ! _messageVisible (message)) {
994+ assert (event.localMessageId == null || outboxMessages.none ((message) =>
995+ message.localMessageId == int .parse (event.localMessageId! , radix: 10 )));
822996 return ;
823997 }
824998 if (! haveNewest) {
@@ -833,8 +1007,20 @@ class MessageListView with ChangeNotifier, _MessageSequence {
8331007 // didn't include this message.
8341008 return ;
8351009 }
836- // TODO insert in middle instead, when appropriate
1010+
1011+ // Remove the outbox messages temporarily.
1012+ // We'll add them back after the new message.
1013+ _removeOutboxMessageItems ();
1014+ // TODO insert in middle of [messages] instead, when appropriate
8371015 _addMessage (message);
1016+ if (event.localMessageId != null ) {
1017+ final localMessageId = int .parse (event.localMessageId! , radix: 10 );
1018+ // [outboxMessages] is expected to be short, so removing the corresponding
1019+ // outbox message and reprocessing them all in linear time is efficient.
1020+ outboxMessages.removeWhere (
1021+ (message) => message.localMessageId == localMessageId);
1022+ }
1023+ _reprocessOutboxMessages ();
8381024 notifyListeners ();
8391025 }
8401026
@@ -955,7 +1141,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
9551141
9561142 /// Notify listeners if the given outbox message is present in this view.
9571143 void notifyListenersIfOutboxMessagePresent (int localMessageId) {
958- // TODO(#1441) implement this
1144+ final isAnyPresent =
1145+ outboxMessages.any ((message) => message.localMessageId == localMessageId);
1146+ if (isAnyPresent) {
1147+ notifyListeners ();
1148+ }
9591149 }
9601150
9611151 /// Called when the app is reassembled during debugging, e.g. for hot reload.
0 commit comments