-
Notifications
You must be signed in to change notification settings - Fork 306
Edit-message (7/n): Implement edit-message methods on MessageStore #1484
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
Conversation
|
a5262ab
to
45b4b05
Compare
45b4b05
to
ea74a55
Compare
Revision pushed, this time putting the tests in test/model/message_test.dart instead of test/model/store_test.dart (but still testing against the whole |
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! Left some comments.
lib/model/message.dart
Outdated
String? takeFailedMessageEdit(int messageId) { | ||
final status = _editMessageRequests.remove(messageId); | ||
if (status == null) { | ||
throw StateError('called takeFailedEdit, but no edit'); |
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: this doesn't match the name of the method: takeFailedMessageEdit
// Request has succeeded; event hasn't arrived | ||
check(store.getEditMessageErrorStatus(message.id)).isNotNull().isFalse(); |
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.
I feel that we can be a bit more certain that the request has complete by awaiting its future. But I guess editMessage
doesn't return it because the UI code doesn't need the future.
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.
Yeah, there's nothing user-facing to do when the future succeeds (we take the event as signaling edit-message success), so editMessage
doesn't return it.
await updateMessage(connection, messageId: messageId, content: content); | ||
// On success, we'll clear `status` from editMessageRequests | ||
// when we get the event. | ||
} catch (e) { |
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.
I can see why an error dialog isn't needed. The error text is shown immediately when the edit request fails. I wonder if the e
is something more interesting/unexpected, do we want to log it?
test/model/message_test.dart
Outdated
check(store.getEditMessageErrorStatus(message.id)).isNull(); | ||
})); | ||
|
||
test('request failure before event arrival; take failed edit in between', () => awaitFakeAsync((async) 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: the format "request fails, then message deleted" seems preferable to me to help understand the sequence of events
check(store.getEditMessageErrorStatus(message.id)).isNull(); | ||
})); | ||
|
||
test('takeFailedMessageEdit throws StateError when nothing to take', () => awaitFakeAsync((async) 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.
I think a similarly useful test would be repeated editMessage
calls that operate on the same message.
test/model/message_test.dart
Outdated
store = eg.store(); | ||
connection = store.connection as FakeApiConnection; | ||
message = eg.streamMessage(); | ||
await store.addMessage(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.
I think we can also test if the listeners are notified whenever an update is expected.
lib/model/message.dart
Outdated
/// See also: | ||
/// * [getEditMessageErrorStatus] | ||
/// * [takeFailedMessageEdit] | ||
void editMessage({required int messageId, required String content}); |
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.
I think we should document the condition when you should not call editMessage
(like we do for takeFailedMessageEdit
), i.e., when there is a current edit message request.
ea74a55
to
9b35f98
Compare
Thanks for the review! Revision pushed. |
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! Just some small comments on tests (and the failing CI might need a fix). Marking this for Greg's review.
// First request has succeeded; event hasn't arrived | ||
// Mid-second request | ||
check(store.getEditMessageErrorStatus(message.id)).isNotNull().isFalse(); | ||
check(store.getEditMessageErrorStatus(otherMessage.id)).isNotNull().isFalse(); |
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.
How about having checkNotNotified
calls here and later? So that we are extra sure that it's the edit event that triggers the notifications.
test/model/message_test.dart
Outdated
check(connection.takeRequests()).length.equals(1); | ||
checkNotifiedOnce(); | ||
|
||
connection.prepare(json: UpdateMessageResult().toJson()); |
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.
I'm a bit surprised that this is needed, since I thought store.editMessage
would fail before firing the request, which is confirmed by the later .isEmpty
check on takeRequests()
. If I missed something, maybe a comment here will be helpful?
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.
Ah yeah, this can be removed.
|
||
async.elapse(Duration(milliseconds: 500)); | ||
// Mid-request | ||
check(store.getEditMessageErrorStatus(message.id)).isNotNull().isFalse(); |
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.
Similarly, having a checkNotNotified
call here might help.
|
||
async.elapse(Duration(milliseconds: 500)); | ||
// Mid-request | ||
check(store.getEditMessageErrorStatus(message.id)).isNotNull().isFalse(); |
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.
Same here.
9b35f98
to
fd7cf3a
Compare
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! This looks great; just nits below, and a few comments on the docs.
lib/model/message.dart
Outdated
/// and the update-message event hasn't arrived. | ||
bool? getEditMessageErrorStatus(int messageId); | ||
|
||
/// Edits a message's content. |
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.
Given the context of being on [MessageStore] (or [PerAccountStore]), this reads as if it's making an edit to the data we have locally. The fact that it's making an API request over the network is an important part of its meaning, so should be made explicit.
E.g.:
/// Edits a message's content. | |
/// Edit a message's content, via a request to the server. |
lib/model/message.dart
Outdated
@@ -35,6 +35,37 @@ mixin MessageStore { | |||
/// All [Message] objects in the resulting list will be present in | |||
/// [this.messages]. | |||
void reconcileMessages(List<Message> messages); | |||
|
|||
/// Whether the current edit request, if any, has failed. |
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 reads to me as if there's one "current edit request" on the store. How about:
/// Whether the current edit request, if any, has failed. | |
/// Whether the current edit request for the given message, if any, has failed. |
lib/model/message.dart
Outdated
/// Will be null if there is no current edit request | ||
/// or the message was deleted. |
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.
I think I'd want to conceptualize "the message was deleted" as one way to (re-)enter the state of there being no current edit request. If the message was deleted, then any edit request for it that had been outstanding no longer is.
lib/model/message.dart
Outdated
@override | ||
bool? getEditMessageErrorStatus(int messageId) => | ||
_editMessageRequests[messageId]?.hasError; | ||
final Map<int, _EditMessageRequestStatus> _editMessageRequests = {}; |
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: this field is a bit more than the backing for this getter-like method:
@override | |
bool? getEditMessageErrorStatus(int messageId) => | |
_editMessageRequests[messageId]?.hasError; | |
final Map<int, _EditMessageRequestStatus> _editMessageRequests = {}; | |
@override | |
bool? getEditMessageErrorStatus(int messageId) => | |
_editMessageRequests[messageId]?.hasError; | |
final Map<int, _EditMessageRequestStatus> _editMessageRequests = {}; |
because in particular it has more data than that exposes.
lib/model/message.dart
Outdated
if (_editMessageRequests.containsKey(messageId)) { | ||
throw StateError('message ID already in editMessageRequests'); |
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: editMessageRequests
isn't quite the name of anything; and the nearest name to that is private so might not be clearest for an error message:
if (_editMessageRequests.containsKey(messageId)) { | |
throw StateError('message ID already in editMessageRequests'); | |
if (_editMessageRequests.containsKey(messageId)) { | |
throw StateError('an edit request is already in progress'); |
(though a private identifier in this message wouldn't be terrible either, since it really should be only developer-facing)
lib/model/message.dart
Outdated
// On success, we'll clear `status` from editMessageRequests | ||
// when we get the event. |
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.
Is there an identifier status
this refers to? I don't see it.
lib/model/store.dart
Outdated
@override | ||
void editMessage({required int messageId, required String content}) { | ||
assert(!_disposed); | ||
return _messages.editMessage(messageId: messageId, content: content); |
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.
test/model/message_test.dart
Outdated
// A function marked async needs an async check, | ||
// but its return type is void, so, cast the return value to Future | ||
final future = store.editMessage(messageId: message.id, content: 'newer content') as Future; | ||
await check(future).throws<StateError>(); |
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.
Hmm interesting. How about:
// A function marked async needs an async check, | |
// but its return type is void, so, cast the return value to Future | |
final future = store.editMessage(messageId: message.id, content: 'newer content') as Future; | |
await check(future).throws<StateError>(); | |
await check(store.editMessage(messageId: message.id, content: 'newer content')) | |
.isA<Future<void>>().throws<StateError>(); |
Effectively that moves the cast into a package:checks
condition.
test/model/message_test.dart
Outdated
async.elapse(Duration(milliseconds: 500)); | ||
check(store.getEditMessageErrorStatus(message.id)).isNull(); | ||
checkNotNotified(); |
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 version is slightly delicate, in that these two checks are only effective given that the elapse
calls add up to at least the delay on the error response. The test is correct, but vulnerable to being defused in a not-entirely-obvious way by a future edit to those numbers.
How about:
async.elapse(Duration(milliseconds: 500)); | |
check(store.getEditMessageErrorStatus(message.id)).isNull(); | |
checkNotNotified(); | |
async.flushTimers(); | |
check(store.getEditMessageErrorStatus(message.id)).isNull(); | |
checkNotNotified(); |
(It's basically OK as is, though; if there weren't such an easy way to make it more robust in that direction, I wouldn't want to attempt anything complex to guard against that sort of edit.)
…impls Thanks Greg for noticing this: zulip#1484 (comment)
fd7cf3a
to
93e84ad
Compare
Thanks for the review! Revision pushed. In this revision, I did a few more things:
|
0b6fbe2
to
1eeb197
Compare
(updated |
Hmm, a CI failure:
I'll rerun and see if that fixes it. |
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! The revision looks good. A couple of comments both on the new commits at the end.
We could merge the first part:
285c1f4 store [nfc]: Move store.sendMessage up near other MessageStore proxy impls
f1c0e0f model: Implement edit-message methods on MessageStore
and then do the next two as a follow-up PR:
2c69364 api: Add prevContentSha256 param to updateMessage route
1eeb197 model: Use prevContentSha256 in edit-message method
lib/api/route/messages.dart
Outdated
// TODO(server-11) remove FL condition and its mention in the dartdoc | ||
if (prevContentSha256 != null && connection.zulipFeatureLevel! >= 379) 'prev_content_sha256': RawParameter(prevContentSha256), |
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.
We should be able to just send this unconditionally — generally the server ignores unexpected parameters in requests:
https://zulip.com/api/rest-error-handling#ignored-parameters
(That doc is about some feedback the server includes in responses when that happens, and the feedback is new in server-7. But I believe the server has tolerated such parameters since forever.)
lib/api/route/messages.dart
Outdated
String? content, | ||
int? streamId, | ||
String? prevContentSha256, |
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: follow order in API doc (which is also a logical order — groups the two content-related parameters together):
String? content, | |
int? streamId, | |
String? prevContentSha256, | |
String? content, | |
String? prevContentSha256, | |
int? streamId, |
4338003
to
f1c0e0f
Compare
…impls Thanks Greg for noticing this: #1484 (comment)
This PR, toward the edit-message feature #126, implements three new methods on
MessageStore
:editMessage
, to be called when the user taps the "Edit message" button in the message action sheet:getEditMessageErrorStatus
, to be called when rendering the message list; showing a loading state when we haven't gotten the update-message event yet, and an error state for a failed edit request:(The UI text isn't final; see updates on CZO: #mobile-design > edit message @ 💬)
takeFailedMessageEdit
, to be called when you tap the error message pictured above, so we can put you back in the edit box, starting with the text you'd tried to edit to. This part is from CZO discussion: #mobile-design > edit message @ 💬Related: #126