Skip to content

Commit 94f51be

Browse files
committed
message: Enhance message move checks
Because of this simplification, it is easier to make sense of the constraints that apply to the inter-related fields. This also allows us to drop the `assert`'s and "TODO(log)"'s, because the stacktrace we need can be retrieved after throwing these `FormatException`'s. This is similar to zulip-mobile code for parsing move data. The main difference is that we now also check the value of `propagate_mode`, which is documented to be present on message moves. See Greg's comment on this: zulip#1311 (comment) Signed-off-by: Zixuan James Li <[email protected]>
1 parent 85cc089 commit 94f51be

File tree

3 files changed

+96
-40
lines changed

3 files changed

+96
-40
lines changed

lib/model/message.dart

+19-40
Original file line numberDiff line numberDiff line change
@@ -173,44 +173,23 @@ class MessageStoreImpl with MessageStore {
173173
// For reference, see: https://zulip.com/api/get-events#update_message
174174

175175
final origStreamId = event.origStreamId;
176-
final newStreamId = event.newStreamId; // null if topic-only move
176+
final newStreamId = event.newStreamId ?? origStreamId;
177177
final origTopic = event.origTopic;
178-
final newTopic = event.newTopic;
178+
final newTopic = event.newTopic ?? origTopic;
179179
final propagateMode = event.propagateMode;
180180

181-
if (origTopic == null) {
181+
if (origTopic == newTopic && origStreamId == newStreamId) {
182+
if (propagateMode != null) {
183+
throw FormatException(
184+
'UpdateMessageEvent: incoherent message-move fields; '
185+
'propagate_mode present but no new channel or topic');
186+
}
182187
// There was no move.
183-
assert(() {
184-
if (newStreamId != null && origStreamId != null
185-
&& newStreamId != origStreamId) {
186-
// This should be impossible; `orig_subject` (aka origTopic) is
187-
// documented to be present when either the stream or topic changed.
188-
debugLog('Malformed UpdateMessageEvent: stream move but no origTopic'); // TODO(log)
189-
}
190-
return true;
191-
}());
192-
return;
193-
}
194-
195-
if (newStreamId == null && newTopic == null) {
196-
// If neither the channel nor topic name changed, nothing moved.
197-
// In that case `orig_subject` (aka origTopic) should have been null.
198-
assert(debugLog('Malformed UpdateMessageEvent: move but no newStreamId or newTopic')); // TODO(log)
199-
return;
200-
}
201-
if (origStreamId == null) {
202-
// The `stream_id` field (aka origStreamId) is documented to be present on moves.
203-
assert(debugLog('Malformed UpdateMessageEvent: move but no origStreamId')); // TODO(log)
204-
return;
205-
}
206-
if (propagateMode == null) {
207-
// The `propagate_mode` field (aka propagateMode) is documented to be present on moves.
208-
assert(debugLog('Malformed UpdateMessageEvent: move but no propagateMode')); // TODO(log)
209188
return;
210189
}
211190

212-
final wasResolveOrUnresolve = newStreamId == null
213-
&& MessageEditState.topicMoveWasResolveOrUnresolve(origTopic, newTopic!);
191+
final wasResolveOrUnresolve = origStreamId == newStreamId
192+
&& MessageEditState.topicMoveWasResolveOrUnresolve(origTopic!, newTopic!);
214193

215194
for (final messageId in event.messageIds) {
216195
final message = messages[messageId];
@@ -221,15 +200,15 @@ class MessageStoreImpl with MessageStore {
221200
continue;
222201
}
223202

224-
if (newStreamId != null) {
225-
message.streamId = newStreamId;
203+
if (origStreamId != newStreamId) {
204+
message.streamId = newStreamId!;
226205
// See [StreamMessage.displayRecipient] on why the invalidation is
227206
// needed.
228207
message.displayRecipient = null;
229208
}
230209

231-
if (newTopic != null) {
232-
message.topic = newTopic;
210+
if (origTopic != newTopic) {
211+
message.topic = newTopic!;
233212
}
234213

235214
if (!wasResolveOrUnresolve
@@ -240,12 +219,12 @@ class MessageStoreImpl with MessageStore {
240219

241220
for (final view in _messageListViews) {
242221
view.messagesMoved(
243-
origStreamId: origStreamId,
244-
newStreamId: newStreamId ?? origStreamId,
245-
origTopic: origTopic,
246-
newTopic: newTopic ?? origTopic,
222+
origStreamId: origStreamId!,
223+
newStreamId: newStreamId!,
224+
origTopic: origTopic!,
225+
newTopic: newTopic!,
247226
messageIds: event.messageIds,
248-
propagateMode: propagateMode,
227+
propagateMode: propagateMode!,
249228
);
250229
}
251230
}

test/api/model/model_checks.dart

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ extension TopicNameChecks on Subject<TopicName> {
5353

5454
extension StreamMessageChecks on Subject<StreamMessage> {
5555
Subject<String?> get displayRecipient => has((e) => e.displayRecipient, 'displayRecipient');
56+
Subject<int> get streamId => has((e) => e.streamId, 'streamId');
5657
Subject<TopicName> get topic => has((e) => e.topic, 'topic');
5758
}
5859

test/model/message_test.dart

+76
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,82 @@ void main() {
377377
});
378378
});
379379

380+
group('message move', () {
381+
final message = eg.streamMessage();
382+
final baseJson = {
383+
'id': 1,
384+
'type': 'update_message',
385+
'user_id': eg.selfUser.userId,
386+
'rendering_only': false,
387+
'message_id': message.id,
388+
'message_ids': [message.id],
389+
'flags': <String>[],
390+
'edit_timestamp': 1718741351,
391+
'stream_id': eg.stream().streamId,
392+
};
393+
final baseMoveJson = { ...baseJson,
394+
'orig_subject': 'foo',
395+
'propagate_mode': 'change_all',
396+
};
397+
398+
Future<void> setupAndHandleEvent(Map<String, Object?> json) async {
399+
await prepare();
400+
await prepareMessages([message]);
401+
await store.handleEvent(Event.fromJson(json) as UpdateMessageEvent);
402+
}
403+
404+
test('smoke', () async {
405+
await setupAndHandleEvent({ ...baseMoveJson,
406+
'stream_id': 1,
407+
'new_stream_id': 2,
408+
});
409+
checkNotified(count: 2);
410+
check(store).messages[message.id].isA<StreamMessage>()
411+
..topic.equals(message.topic)
412+
..streamId.equals(2);
413+
});
414+
415+
test('no message move', () async {
416+
await setupAndHandleEvent({ ...baseJson,
417+
'orig_content': 'foo',
418+
'orig_rendered_content': 'foo',
419+
'content': 'bar',
420+
'rendered_content': 'bar',
421+
});
422+
checkNotifiedOnce();
423+
});
424+
425+
test('stream move but no orig_subject', () async {
426+
await check(setupAndHandleEvent({ ...baseMoveJson,
427+
'stream_id': 1,
428+
'new_stream_id': 2,
429+
'orig_subject': null,
430+
})).throws();
431+
});
432+
433+
test('move but no subject or new_stream_id', () async {
434+
await check(setupAndHandleEvent({ ...baseMoveJson,
435+
'new_stream_id': null,
436+
'subject': null,
437+
})).throws<FormatException>();
438+
});
439+
440+
test('move but no orig_stream_id', () async {
441+
await check(setupAndHandleEvent({ ...baseMoveJson,
442+
'stream_id': null,
443+
'new_stream_id': 2,
444+
})).throws();
445+
});
446+
447+
test('move but no propagate_mode', () async {
448+
await check(setupAndHandleEvent({ ...baseMoveJson,
449+
'orig_subject': 'foo',
450+
'subject': 'bar',
451+
'propagate_mode': null,
452+
})).throws();
453+
});
454+
});
455+
380456
group('handleDeleteMessageEvent', () {
381457
test('delete an unknown message', () async {
382458
final message1 = eg.streamMessage();

0 commit comments

Comments
 (0)