Skip to content

Commit 6d8aa56

Browse files
gnpricePIG208
authored andcommitted
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. 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. 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 a684f8d commit 6d8aa56

File tree

4 files changed

+440
-21
lines changed

4 files changed

+440
-21
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: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,12 @@ class MessageStoreImpl with MessageStore {
9797
assert(event.messageIds.contains(event.messageId), "See https://github.com/zulip/zulip-flutter/pull/753#discussion_r1649463633");
9898
_handleUpdateMessageEventTimestamp(event);
9999
_handleUpdateMessageEventContent(event);
100-
_handleUpdateMessageEventMove(event);
100+
final viewsWithMovedMessages = _handleUpdateMessageEventMove(event);
101101
for (final view in _messageListViews) {
102+
if (viewsWithMovedMessages.contains(view)) {
103+
view.notifyListeners();
104+
continue;
105+
}
102106
view.notifyListenersIfAnyMessagePresent(event.messageIds);
103107
}
104108
}
@@ -144,7 +148,7 @@ class MessageStoreImpl with MessageStore {
144148
}
145149
}
146150

147-
void _handleUpdateMessageEventMove(UpdateMessageEvent event) {
151+
Set<MessageListView> _handleUpdateMessageEventMove(UpdateMessageEvent event) {
148152
// The interaction between the fields of these events are a bit tricky.
149153
// For reference, see: https://zulip.com/api/get-events#update_message
150154

@@ -164,37 +168,60 @@ class MessageStoreImpl with MessageStore {
164168
}
165169
return true;
166170
}());
167-
return;
171+
return {};
168172
}
169173

170174
if (newTopic == null) {
171175
// The `subject` field (aka newTopic) is documented to be present on moves.
172176
assert(debugLog('Malformed UpdateMessageEvent: move but no newTopic')); // TODO(log)
173-
return;
177+
return {};
174178
}
175179
if (origStreamId == null) {
176180
// The `stream_id` field (aka origStreamId) is documented to be present on moves.
177181
assert(debugLog('Malformed UpdateMessageEvent: move but no origStreamId')); // TODO(log)
178-
return;
182+
return {};
179183
}
180184

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-
}
185+
final wasResolveOrUnresolve = (newStreamId == null
186+
&& MessageEditState.topicMoveWasResolveOrUnresolve(origTopic, newTopic));
187187

188-
// TODO(#150): Handle message moves. The views' recipient headers
189-
// may need updating, and consequently showSender too.
190-
// Currently only editState gets updated.
191188
for (final messageId in event.messageIds) {
192189
final message = messages[messageId];
193190
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;
191+
192+
if (message is! StreamMessage) {
193+
assert(debugLog('Bad UpdateMessageEvent: stream/topic move on a DM')); // TODO(log)
194+
continue;
195+
}
196+
197+
if (newStreamId != null) {
198+
message.streamId = newStreamId;
199+
// TODO update [StreamMessage.displayRecipient]; doesn't usually
200+
// matter, because we only consult it when the stream is unknown
201+
}
202+
203+
message.topic = newTopic;
204+
205+
if (!wasResolveOrUnresolve
206+
&& message.editState == MessageEditState.none) {
207+
message.editState = MessageEditState.moved;
208+
}
209+
}
210+
211+
212+
final viewsWithMovedMessages = <MessageListView>{};
213+
for (final view in _messageListViews) {
214+
if (view.messagesMoved(
215+
origStreamId: origStreamId,
216+
newStreamId: newStreamId ?? origStreamId,
217+
origTopic: origTopic,
218+
newTopic: newTopic,
219+
messageIds: event.messageIds,
220+
)) {
221+
viewsWithMovedMessages.add(view);
222+
}
197223
}
224+
return viewsWithMovedMessages;
198225
}
199226

200227
void handleDeleteMessageEvent(DeleteMessageEvent event) {

lib/model/message_list.dart

Lines changed: 86 additions & 2 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) {
@@ -448,8 +466,10 @@ class MessageListView with ChangeNotifier, _MessageSequence {
448466
_haveOldest = result.foundOldest;
449467
} finally {
450468
_fetchingOlder = false;
451-
_updateEndMarkers();
452-
notifyListeners();
469+
if (this.generation == generation) {
470+
_updateEndMarkers();
471+
notifyListeners();
472+
}
453473
}
454474
}
455475

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

508+
bool _messagesMovedInternally(List<int> messageIds) {
509+
for (final messageId in messageIds) {
510+
if (_findMessageWithId(messageId) != -1) {
511+
_reprocessAll();
512+
return true;
513+
}
514+
}
515+
return false;
516+
}
517+
518+
bool _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+
fetchInitial();
525+
return true;
526+
}
527+
528+
bool _messagesMovedFromNarrow(List<int> messageIds) {
529+
return _removeMessagesById(messageIds);
530+
}
531+
532+
bool 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 false;
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+
return _messagesMovedInternally(messageIds);
551+
552+
case StreamNarrow(:final streamId):
553+
switch ((origStreamId == streamId, newStreamId == streamId)) {
554+
case (false, false): return false;
555+
case (true, true ): return _messagesMovedInternally(messageIds);
556+
case (false, true ): return _messagesMovedIntoNarrow();
557+
case (true, false): return _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 false;
565+
case (true, true ): return _messagesMovedInternally(messageIds);
566+
case (false, true ): return _messagesMovedIntoNarrow();
567+
case (true, false): return _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)