-
Notifications
You must be signed in to change notification settings - Fork 306
Handle moved messages for unreads #1311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5f7cccd
2641320
00685c5
d396e28
5149268
6c3ee14
0e4f455
5dc21f7
98d6ec1
34e6201
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -718,16 +718,8 @@ class UpdateMessageEvent extends Event { | |
|
||
// final String? streamName; // ignore | ||
|
||
@JsonKey(name: 'stream_id') | ||
final int? origStreamId; | ||
final int? newStreamId; | ||
|
||
final PropagateMode? propagateMode; | ||
|
||
@JsonKey(name: 'orig_subject') | ||
final TopicName? origTopic; | ||
@JsonKey(name: 'subject') | ||
final TopicName? newTopic; | ||
@JsonKey(readValue: _readMoveData, fromJson: UpdateMessageMoveData.tryParseFromJson, includeToJson: false) | ||
final UpdateMessageMoveData? moveData; | ||
|
||
// final List<TopicLink> topicLinks; // TODO handle | ||
|
||
|
@@ -747,25 +739,93 @@ class UpdateMessageEvent extends Event { | |
required this.messageIds, | ||
required this.flags, | ||
required this.editTimestamp, | ||
required this.origStreamId, | ||
required this.newStreamId, | ||
required this.propagateMode, | ||
required this.origTopic, | ||
required this.newTopic, | ||
required this.moveData, | ||
required this.origContent, | ||
required this.origRenderedContent, | ||
required this.content, | ||
required this.renderedContent, | ||
required this.isMeMessage, | ||
}); | ||
|
||
static Map<String, dynamic> _readMoveData(Map<dynamic, dynamic> json, String key) { | ||
// Parsing [UpdateMessageMoveData] requires `json`, not the default `json[key]`. | ||
assert(json is Map<String, dynamic>); // value came through `fromJson` with this type | ||
return json as Map<String, dynamic>; | ||
} | ||
|
||
factory UpdateMessageEvent.fromJson(Map<String, dynamic> json) => | ||
_$UpdateMessageEventFromJson(json); | ||
|
||
@override | ||
Map<String, dynamic> toJson() => _$UpdateMessageEventToJson(this); | ||
} | ||
|
||
/// Data structure representing a message move. | ||
class UpdateMessageMoveData { | ||
final int origStreamId; | ||
final int newStreamId; | ||
final TopicName origTopic; | ||
final TopicName newTopic; | ||
final PropagateMode propagateMode; | ||
|
||
UpdateMessageMoveData({ | ||
required this.origStreamId, | ||
required this.newStreamId, | ||
required this.origTopic, | ||
required this.newTopic, | ||
required this.propagateMode, | ||
}) : assert(newStreamId != origStreamId || newTopic != origTopic); | ||
|
||
/// Try to extract [UpdateMessageMoveData] from the JSON object for an | ||
/// [UpdateMessageEvent]. | ||
/// | ||
/// Returns `null` if there was no message move. | ||
/// | ||
/// Throws an error if the data is malformed. | ||
// When parsing this, 'stream_id', which is also present when there was only | ||
// a content edit, cannot be recovered if this ends up returning `null`. | ||
// This may matter if we ever need 'stream_id' when no message move occurred. | ||
static UpdateMessageMoveData? tryParseFromJson(Map<String, Object?> json) { | ||
final origStreamId = (json['stream_id'] as num?)?.toInt(); | ||
final newStreamIdRaw = (json['new_stream_id'] as num?)?.toInt(); | ||
final newStreamId = newStreamIdRaw ?? origStreamId; | ||
|
||
final origTopic = json['orig_subject'] == null ? null | ||
: TopicName.fromJson(json['orig_subject'] as String); | ||
final newTopicRaw = json['subject'] == null ? null | ||
: TopicName.fromJson(json['subject'] as String); | ||
final newTopic = newTopicRaw ?? origTopic; | ||
|
||
final propagateModeString = json['propagate_mode'] as String?; | ||
final propagateMode = propagateModeString == null ? null | ||
: PropagateMode.fromRawString(propagateModeString); | ||
|
||
if (newStreamId == origStreamId && newTopic == origTopic) { | ||
if (propagateMode != null) { | ||
throw FormatException( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit in commit message:
I see this second sentence as not so closely related to the first one; the first sentence is already true without any caveats related to the Instead let's have a short paragraph at the end, after the "This change is useful" paragraph:
|
||
'Malformed UpdateMessageEvent: incoherent message-move fields; ' | ||
'propagate_mode present but no new channel or topic'); | ||
} | ||
return null; | ||
} | ||
|
||
return UpdateMessageMoveData( | ||
// The `stream_id` field (aka origStreamId) is documented to be present on moves; | ||
// newStreamId should not be null either because it falls back to origStreamId. | ||
origStreamId: origStreamId!, | ||
newStreamId: newStreamId!, | ||
|
||
// The `orig_subject` field (aka origTopic) is documented to be present on moves; | ||
// newTopic should not be null either because it falls back to origTopic. | ||
origTopic: origTopic!, | ||
newTopic: newTopic!, | ||
|
||
// The `propagate_mode` field (aka propagateMode) is documented to be present on moves. | ||
propagateMode: propagateMode!, | ||
); | ||
} | ||
} | ||
|
||
/// A Zulip event of type `delete_message`: https://zulip.com/api/get-events#delete_message | ||
@JsonSerializable(fieldRename: FieldRename.snake) | ||
class DeleteMessageEvent extends Event { | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -954,4 +954,15 @@ enum PropagateMode { | |
changeAll; | ||
|
||
String toJson() => _$PropagateModeEnumMap[this]!; | ||
|
||
/// Get a [PropagateMode] from a raw string. Throws if the string is | ||
/// unrecognized. | ||
/// | ||
/// Example: | ||
/// 'change_one' -> PropagateMode.changeOne | ||
static PropagateMode fromRawString(String raw) => _byRawString[raw]!; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: use "api" for prefix
|
||
|
||
// _$…EnumMap is thanks to `alwaysCreate: true` and `fieldRename: FieldRename.snake` | ||
static final _byRawString = _$PropagateModeEnumMap | ||
.map((key, value) => MapEntry(value, key)); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -169,48 +169,17 @@ class MessageStoreImpl with MessageStore { | |
} | ||
|
||
void _handleUpdateMessageEventMove(UpdateMessageEvent event) { | ||
// The interaction between the fields of these events are a bit tricky. | ||
// For reference, see: https://zulip.com/api/get-events#update_message | ||
|
||
final origStreamId = event.origStreamId; | ||
final newStreamId = event.newStreamId; // null if topic-only move | ||
final origTopic = event.origTopic; | ||
final newTopic = event.newTopic; | ||
final propagateMode = event.propagateMode; | ||
|
||
if (origTopic == null) { | ||
final messageMove = event.moveData; | ||
if (messageMove == null) { | ||
// There was no move. | ||
assert(() { | ||
if (newStreamId != null && origStreamId != null | ||
&& newStreamId != origStreamId) { | ||
// This should be impossible; `orig_subject` (aka origTopic) is | ||
// documented to be present when either the stream or topic changed. | ||
debugLog('Malformed UpdateMessageEvent: stream move but no origTopic'); // TODO(log) | ||
} | ||
return true; | ||
}()); | ||
return; | ||
} | ||
|
||
if (newStreamId == null && newTopic == null) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Does this really make the null-checks more comprehensive? The old null-checks look pretty comprehensive to me — there's the one on this line, and two more below it. In particular, is there a case where this version rejects an event which the old code would have acted on? It seems to me like the main point of this commit is the way it moves the two fallbacks like |
||
// If neither the channel nor topic name changed, nothing moved. | ||
// In that case `orig_subject` (aka origTopic) should have been null. | ||
Comment on lines
-195
to
-197
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you try splitting this commit: into one commit that introduces the new logic for interpreting these, but keeps it here in this method, and then a commit that introduces UpdateMessageMoveData and moves the logic there? I think that'd be helpful for reading the changes. In particular it'd be helpful if that second commit is NFC for debug builds — i.e. if its only functional change is that in some conditions where the old code would assert, the new code will throw while parsing. |
||
assert(debugLog('Malformed UpdateMessageEvent: move but no newStreamId or newTopic')); // TODO(log) | ||
return; | ||
} | ||
if (origStreamId == null) { | ||
// The `stream_id` field (aka origStreamId) is documented to be present on moves. | ||
assert(debugLog('Malformed UpdateMessageEvent: move but no origStreamId')); // TODO(log) | ||
return; | ||
} | ||
if (propagateMode == null) { | ||
// The `propagate_mode` field (aka propagateMode) is documented to be present on moves. | ||
assert(debugLog('Malformed UpdateMessageEvent: move but no propagateMode')); // TODO(log) | ||
return; | ||
} | ||
final UpdateMessageMoveData( | ||
:origStreamId, :newStreamId, :origTopic, :newTopic) = messageMove; | ||
|
||
final wasResolveOrUnresolve = (newStreamId == null | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This change is trivial and self-explanatory, so it can just get squashed into the substantive change that's touchinig the same code. |
||
&& MessageEditState.topicMoveWasResolveOrUnresolve(origTopic, newTopic!)); | ||
final wasResolveOrUnresolve = newStreamId == origStreamId | ||
&& MessageEditState.topicMoveWasResolveOrUnresolve(origTopic, newTopic); | ||
|
||
for (final messageId in event.messageIds) { | ||
final message = messages[messageId]; | ||
|
@@ -221,14 +190,14 @@ class MessageStoreImpl with MessageStore { | |
continue; | ||
} | ||
|
||
if (newStreamId != null) { | ||
if (newStreamId != origStreamId) { | ||
message.streamId = newStreamId; | ||
// See [StreamMessage.displayRecipient] on why the invalidation is | ||
// needed. | ||
message.displayRecipient = null; | ||
} | ||
|
||
if (newTopic != null) { | ||
if (newTopic != origTopic) { | ||
message.topic = newTopic; | ||
} | ||
|
||
|
@@ -239,14 +208,7 @@ class MessageStoreImpl with MessageStore { | |
} | ||
|
||
for (final view in _messageListViews) { | ||
view.messagesMoved( | ||
origStreamId: origStreamId, | ||
newStreamId: newStreamId ?? origStreamId, | ||
origTopic: origTopic, | ||
newTopic: newTopic ?? origTopic, | ||
messageIds: event.messageIds, | ||
propagateMode: propagateMode, | ||
); | ||
view.messagesMoved(messageMove: messageMove, messageIds: event.messageIds); | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
s/with/within/ — i.e., we're now throwing errors from inside the parsing code