Skip to content

Commit 9c50ba5

Browse files
committed
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.
1 parent aef0ab1 commit 9c50ba5

File tree

5 files changed

+524
-44
lines changed

5 files changed

+524
-44
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;
@@ -577,7 +577,7 @@ class StreamMessage extends Message {
577577
String get type => 'stream';
578578

579579
final String displayRecipient;
580-
final int streamId;
580+
int streamId;
581581

582582
StreamMessage({
583583
required super.client,

lib/model/message.dart

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -178,22 +178,40 @@ class MessageStoreImpl with MessageStore {
178178
return;
179179
}
180180

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

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

lib/model/message_list.dart

Lines changed: 89 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);
@@ -396,12 +410,14 @@ class MessageListView with ChangeNotifier, _MessageSequence {
396410
assert(!fetched && !haveOldest && !fetchingOlder);
397411
assert(messages.isEmpty && contents.isEmpty);
398412
// TODO schedule all this in another isolate
413+
final generation = this.generation;
399414
final result = await getMessages(store.connection,
400415
narrow: narrow.apiEncode(),
401416
anchor: AnchorCode.newest,
402417
numBefore: kMessageListFetchBatchSize,
403418
numAfter: 0,
404419
);
420+
if (this.generation > generation) return;
405421
store.reconcileMessages(result.messages);
406422
for (final message in result.messages) {
407423
if (_messageVisible(message)) {
@@ -423,6 +439,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
423439
_fetchingOlder = true;
424440
_updateEndMarkers();
425441
notifyListeners();
442+
final generation = this.generation;
426443
try {
427444
final result = await getMessages(store.connection,
428445
narrow: narrow.apiEncode(),
@@ -431,6 +448,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
431448
numBefore: kMessageListFetchBatchSize,
432449
numAfter: 0,
433450
);
451+
if (this.generation > generation) return;
434452

435453
if (result.messages.isNotEmpty
436454
&& result.messages.last.id == messages[0].id) {
@@ -447,9 +465,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
447465
_insertAllMessages(0, fetchedMessages);
448466
_haveOldest = result.foundOldest;
449467
} finally {
450-
_fetchingOlder = false;
451-
_updateEndMarkers();
452-
notifyListeners();
468+
if (this.generation == generation) {
469+
_fetchingOlder = false;
470+
_updateEndMarkers();
471+
notifyListeners();
472+
}
453473
}
454474
}
455475

@@ -485,6 +505,72 @@ class MessageListView with ChangeNotifier, _MessageSequence {
485505
}
486506
}
487507

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

0 commit comments

Comments
 (0)