Skip to content

Commit 1d410a7

Browse files
committed
dialog [nfc]: showSuggestedActionDialog returns DialogStatus with result
With this API, we can use showSuggestedActionDialog in an "early return" style -- await the user's choice, and early return if it was "Cancel". That's particularly helpful when the confirmation dialog belongs in an `if` block. We'll use this for the upcoming "edit message" feature (zulip#126), where we plan to offer a confirmation dialog before entering an edit-message session, but only if the compose box has text for a new message in it (which would be discarded if the user wants to go ahead).
1 parent 07d60a7 commit 1d410a7

File tree

4 files changed

+86
-28
lines changed

4 files changed

+86
-28
lines changed

lib/widgets/app.dart

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -308,16 +308,17 @@ class ChooseAccountPage extends StatelessWidget {
308308
trailing: MenuAnchor(
309309
menuChildren: [
310310
MenuItemButton(
311-
onPressed: () {
312-
showSuggestedActionDialog(context: context,
311+
onPressed: () async {
312+
final dialog = showSuggestedActionDialog(context: context,
313313
title: zulipLocalizations.logOutConfirmationDialogTitle,
314314
message: zulipLocalizations.logOutConfirmationDialogMessage,
315315
// TODO(#1032) "destructive" style for action button
316-
actionButtonText: zulipLocalizations.logOutConfirmationDialogConfirmButton,
317-
onActionButtonPress: () {
318-
// TODO error handling if db write fails?
319-
logOutAccount(GlobalStoreWidget.of(context), accountId);
320-
});
316+
actionButtonText: zulipLocalizations.logOutConfirmationDialogConfirmButton);
317+
if (await dialog.closed == SuggestedActionDialogResult.doAction) {
318+
if (!context.mounted) return;
319+
// TODO error handling if db write fails?
320+
unawaited(logOutAccount(GlobalStoreWidget.of(context), accountId));
321+
}
321322
},
322323
child: Text(zulipLocalizations.chooseAccountPageLogOutButton)),
323324
],

lib/widgets/compose_box.dart

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:async';
12
import 'dart:math';
23

34
import 'package:app_settings/app_settings.dart';
@@ -907,13 +908,13 @@ Future<Iterable<_File>> _getFilePickerFiles(BuildContext context, FileType type)
907908
// If the user hasn't checked "Don't ask again", they can always dismiss
908909
// our prompt and retry, and the permissions request will reappear,
909910
// letting them grant permissions and complete the upload.
910-
showSuggestedActionDialog(context: context,
911+
final dialog = showSuggestedActionDialog(context: context,
911912
title: zulipLocalizations.permissionsNeededTitle,
912913
message: zulipLocalizations.permissionsDeniedReadExternalStorage,
913-
actionButtonText: zulipLocalizations.permissionsNeededOpenSettings,
914-
onActionButtonPress: () {
915-
AppSettings.openAppSettings();
916-
});
914+
actionButtonText: zulipLocalizations.permissionsNeededOpenSettings);
915+
if (await dialog.closed == SuggestedActionDialogResult.doAction) {
916+
unawaited(AppSettings.openAppSettings());
917+
}
917918
} else {
918919
showErrorDialog(context: context,
919920
title: zulipLocalizations.errorDialogTitle,
@@ -1008,13 +1009,13 @@ class _AttachFromCameraButton extends _AttachUploadsButton {
10081009
// permission-request alert once, the first time the app wants to
10091010
// use a protected resource. After that, the only way the user can
10101011
// grant it is in Settings.
1011-
showSuggestedActionDialog(context: context,
1012+
final dialog = showSuggestedActionDialog(context: context,
10121013
title: zulipLocalizations.permissionsNeededTitle,
10131014
message: zulipLocalizations.permissionsDeniedCameraAccess,
1014-
actionButtonText: zulipLocalizations.permissionsNeededOpenSettings,
1015-
onActionButtonPress: () {
1016-
AppSettings.openAppSettings();
1017-
});
1015+
actionButtonText: zulipLocalizations.permissionsNeededOpenSettings);
1016+
if (await dialog.closed == SuggestedActionDialogResult.doAction) {
1017+
unawaited(AppSettings.openAppSettings());
1018+
}
10181019
} else {
10191020
showErrorDialog(context: context,
10201021
title: zulipLocalizations.errorDialogTitle,

lib/widgets/dialog.dart

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,22 @@ Widget _dialogActionText(String text) {
1919

2020
/// Tracks the status of a dialog, in being still open or already closed.
2121
///
22+
/// For dialog interactions with multiple possible outcomes,
23+
/// pass for [T] an enum representing the possible outcomes
24+
/// (e.g., [SuggestedActionDialogResult]).
25+
///
2226
/// See also:
2327
/// * [showDialog], whose return value this class is intended to wrap.
24-
class DialogStatus {
28+
class DialogStatus<T> {
2529
const DialogStatus(this.closed);
2630

2731
/// Resolves when the dialog is closed.
28-
final Future<void> closed;
32+
///
33+
/// If the dialog interaction has multiple possible outcomes,
34+
/// resolves with a [T] value representing the outcome of the interaction.
35+
///
36+
/// See, e.g., [showSuggestedActionDialog] and [SuggestedActionDialogResult].
37+
final Future<T> closed;
2938
}
3039

3140
/// Displays an [AlertDialog] with a dismiss button
@@ -37,7 +46,7 @@ class DialogStatus {
3746
// [showDialog]'s return value, a [Future], inside [DialogStatus]
3847
// whose documentation can be accessed. This helps avoid confusion when
3948
// intepreting the meaning of the [Future].
40-
DialogStatus showErrorDialog({
49+
DialogStatus<void> showErrorDialog({
4150
required BuildContext context,
4251
required String title,
4352
String? message,
@@ -61,28 +70,41 @@ DialogStatus showErrorDialog({
6170
return DialogStatus(future);
6271
}
6372

64-
void showSuggestedActionDialog({
73+
DialogStatus<SuggestedActionDialogResult> showSuggestedActionDialog({
6574
required BuildContext context,
6675
required String title,
6776
required String message,
6877
required String? actionButtonText,
69-
required VoidCallback onActionButtonPress,
7078
}) {
7179
final zulipLocalizations = ZulipLocalizations.of(context);
72-
showDialog<void>(
80+
final future = showDialog<SuggestedActionDialogResult>(
7381
context: context,
7482
builder: (BuildContext context) => AlertDialog(
7583
title: Text(title),
7684
content: SingleChildScrollView(child: Text(message)),
7785
actions: [
7886
TextButton(
79-
onPressed: () => Navigator.pop(context),
87+
onPressed: () => Navigator.pop<SuggestedActionDialogResult>(context,
88+
SuggestedActionDialogResult.cancel),
8089
child: _dialogActionText(zulipLocalizations.dialogCancel)),
8190
TextButton(
82-
onPressed: () {
83-
onActionButtonPress();
84-
Navigator.pop(context);
85-
},
91+
onPressed: () => Navigator.pop<SuggestedActionDialogResult>(context,
92+
SuggestedActionDialogResult.doAction),
8693
child: _dialogActionText(actionButtonText ?? zulipLocalizations.dialogContinue)),
8794
]));
95+
return DialogStatus(_wrapFutureExpectingNonNull(future));
96+
}
97+
98+
enum SuggestedActionDialogResult {
99+
cancel,
100+
doAction,
101+
}
102+
103+
Future<T> _wrapFutureExpectingNonNull<T>(Future<T?> future) {
104+
return future.then((value) {
105+
if (value == null) {
106+
throw StateError('future unexpectedly completed with null');
107+
}
108+
return value;
109+
});
88110
}

test/widgets/dialog_test.dart

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,38 @@ void main() {
2626
mode: LaunchMode.inAppBrowserView));
2727
});
2828
});
29+
30+
group('showSuggestedActionDialog', () {
31+
testWidgets('tap action button', (tester) async {
32+
addTearDown(testBinding.reset);
33+
await tester.pumpWidget(TestZulipApp());
34+
await tester.pump();
35+
final element = tester.element(find.byType(Placeholder));
36+
37+
final dialog = showSuggestedActionDialog(context: element,
38+
title: 'Continue?',
39+
message: 'Do the thing?',
40+
actionButtonText: 'Sure',);
41+
await tester.pump();
42+
await tester.tap(find.text('Sure'));
43+
await check(dialog.closed)
44+
.completes((it) => it.equals(SuggestedActionDialogResult.doAction));
45+
});
46+
47+
testWidgets('tap cancel', (tester) async {
48+
addTearDown(testBinding.reset);
49+
await tester.pumpWidget(TestZulipApp());
50+
await tester.pump();
51+
final element = tester.element(find.byType(Placeholder));
52+
53+
final dialog = showSuggestedActionDialog(context: element,
54+
title: 'Continue?',
55+
message: 'Do the thing?',
56+
actionButtonText: 'Sure',);
57+
await tester.pump();
58+
await tester.tap(find.text('Cancel'));
59+
await check(dialog.closed)
60+
.completes((it) => it.equals(SuggestedActionDialogResult.cancel));
61+
});
62+
});
2963
}

0 commit comments

Comments
 (0)