diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 6a5cfd322..d8cdb1128 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -20,7 +20,7 @@ import 'package:fluffychat/views/ui/login_ui.dart'; import 'package:fluffychat/views/new_group.dart'; import 'package:fluffychat/views/new_private_chat.dart'; import 'package:fluffychat/views/search.dart'; -import 'package:fluffychat/views/ui/settings_ui.dart'; +import 'package:fluffychat/views/settings.dart'; import 'package:fluffychat/views/settings_3pid.dart'; import 'package:fluffychat/views/device_settings.dart'; import 'package:fluffychat/views/settings_ignore_list.dart'; diff --git a/lib/views/settings.dart b/lib/views/settings.dart new file mode 100644 index 000000000..6141f49bf --- /dev/null +++ b/lib/views/settings.dart @@ -0,0 +1,385 @@ +import 'dart:async'; + +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:adaptive_page_layout/adaptive_page_layout.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:file_picker_cross/file_picker_cross.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/sentry_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_app_lock/flutter_app_lock.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_screen_lock/functions.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:image_picker/image_picker.dart'; + +import 'ui/settings_ui.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'widgets/dialogs/bootstrap_dialog.dart'; +import 'widgets/matrix.dart'; +import '../config/app_config.dart'; +import '../config/setting_keys.dart'; + +class Settings extends StatefulWidget { + @override + SettingsController createState() => SettingsController(); +} + +class SettingsController extends State { + Future profileFuture; + dynamic profile; + Future crossSigningCachedFuture; + bool crossSigningCached; + Future megolmBackupCachedFuture; + bool megolmBackupCached; + + void logoutAction() async { + if (await showOkCancelAlertDialog( + context: context, + title: L10n.of(context).areYouSureYouWantToLogout, + okLabel: L10n.of(context).yes, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + ) == + OkCancelResult.cancel) { + return; + } + final matrix = Matrix.of(context); + await showFutureLoadingDialog( + context: context, + future: () => matrix.client.logout(), + ); + } + + void changePasswordAccountAction() async { + final input = await showTextInputDialog( + context: context, + title: L10n.of(context).changePassword, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + textFields: [ + DialogTextField( + hintText: L10n.of(context).pleaseEnterYourPassword, + obscureText: true, + minLines: 1, + maxLines: 1, + ), + DialogTextField( + hintText: L10n.of(context).chooseAStrongPassword, + obscureText: true, + minLines: 1, + maxLines: 1, + ), + ], + ); + if (input == null) return; + final success = await showFutureLoadingDialog( + context: context, + future: () => Matrix.of(context) + .client + .changePassword(input.last, oldPassword: input.first), + ); + if (success.error == null) { + AdaptivePageLayout.of(context).showSnackBar( + SnackBar(content: Text(L10n.of(context).passwordHasBeenChanged))); + } + } + + void deleteAccountAction() async { + if (await showOkCancelAlertDialog( + context: context, + title: L10n.of(context).warning, + message: L10n.of(context).deactivateAccountWarning, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + ) == + OkCancelResult.cancel) { + return; + } + if (await showOkCancelAlertDialog( + context: context, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).yes, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + ) == + OkCancelResult.cancel) { + return; + } + final input = await showTextInputDialog( + context: context, + title: L10n.of(context).pleaseEnterYourPassword, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + textFields: [ + DialogTextField( + obscureText: true, + hintText: '******', + minLines: 1, + maxLines: 1, + ) + ], + ); + if (input == null) return; + await showFutureLoadingDialog( + context: context, + future: () => Matrix.of(context).client.deactivateAccount( + auth: AuthenticationPassword( + password: input.single, + user: Matrix.of(context).client.userID, + identifier: AuthenticationUserIdentifier( + user: Matrix.of(context).client.userID), + ), + ), + ); + } + + void setJitsiInstanceAction() async { + const prefix = 'https://'; + final input = await showTextInputDialog( + context: context, + title: L10n.of(context).editJitsiInstance, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + textFields: [ + DialogTextField( + initialText: AppConfig.jitsiInstance.replaceFirst(prefix, ''), + prefixText: prefix, + ), + ], + ); + if (input == null) return; + var jitsi = prefix + input.single; + if (!jitsi.endsWith('/')) { + jitsi += '/'; + } + final matrix = Matrix.of(context); + await matrix.store.setItem(SettingKeys.jitsiInstance, jitsi); + AppConfig.jitsiInstance = jitsi; + } + + void setDisplaynameAction() async { + final input = await showTextInputDialog( + context: context, + title: L10n.of(context).editDisplayname, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + textFields: [ + DialogTextField( + initialText: profile?.displayname ?? + Matrix.of(context).client.userID.localpart, + ) + ], + ); + if (input == null) return; + final matrix = Matrix.of(context); + final success = await showFutureLoadingDialog( + context: context, + future: () => + matrix.client.setDisplayname(matrix.client.userID, input.single), + ); + if (success.error == null) { + setState(() { + profileFuture = null; + profile = null; + }); + } + } + + void setAvatarAction() async { + MatrixFile file; + if (PlatformInfos.isMobile) { + final result = await ImagePicker().getImage( + source: ImageSource.gallery, + imageQuality: 50, + maxWidth: 1600, + maxHeight: 1600); + if (result == null) return; + file = MatrixFile( + bytes: await result.readAsBytes(), + name: result.path, + ); + } else { + final result = + await FilePickerCross.importFromStorage(type: FileTypeCross.image); + if (result == null) return; + file = MatrixFile( + bytes: result.toUint8List(), + name: result.fileName, + ); + } + final matrix = Matrix.of(context); + final success = await showFutureLoadingDialog( + context: context, + future: () => matrix.client.setAvatar(file), + ); + if (success.error == null) { + setState(() { + profileFuture = null; + profile = null; + }); + } + } + + Future requestSSSSCache() async { + final handle = Matrix.of(context).client.encryption.ssss.open(); + final input = await showTextInputDialog( + context: context, + title: L10n.of(context).askSSSSCache, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + textFields: [ + DialogTextField( + hintText: L10n.of(context).passphraseOrKey, + obscureText: true, + minLines: 1, + maxLines: 1, + ) + ], + ); + if (input != null) { + final valid = await showFutureLoadingDialog( + context: context, + future: () async { + // make sure the loading spinner shows before we test the keys + await Future.delayed(Duration(milliseconds: 100)); + var valid = false; + try { + await handle.unlock(recoveryKey: input.single); + valid = true; + } catch (e, s) { + SentryController.captureException(e, s); + } + return valid; + }); + + if (valid.result == true) { + await handle.maybeCacheAll(); + await showOkAlertDialog( + context: context, + message: L10n.of(context).cachedKeys, + okLabel: L10n.of(context).ok, + useRootNavigator: false, + ); + setState(() { + crossSigningCachedFuture = null; + crossSigningCached = null; + megolmBackupCachedFuture = null; + megolmBackupCached = null; + }); + } else { + await showOkAlertDialog( + context: context, + message: L10n.of(context).incorrectPassphraseOrKey, + okLabel: L10n.of(context).ok, + useRootNavigator: false, + ); + } + } + } + + void setAppLockAction() async { + final currentLock = + await FlutterSecureStorage().read(key: SettingKeys.appLockKey); + if (currentLock?.isNotEmpty ?? false) { + var unlocked = false; + await screenLock( + context: context, + correctString: currentLock, + didConfirmed: (_) => unlocked = true, + ); + if (unlocked != true) return; + } + final newLock = await showTextInputDialog( + context: context, + title: L10n.of(context).pleaseChooseAPasscode, + message: L10n.of(context).pleaseEnter4Digits, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + textFields: [ + DialogTextField( + validator: (text) { + if (text.isEmpty || (text.length == 4 && int.tryParse(text) >= 0)) { + return null; + } + return L10n.of(context).pleaseEnter4Digits; + }, + keyboardType: TextInputType.number, + obscureText: true, + maxLines: 1, + minLines: 1, + ) + ], + ); + if (newLock != null) { + await FlutterSecureStorage() + .write(key: SettingKeys.appLockKey, value: newLock.single); + if (newLock.single.isEmpty) { + AppLock.of(context).disable(); + } else { + AppLock.of(context).enable(); + } + } + } + + void bootstrapSettingsAction() async { + if (await Matrix.of(context).client.encryption.keyManager.isCached()) { + if (OkCancelResult.ok == + await showOkCancelAlertDialog( + context: context, + title: L10n.of(context).keysCached, + message: L10n.of(context).wipeChatBackup, + isDestructiveAction: true, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + )) { + await BootstrapDialog( + client: Matrix.of(context).client, + wipe: true, + ).show(context); + } + return; + } + await BootstrapDialog( + client: Matrix.of(context).client, + ).show(context); + } + + void firstRunBootstrapAction() async { + await BootstrapDialog( + client: Matrix.of(context).client, + ).show(context); + AdaptivePageLayout.of(context).popUntilIsFirst(); + } + + @override + Widget build(BuildContext context) { + final client = Matrix.of(context).client; + profileFuture ??= client.ownProfile.then((p) { + if (mounted) setState(() => profile = p); + return p; + }); + if (client.encryption != null) { + crossSigningCachedFuture ??= + client.encryption?.crossSigning?.isCached()?.then((c) { + if (mounted) setState(() => crossSigningCached = c); + return c; + }); + megolmBackupCachedFuture ??= + client.encryption?.keyManager?.isCached()?.then((c) { + if (mounted) setState(() => megolmBackupCached = c); + return c; + }); + } + return SettingsUI(this); + } +} diff --git a/lib/views/ui/settings_ui.dart b/lib/views/ui/settings_ui.dart index 23b435833..e94e806f5 100644 --- a/lib/views/ui/settings_ui.dart +++ b/lib/views/ui/settings_ui.dart @@ -1,359 +1,31 @@ -import 'dart:async'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart'; -import 'package:fluffychat/views/widgets/dialogs/bootstrap_dialog.dart'; import 'package:fluffychat/views/widgets/sentry_switch_list_tile.dart'; import 'package:fluffychat/views/widgets/settings_switch_list_tile.dart'; import 'package:famedlysdk/famedlysdk.dart'; -import 'package:file_picker_cross/file_picker_cross.dart'; import 'package:fluffychat/utils/beautify_string_extension.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/utils/sentry_controller.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_app_lock/flutter_app_lock.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_screen_lock/functions.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:image_picker/image_picker.dart'; import 'package:url_launcher/url_launcher.dart'; import '../widgets/content_banner.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import '../widgets/matrix.dart'; import '../../config/app_config.dart'; import '../../config/setting_keys.dart'; +import '../settings.dart'; -class Settings extends StatefulWidget { - @override - _SettingsState createState() => _SettingsState(); -} - -class _SettingsState extends State { - Future profileFuture; - dynamic profile; - Future crossSigningCachedFuture; - bool crossSigningCached; - Future megolmBackupCachedFuture; - bool megolmBackupCached; - - void logoutAction(BuildContext context) async { - if (await showOkCancelAlertDialog( - context: context, - title: L10n.of(context).areYouSureYouWantToLogout, - okLabel: L10n.of(context).yes, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - ) == - OkCancelResult.cancel) { - return; - } - final matrix = Matrix.of(context); - await showFutureLoadingDialog( - context: context, - future: () => matrix.client.logout(), - ); - } - - void _changePasswordAccountAction(BuildContext context) async { - final input = await showTextInputDialog( - context: context, - title: L10n.of(context).changePassword, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - textFields: [ - DialogTextField( - hintText: L10n.of(context).pleaseEnterYourPassword, - obscureText: true, - minLines: 1, - maxLines: 1, - ), - DialogTextField( - hintText: L10n.of(context).chooseAStrongPassword, - obscureText: true, - minLines: 1, - maxLines: 1, - ), - ], - ); - if (input == null) return; - final success = await showFutureLoadingDialog( - context: context, - future: () => Matrix.of(context) - .client - .changePassword(input.last, oldPassword: input.first), - ); - if (success.error == null) { - AdaptivePageLayout.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context).passwordHasBeenChanged))); - } - } - - void _deleteAccountAction(BuildContext context) async { - if (await showOkCancelAlertDialog( - context: context, - title: L10n.of(context).warning, - message: L10n.of(context).deactivateAccountWarning, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - ) == - OkCancelResult.cancel) { - return; - } - if (await showOkCancelAlertDialog( - context: context, - title: L10n.of(context).areYouSure, - okLabel: L10n.of(context).yes, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - ) == - OkCancelResult.cancel) { - return; - } - final input = await showTextInputDialog( - context: context, - title: L10n.of(context).pleaseEnterYourPassword, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - textFields: [ - DialogTextField( - obscureText: true, - hintText: '******', - minLines: 1, - maxLines: 1, - ) - ], - ); - if (input == null) return; - await showFutureLoadingDialog( - context: context, - future: () => Matrix.of(context).client.deactivateAccount( - auth: AuthenticationPassword( - password: input.single, - user: Matrix.of(context).client.userID, - identifier: AuthenticationUserIdentifier( - user: Matrix.of(context).client.userID), - ), - ), - ); - } - - void setJitsiInstanceAction(BuildContext context) async { - const prefix = 'https://'; - final input = await showTextInputDialog( - context: context, - title: L10n.of(context).editJitsiInstance, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - textFields: [ - DialogTextField( - initialText: AppConfig.jitsiInstance.replaceFirst(prefix, ''), - prefixText: prefix, - ), - ], - ); - if (input == null) return; - var jitsi = prefix + input.single; - if (!jitsi.endsWith('/')) { - jitsi += '/'; - } - final matrix = Matrix.of(context); - await matrix.store.setItem(SettingKeys.jitsiInstance, jitsi); - AppConfig.jitsiInstance = jitsi; - } - - void setDisplaynameAction(BuildContext context) async { - final input = await showTextInputDialog( - context: context, - title: L10n.of(context).editDisplayname, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - textFields: [ - DialogTextField( - initialText: profile?.displayname ?? - Matrix.of(context).client.userID.localpart, - ) - ], - ); - if (input == null) return; - final matrix = Matrix.of(context); - final success = await showFutureLoadingDialog( - context: context, - future: () => - matrix.client.setDisplayname(matrix.client.userID, input.single), - ); - if (success.error == null) { - setState(() { - profileFuture = null; - profile = null; - }); - } - } - - void setAvatarAction(BuildContext context) async { - MatrixFile file; - if (PlatformInfos.isMobile) { - final result = await ImagePicker().getImage( - source: ImageSource.gallery, - imageQuality: 50, - maxWidth: 1600, - maxHeight: 1600); - if (result == null) return; - file = MatrixFile( - bytes: await result.readAsBytes(), - name: result.path, - ); - } else { - final result = - await FilePickerCross.importFromStorage(type: FileTypeCross.image); - if (result == null) return; - file = MatrixFile( - bytes: result.toUint8List(), - name: result.fileName, - ); - } - final matrix = Matrix.of(context); - final success = await showFutureLoadingDialog( - context: context, - future: () => matrix.client.setAvatar(file), - ); - if (success.error == null) { - setState(() { - profileFuture = null; - profile = null; - }); - } - } - - Future requestSSSSCache(BuildContext context) async { - final handle = Matrix.of(context).client.encryption.ssss.open(); - final input = await showTextInputDialog( - context: context, - title: L10n.of(context).askSSSSCache, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - textFields: [ - DialogTextField( - hintText: L10n.of(context).passphraseOrKey, - obscureText: true, - minLines: 1, - maxLines: 1, - ) - ], - ); - if (input != null) { - final valid = await showFutureLoadingDialog( - context: context, - future: () async { - // make sure the loading spinner shows before we test the keys - await Future.delayed(Duration(milliseconds: 100)); - var valid = false; - try { - await handle.unlock(recoveryKey: input.single); - valid = true; - } catch (e, s) { - SentryController.captureException(e, s); - } - return valid; - }); +class SettingsUI extends StatelessWidget { + final SettingsController controller; - if (valid.result == true) { - await handle.maybeCacheAll(); - await showOkAlertDialog( - context: context, - message: L10n.of(context).cachedKeys, - okLabel: L10n.of(context).ok, - useRootNavigator: false, - ); - setState(() { - crossSigningCachedFuture = null; - crossSigningCached = null; - megolmBackupCachedFuture = null; - megolmBackupCached = null; - }); - } else { - await showOkAlertDialog( - context: context, - message: L10n.of(context).incorrectPassphraseOrKey, - okLabel: L10n.of(context).ok, - useRootNavigator: false, - ); - } - } - } - - void _setAppLockAction(BuildContext context) async { - final currentLock = - await FlutterSecureStorage().read(key: SettingKeys.appLockKey); - if (currentLock?.isNotEmpty ?? false) { - var unlocked = false; - await screenLock( - context: context, - correctString: currentLock, - didConfirmed: (_) => unlocked = true, - ); - if (unlocked != true) return; - } - final newLock = await showTextInputDialog( - context: context, - title: L10n.of(context).pleaseChooseAPasscode, - message: L10n.of(context).pleaseEnter4Digits, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - textFields: [ - DialogTextField( - validator: (text) { - if (text.isEmpty || (text.length == 4 && int.tryParse(text) >= 0)) { - return null; - } - return L10n.of(context).pleaseEnter4Digits; - }, - keyboardType: TextInputType.number, - obscureText: true, - maxLines: 1, - minLines: 1, - ) - ], - ); - if (newLock != null) { - await FlutterSecureStorage() - .write(key: SettingKeys.appLockKey, value: newLock.single); - if (newLock.single.isEmpty) { - AppLock.of(context).disable(); - } else { - AppLock.of(context).enable(); - } - } - } + const SettingsUI(this.controller, {Key key}) : super(key: key); @override Widget build(BuildContext context) { final client = Matrix.of(context).client; - profileFuture ??= client.ownProfile.then((p) { - if (mounted) setState(() => profile = p); - return p; - }); - if (client.encryption != null) { - crossSigningCachedFuture ??= - client.encryption?.crossSigning?.isCached()?.then((c) { - if (mounted) setState(() => crossSigningCached = c); - return c; - }); - megolmBackupCachedFuture ??= - client.encryption?.keyManager?.isCached()?.then((c) { - if (mounted) setState(() => megolmBackupCached = c); - return c; - }); - } return Scaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) => @@ -375,7 +47,7 @@ class _SettingsState extends State { .color)), actions: [ FutureBuilder( - future: crossSigningCachedFuture, + future: controller.crossSigningCachedFuture, builder: (context, snapshot) { final needsBootstrap = Matrix.of(context) .client @@ -394,19 +66,14 @@ class _SettingsState extends State { L10n.of(context).chatBackup, style: TextStyle(color: Colors.red), ), - onPressed: () async { - await BootstrapDialog( - client: Matrix.of(context).client, - ).show(context); - AdaptivePageLayout.of(context).popUntilIsFirst(); - }, + onPressed: controller.firstRunBootstrapAction, ); }), ], backgroundColor: Theme.of(context).appBarTheme.color, flexibleSpace: FlexibleSpaceBar( - background: ContentBanner(profile?.avatarUrl, - onEdit: () => setAvatarAction(context)), + background: ContentBanner(controller.profile?.avatarUrl, + onEdit: controller.setAvatarAction), ), ), ], @@ -480,14 +147,15 @@ class _SettingsState extends State { ListTile( trailing: Icon(Icons.edit_outlined), title: Text(L10n.of(context).editDisplayname), - subtitle: Text(profile?.displayname ?? client.userID.localpart), - onTap: () => setDisplaynameAction(context), + subtitle: Text( + controller.profile?.displayname ?? client.userID.localpart), + onTap: controller.setDisplaynameAction, ), ListTile( trailing: Icon(Icons.phone_outlined), title: Text(L10n.of(context).editJitsiInstance), subtitle: Text(AppConfig.jitsiInstance), - onTap: () => setJitsiInstanceAction(context), + onTap: controller.setJitsiInstanceAction, ), ListTile( trailing: Icon(Icons.devices_other_outlined), @@ -508,7 +176,7 @@ class _SettingsState extends State { title: Text( L10n.of(context).changePassword, ), - onTap: () => _changePasswordAccountAction(context), + onTap: controller.changePasswordAccountAction, ), ListTile( trailing: Icon(Icons.email_outlined), @@ -519,7 +187,7 @@ class _SettingsState extends State { ListTile( trailing: Icon(Icons.exit_to_app_outlined), title: Text(L10n.of(context).logout), - onTap: () => logoutAction(context), + onTap: controller.logoutAction, ), ListTile( trailing: Icon(Icons.delete_forever_outlined), @@ -527,7 +195,7 @@ class _SettingsState extends State { L10n.of(context).deleteAccount, style: TextStyle(color: Colors.red), ), - onTap: () => _deleteAccountAction(context), + onTap: controller.deleteAccountAction, ), if (client.encryption != null) ...{ Divider(thickness: 1), @@ -544,7 +212,7 @@ class _SettingsState extends State { ListTile( trailing: Icon(Icons.lock_outlined), title: Text(L10n.of(context).appLock), - onTap: () => _setAppLockAction(context), + onTap: controller.setAppLockAction, ), ListTile( title: Text(L10n.of(context).yourPublicKey), @@ -562,29 +230,7 @@ class _SettingsState extends State { trailing: Icon(Icons.wb_cloudy_outlined), subtitle: Text( '${client.encryption.keyManager.enabled ? L10n.of(context).onlineKeyBackupEnabled : L10n.of(context).onlineKeyBackupDisabled}\n${client.encryption.crossSigning.enabled ? L10n.of(context).crossSigningEnabled : L10n.of(context).crossSigningDisabled}'), - onTap: () async { - if (await client.encryption.keyManager.isCached()) { - if (OkCancelResult.ok == - await showOkCancelAlertDialog( - context: context, - title: L10n.of(context).keysCached, - message: L10n.of(context).wipeChatBackup, - isDestructiveAction: true, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - )) { - await BootstrapDialog( - client: Matrix.of(context).client, - wipe: true, - ).show(context); - } - return; - } - await BootstrapDialog( - client: Matrix.of(context).client, - ).show(context); - }, + onTap: controller.bootstrapSettingsAction, ), }, Divider(thickness: 1),