Skip to content

Commit 5e609f0

Browse files
gnpricePIG208
andcommitted
message: Handle moved messages from UpdateMessageEvent.
We already handle the case where only a message's content is edited. This handles the case where messages are moved too, between topics and/or channels. This introduces more notifyListener calls, and the listeners can be notified more than once per UpdateMessage event. This is expected. --- If the `generation += 1` line is commented out, the message list has a race bug where a fetchOlder starts; we reset (because messages were moved into the narrow); and then the fetch returns and appends in the wrong spot. These races are detailed in the "fetch races" test group. --- StreamMessage.displayRecipient no longer carries the up-to-date display name of the stream when the message has been moved to another stream. We invalidate it by setting it to null. The only time StreamMessage.displayRecipient is useful is when we don't have data on that stream. This is the reason why we don't go ahead and lookup the stream store when such a stream move happen, because the stream store likely will not have that information if we ever need to use displayRecipient as the fallback. --- We have relatively more tests for the topic moves, because there are more cases that make a difference to consider. There is some overlap between the tests like "(channel, topic) -> (new channel, new topic)" and other tests where only one of topic/channel is changed. We include both because the parameterized test cases are easy to modify and it doesn't hurt to cover another realistic scenario handled by the same code path. Co-authored-by: Zixuan James Li <[email protected]> Signed-off-by: Zixuan James Li <[email protected]>
1 parent b97c9c8 commit 5e609f0

File tree

5 files changed

+548
-26
lines changed

5 files changed

+548
-26
lines changed

lib/api/model/model.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ sealed class Message {
480480
final int senderId;
481481
final String senderRealmStr;
482482
@JsonKey(name: 'subject')
483-
final String topic;
483+
String topic;
484484
// final List<string> submessages; // TODO handle
485485
final int timestamp;
486486
String get type;
@@ -581,7 +581,7 @@ class StreamMessage extends Message {
581581
// invalidated.
582582
@JsonKey(required: true, disallowNullValue: true)
583583
String? displayRecipient;
584-
final int streamId;
584+
int streamId;
585585

586586
StreamMessage({
587587
required super.client,

lib/model/message.dart

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -179,22 +179,43 @@ class MessageStoreImpl with MessageStore {
179179
return;
180180
}
181181

182-
if (newStreamId == null
183-
&& MessageEditState.topicMoveWasResolveOrUnresolve(origTopic, newTopic!)) {
184-
// The topic was only resolved/unresolved.
185-
// No change to the messages' editState.
186-
return;
187-
}
182+
final wasResolveOrUnresolve = (newStreamId == null
183+
&& MessageEditState.topicMoveWasResolveOrUnresolve(origTopic, newTopic!));
188184

189-
// TODO(#150): Handle message moves. The views' recipient headers
190-
// may need updating, and consequently showSender too.
191-
// Currently only editState gets updated.
192185
for (final messageId in event.messageIds) {
193186
final message = messages[messageId];
194187
if (message == null) continue;
195-
// Do not override the edited marker if the message has also been moved.
196-
if (message.editState == MessageEditState.edited) continue;
197-
message.editState = MessageEditState.moved;
188+
189+
if (message is! StreamMessage) {
190+
assert(debugLog('Bad UpdateMessageEvent: stream/topic move on a DM')); // TODO(log)
191+
continue;
192+
}
193+
194+
if (newStreamId != null) {
195+
message.streamId = newStreamId;
196+
// See [StreamMessage.displayRecipient] on why the invalidation is
197+
// needed.
198+
message.displayRecipient = null;
199+
}
200+
201+
if (newTopic != null) {
202+
message.topic = newTopic;
203+
}
204+
205+
if (!wasResolveOrUnresolve
206+
&& message.editState == MessageEditState.none) {
207+
message.editState = MessageEditState.moved;
208+
}
209+
}
210+
211+
for (final view in _messageListViews) {
212+
view.messagesMoved(
213+
origStreamId: origStreamId,
214+
newStreamId: newStreamId ?? origStreamId,
215+
origTopic: origTopic,
216+
newTopic: newTopic ?? origTopic,
217+
messageIds: event.messageIds,
218+
);
198219
}
199220
}
200221

lib/model/message_list.dart

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ class MessageListHistoryStartItem extends MessageListItem {
6565
///
6666
/// This comprises much of the guts of [MessageListView].
6767
mixin _MessageSequence {
68+
/// A sequence number for invalidating stale fetches.
69+
int generation = 0;
70+
6871
/// The messages.
6972
///
7073
/// See also [contents] and [items].
@@ -192,6 +195,17 @@ mixin _MessageSequence {
192195
_reprocessAll();
193196
}
194197

198+
/// Reset all [_MessageSequence] data, and cancel any active fetches.
199+
void _reset() {
200+
generation += 1;
201+
messages.clear();
202+
_fetched = false;
203+
_haveOldest = false;
204+
_fetchingOlder = false;
205+
contents.clear();
206+
items.clear();
207+
}
208+
195209
/// Redo all computations from scratch, based on [messages].
196210
void _recompute() {
197211
assert(contents.length == messages.length);
@@ -398,12 +412,14 @@ class MessageListView with ChangeNotifier, _MessageSequence {
398412
assert(!fetched && !haveOldest && !fetchingOlder);
399413
assert(messages.isEmpty && contents.isEmpty);
400414
// TODO schedule all this in another isolate
415+
final generation = this.generation;
401416
final result = await getMessages(store.connection,
402417
narrow: narrow.apiEncode(),
403418
anchor: AnchorCode.newest,
404419
numBefore: kMessageListFetchBatchSize,
405420
numAfter: 0,
406421
);
422+
if (this.generation > generation) return;
407423
store.reconcileMessages(result.messages);
408424
store.recentSenders.handleMessages(result.messages); // TODO(#824)
409425
for (final message in result.messages) {
@@ -426,6 +442,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
426442
_fetchingOlder = true;
427443
_updateEndMarkers();
428444
notifyListeners();
445+
final generation = this.generation;
429446
try {
430447
final result = await getMessages(store.connection,
431448
narrow: narrow.apiEncode(),
@@ -434,6 +451,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
434451
numBefore: kMessageListFetchBatchSize,
435452
numAfter: 0,
436453
);
454+
if (this.generation > generation) return;
437455

438456
if (result.messages.isNotEmpty
439457
&& result.messages.last.id == messages[0].id) {
@@ -451,9 +469,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
451469
_insertAllMessages(0, fetchedMessages);
452470
_haveOldest = result.foundOldest;
453471
} finally {
454-
_fetchingOlder = false;
455-
_updateEndMarkers();
456-
notifyListeners();
472+
if (this.generation == generation) {
473+
_fetchingOlder = false;
474+
_updateEndMarkers();
475+
notifyListeners();
476+
}
457477
}
458478
}
459479

@@ -489,6 +509,73 @@ class MessageListView with ChangeNotifier, _MessageSequence {
489509
}
490510
}
491511

512+
void _messagesMovedInternally(List<int> messageIds) {
513+
for (final messageId in messageIds) {
514+
if (_findMessageWithId(messageId) != -1) {
515+
_reprocessAll();
516+
notifyListeners();
517+
return;
518+
}
519+
}
520+
}
521+
522+
void _messagesMovedIntoNarrow() {
523+
// If there are some messages we don't have in [MessageStore], and they
524+
// occur later than the messages we have here, then we just have to
525+
// re-fetch from scratch. That's always valid, so just do that always.
526+
// TODO in cases where we do have data to do better, do better.
527+
_reset();
528+
notifyListeners();
529+
fetchInitial();
530+
}
531+
532+
void _messagesMovedFromNarrow(List<int> messageIds) {
533+
if (_removeMessagesById(messageIds)) {
534+
notifyListeners();
535+
}
536+
}
537+
538+
void messagesMoved({
539+
required int origStreamId,
540+
required int newStreamId,
541+
required String origTopic,
542+
required String newTopic,
543+
required List<int> messageIds,
544+
}) {
545+
switch (narrow) {
546+
case DmNarrow():
547+
// DMs can't be moved (nor created by moves),
548+
// so the messages weren't in this narrow and still aren't.
549+
return;
550+
551+
case CombinedFeedNarrow():
552+
case MentionsNarrow():
553+
// The messages were and remain in this narrow.
554+
// TODO(#421): … except they may have become muted or not.
555+
// We'll handle that at the same time as we handle muting itself changing.
556+
// Recipient headers, and downstream of those, may change, though.
557+
_messagesMovedInternally(messageIds);
558+
559+
case ChannelNarrow(:final streamId):
560+
switch ((origStreamId == streamId, newStreamId == streamId)) {
561+
case (false, false): return;
562+
case (true, true ): _messagesMovedInternally(messageIds);
563+
case (false, true ): _messagesMovedIntoNarrow();
564+
case (true, false): _messagesMovedFromNarrow(messageIds);
565+
}
566+
567+
case TopicNarrow(:final streamId, :final topic):
568+
final oldMatch = (origStreamId == streamId && origTopic == topic);
569+
final newMatch = (newStreamId == streamId && newTopic == topic);
570+
switch ((oldMatch, newMatch)) {
571+
case (false, false): return;
572+
case (true, true ): return; // TODO(log) no-op move
573+
case (false, true ): _messagesMovedIntoNarrow();
574+
case (true, false): _messagesMovedFromNarrow(messageIds); // TODO handle propagateMode
575+
}
576+
}
577+
}
578+
492579
// Repeal the `@protected` annotation that applies on the base implementation,
493580
// so we can call this method from [MessageStoreImpl].
494581
@override

0 commit comments

Comments
 (0)