@@ -63,6 +63,21 @@ class MessageListMessageItem extends MessageListMessageBaseItem {
6363 });
6464}
6565
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+
6681/// Indicates the app is loading more messages at the top.
6782// TODO(#80): or loading at the bottom, by adding a [MessageListDirection.newer]
6883class MessageListLoadingItem extends MessageListItem {
@@ -90,7 +105,15 @@ mixin _MessageSequence {
90105 /// See also [contents] and [items] .
91106 final List <Message > messages = [];
92107
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.
94117 ///
95118 /// This allows the UI to distinguish "still working on fetching messages"
96119 /// from "there are in fact no messages here".
@@ -142,11 +165,12 @@ mixin _MessageSequence {
142165 /// The messages and their siblings in the UI, in order.
143166 ///
144167 /// 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.
147171 ///
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] .
150174 /// It exists as an optimization, to memoize that computation.
151175 final QueueList <MessageListItem > items = QueueList ();
152176
@@ -170,6 +194,7 @@ mixin _MessageSequence {
170194 case MessageListDateSeparatorItem (: var message):
171195 return message.id != null && message.id! <= messageId ? - 1 : 1 ;
172196 case MessageListMessageItem (: var message): return message.id.compareTo (messageId);
197+ case MessageListOutboxMessageItem (): return 1 ;
173198 }
174199 }
175200
@@ -277,6 +302,7 @@ mixin _MessageSequence {
277302 void _reset () {
278303 generation += 1 ;
279304 messages.clear ();
305+ outboxMessages.clear ();
280306 _fetched = false ;
281307 _haveOldest = false ;
282308 _fetchingOlder = false ;
@@ -300,7 +326,8 @@ mixin _MessageSequence {
300326 ///
301327 /// Returns whether an item has been appended or not.
302328 ///
303- /// The caller must append a [MessageListMessageBaseItem] after this.
329+ /// The caller must append a [MessageListMessageBaseItem] for [message]
330+ /// after this.
304331 bool _maybeAppendAuxillaryItem (MessageBase message, {
305332 required MessageBase ? prevMessage,
306333 }) {
@@ -337,6 +364,40 @@ mixin _MessageSequence {
337364 isLastInBlock: true ));
338365 }
339366
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+
340401 /// Update [items] to include markers at start and end as appropriate.
341402 void _updateEndMarkers () {
342403 assert (fetched);
@@ -361,12 +422,16 @@ mixin _MessageSequence {
361422 }
362423 }
363424
364- /// Recompute [items] from scratch, based on [messages] , [contents] , and flags.
425+ /// Recompute [items] from scratch, based on [messages] , [contents] ,
426+ /// [outboxMessages] and flags.
365427 void _reprocessAll () {
366428 items.clear ();
367429 for (var i = 0 ; i < messages.length; i++ ) {
368430 _processMessage (i);
369431 }
432+ for (var i = 0 ; i < outboxMessages.length; i++ ) {
433+ _processOutboxMessage (i);
434+ }
370435 _updateEndMarkers ();
371436 }
372437}
@@ -527,7 +592,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
527592 // TODO(#80): fetch from anchor firstUnread, instead of newest
528593 // TODO(#82): fetch from a given message ID as anchor
529594 assert (! fetched && ! haveOldest && ! fetchingOlder && ! fetchOlderCoolingDown);
530- assert (messages.isEmpty && contents.isEmpty);
595+ assert (messages.isEmpty && contents.isEmpty && outboxMessages.isEmpty );
531596 // TODO schedule all this in another isolate
532597 final generation = this .generation;
533598 final result = await getMessages (store.connection,
@@ -545,6 +610,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
545610 _addMessage (message);
546611 }
547612 }
613+ for (final outboxMessage in store.outboxMessages.values) {
614+ _maybeAddOutboxMessage (outboxMessage);
615+ }
548616 _fetched = true ;
549617 _haveOldest = result.foundOldest;
550618 _updateEndMarkers ();
@@ -651,15 +719,43 @@ class MessageListView with ChangeNotifier, _MessageSequence {
651719 }
652720 }
653721
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+
654738 void handleOutboxMessage (OutboxMessage outboxMessage) {
655- // TODO: implement this
739+ if (! fetched) return ;
740+ if (_maybeAddOutboxMessage (outboxMessage)) {
741+ notifyListeners ();
742+ }
656743 }
657744
658745 /// Remove the [outboxMessage] from the view.
659746 ///
660747 /// This is a no-op if the message is not found.
661748 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 ();
663759 }
664760
665761 void handleUserTopicEvent (UserTopicEvent event) {
@@ -697,14 +793,29 @@ class MessageListView with ChangeNotifier, _MessageSequence {
697793 void handleMessageEvent (MessageEvent event) {
698794 final message = event.message;
699795 if (! narrow.containsMessage (message) || ! _messageVisible (message)) {
796+ assert (event.localMessageId == null || outboxMessages.none ((message) =>
797+ message.localMessageId == int .parse (event.localMessageId! , radix: 10 )));
700798 return ;
701799 }
702800 if (! _fetched) {
703801 // TODO mitigate this fetch/event race: save message to add to list later
704802 return ;
705803 }
804+ // We always remove all outbox message items
805+ // to ensure that message items come before them.
806+ _removeOutboxMessageItems ();
706807 // TODO insert in middle instead, when appropriate
707808 _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+ }
708819 notifyListeners ();
709820 }
710821
@@ -825,7 +936,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
825936
826937 /// Notify listeners if the given outbox message is present in this view.
827938 void notifyListenersIfOutboxMessagePresent (int localMessageId) {
828- // TODO: implement this
939+ final isAnyPresent =
940+ outboxMessages.any ((message) => message.localMessageId == localMessageId);
941+ if (isAnyPresent) {
942+ notifyListeners ();
943+ }
829944 }
830945
831946 /// Called when the app is reassembled during debugging, e.g. for hot reload.
0 commit comments