@@ -66,6 +66,22 @@ class MessageListMessageItem extends MessageListMessageBaseItem {
66
66
});
67
67
}
68
68
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
+
69
85
/// The status of outstanding or recent fetch requests from a [MessageListView] .
70
86
enum FetchingStatus {
71
87
/// The model has not made any fetch requests (since its last reset, if any).
@@ -158,14 +174,24 @@ mixin _MessageSequence {
158
174
/// It exists as an optimization, to memoize the work of parsing.
159
175
final List <ZulipMessageContent > contents = [];
160
176
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
+
161
186
/// The messages and their siblings in the UI, in order.
162
187
///
163
188
/// This has a [MessageListMessageItem] corresponding to each element
164
189
/// 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] .
166
192
///
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] .
169
195
/// It exists as an optimization, to memoize that computation.
170
196
///
171
197
/// See also [middleItem] , an index which divides this list
@@ -177,11 +203,14 @@ mixin _MessageSequence {
177
203
/// The indices 0 to before [middleItem] are the top slice of [items] ,
178
204
/// and the indices from [middleItem] to the end are the bottom slice.
179
205
///
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] .
185
214
int middleItem = 0 ;
186
215
187
216
int _findMessageWithId (int messageId) {
@@ -197,9 +226,10 @@ mixin _MessageSequence {
197
226
switch (item) {
198
227
case MessageListRecipientHeaderItem (: var message):
199
228
case MessageListDateSeparatorItem (: var message):
200
- if (message.id == null ) return 1 ; // TODO(#1441): test
229
+ if (message.id == null ) return 1 ;
201
230
return message.id! <= messageId ? - 1 : 1 ;
202
231
case MessageListMessageItem (: var message): return message.id.compareTo (messageId);
232
+ case MessageListOutboxMessageItem (): return 1 ;
203
233
}
204
234
}
205
235
@@ -316,11 +346,48 @@ mixin _MessageSequence {
316
346
_reprocessAll ();
317
347
}
318
348
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
+
319
385
/// Reset all [_MessageSequence] data, and cancel any active fetches.
320
386
void _reset () {
321
387
generation += 1 ;
322
388
messages.clear ();
323
389
middleMessage = 0 ;
390
+ outboxMessages.clear ();
324
391
_haveOldest = false ;
325
392
_haveNewest = false ;
326
393
_status = FetchingStatus .unstarted;
@@ -379,7 +446,6 @@ mixin _MessageSequence {
379
446
assert (item.showSender == ! canShareSender);
380
447
assert (item.isLastInBlock);
381
448
if (shouldSetMiddleItem) {
382
- assert (item is MessageListMessageItem );
383
449
middleItem = items.length;
384
450
}
385
451
items.add (item);
@@ -390,6 +456,7 @@ mixin _MessageSequence {
390
456
/// The previous messages in the list must already have been processed.
391
457
/// This message must already have been parsed and reflected in [contents] .
392
458
void _processMessage (int index) {
459
+ assert (items.lastOrNull is ! MessageListOutboxMessageItem );
393
460
final prevMessage = index == 0 ? null : messages[index - 1 ];
394
461
final message = messages[index];
395
462
final content = contents[index];
@@ -401,13 +468,67 @@ mixin _MessageSequence {
401
468
message, content, showSender: ! canShareSender, isLastInBlock: true ));
402
469
}
403
470
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.
405
523
void _reprocessAll () {
406
524
items.clear ();
407
525
for (var i = 0 ; i < messages.length; i++ ) {
408
526
_processMessage (i);
409
527
}
410
528
if (middleMessage == messages.length) middleItem = items.length;
529
+ for (var i = 0 ; i < outboxMessages.length; i++ ) {
530
+ _processOutboxMessage (i);
531
+ }
411
532
}
412
533
}
413
534
@@ -602,6 +723,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
602
723
}
603
724
_haveOldest = result.foundOldest;
604
725
_haveNewest = result.foundNewest;
726
+
727
+ if (haveNewest) {
728
+ _syncOutboxMessagesFromStore ();
729
+ }
730
+
605
731
_setStatus (FetchingStatus .idle, was: FetchingStatus .fetchInitial);
606
732
}
607
733
@@ -706,6 +832,10 @@ class MessageListView with ChangeNotifier, _MessageSequence {
706
832
}
707
833
}
708
834
_haveNewest = result.foundNewest;
835
+
836
+ if (haveNewest) {
837
+ _syncOutboxMessagesFromStore ();
838
+ }
709
839
});
710
840
}
711
841
@@ -756,9 +886,42 @@ class MessageListView with ChangeNotifier, _MessageSequence {
756
886
}
757
887
}
758
888
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
+
759
913
/// Add [outboxMessage] if it belongs to the view.
760
914
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
+ }
762
925
}
763
926
764
927
/// Remove the [outboxMessage] from the view.
@@ -767,7 +930,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
767
930
///
768
931
/// This should only be called from [MessageStore.takeOutboxMessage] .
769
932
void removeOutboxMessage (OutboxMessage outboxMessage) {
770
- // TODO(#1441) implement this
933
+ if (_removeOutboxMessage (outboxMessage)) {
934
+ notifyListeners ();
935
+ }
771
936
}
772
937
773
938
void handleUserTopicEvent (UserTopicEvent event) {
@@ -776,10 +941,17 @@ class MessageListView with ChangeNotifier, _MessageSequence {
776
941
return ;
777
942
778
943
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) {
783
955
notifyListeners ();
784
956
}
785
957
@@ -805,6 +977,8 @@ class MessageListView with ChangeNotifier, _MessageSequence {
805
977
void handleMessageEvent (MessageEvent event) {
806
978
final message = event.message;
807
979
if (! narrow.containsMessage (message) || ! _messageVisible (message)) {
980
+ assert (event.localMessageId == null || outboxMessages.none ((message) =>
981
+ message.localMessageId == int .parse (event.localMessageId! , radix: 10 )));
808
982
return ;
809
983
}
810
984
if (! haveNewest) {
@@ -819,8 +993,20 @@ class MessageListView with ChangeNotifier, _MessageSequence {
819
993
// didn't include this message.
820
994
return ;
821
995
}
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
823
1001
_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 ();
824
1010
notifyListeners ();
825
1011
}
826
1012
@@ -941,7 +1127,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
941
1127
942
1128
/// Notify listeners if the given outbox message is present in this view.
943
1129
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
+ }
945
1135
}
946
1136
947
1137
/// Called when the app is reassembled during debugging, e.g. for hot reload.
0 commit comments