Skip to content

Commit 66f842c

Browse files
committed
action_sheet [nfc]: Avoid passing footgun context to onPressed methods
The usual Flutter idiom, which we use across the rest of the app, would call for using the parameter called `context` wherever a BuildContext is required: for getting the store or localizations, or passing to a method like showErrorDialog, and so on. That idiom doesn't work here, because that context belongs to the action sheet itself, which gets dismissed at the start of the method. Worse, it will sometimes seem to work, nondeterministically: the context only gets unmounted after the animation to dismiss the action sheet is complete, which takes something like 200ms. So the temptation to use it has been a recurring source of small bugs. Fix the problem by not passing the action sheet's context to these methods at all.
1 parent 5ca2784 commit 66f842c

File tree

1 file changed

+35
-29
lines changed

1 file changed

+35
-29
lines changed

lib/widgets/action_sheet.dart

+35-29
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,13 @@ abstract class MessageActionSheetMenuItemButton extends StatelessWidget {
100100
IconData get icon;
101101
String label(ZulipLocalizations zulipLocalizations);
102102

103-
/// Called when the button is pressed.
103+
/// Called when the button is pressed, after dismissing the action sheet.
104104
///
105-
/// Generally this method's implementation should begin by dismissing the
106-
/// action sheet with [NavigatorState.pop].
107-
/// After that step, the given `context` may no longer be mounted;
108-
/// operations that need a [BuildContext] should typically use [pageContext].
109-
void onPressed(BuildContext context);
105+
/// If the action may take a long time, this method is responsible for
106+
/// arranging any form of progress feedback that may be desired.
107+
///
108+
/// For operations that need a [BuildContext], see [pageContext].
109+
void onPressed();
110110

111111
final Message message;
112112

@@ -124,6 +124,15 @@ abstract class MessageActionSheetMenuItemButton extends StatelessWidget {
124124
return MessageListPage.ancestorOf(pageContext);
125125
}
126126

127+
void _handlePressed(BuildContext context) {
128+
// Dismiss the enclosing action sheet immediately,
129+
// for swift UI feedback that the user's selection was received.
130+
Navigator.of(context).pop();
131+
132+
assert(pageContext.mounted);
133+
onPressed();
134+
}
135+
127136
@override
128137
Widget build(BuildContext context) {
129138
final designVariables = DesignVariables.of(context);
@@ -137,7 +146,7 @@ abstract class MessageActionSheetMenuItemButton extends StatelessWidget {
137146
).copyWith(backgroundColor: WidgetStateColor.resolveWith((states) =>
138147
designVariables.contextMenuItemBg.withValues(
139148
alpha: states.contains(WidgetState.pressed) ? 0.20 : 0.12))),
140-
onPressed: () => onPressed(context),
149+
onPressed: () => _handlePressed(context),
141150
child: Text(label(zulipLocalizations),
142151
style: const TextStyle(fontSize: 20, height: 24 / 20)
143152
.merge(weightVariableTextStyle(context, wght: 600)),
@@ -186,8 +195,7 @@ class AddThumbsUpButton extends MessageActionSheetMenuItemButton {
186195
return 'React with 👍'; // TODO(i18n) skip translation for now
187196
}
188197

189-
@override void onPressed(BuildContext context) async {
190-
Navigator.of(context).pop();
198+
@override void onPressed() async {
191199
String? errorMessage;
192200
try {
193201
await addReaction(PerAccountStoreWidget.of(pageContext).connection,
@@ -231,8 +239,7 @@ class StarButton extends MessageActionSheetMenuItemButton {
231239
: zulipLocalizations.actionSheetOptionStarMessage;
232240
}
233241

234-
@override void onPressed(BuildContext context) async {
235-
Navigator.of(context).pop();
242+
@override void onPressed() async {
236243
final zulipLocalizations = ZulipLocalizations.of(pageContext);
237244
final op = message.flags.contains(MessageFlag.starred)
238245
? UpdateMessageFlagsOp.remove
@@ -325,10 +332,7 @@ class QuoteAndReplyButton extends MessageActionSheetMenuItemButton {
325332
return zulipLocalizations.actionSheetOptionQuoteAndReply;
326333
}
327334

328-
@override void onPressed(BuildContext context) async {
329-
// Close the message action sheet. We'll show the request progress
330-
// in the compose-box content input with a "[Quoting…]" placeholder.
331-
Navigator.of(context).pop();
335+
@override void onPressed() async {
332336
final zulipLocalizations = ZulipLocalizations.of(pageContext);
333337

334338
// This will be null only if the compose box disappeared after the
@@ -343,6 +347,9 @@ class QuoteAndReplyButton extends MessageActionSheetMenuItemButton {
343347
) {
344348
topicController.value = TextEditingValue(text: message.topic);
345349
}
350+
351+
// This inserts a "[Quoting…]" placeholder into the content input,
352+
// giving the user a form of progress feedback.
346353
final tag = composeBoxController.contentController
347354
.registerQuoteAndReplyStart(PerAccountStoreWidget.of(pageContext),
348355
message: message,
@@ -388,8 +395,7 @@ class MarkAsUnreadButton extends MessageActionSheetMenuItemButton {
388395
return zulipLocalizations.actionSheetOptionMarkAsUnread;
389396
}
390397

391-
@override void onPressed(BuildContext context) async {
392-
Navigator.of(context).pop();
398+
@override void onPressed() async {
393399
unawaited(markNarrowAsUnreadFromMessage(pageContext, message, narrow));
394400
}
395401
}
@@ -408,11 +414,11 @@ class CopyMessageTextButton extends MessageActionSheetMenuItemButton {
408414
return zulipLocalizations.actionSheetOptionCopyMessageText;
409415
}
410416

411-
@override void onPressed(BuildContext context) async {
412-
// Close the message action sheet. We won't be showing request progress,
413-
// but hopefully it won't take long at all, and
417+
@override void onPressed() async {
418+
// This action doesn't show request progress.
419+
// But hopefully it won't take long at all; and
414420
// fetchRawContentWithFeedback has a TODO for giving feedback if it does.
415-
Navigator.of(context).pop();
421+
416422
final zulipLocalizations = ZulipLocalizations.of(pageContext);
417423

418424
final rawContent = await fetchRawContentWithFeedback(
@@ -445,8 +451,7 @@ class CopyMessageLinkButton extends MessageActionSheetMenuItemButton {
445451
return zulipLocalizations.actionSheetOptionCopyMessageLink;
446452
}
447453

448-
@override void onPressed(BuildContext context) {
449-
Navigator.of(context).pop();
454+
@override void onPressed() {
450455
final zulipLocalizations = ZulipLocalizations.of(pageContext);
451456

452457
final store = PerAccountStoreWidget.of(pageContext);
@@ -479,15 +484,16 @@ class ShareButton extends MessageActionSheetMenuItemButton {
479484
return zulipLocalizations.actionSheetOptionShare;
480485
}
481486

482-
@override void onPressed(BuildContext context) async {
483-
// Close the message action sheet; we're about to show the share
484-
// sheet. (We could do this after the sharing Future settles
485-
// with [ShareResultStatus.success], but on iOS I get impatient with
486-
// how slowly our action sheet dismisses in that case.)
487+
@override void onPressed() async {
487488
// TODO(#591): Fix iOS bug where if the keyboard was open before the call
488489
// to `showMessageActionSheet`, it reappears briefly between
489490
// the `pop` of the action sheet and the appearance of the share sheet.
490-
Navigator.of(context).pop();
491+
//
492+
// (Alternatively we could delay the [NavigatorState.pop] that
493+
// dismisses the action sheet until after the sharing Future settles
494+
// with [ShareResultStatus.success]. But on iOS one gets impatient with
495+
// how slowly our action sheet dismisses in that case.)
496+
491497
final zulipLocalizations = ZulipLocalizations.of(pageContext);
492498

493499
final rawContent = await fetchRawContentWithFeedback(

0 commit comments

Comments
 (0)