@@ -10,6 +10,7 @@ import '../api/route/messages.dart';
10
10
import 'algorithms.dart' ;
11
11
import 'channel.dart' ;
12
12
import 'content.dart' ;
13
+ import 'message.dart' ;
13
14
import 'narrow.dart' ;
14
15
import 'store.dart' ;
15
16
@@ -62,6 +63,21 @@ class MessageListMessageItem extends MessageListMessageBaseItem {
62
63
});
63
64
}
64
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
+
65
81
/// Indicates the app is loading more messages at the top.
66
82
// TODO(#80): or loading at the bottom, by adding a [MessageListDirection.newer]
67
83
class MessageListLoadingItem extends MessageListItem {
@@ -89,7 +105,15 @@ mixin _MessageSequence {
89
105
/// See also [contents] and [items] .
90
106
final List <Message > messages = [];
91
107
92
- /// 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.
93
117
///
94
118
/// This allows the UI to distinguish "still working on fetching messages"
95
119
/// from "there are in fact no messages here".
@@ -141,11 +165,12 @@ mixin _MessageSequence {
141
165
/// The messages and their siblings in the UI, in order.
142
166
///
143
167
/// This has a [MessageListMessageItem] corresponding to each element
144
- /// of [messages] , in order. It may have additional items interspersed
145
- /// 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.
146
171
///
147
- /// This information is completely derived from [messages] and
148
- /// the flags [haveOldest] , [fetchingOlder] and [fetchOlderCoolingDown] .
172
+ /// This information is completely derived from [messages] , [outboxMessages]
173
+ /// and the flags [haveOldest] , [fetchingOlder] and [fetchOlderCoolingDown] .
149
174
/// It exists as an optimization, to memoize that computation.
150
175
final QueueList <MessageListItem > items = QueueList ();
151
176
@@ -170,6 +195,7 @@ mixin _MessageSequence {
170
195
if (message.id == null ) return 1 ;
171
196
return message.id! <= messageId ? - 1 : 1 ;
172
197
case MessageListMessageItem (: var message): return message.id.compareTo (messageId);
198
+ case MessageListOutboxMessageItem (): return 1 ;
173
199
}
174
200
}
175
201
@@ -277,6 +303,7 @@ mixin _MessageSequence {
277
303
void _reset () {
278
304
generation += 1 ;
279
305
messages.clear ();
306
+ outboxMessages.clear ();
280
307
_fetched = false ;
281
308
_haveOldest = false ;
282
309
_fetchingOlder = false ;
@@ -300,7 +327,8 @@ mixin _MessageSequence {
300
327
///
301
328
/// Returns whether an item has been appended or not.
302
329
///
303
- /// The caller must append a [MessageListMessageBaseItem] after this.
330
+ /// The caller must append a [MessageListMessageBaseItem] for [message]
331
+ /// after this.
304
332
bool _maybeAppendAuxillaryItem (MessageBase message, {
305
333
required MessageBase ? prevMessage,
306
334
}) {
@@ -337,6 +365,40 @@ mixin _MessageSequence {
337
365
isLastInBlock: true ));
338
366
}
339
367
368
+ /// Append to [items] based on the index-th outbox message.
369
+ ///
370
+ /// All [messages] and previous messages in [outboxMessages] must already have
371
+ /// been processed.
372
+ void _processOutboxMessage (int index) {
373
+ final prevMessage = index == 0 ? messages.lastOrNull : outboxMessages[index - 1 ];
374
+ final message = outboxMessages[index];
375
+
376
+ final appended = _maybeAppendAuxillaryItem (message, prevMessage: prevMessage);
377
+ items.add (MessageListOutboxMessageItem (message,
378
+ showSender: appended || prevMessage? .senderId != message.senderId,
379
+ isLastInBlock: true ));
380
+ }
381
+
382
+ /// Remove items associated with [outboxMessages] from [items] .
383
+ ///
384
+ /// This is efficient due to the expected small size of [outboxMessages] .
385
+ void _removeOutboxMessageItems () {
386
+ // This loop relies on the assumption that all [MessageListMessageItem]
387
+ // items comes before those associated with outbox messages. If there
388
+ // is no [MessageListMessageItem] at all, this will end up removing
389
+ // end markers as well.
390
+ while (items.isNotEmpty && items.last is ! MessageListMessageItem ) {
391
+ items.removeLast ();
392
+ }
393
+ assert (items.none ((e) => e is MessageListOutboxMessageItem ));
394
+
395
+ if (items.isNotEmpty) {
396
+ final lastItem = items.last as MessageListMessageItem ;
397
+ lastItem.isLastInBlock = true ;
398
+ }
399
+ _updateEndMarkers ();
400
+ }
401
+
340
402
/// Update [items] to include markers at start and end as appropriate.
341
403
void _updateEndMarkers () {
342
404
assert (fetched);
@@ -361,12 +423,16 @@ mixin _MessageSequence {
361
423
}
362
424
}
363
425
364
- /// Recompute [items] from scratch, based on [messages] , [contents] , and flags.
426
+ /// Recompute [items] from scratch, based on [messages] , [contents] ,
427
+ /// [outboxMessages] and flags.
365
428
void _reprocessAll () {
366
429
items.clear ();
367
430
for (var i = 0 ; i < messages.length; i++ ) {
368
431
_processMessage (i);
369
432
}
433
+ for (var i = 0 ; i < outboxMessages.length; i++ ) {
434
+ _processOutboxMessage (i);
435
+ }
370
436
_updateEndMarkers ();
371
437
}
372
438
}
@@ -527,7 +593,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
527
593
// TODO(#80): fetch from anchor firstUnread, instead of newest
528
594
// TODO(#82): fetch from a given message ID as anchor
529
595
assert (! fetched && ! haveOldest && ! fetchingOlder && ! fetchOlderCoolingDown);
530
- assert (messages.isEmpty && contents.isEmpty);
596
+ assert (messages.isEmpty && contents.isEmpty && outboxMessages.isEmpty );
531
597
// TODO schedule all this in another isolate
532
598
final generation = this .generation;
533
599
final result = await getMessages (store.connection,
@@ -545,6 +611,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
545
611
_addMessage (message);
546
612
}
547
613
}
614
+ for (final outboxMessage in store.outboxMessages.values) {
615
+ _maybeAddOutboxMessage (outboxMessage);
616
+ }
548
617
_fetched = true ;
549
618
_haveOldest = result.foundOldest;
550
619
_updateEndMarkers ();
@@ -651,6 +720,45 @@ class MessageListView with ChangeNotifier, _MessageSequence {
651
720
}
652
721
}
653
722
723
+ /// Add [outboxMessage] if it belongs to the view.
724
+ ///
725
+ /// Returns true if the message was added, false otherwise.
726
+ bool _maybeAddOutboxMessage (OutboxMessage outboxMessage) {
727
+ assert (outboxMessages.none (
728
+ (message) => message.localMessageId == outboxMessage.localMessageId));
729
+ if (! outboxMessage.hidden
730
+ && narrow.containsMessage (outboxMessage)
731
+ && _messageVisible (outboxMessage)) {
732
+ outboxMessages.add (outboxMessage);
733
+ _processOutboxMessage (outboxMessages.length - 1 );
734
+ return true ;
735
+ }
736
+ return false ;
737
+ }
738
+
739
+ void handleOutboxMessage (OutboxMessage outboxMessage) {
740
+ if (! fetched) return ;
741
+ if (_maybeAddOutboxMessage (outboxMessage)) {
742
+ notifyListeners ();
743
+ }
744
+ }
745
+
746
+ /// Remove the [outboxMessage] from the view.
747
+ ///
748
+ /// This is a no-op if the message is not found.
749
+ void removeOutboxMessageIfExists (OutboxMessage outboxMessage) {
750
+ final removed = outboxMessages.remove (outboxMessage);
751
+ if (! removed) {
752
+ return ;
753
+ }
754
+
755
+ _removeOutboxMessageItems ();
756
+ for (int i = 0 ; i < outboxMessages.length; i++ ) {
757
+ _processOutboxMessage (i);
758
+ }
759
+ notifyListeners ();
760
+ }
761
+
654
762
void handleUserTopicEvent (UserTopicEvent event) {
655
763
switch (_canAffectVisibility (event)) {
656
764
case VisibilityEffect .none:
@@ -686,14 +794,29 @@ class MessageListView with ChangeNotifier, _MessageSequence {
686
794
void handleMessageEvent (MessageEvent event) {
687
795
final message = event.message;
688
796
if (! narrow.containsMessage (message) || ! _messageVisible (message)) {
797
+ assert (event.localMessageId == null || outboxMessages.none ((message) =>
798
+ message.localMessageId == int .parse (event.localMessageId! , radix: 10 )));
689
799
return ;
690
800
}
691
801
if (! _fetched) {
692
802
// TODO mitigate this fetch/event race: save message to add to list later
693
803
return ;
694
804
}
805
+ // We always remove all outbox message items
806
+ // to ensure that message items come before them.
807
+ _removeOutboxMessageItems ();
695
808
// TODO insert in middle instead, when appropriate
696
809
_addMessage (message);
810
+ if (event.localMessageId != null ) {
811
+ final localMessageId = int .parse (event.localMessageId! );
812
+ // [outboxMessages] is epxected to be short, so removing the corresponding
813
+ // outbox message and reprocessing them all in linear time is efficient.
814
+ outboxMessages.removeWhere (
815
+ (message) => message.localMessageId == localMessageId);
816
+ }
817
+ for (int i = 0 ; i < outboxMessages.length; i++ ) {
818
+ _processOutboxMessage (i);
819
+ }
697
820
notifyListeners ();
698
821
}
699
822
@@ -812,6 +935,15 @@ class MessageListView with ChangeNotifier, _MessageSequence {
812
935
}
813
936
}
814
937
938
+ /// Notify listeners if the given outbox message is present in this view.
939
+ void notifyListenersIfOutboxMessagePresent (int localMessageId) {
940
+ final isAnyPresent =
941
+ outboxMessages.any ((message) => message.localMessageId == localMessageId);
942
+ if (isAnyPresent) {
943
+ notifyListeners ();
944
+ }
945
+ }
946
+
815
947
/// Called when the app is reassembled during debugging, e.g. for hot reload.
816
948
///
817
949
/// This will redo from scratch any computations we can, such as parsing
0 commit comments