@@ -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
@@ -36,20 +37,47 @@ class MessageListDateSeparatorItem extends MessageListItem {
36
37
}
37
38
38
39
/// A message to show in the message list.
39
- class MessageListMessageItem extends MessageListItem {
40
- final Message message;
41
- ZulipMessageContent content;
40
+ sealed class MessageListDisplayableMessageItem extends MessageListItem {
41
+ DisplayableMessage get message;
42
+ ZulipMessageContent get content;
42
43
bool showSender;
43
44
bool isLastInBlock;
44
45
46
+ MessageListDisplayableMessageItem ({
47
+ required this .showSender,
48
+ required this .isLastInBlock,
49
+ });
50
+ }
51
+
52
+ class MessageListMessageItem extends MessageListDisplayableMessageItem {
53
+ @override
54
+ final Message message;
55
+ @override
56
+ ZulipMessageContent content;
57
+
45
58
MessageListMessageItem (
46
59
this .message,
47
60
this .content, {
48
- required this .showSender,
49
- required this .isLastInBlock,
61
+ required super .showSender,
62
+ required super .isLastInBlock,
50
63
});
51
64
}
52
65
66
+ class MessageListOutboxMessageItem extends MessageListDisplayableMessageItem {
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
+
53
81
/// Indicates the app is loading more messages at the top.
54
82
// TODO(#80): or loading at the bottom, by adding a [MessageListDirection.newer]
55
83
class MessageListLoadingItem extends MessageListItem {
@@ -77,6 +105,13 @@ mixin _MessageSequence {
77
105
/// See also [contents] and [items] .
78
106
final List <Message > messages = [];
79
107
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
+
80
115
/// Whether [messages] and [items] represent the results of a fetch.
81
116
///
82
117
/// This allows the UI to distinguish "still working on fetching messages"
@@ -129,11 +164,12 @@ mixin _MessageSequence {
129
164
/// The messages and their siblings in the UI, in order.
130
165
///
131
166
/// This has a [MessageListMessageItem] corresponding to each element
132
- /// of [messages] , in order. It may have additional items interspersed
133
- /// before, between, or after the messages.
167
+ /// of [messages] , followed by each element in [outboxMessages] in order.
168
+ /// It may have additional items interspersed before, between, or after the
169
+ /// messages.
134
170
///
135
- /// This information is completely derived from [messages] and
136
- /// the flags [haveOldest] , [fetchingOlder] and [fetchOlderCoolingDown] .
171
+ /// This information is completely derived from [messages] , [outboxMessages]
172
+ /// and the flags [haveOldest] , [fetchingOlder] and [fetchOlderCoolingDown] .
137
173
/// It exists as an optimization, to memoize that computation.
138
174
final QueueList <MessageListItem > items = QueueList ();
139
175
@@ -157,6 +193,7 @@ mixin _MessageSequence {
157
193
case MessageListDateSeparatorItem (: var message):
158
194
return message.id != null && message.id! <= messageId ? - 1 : 1 ;
159
195
case MessageListMessageItem (: var message): return message.id.compareTo (messageId);
196
+ case MessageListOutboxMessageItem (): return 1 ;
160
197
}
161
198
}
162
199
@@ -264,6 +301,7 @@ mixin _MessageSequence {
264
301
void _reset () {
265
302
generation += 1 ;
266
303
messages.clear ();
304
+ outboxMessages.clear ();
267
305
_fetched = false ;
268
306
_haveOldest = false ;
269
307
_fetchingOlder = false ;
@@ -282,17 +320,18 @@ mixin _MessageSequence {
282
320
_reprocessAll ();
283
321
}
284
322
285
- /// Prepare [items] before a [MessageListMessageItem ] can be appended.
323
+ /// Prepare [items] before a [MessageListDisplayableMessageItem ] can be appended.
286
324
///
287
325
/// Returns whether the sender can be shared between the items for both
288
- /// messages, as in the negation of [MessageListMessageItem.showSender] .
289
- bool _prepareTailForMessage (Message message, {required Message ? prevMessage}) {
326
+ /// messages, as in the negation of [MessageListDisplayableMessageItem.showSender] .
327
+ bool _prepareTailForMessage (DisplayableMessage message, {
328
+ required DisplayableMessage ? prevMessage,
329
+ }) {
290
330
if (prevMessage == null || ! haveSameRecipient (prevMessage, message)) {
291
331
items.add (MessageListRecipientHeaderItem (message));
292
332
return false ;
293
333
} else {
294
- assert (items.last is MessageListMessageItem );
295
- final prevMessageItem = items.last as MessageListMessageItem ;
334
+ final prevMessageItem = items.last as MessageListDisplayableMessageItem ;
296
335
assert (identical (prevMessageItem.message, prevMessage));
297
336
assert (prevMessageItem.isLastInBlock);
298
337
prevMessageItem.isLastInBlock = false ;
@@ -320,6 +359,28 @@ mixin _MessageSequence {
320
359
showSender: ! canShareSender, isLastInBlock: true ));
321
360
}
322
361
362
+ void _processOutboxMessage (int index) {
363
+ final prevMessage = index == 0 ? messages.last : outboxMessages[index - 1 ];
364
+ final message = outboxMessages[index];
365
+
366
+ final canShareSender = _prepareTailForMessage (message, prevMessage: prevMessage);
367
+ items.add (MessageListOutboxMessageItem (message,
368
+ showSender: ! canShareSender, isLastInBlock: true ));
369
+ }
370
+
371
+ /// Remove from [items] the ones associated with [outboxMessages] .
372
+ void _removeOutboxMessageItems () {
373
+ while (items.isNotEmpty && items.last is ! MessageListMessageItem ) {
374
+ items.removeLast ();
375
+ }
376
+ final lastMessageItem = items.lastOrNull;
377
+ assert (items.none ((e) => e is MessageListOutboxMessageItem ));
378
+ if (lastMessageItem is MessageListDisplayableMessageItem ) {
379
+ assert (lastMessageItem is MessageListMessageItem );
380
+ lastMessageItem.isLastInBlock = true ;
381
+ }
382
+ }
383
+
323
384
/// Update [items] to include markers at start and end as appropriate.
324
385
void _updateEndMarkers () {
325
386
assert (fetched);
@@ -344,23 +405,27 @@ mixin _MessageSequence {
344
405
}
345
406
}
346
407
347
- /// Recompute [items] from scratch, based on [messages] , [contents] , and flags.
408
+ /// Recompute [items] from scratch, based on [messages] , [contents] ,
409
+ /// [outboxMessages] and flags.
348
410
void _reprocessAll () {
349
411
items.clear ();
350
412
for (var i = 0 ; i < messages.length; i++ ) {
351
413
_processMessage (i);
352
414
}
415
+ for (var i = 0 ; i < outboxMessages.length; i++ ) {
416
+ _processOutboxMessage (i);
417
+ }
353
418
_updateEndMarkers ();
354
419
}
355
420
}
356
421
357
422
@visibleForTesting
358
- bool haveSameRecipient (Message prevMessage, Message message) {
359
- if (prevMessage is StreamMessage && message is StreamMessage ) {
360
- if (prevMessage.streamId != message.streamId) return false ;
361
- if (prevMessage.topic.canonicalize () != message.topic.canonicalize ()) return false ;
362
- } else if (prevMessage is DmMessage && message is DmMessage ) {
363
- if (! _equalIdSequences (prevMessage.allRecipientIds , message.allRecipientIds )) {
423
+ bool haveSameRecipient (DisplayableMessage prevMessage, DisplayableMessage message) {
424
+ if (prevMessage is DisplayableMessage < StreamDestination > && message is DisplayableMessage < StreamDestination > ) {
425
+ if (prevMessage.destination. streamId != message.destination .streamId) return false ;
426
+ if (prevMessage.destination. topic.canonicalize () != message.destination .topic.canonicalize ()) return false ;
427
+ } else if (prevMessage is DisplayableMessage < DmDestination > && message is DisplayableMessage < DmDestination > ) {
428
+ if (! _equalIdSequences (prevMessage.destination.userIds , message.destination.userIds )) {
364
429
return false ;
365
430
}
366
431
} else {
@@ -379,7 +444,7 @@ bool haveSameRecipient(Message prevMessage, Message message) {
379
444
}
380
445
381
446
@visibleForTesting
382
- bool messagesSameDay (Message prevMessage, Message message) {
447
+ bool messagesSameDay (DisplayableMessage prevMessage, DisplayableMessage message) {
383
448
// TODO memoize [DateTime]s... also use memoized for showing date/time in msglist
384
449
final prevTime = DateTime .fromMillisecondsSinceEpoch (prevMessage.timestamp * 1000 );
385
450
final time = DateTime .fromMillisecondsSinceEpoch (message.timestamp * 1000 );
@@ -444,19 +509,20 @@ class MessageListView with ChangeNotifier, _MessageSequence {
444
509
/// one way or another.
445
510
///
446
511
/// See also [_allMessagesVisible] .
447
- bool _messageVisible (Message message) {
512
+ bool _messageVisible (DisplayableMessage message) {
448
513
switch (narrow) {
449
514
case CombinedFeedNarrow ():
450
- return switch (message) {
451
- StreamMessage ( ) =>
452
- store.isTopicVisible (message. streamId, message. topic),
453
- DmMessage () => true ,
515
+ return switch (message.destination ) {
516
+ StreamDestination ( : final streamId, : final topic ) =>
517
+ store.isTopicVisible (streamId, topic),
518
+ DmDestination () => true ,
454
519
};
455
520
456
521
case ChannelNarrow (: final streamId):
457
- assert (message is StreamMessage && message.streamId == streamId);
458
- if (message is ! StreamMessage ) return false ;
459
- return store.isTopicVisibleInStream (streamId, message.topic);
522
+ assert (message is DisplayableMessage <StreamDestination >
523
+ && message.destination.streamId == streamId);
524
+ if (message is ! DisplayableMessage <StreamDestination >) return false ;
525
+ return store.isTopicVisibleInStream (streamId, message.destination.topic);
460
526
461
527
case TopicNarrow ():
462
528
case DmNarrow ():
@@ -525,6 +591,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
525
591
_addMessage (message);
526
592
}
527
593
}
594
+ for (final outboxMessage in store.outboxMessages.values) {
595
+ handleOutboxMessage (outboxMessage);
596
+ }
528
597
_fetched = true ;
529
598
_haveOldest = result.foundOldest;
530
599
_updateEndMarkers ();
@@ -631,6 +700,19 @@ class MessageListView with ChangeNotifier, _MessageSequence {
631
700
}
632
701
}
633
702
703
+ void handleOutboxMessage (OutboxMessage outboxMessage) {
704
+ if (outboxMessage.hidden) return ;
705
+ if (narrow.containsMessage (outboxMessage) && _messageVisible (outboxMessage)) {
706
+ outboxMessages.add (outboxMessage);
707
+ _processOutboxMessage (outboxMessages.length - 1 );
708
+ notifyListeners ();
709
+ }
710
+ }
711
+
712
+ void handleUpdateOutboxMessage (int localMessageId) {
713
+ notifyListenersIfOutboxMessagePresent (localMessageId);
714
+ }
715
+
634
716
void handleUserTopicEvent (UserTopicEvent event) {
635
717
switch (_canAffectVisibility (event)) {
636
718
case VisibilityEffect .none:
@@ -666,14 +748,26 @@ class MessageListView with ChangeNotifier, _MessageSequence {
666
748
void handleMessageEvent (MessageEvent event) {
667
749
final message = event.message;
668
750
if (! narrow.containsMessage (message) || ! _messageVisible (message)) {
751
+ assert (event.localMessageId == null
752
+ || outboxMessages.every ((message) =>
753
+ message.localMessageId != event.localMessageId! ));
669
754
return ;
670
755
}
671
756
if (! _fetched) {
672
757
// TODO mitigate this fetch/event race: save message to add to list later
673
758
return ;
674
759
}
760
+ _removeOutboxMessageItems ();
675
761
// TODO insert in middle instead, when appropriate
676
762
_addMessage (message);
763
+ final localMessageId = event.localMessageId;
764
+ if (localMessageId != null ) {
765
+ outboxMessages.removeWhere (
766
+ (message) => message.localMessageId == localMessageId);
767
+ for (int i = 0 ; i < outboxMessages.length; i++ ) {
768
+ _processOutboxMessage (i);
769
+ }
770
+ }
677
771
notifyListeners ();
678
772
}
679
773
@@ -792,6 +886,15 @@ class MessageListView with ChangeNotifier, _MessageSequence {
792
886
}
793
887
}
794
888
889
+ /// Notify listeners if the given outbox message is present in this view.
890
+ void notifyListenersIfOutboxMessagePresent (int localMessageId) {
891
+ final isAnyPresent =
892
+ outboxMessages.any ((message) => message.localMessageId == localMessageId);
893
+ if (isAnyPresent) {
894
+ notifyListeners ();
895
+ }
896
+ }
897
+
795
898
/// Called when the app is reassembled during debugging, e.g. for hot reload.
796
899
///
797
900
/// This will redo from scratch any computations we can, such as parsing
0 commit comments