Skip to content
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

Show alert dialog when ActionSheet action fails #3795

Merged
merged 2 commits into from
Feb 1, 2020

Conversation

agrawal-d
Copy link
Member

@agrawal-d agrawal-d commented Jan 13, 2020

Fixes #3788.

An alert dialog is now shown, instead of silently ignoring the error.

Attached below is a screenshot showing this change:
image

The dialog is similar to what is shown when editing a message fails:
image

@@ -58,7 +58,9 @@ const deleteMessage = async ({ auth, message, dispatch }) => {
if (isAnOutboxMessage(message)) {
dispatch(deleteOutboxMessage(message.timestamp));
} else {
api.deleteMessage(auth, message.id);
api.deleteMessage(auth, message.id).catch((err: Error) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good follow up of this will be to make sure this message is translated based on user selected language (also for fail to edit message).

Copy link
Member Author

@agrawal-d agrawal-d Jan 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please tell me how I can do that? The /howto/translations.md does not explain much.

@agrawal-d
Copy link
Member Author

agrawal-d commented Jan 15, 2020

Thanks for the review @ray-kraesig . Have made some changes.

Now works for all possible actions, and all errors are handled in a single place.

A generic message is shown as the alert title, while the alert body message itself is extracted from the error thrown.
image

@ray-kraesig please review.

Copy link
Contributor

@rk-for-zulip rk-for-zulip left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Localization was mentioned earlier; @gnprice will have to confirm, but I think all that's needed for that is to:

  1. wrap the new string(s) in an _ call (see, for reference, the showToast call in copyToClipboard, and the eventual use of the .title string); and
  2. add the relevant strings to messages_en.json.

(That would probably best go in a separate commit, though, and possibly even a separate PR if I'm wrong about how involved it is; don't let it block you.)

@@ -12,7 +12,7 @@ import { navigateToMessageReactionScreen } from '../nav/navActions';
// TODO really this belongs in a libdef.
export type ShowActionSheetWithOptions = (
{ options: string[], cancelButtonIndex: number },
(number) => void,
(number) => Promise<void>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't our type to modify, unfortunately – as the comment notes, it shouldn't really be here at all. (Also, this modification results in a Flow error.)

On the other hand, ButtonDescription (seen below) is ours.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

...params,
});
} catch (err) {
Alert.alert('Couldnt perform the requested action', err.message);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: Couldn't (although preferentially Could not).

That said, it's probably worth the trouble to do better than a generic Could not perform the requested action here. We can associate a failure message with each action (Could not delete message, etc.) the same way we're associating a title.

Copy link
Member Author

@agrawal-d agrawal-d Jan 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have replaced the message with a simple 'Error'. This will not be confusing because the alert message contains context-specific detailed message.

Copy link
Contributor

@rk-for-zulip rk-for-zulip Jan 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Counterpoint:

Screenshot_20200116_130132

(Edit: This is actually the worst-case scenario, because this class of failure is often delayed: the user may not see this message until several seconds later.)

@agrawal-d
Copy link
Member Author

Thanks for reviewing, @ray-kraesig. Have made some changes.

@agrawal-d
Copy link
Member Author

agrawal-d commented Jan 16, 2020

image

(#3795 (comment))

@agrawal-d
Copy link
Member Author

agrawal-d commented Jan 17, 2020

@ray-kraesig , so how do you propose we do it?

  • Hardcode the alert titles for each action?
  • Or generate it, for e.g. The action ${button.title} failed?

@agrawal-d
Copy link
Member Author

agrawal-d commented Jan 17, 2020

This looks better:
image

@ray-kraesig have pushed come changes.

Copy link
Contributor

@rk-for-zulip rk-for-zulip left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ray-kraesig , so how do you propose we do it?

  • Hardcode the alert titles for each action?
  • Or generate it, for e.g. The action ${button.title} failed?

Only the first is an option. Even if you try to do the second, you'll end up having to add all the individual generated strings to the translation databases.

I've since confirmed that the two steps I noted previously are all that's necessary for localization in most PRs, incidentally:

  1. wrap the new string(s) in an _ call (see, for reference, the showToast call in copyToClipboard, and the eventual use of the .title string); and
  2. add the relevant strings to messages_en.json.

...params,
});
} catch (err) {
const titleString = allButtons[optionCodes[buttonIndex]].title;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's harmless right now, but it's probably better to cache allButtons[optionCodes[buttonIndex]] before the await is performed.

(allButtons shouldn't ever be modified – but if it is, we might end up accessing different values of allButtons before and after the await, if two actions are performed in quick succession before the first one fails.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not being used in the latest change at all, so haven't cached it.

@agrawal-d
Copy link
Member Author

@ray-kraesig have pushed some changes. Please review.

@agrawal-d agrawal-d changed the title Show alert dialog when deleting message fails Show alert dialog when action sheet action fails Jan 22, 2020
@agrawal-d agrawal-d changed the title Show alert dialog when action sheet action fails Show alert dialog when ActionSheet action fails Jan 22, 2020
Copy link
Contributor

@rk-for-zulip rk-for-zulip left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't the callback have a closure over allButtons[optionCodes[buttonIndex]], so that even if another action is performed successively, the previous callback isn't affected?

Terminological nit: the callback is a closure. It closes over the variables (not values!) allButtons and optionCodes, and has buttonIndex as a parameter.

optionCodes is local to showActionSheet, and so will be a different variable for each instance of the callback. allButtons, on the other hand, is global to the module, and anyway is a refernce to an array; if either the variable or the array are modified while we're awaiting here, the first evaluation of allButtons[optionCodes[buttonIndex]] needn't yield the same value as the second.

(Of course, in the current codebase, it shouldn't ever be modified. But code does change.)

Comment on lines 122 to 134
"Failed to add reaction":"Failed to add reaction",
"Failed to reply":"Failed to reply",
"Failed to copy message to clipboard":"Failed to copy message to clipboard",
"Failed to share message":"Failed to share message",
"Failed to edit message":"Failed to edit message",
"Failed to delete message":"Failed to delete message",
"Failed to star message":"Failed to star message",
"Failed to unstar message":"Failed to unstar message",
"Failed to show reactions":"Failed to show reactions",
"Failed to unmute topic":"Failed to unmute topic",
"Failed to mute topic":"Failed to mute topic",
"Failed to mute stream":"Failed to mute stream",
"Failed to unmute stream":"Failed to unmute stream",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces after the colons here, again.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im sorry, I am used to the prettier taking care of formatting, but I guess it doesn't add space after colons. Will keep this in mind in the future.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prettier is intended for code formatting, and not necessarily data formatting. We don't let it touch .json files: those are typically both generated and consumed by tooling.

Comment on lines 144 to 158
const errorMessages = [
'Failed to add reaction',
'Failed to reply',
'Failed to copy message to clipboard',
'Failed to share message',
'Failed to edit message',
'Failed to delete message',
'Failed to star message',
'Failed to unstar message',
'Failed to show reactions',
'Failed to unmute topic',
'Failed to mute topic',
'Failed to mute stream',
'Failed to unmute stream',
];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's nothing keeping this lined up with the actions themselves. If your earlier delete topic PR were applied, for example, this would be suddenly and quietly broken.

It would be safer to associate actions with failure messages in the same way they're associated with titles, as I suggested earlier.

@agrawal-d agrawal-d force-pushed the deleting-messages branch 2 times, most recently from cad0519 to d88e276 Compare January 24, 2020 07:25
@agrawal-d
Copy link
Member Author

agrawal-d commented Jan 24, 2020

Thank you for the review, @ray-kraesig. I have (hopefully) made the changes your requested, and added a commit message.

I have not added any documentary comments in the code, because it's a simple self-explanatory change; I did not feel that this change needs to be documented, but if required, I will add some.

Please review these changes, thanks.

Copy link
Contributor

@rk-for-zulip rk-for-zulip left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not added any documentary comments in the code, because it's a simple self-explanatory change; I did not feel that this change needs to be documented, but if required, I will add some.

Terminology nitpick: The change does need to be documented – in the commit summary. (And your commit summary does that, so all's well there.)

Can you please give me an example of how you expect the alert titles to be associated, because I am unable to understand what we want here. Thanks.

An example is present in this file: the .title field of ButtonDescription.

              type ButtonDescription = {
                // ...
/* --> */       title: string,
              };

              const reply = /* ... */;
/* --> */     reply.title = 'Reply';

I Hardcoded the error titles because you mentioned that this is expected in a comment. But I guess you meant something else.

Hardcoding the error titles is expected. The issue with the last version was putting the hardcoded titles in their own array with no explicit connection to the actions they're associated with.

(async () => {
const pressedButton: ButtonDescription = allButtons[optionCodes[buttonIndex]];
try {
await allButtons[optionCodes[buttonIndex]]({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pressedButton, surely?

Comment on lines 253 to 274
} catch (err) {
Alert.alert(_(`Failed to ${pressedButton.title.toLowerCase()}`), err.message);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the approach I specifically warned against. It is much too easy to add an action to the action-sheet and its description to the localization database without noticing that the action's failure message needs to be considered as well. (It's much too easy in general to fail to localize UI strings, but that's no excuse to make it even easier.)

@agrawal-d
Copy link
Member Author

Thanks for the review, @ray-kraesig. Have made some changes.

The errorMessage property in type ButtonDescription is optional because the callback associated with the button Cancel ( and possibly other buttons we may add in the future ) can not throw an error.

Please review these changes, thanks!

Copy link
Contributor

@rk-for-zulip rk-for-zulip left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ideal solution here would be to declare a callback as possibly having the quality of not throwing exceptions ("being nothrow"), and then tie the necessity for an .errorMessage to whether or not the callback was nothrow. There are type systems that can do that. Sadly, Flow is not one of them.

The most straightforward solution here would be to make .errorMessage required, and give it a dummy error message (but a reasonable one, if somehow displayed). This would require at least one explanatory comment.

A precise solution here would be to make "Cancel" a special object that doesn't even have a callback to be invoked, and so is guaranteed not to throw because of that. This would a) come with more pitfalls, b) require more explanatory comments, c) need to be broken out into its own commit, and d) not be trivially extensible to other nothrow functions. (Useful provably-nothrow functions are very rare in JavaScript, and most of them don't return void; I don't think d) is a blocker.)


The errorMessage property in type ButtonDescription is optional because the callback associated with the button Cancel ( and possibly other buttons we may add in the future ) can not throw an error.

(If this were an acceptable tradeoff, it would have to be noted in inline comments.)


There is a minor typo (extra space) in the commit message. The commit summary is also a few too many characters long for GitHub to be happy with it. (Our current documented limit is 76, but perhaps we should reduce this slightly.)

@@ -33,6 +33,7 @@ type ButtonDescription = {
_: GetText,
}): void | Promise<void>,
title: string,
errorMessage?: string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that it's possible to accidentally omit errorMessage – or misspell it as error_message and not notice.

Comment on lines 268 to 270
if (pressedButton.errorMessage !== undefined) {
Alert.alert(_(pressedButton.errorMessage), err.message);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that if an unexpected failure occurs in a callback that doesn't have an associated .errorMessage (for whatever reason), that failure will be silent: neither the user nor we will ever hear about it.

@agrawal-d agrawal-d force-pushed the deleting-messages branch 3 times, most recently from ac797c4 to 9c945ed Compare January 30, 2020 19:36
@agrawal-d
Copy link
Member Author

agrawal-d commented Jan 30, 2020

Thanks for the review, @ray-kraesig! I went ahead with the straightforward solution you mentioned. Please review, thanks!

The commit summary is also a few too many characters long for GitHub to be happy with it. (Our current documented limit is 76, but perhaps we should reduce this slightly.)

Probably because of the 'Verified' commit badge, as I have added a GPG key to my GitHub account. I have shortened the commit summary regardless.

rk-for-zulip and others added 2 commits January 31, 2020 15:14
`toggleMessageStarred` immediately returns void, not reporting the
success or failure of its async operation to its caller.

Instead, propagate its implementation's Promise<ApiResponse> upward.
Currently, there is no error handling for any of the actions
performed through messageActionSheet.

Add error handling for all actions, and show alert dialog if any
action fails.

[Comments amended by Ray Kraesig.]

Fixes zulip#3788.
Copy link
Contributor

@rk-for-zulip rk-for-zulip left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid another go-round (which would probably be more trouble than it's worth), I've taken care of both of the issues I've mentioned here.

Comment on lines 142 to 145
// In practice, the callback for `cancel` button will never throw
// an error, and the `errorMessage` property never be used, but it
// has still been added to ensure that `errorMessage` property can
// be made required for the type `ButtonDescription`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cancel is not the only callback for which this is true: addReaction, showReactions, and shareMessage also don't throw (though this is implementation-detail-dependent).

It also immediately raises the question of why errorMessage has been made required, which (as evidenced by the fact that we had to have the previous discussion) is not obvious.

const starMessage = ({ auth, message }) => {
api.toggleMessageStarred(auth, [message.id], true);
const starMessage = async ({ auth, message }) => {
await api.toggleMessageStarred(auth, [message.id], true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not obvious, but this is awaiting on void.

(That's really a bug in toggleMessageStarred, though.)

@rk-for-zulip rk-for-zulip merged commit fc660d6 into zulip:master Feb 1, 2020
@rk-for-zulip
Copy link
Contributor

– and merged. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

No error message is shown when deletion fails
3 participants