Skip to content

Commit 4bf6503

Browse files
committed
api: Parse UpdateMessageMoveData with inter-related constraints
This moves some assertions so that we can make all fields non-nullable, with reasonable fallback values. As of writing, we do not use origStreamId (a.k.a.: 'stream_id') when there was no message move, even though it is present if there were content edits. This makes dropping 'stream_id' when parsing `moveData` into `null` acceptable for now. 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. Signed-off-by: Zixuan James Li <[email protected]>
1 parent 28895d4 commit 4bf6503

File tree

5 files changed

+107
-75
lines changed

5 files changed

+107
-75
lines changed

lib/api/model/events.dart

+44-11
Original file line numberDiff line numberDiff line change
@@ -703,13 +703,13 @@ class MessageEvent extends Event {
703703

704704
/// Data structure representing a message move.
705705
class UpdateMessageMoveData {
706-
final int? origStreamId;
707-
final int? newStreamId;
706+
final int origStreamId;
707+
final int newStreamId;
708708

709-
final PropagateMode? propagateMode;
709+
final PropagateMode propagateMode;
710710

711-
final TopicName? origTopic;
712-
final TopicName? newTopic;
711+
final TopicName origTopic;
712+
final TopicName newTopic;
713713

714714
UpdateMessageMoveData({
715715
required this.origStreamId,
@@ -719,11 +719,16 @@ class UpdateMessageMoveData {
719719
required this.newTopic,
720720
});
721721

722-
/// Extract [UpdateMessageMoveData] from the JSON object for an
722+
/// Try to extract [UpdateMessageMoveData] from the JSON object for an
723723
/// [UpdateMessageEvent].
724724
///
725+
/// Returns `null` if there was no message move.
726+
///
725727
/// Throws an error if the data is malformed.
726-
factory UpdateMessageMoveData.fromJson(Object? json) {
728+
// When parsing this, 'stream_id', which is also present when there was only
729+
// a content edit, cannot be recovered if this ends up returning `null`.
730+
// This may matter if we ever need 'stream_id' when no message move occurred.
731+
static UpdateMessageMoveData? tryParseFromJson(Object? json) {
727732
json as Map<String, Object?>;
728733
final origStreamId = (json['stream_id'] as num?)?.toInt();
729734
final newStreamId = (json['new_stream_id'] as num?)?.toInt();
@@ -735,12 +740,40 @@ class UpdateMessageMoveData {
735740
final newTopic = json['subject'] == null ? null
736741
: TopicName.fromJson(json['subject'] as String);
737742

743+
if (origTopic == null) {
744+
// There was no move.
745+
assert(() {
746+
if (newStreamId != null && origStreamId != null
747+
&& newStreamId != origStreamId) {
748+
// This should be impossible; `orig_subject` (aka origTopic) is
749+
// documented to be present when either the stream or topic changed.
750+
throw FormatException('stream move but no origTopic');
751+
}
752+
return true;
753+
}());
754+
return null;
755+
}
756+
757+
if (newStreamId == null && newTopic == null) {
758+
// If neither the channel nor topic name changed, nothing moved.
759+
// In that case `orig_subject` (aka origTopic) should have been null.
760+
throw FormatException('move but no newStreamId or newTopic');
761+
}
762+
if (origStreamId == null) {
763+
// The `stream_id` field (aka origStreamId) is documented to be present on moves.
764+
throw FormatException('move but no origStreamId');
765+
}
766+
if (propagateMode == null) {
767+
// The `propagate_mode` field (aka propagateMode) is documented to be present on moves.
768+
throw FormatException('move but no propagateMode');
769+
}
770+
738771
return UpdateMessageMoveData(
739772
origStreamId: origStreamId,
740-
newStreamId: newStreamId,
773+
newStreamId: newStreamId ?? origStreamId,
741774
propagateMode: propagateMode,
742775
origTopic: origTopic,
743-
newTopic: newTopic,
776+
newTopic: newTopic ?? origTopic,
744777
);
745778
}
746779

@@ -764,8 +797,8 @@ class UpdateMessageEvent extends Event {
764797

765798
// final String? streamName; // ignore
766799

767-
@JsonKey(readValue: _readMoveData)
768-
final UpdateMessageMoveData moveData;
800+
@JsonKey(readValue: _readMoveData, fromJson: UpdateMessageMoveData.tryParseFromJson)
801+
final UpdateMessageMoveData? moveData;
769802

770803
// final List<TopicLink> topicLinks; // TODO handle
771804

lib/api/model/events.g.dart

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/model/message.dart

+16-47
Original file line numberDiff line numberDiff line change
@@ -172,47 +172,12 @@ class MessageStoreImpl with MessageStore {
172172
// The interaction between the fields of these events are a bit tricky.
173173
// For reference, see: https://zulip.com/api/get-events#update_message
174174

175-
final moveData = event.moveData;
176-
final origStreamId = moveData.origStreamId;
177-
final newStreamId = moveData.newStreamId; // null if topic-only move
178-
final origTopic = moveData.origTopic;
179-
final newTopic = moveData.newTopic;
180-
final propagateMode = moveData.propagateMode;
181-
182-
if (origTopic == null) {
183-
// There was no move.
184-
assert(() {
185-
if (newStreamId != null && origStreamId != null
186-
&& newStreamId != origStreamId) {
187-
// This should be impossible; `orig_subject` (aka origTopic) is
188-
// documented to be present when either the stream or topic changed.
189-
debugLog('Malformed UpdateMessageEvent: stream move but no origTopic'); // TODO(log)
190-
}
191-
return true;
192-
}());
175+
final messageMove = event.moveData;
176+
if (messageMove == null) {
177+
// There is no message move.
193178
return;
194179
}
195180

196-
if (newStreamId == null && newTopic == null) {
197-
// If neither the channel nor topic name changed, nothing moved.
198-
// In that case `orig_subject` (aka origTopic) should have been null.
199-
assert(debugLog('Malformed UpdateMessageEvent: move but no newStreamId or newTopic')); // TODO(log)
200-
return;
201-
}
202-
if (origStreamId == null) {
203-
// The `stream_id` field (aka origStreamId) is documented to be present on moves.
204-
assert(debugLog('Malformed UpdateMessageEvent: move but no origStreamId')); // TODO(log)
205-
return;
206-
}
207-
if (propagateMode == null) {
208-
// The `propagate_mode` field (aka propagateMode) is documented to be present on moves.
209-
assert(debugLog('Malformed UpdateMessageEvent: move but no propagateMode')); // TODO(log)
210-
return;
211-
}
212-
213-
final wasResolveOrUnresolve = (newStreamId == null
214-
&& MessageEditState.topicMoveWasResolveOrUnresolve(origTopic, newTopic!));
215-
216181
for (final messageId in event.messageIds) {
217182
final message = messages[messageId];
218183
if (message == null) continue;
@@ -222,17 +187,21 @@ class MessageStoreImpl with MessageStore {
222187
continue;
223188
}
224189

225-
if (newStreamId != null) {
226-
message.streamId = newStreamId;
190+
if (messageMove.origStreamId != messageMove.newStreamId) {
191+
message.streamId = messageMove.newStreamId;
227192
// See [StreamMessage.displayRecipient] on why the invalidation is
228193
// needed.
229194
message.displayRecipient = null;
230195
}
231196

232-
if (newTopic != null) {
233-
message.topic = newTopic;
197+
if (messageMove.origTopic != messageMove.newTopic) {
198+
message.topic = messageMove.newTopic;
234199
}
235200

201+
final wasResolveOrUnresolve =
202+
messageMove.origStreamId == messageMove.newStreamId
203+
&& MessageEditState.topicMoveWasResolveOrUnresolve(
204+
messageMove.origTopic, messageMove.newTopic);
236205
if (!wasResolveOrUnresolve
237206
&& message.editState == MessageEditState.none) {
238207
message.editState = MessageEditState.moved;
@@ -241,12 +210,12 @@ class MessageStoreImpl with MessageStore {
241210

242211
for (final view in _messageListViews) {
243212
view.messagesMoved(
244-
origStreamId: origStreamId,
245-
newStreamId: newStreamId ?? origStreamId,
246-
origTopic: origTopic,
247-
newTopic: newTopic ?? origTopic,
213+
origStreamId: messageMove.origStreamId,
214+
newStreamId: messageMove.newStreamId,
215+
origTopic: messageMove.origTopic,
216+
newTopic: messageMove.newTopic,
248217
messageIds: event.messageIds,
249-
propagateMode: propagateMode,
218+
propagateMode: messageMove.propagateMode,
250219
);
251220
}
252221
}

test/api/model/events_test.dart

+43-7
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ void main() {
127127
..origStreamId.equals(1)
128128
..newStreamId.equals(2)
129129
..origTopic.equals(TopicName('foo'))
130-
..newTopic.isNull();
130+
..newTopic.equals(TopicName('foo'));
131131
});
132132

133133
test('orig_subject -> origTopic, subject -> newTopic, new_stream_id = stream_id', () {
@@ -139,7 +139,7 @@ void main() {
139139
'propagate_mode': 'change_all',
140140
})).isA<UpdateMessageEvent>().moveData.isNotNull()
141141
..origStreamId.equals(1)
142-
..newStreamId.isNull()
142+
..newStreamId.equals(1)
143143
..origTopic.equals(const TopicName('foo'))
144144
..newTopic.equals(const TopicName('bar'));
145145
});
@@ -151,11 +151,47 @@ void main() {
151151
'orig_rendered_content': 'foo',
152152
'content': 'bar',
153153
'rendered_content': 'bar',
154-
})).isA<UpdateMessageEvent>().moveData.isNotNull()
155-
..origStreamId.equals(1)
156-
..newStreamId.isNull()
157-
..origTopic.isNull()
158-
..newTopic.isNull();
154+
})).isA<UpdateMessageEvent>().moveData.isNull();
155+
});
156+
157+
test('stream move but no orig_subject', () {
158+
check(() => Event.fromJson({...baseJson,
159+
'stream_id': 1,
160+
'new_stream_id': 2,
161+
'orig_subject': null,
162+
'subject': null,
163+
'propagate_mode': 'change_all',
164+
})).throws<FormatException>();
165+
});
166+
167+
test('move but no subject or new_stream_id', () {
168+
check(() => Event.fromJson({...baseJson,
169+
'stream_id': 1,
170+
'new_stream_id': null,
171+
'orig_subject': 'foo',
172+
'subject': null,
173+
'propagate_mode': 'change_all',
174+
})).throws<FormatException>();
175+
});
176+
177+
test('move but no orig_stream_id', () {
178+
check(() => Event.fromJson({...baseJson,
179+
'stream_id': null,
180+
'new_stream_id': 2,
181+
'orig_subject': 'foo',
182+
'subject': 'bar',
183+
'propagate_mode': 'change_all',
184+
})).throws<FormatException>();
185+
});
186+
187+
test('move but no propagate_mode', () {
188+
check(() => Event.fromJson({...baseJson,
189+
'stream_id': 1,
190+
'new_stream_id': 2,
191+
'orig_subject': 'foo',
192+
'subject': 'bar',
193+
'propagate_mode': null,
194+
})).throws<FormatException>();
159195
});
160196
});
161197

test/example_data.dart

+3-9
Original file line numberDiff line numberDiff line change
@@ -653,13 +653,7 @@ UpdateMessageEvent updateMessageEditEvent(
653653
messageIds: [messageId],
654654
flags: flags ?? origMessage.flags,
655655
editTimestamp: editTimestamp ?? 1234567890, // TODO generate timestamp
656-
moveData: UpdateMessageMoveData(
657-
origStreamId: origMessage is StreamMessage ? origMessage.streamId : null,
658-
newStreamId: null,
659-
propagateMode: null,
660-
origTopic: null,
661-
newTopic: null,
662-
),
656+
moveData: null,
663657
origContent: 'some probably-mismatched old Markdown',
664658
origRenderedContent: origMessage.content,
665659
content: 'some probably-mismatched new Markdown',
@@ -694,10 +688,10 @@ UpdateMessageEvent _updateMessageMoveEvent(
694688
editTimestamp: 1234567890, // TODO generate timestamp
695689
moveData: UpdateMessageMoveData(
696690
origStreamId: origStreamId,
697-
newStreamId: newStreamId,
691+
newStreamId: newStreamId ?? origStreamId,
698692
propagateMode: propagateMode,
699693
origTopic: origTopic,
700-
newTopic: newTopic,
694+
newTopic: newTopic ?? origTopic,
701695
),
702696
origContent: origContent,
703697
origRenderedContent: origContent,

0 commit comments

Comments
 (0)