Skip to content

Commit bdb5f68

Browse files
committed
wip 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. TODO: tests. There's a variety of kinds of test cases this needs, but here's one in particular that may not be apparent: 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. Also a related race bug where it's fetchInitial, and it lacks the moved messages. So there should be test cases exercising each of those situations, which would fail if that line gets commented out. TODO for followup commits: * Update StreamMessage.displayRecipient. If we wanted, we could update it by looking up the new stream's name. To enable MessageStoreImpl to do that, we'd pass it a StreamStore, the same way we do with Unreads. But in fact the only time displayRecipient is useful is when we don't have data on that stream. So there's not much point in looking it up here. Instead, make the field nullable, and just set it to null when the message is moved between streams. * Handle UpdateMessageEvent.propagateMode. This probably means _MessageListPageState having its own notion of the current narrow, which is initialized with `widget.narrow` but can change. Then `_MessageListState._modelChanged` can check if the model's narrow has changed, and if so tell the page state to update. For comparison, see zulip-mobile's `src/events/doEventActionSideEffects.js`, or web.
1 parent d8f2f89 commit bdb5f68

File tree

3 files changed

+116
-14
lines changed

3 files changed

+116
-14
lines changed

lib/api/model/model.dart

+2-2
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

+30-12
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

+84
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)) {
@@ -424,13 +440,15 @@ class MessageListView with ChangeNotifier, _MessageSequence {
424440
_updateEndMarkers();
425441
notifyListeners();
426442
try {
443+
final generation = this.generation;
427444
final result = await getMessages(store.connection,
428445
narrow: narrow.apiEncode(),
429446
anchor: NumericAnchor(messages[0].id),
430447
includeAnchor: false,
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) {
@@ -485,6 +503,72 @@ class MessageListView with ChangeNotifier, _MessageSequence {
485503
}
486504
}
487505

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

0 commit comments

Comments
 (0)