-
Notifications
You must be signed in to change notification settings - Fork 238
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
api: Add getMessageCompat helper, for servers with and without FL 120 #177
Conversation
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.
Thanks! Comments below.
lib/api/route/messages.dart
Outdated
final supportsApplyMarkdown = connection.zulipFeatureLevel! >= 120; // TODO(server-5) | ||
assert(supportsApplyMarkdown || applyMarkdown == null); | ||
return connection.get('getMessage', GetMessageResult.fromJson, 'messages/$messageId', { | ||
'message_id': messageId, |
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.
lib/api/route/messages.dart
Outdated
|
||
@JsonSerializable(fieldRename: FieldRename.snake) | ||
class GetMessageResult { | ||
final String? rawContent; // (Marked optional for a future where this is removed) |
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.
final String? rawContent; // (Marked optional for a future where this is removed) | |
// final String rawContent; // deprecated; ignore |
It's always safe to leave properties out of the result types, because we never have the deserialization look for unexpected properties to complain about. (Because that would risk incompatibility with future server versions.)
Generally when implementing a route binding we include properties even when we don't anticipate using them, for the reasons at https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#lazy-programming . But when the property is actually deprecated, that changes things: the ideal client in a world of ideal software would then leave the property out, so we leave it out unless there's some practical reason we currently need it.
lib/api/route/messages.dart
Outdated
final messages = response.messages; | ||
if (messages.isEmpty) { | ||
return null; | ||
} | ||
return messages.first; |
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.
final messages = response.messages; | |
if (messages.isEmpty) { | |
return null; | |
} | |
return messages.first; | |
return response.messages.firstOrNull; |
lib/api/route/messages.dart
Outdated
// message property present at this feature level | ||
return response.message!; |
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.
This is probably fine in practice, but it's not the ideal behavior. I'd like to try to stick to the ideal behavior, to see if we have a pattern that makes it comfortable to do so.
The reason it's not ideal is that the !
makes it effectively an internal error in our code if the property is missing. Whereas if it is missing, that signals instead that we have a malformed JSON response from the server — one that doesn't match our expectations from the API. Ordinarily, that would cause a MalformedServerResponseException, per this bit of ApiConnection.send
:
try {
return fromJson(json);
} catch (e) {
throw MalformedServerResponseException(
routeName: routeName, httpStatus: httpStatus, data: json);
}
So this could instead check for null and throw such an exception in that case.
In this instance, though, I think there's actually a simpler way; see next comment.
lib/api/route/messages.dart
Outdated
@JsonSerializable(fieldRename: FieldRename.snake) | ||
class GetMessageResult { | ||
final String? rawContent; // (Marked optional for a future where this is removed) | ||
final Message? message; // TODO(server-5) |
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.
Since rawContent
is deprecated and we don't intend to use it, this endpoint doesn't have a remaining use case for older servers where this message
property is absent.
Given that, I think we can simplify things a bit by effectively pretending that the pre-120 version of this endpoint doesn't exist. That means:
- Have
Message message
, unconditionally. - Have
getMessage
assert that the feature level is at least 120.
(The latter step isn't strictly necessary — without it we'd get an error anyway — but could help clarify things a bit.)
Then that also means getMessageCompat
could just return result.message
without !
.
test/api/route/messages_test.dart
Outdated
}); | ||
|
||
test('legacy; message not found', () { | ||
return FakeApiConnection.with_((connection) async { |
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:
return FakeApiConnection.with_((connection) async { | |
return FakeApiConnection.with_(zulipFeatureLevel: 119, (connection) async { |
(instead of at the end of the callback).
Dart didn't support that syntax until something like a year or two ago — previously named arguments had to come after positional arguments — which is a reason you'll often still see code not take advantage of it. But I think a case like this where the named argument is short and important for understanding the code, and the positional one is many lines long, is an excellent use of it.
test/api/route/messages_test.dart
Outdated
expectLegacy | ||
? (check(connection.lastRequest).isA<http.Request>() |
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.
Seems a bit cleaner to make this a normal if/else statement, since we're not using the expression nature of the ?:
operator.
test/api/route/messages_test.dart
Outdated
final message = eg.streamMessage(); | ||
final fakeResult = GetMessageResult(message: message); | ||
connection.prepare(json: fakeResult.toJson()); | ||
final result = await checkGetMessageCompat(connection, false, |
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.
Let's make expectLegacy
a named parameter; I think that will make these call sites easier to read, without having to refer back to the definition to remember what the boolean means and which direction it runs in.
2305e03
to
3a96b23
Compare
Thanks for the review! Revision pushed. |
e9c3582
to
881c233
Compare
We can use this to get raw Markdown content for quote-and-reply (zulip#116) and for the "Share" option on a message. For those, we only care about the raw Markdown content and so could just as well have used the `raw_content` field on the get-single-message response, for servers pre-120. But... We can also use this for zulip#73, "Handle Zulip-internal links by navigation", to follow /near/<id> links through topic/stream moves (see implementation in zulip-mobile). For that, we'll need more than just the message's raw Markdown.
881c233
to
631f4d6
Compare
Thanks! Looks good; merging. |
We can use this to get raw Markdown content for quote-and-reply
(#116) and for the "Share" option on a message. For those, we only
care about the raw Markdown content and so could just as well have
used the
raw_content
field on the get-single-message response, forservers pre-120. But...
We can also use this for #73, "Handle Zulip-internal links by
navigation", to follow /near/ links through topic/stream moves
(see implementation in zulip-mobile). For that, we'll need more than
just the message's raw Markdown.
Fixes: #140