Skip to content

Commit e9fac62

Browse files
committed
api: Extract and parse UpdateMessageMoveData from UpdateMessageEvent
This data structure encapsulates some checks 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. This is similar to zulip-mobile code for parsing move data. The main difference is that we check the value of `propagate_mode`, which is documented to be present on message moves. With this single indicator, the logic is crisp for ruling out non-move message update events. See Greg's comment on this: zulip#1311 (comment) Signed-off-by: Zixuan James Li <[email protected]>
1 parent c961157 commit e9fac62

File tree

6 files changed

+201
-99
lines changed

6 files changed

+201
-99
lines changed

lib/api/model/events.dart

+73-15
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,70 @@ class MessageEvent extends Event {
701701
}
702702
}
703703

704+
/// Data structure representing a message move.
705+
class UpdateMessageMoveData {
706+
final int origStreamId;
707+
final int newStreamId;
708+
709+
final PropagateMode propagateMode;
710+
711+
final TopicName origTopic;
712+
final TopicName newTopic;
713+
714+
UpdateMessageMoveData({
715+
required this.origStreamId,
716+
required this.newStreamId,
717+
required this.propagateMode,
718+
required this.origTopic,
719+
required this.newTopic,
720+
}) : assert(origStreamId != newStreamId || origTopic != newTopic);
721+
722+
/// Try to extract [UpdateMessageMoveData] from the JSON object for an
723+
/// [UpdateMessageEvent].
724+
///
725+
/// Returns `null` if there was no message move.
726+
///
727+
/// Throws an error if the data is malformed.
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(Map<String, Object?> json) {
732+
final origStreamId = (json['stream_id'] as num?)?.toInt();
733+
final newStreamId = (json['new_stream_id'] as num?)?.toInt() ?? origStreamId;
734+
final propagateModeString = json['propagate_mode'] as String?;
735+
final propagateMode = propagateModeString == null ? null
736+
: PropagateMode.fromRawString(propagateModeString);
737+
final origTopic = json['orig_subject'] == null ? null
738+
: TopicName.fromJson(json['orig_subject'] as String);
739+
final newTopic = json['subject'] == null ? origTopic
740+
: TopicName.fromJson(json['subject'] as String);
741+
742+
if (propagateMode == null) {
743+
if (origTopic != newTopic || origStreamId != newStreamId) {
744+
throw FormatException('move but no propagateMode');
745+
}
746+
// There was no move.
747+
return null;
748+
}
749+
750+
if (origStreamId == newStreamId && origTopic == newTopic) {
751+
// If neither the channel nor topic name changed, nothing moved.
752+
// In that case `propagate_mode` (aka propagateMode) should have been null.
753+
throw FormatException('move but unchanged newStreamId and newTopic');
754+
}
755+
756+
return UpdateMessageMoveData(
757+
origStreamId: origStreamId!,
758+
newStreamId: newStreamId!,
759+
propagateMode: propagateMode,
760+
origTopic: origTopic!,
761+
newTopic: newTopic!,
762+
);
763+
}
764+
765+
Object? toJson() => null;
766+
}
767+
704768
/// A Zulip event of type `update_message`: https://zulip.com/api/get-events#update_message
705769
@JsonSerializable(fieldRename: FieldRename.snake)
706770
class UpdateMessageEvent extends Event {
@@ -718,16 +782,8 @@ class UpdateMessageEvent extends Event {
718782

719783
// final String? streamName; // ignore
720784

721-
@JsonKey(name: 'stream_id')
722-
final int? origStreamId;
723-
final int? newStreamId;
724-
725-
final PropagateMode? propagateMode;
726-
727-
@JsonKey(name: 'orig_subject')
728-
final TopicName? origTopic;
729-
@JsonKey(name: 'subject')
730-
final TopicName? newTopic;
785+
@JsonKey(readValue: _readMoveData, fromJson: UpdateMessageMoveData.tryParseFromJson)
786+
final UpdateMessageMoveData? moveData;
731787

732788
// final List<TopicLink> topicLinks; // TODO handle
733789

@@ -747,18 +803,20 @@ class UpdateMessageEvent extends Event {
747803
required this.messageIds,
748804
required this.flags,
749805
required this.editTimestamp,
750-
required this.origStreamId,
751-
required this.newStreamId,
752-
required this.propagateMode,
753-
required this.origTopic,
754-
required this.newTopic,
806+
required this.moveData,
755807
required this.origContent,
756808
required this.origRenderedContent,
757809
required this.content,
758810
required this.renderedContent,
759811
required this.isMeMessage,
760812
});
761813

814+
static Map<String, Object?> _readMoveData(Map<Object?, Object?> json, String key) {
815+
// Parsing [UpdateMessageMoveData] requires `json`, not the default `json[key]`.
816+
assert(json is Map<String, Object?>); // value came through `fromJson` with this type
817+
return json as Map<String, Object?>;
818+
}
819+
762820
factory UpdateMessageEvent.fromJson(Map<String, dynamic> json) =>
763821
_$UpdateMessageEventFromJson(json);
764822

lib/api/model/events.g.dart

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

lib/model/message.dart

+16-46
Original file line numberDiff line numberDiff line change
@@ -172,46 +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 origStreamId = event.origStreamId;
176-
final newStreamId = event.newStreamId; // null if topic-only move
177-
final origTopic = event.origTopic;
178-
final newTopic = event.newTopic;
179-
final propagateMode = event.propagateMode;
180-
181-
if (origTopic == null) {
182-
// 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-
}());
175+
final messageMove = event.moveData;
176+
if (messageMove == null) {
177+
// There is no message move.
192178
return;
193179
}
194180

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)
209-
return;
210-
}
211-
212-
final wasResolveOrUnresolve = (newStreamId == null
213-
&& MessageEditState.topicMoveWasResolveOrUnresolve(origTopic, newTopic!));
214-
215181
for (final messageId in event.messageIds) {
216182
final message = messages[messageId];
217183
if (message == null) continue;
@@ -221,17 +187,21 @@ class MessageStoreImpl with MessageStore {
221187
continue;
222188
}
223189

224-
if (newStreamId != null) {
225-
message.streamId = newStreamId;
190+
if (messageMove.origStreamId != messageMove.newStreamId) {
191+
message.streamId = messageMove.newStreamId;
226192
// See [StreamMessage.displayRecipient] on why the invalidation is
227193
// needed.
228194
message.displayRecipient = null;
229195
}
230196

231-
if (newTopic != null) {
232-
message.topic = newTopic;
197+
if (messageMove.origTopic != messageMove.newTopic) {
198+
message.topic = messageMove.newTopic;
233199
}
234200

201+
final wasResolveOrUnresolve =
202+
messageMove.origStreamId == messageMove.newStreamId
203+
&& MessageEditState.topicMoveWasResolveOrUnresolve(
204+
messageMove.origTopic, messageMove.newTopic);
235205
if (!wasResolveOrUnresolve
236206
&& message.editState == MessageEditState.none) {
237207
message.editState = MessageEditState.moved;
@@ -240,12 +210,12 @@ class MessageStoreImpl with MessageStore {
240210

241211
for (final view in _messageListViews) {
242212
view.messagesMoved(
243-
origStreamId: origStreamId,
244-
newStreamId: newStreamId ?? origStreamId,
245-
origTopic: origTopic,
246-
newTopic: newTopic ?? origTopic,
213+
origStreamId: messageMove.origStreamId,
214+
newStreamId: messageMove.newStreamId,
215+
origTopic: messageMove.origTopic,
216+
newTopic: messageMove.newTopic,
247217
messageIds: event.messageIds,
248-
propagateMode: propagateMode,
218+
propagateMode: messageMove.propagateMode,
249219
);
250220
}
251221
}

test/api/model/events_checks.dart

+10-5
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,23 @@ extension UpdateMessageEventChecks on Subject<UpdateMessageEvent> {
4848
Subject<List<int>> get messageIds => has((e) => e.messageIds, 'messageIds');
4949
Subject<List<MessageFlag>> get flags => has((e) => e.flags, 'flags');
5050
Subject<int?> get editTimestamp => has((e) => e.editTimestamp, 'editTimestamp');
51-
Subject<int?> get origStreamId => has((e) => e.origStreamId, 'origStreamId');
52-
Subject<int?> get newStreamId => has((e) => e.newStreamId, 'newStreamId');
53-
Subject<PropagateMode?> get propagateMode => has((e) => e.propagateMode, 'propagateMode');
54-
Subject<TopicName?> get origTopic => has((e) => e.origTopic, 'origTopic');
55-
Subject<TopicName?> get newTopic => has((e) => e.newTopic, 'newTopic');
51+
Subject<UpdateMessageMoveData?> get moveData => has((e) => e.moveData, 'moveData');
5652
Subject<String?> get origContent => has((e) => e.origContent, 'origContent');
5753
Subject<String?> get origRenderedContent => has((e) => e.origRenderedContent, 'origRenderedContent');
5854
Subject<String?> get content => has((e) => e.content, 'content');
5955
Subject<String?> get renderedContent => has((e) => e.renderedContent, 'renderedContent');
6056
Subject<bool?> get isMeMessage => has((e) => e.isMeMessage, 'isMeMessage');
6157
}
6258

59+
60+
extension UpdateMessageMoveDataChecks on Subject<UpdateMessageMoveData> {
61+
Subject<int?> get origStreamId => has((e) => e.origStreamId, 'origStreamId');
62+
Subject<int?> get newStreamId => has((e) => e.newStreamId, 'newStreamId');
63+
Subject<PropagateMode?> get propagateMode => has((e) => e.propagateMode, 'propagateMode');
64+
Subject<TopicName?> get origTopic => has((e) => e.origTopic, 'origTopic');
65+
Subject<TopicName?> get newTopic => has((e) => e.newTopic, 'newTopic');
66+
}
67+
6368
extension DeleteMessageEventChecks on Subject<DeleteMessageEvent> {
6469
Subject<MessageType?> get messageType => has((e) => e.messageType, 'messageType');
6570
}

0 commit comments

Comments
 (0)