Skip to content

Commit c7f1bea

Browse files
PIG208chrisbobbe
andcommitted
msglist: Add and manage outbox message objects in message list view
This adds some overhead on message-event handling, linear in the number of outbox messages in a view. We rely on that number being small. We add outboxMessages as a list independent from messages on _MessageSequence. Because outbox messages are not rendered (the raw content is shown as plain text), we leave the 1-1 relationship between `messages` and `contents` unchanged. When computing `items`, we now start to look at `outboxMessages` as well, with the guarantee that the items related to outbox messages always come after those for other messages. Look for places that call `_processOutboxMessage(int index)` for references, and the changes to `checkInvariants` on how this affects the message list invariants. This implements minimal support to display outbox message message item widgets in the message list, without indicators for theirs states. Retrieving content from failed sent requests and the full UI are implemented in a later commit. Co-authored-by: Chris Bobbe <[email protected]>
1 parent e10a4f0 commit c7f1bea

File tree

7 files changed

+914
-58
lines changed

7 files changed

+914
-58
lines changed

lib/model/message.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,8 @@ class MessageStoreImpl extends PerAccountStoreBase with MessageStore, _OutboxMes
394394
}
395395
}
396396

397+
// TODO predict outbox message moves using propagateMode
398+
397399
for (final view in _messageListViews) {
398400
view.messagesMoved(messageMove: messageMove, messageIds: event.messageIds);
399401
}

lib/model/message_list.dart

Lines changed: 209 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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].
7086
enum 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

@@ -756,9 +886,42 @@ class MessageListView with ChangeNotifier, _MessageSequence {
756886
}
757887
}
758888

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+
759913
/// Add [outboxMessage] if it belongs to the view.
760914
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+
}
762925
}
763926

764927
/// Remove the [outboxMessage] from the view.
@@ -767,7 +930,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
767930
///
768931
/// This should only be called from [MessageStore.takeOutboxMessage].
769932
void removeOutboxMessage(OutboxMessage outboxMessage) {
770-
// TODO(#1441) implement this
933+
if (_removeOutboxMessage(outboxMessage)) {
934+
notifyListeners();
935+
}
771936
}
772937

773938
void handleUserTopicEvent(UserTopicEvent event) {
@@ -776,10 +941,17 @@ class MessageListView with ChangeNotifier, _MessageSequence {
776941
return;
777942

778943
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) {
783955
notifyListeners();
784956
}
785957

@@ -805,6 +977,8 @@ class MessageListView with ChangeNotifier, _MessageSequence {
805977
void handleMessageEvent(MessageEvent event) {
806978
final message = event.message;
807979
if (!narrow.containsMessage(message) || !_messageVisible(message)) {
980+
assert(event.localMessageId == null || outboxMessages.none((message) =>
981+
message.localMessageId == int.parse(event.localMessageId!, radix: 10)));
808982
return;
809983
}
810984
if (!haveNewest) {
@@ -819,8 +993,20 @@ class MessageListView with ChangeNotifier, _MessageSequence {
819993
// didn't include this message.
820994
return;
821995
}
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
8231001
_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();
8241010
notifyListeners();
8251011
}
8261012

@@ -941,7 +1127,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
9411127

9421128
/// Notify listeners if the given outbox message is present in this view.
9431129
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+
}
9451135
}
9461136

9471137
/// Called when the app is reassembled during debugging, e.g. for hot reload.

0 commit comments

Comments
 (0)