From 31a204f1ea6c3a8b2bbe33fffd31c88048468e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sat, 22 Nov 2025 14:58:07 +0100 Subject: [PATCH] refactor: Always open Chat Backup as page right after login --- lib/config/routes.dart | 12 ++ lib/l10n/intl_en.arb | 8 +- lib/pages/bootstrap/bootstrap_dialog.dart | 111 +++++++++++------- lib/pages/chat/events/message_content.dart | 6 +- lib/pages/chat_list/chat_list.dart | 10 -- lib/pages/settings/settings.dart | 6 +- .../settings_security/settings_security.dart | 7 -- lib/widgets/matrix.dart | 4 +- 8 files changed, 95 insertions(+), 69 deletions(-) diff --git a/lib/config/routes.dart b/lib/config/routes.dart index a598ffd70..f33d9044b 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -7,6 +7,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/archive/archive.dart'; +import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat_access_settings/chat_access_settings_controller.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; @@ -101,6 +102,17 @@ abstract class AppRoutes { const ConfigViewer(), ), ), + GoRoute( + path: '/backup', + redirect: loggedOutRedirect, + pageBuilder: (context, state) => defaultPageBuilder( + context, + state, + BootstrapDialog( + wipe: state.uri.queryParameters['wipe'] == 'true', + ), + ), + ), ShellRoute( // Never use a transition on the shell route. Changing the PageBuilder // here based on a MediaQuery causes the child to briefly be rendered diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 99e856036..b09d93862 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -502,7 +502,7 @@ "type": "String", "placeholders": {} }, - "chatBackupDescription": "Your old messages are secured with a recovery key. Please make sure you don't lose it.", + "chatBackupDescription": "Your messages are secured with a recovery key. Please make sure you don't lose it.", "@chatBackupDescription": { "type": "String", "placeholders": {} @@ -3460,5 +3460,9 @@ "stickerPackNameAlreadyExists": "Sticker pack name already exists", "newStickerPack": "New sticker pack", "stickerPackName": "Sticker pack name", - "attribution": "Attribution" + "attribution": "Attribution", + "skipChatBackup": "Skip chat backup", + "skipChatBackupWarning": "Are you sure? Without enabling the chat backup you may lose access to your messages if you switch your device.", + "loadingMessages": "Loading messages", + "setupChatBackup": "Set up chat backup" } diff --git a/lib/pages/bootstrap/bootstrap_dialog.dart b/lib/pages/bootstrap/bootstrap_dialog.dart index a303c3530..3d8a2c2ba 100644 --- a/lib/pages/bootstrap/bootstrap_dialog.dart +++ b/lib/pages/bootstrap/bootstrap_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:go_router/go_router.dart'; import 'package:matrix/encryption.dart'; import 'package:matrix/matrix.dart'; @@ -10,26 +11,21 @@ import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/sync_status_localization.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; -import '../../utils/adaptive_bottom_sheet.dart'; +import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import '../key_verification/key_verification_dialog.dart'; class BootstrapDialog extends StatefulWidget { final bool wipe; - final Client client; const BootstrapDialog({ super.key, this.wipe = false, - required this.client, }); - Future show(BuildContext context) => showAdaptiveBottomSheet( - context: context, - builder: (context) => this, - ); - @override BootstrapDialogState createState() => BootstrapDialogState(); } @@ -38,7 +34,7 @@ class BootstrapDialogState extends State { final TextEditingController _recoveryKeyTextEditingController = TextEditingController(); - late Bootstrap bootstrap; + Bootstrap? bootstrap; String? _recoveryKeyInputError; @@ -54,7 +50,7 @@ class BootstrapDialogState extends State { bool? _wipe; String get _secureStorageKey => - 'ssss_recovery_key_${bootstrap.client.userID}'; + 'ssss_recovery_key_${bootstrap!.client.userID}'; bool get _supportsSecureStorage => PlatformInfos.isMobile || PlatformInfos.isDesktop; @@ -69,18 +65,42 @@ class BootstrapDialogState extends State { return L10n.of(context).storeSecurlyOnThisDevice; } + late final Client client; + @override void initState() { - _createBootstrap(widget.wipe); super.initState(); + client = Matrix.of(context).client; + _createBootstrap(widget.wipe); } + void _cancelAction() async { + final consent = await showOkCancelAlertDialog( + context: context, + title: L10n.of(context).skipChatBackup, + message: L10n.of(context).skipChatBackupWarning, + okLabel: L10n.of(context).skip, + isDestructive: true, + ); + if (consent != OkCancelResult.ok) return; + if (!mounted) return; + _goBackAction(false); + } + + void _goBackAction(bool success) => + context.canPop() ? context.pop(success) : context.go('/rooms'); + void _createBootstrap(bool wipe) async { + await client.roomsLoading; + await client.accountDataLoading; + await client.userDeviceKeysLoading; + while (client.prevBatch == null) { + await client.onSync.stream.first; + } _wipe = wipe; titleText = null; _recoveryKeyStored = false; - bootstrap = - widget.client.encryption!.bootstrap(onUpdate: (_) => setState(() {})); + bootstrap = client.encryption!.bootstrap(onUpdate: (_) => setState(() {})); final key = await const FlutterSecureStorage().read(key: _secureStorageKey); if (key == null) return; _recoveryKeyTextEditingController.text = key; @@ -89,22 +109,45 @@ class BootstrapDialogState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final bootstrap = this.bootstrap; + if (bootstrap == null) { + return LoginScaffold( + appBar: AppBar( + centerTitle: true, + leading: CloseButton(onPressed: _cancelAction), + title: Text(L10n.of(context).loadingMessages), + ), + body: Center( + child: StreamBuilder( + stream: client.onSyncStatus.stream, + builder: (context, snapshot) { + final status = snapshot.data; + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator.adaptive(value: status?.progress), + if (status != null) Text(status.calcLocalizedString(context)), + ], + ); + }, + ), + ), + ); + } + _wipe ??= widget.wipe; final buttons = []; - Widget body = const CircularProgressIndicator.adaptive(); + Widget body = const Center(child: CircularProgressIndicator.adaptive()); titleText = L10n.of(context).loadingPleaseWait; if (bootstrap.newSsssKey?.recoveryKey != null && _recoveryKeyStored == false) { final key = bootstrap.newSsssKey!.recoveryKey; titleText = L10n.of(context).recoveryKey; - return Scaffold( + return LoginScaffold( appBar: AppBar( centerTitle: true, - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: Navigator.of(context).pop, - ), + leading: CloseButton(onPressed: _cancelAction), title: Text(L10n.of(context).recoveryKey), ), body: Center( @@ -220,14 +263,11 @@ class BootstrapDialogState extends State { break; case BootstrapState.openExistingSsss: _recoveryKeyStored = true; - return Scaffold( + return LoginScaffold( appBar: AppBar( centerTitle: true, - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: Navigator.of(context).pop, - ), - title: Text(L10n.of(context).chatBackup), + leading: CloseButton(onPressed: _cancelAction), + title: Text(L10n.of(context).setupChatBackup), ), body: Center( child: ConstrainedBox( @@ -373,16 +413,14 @@ class BootstrapDialogState extends State { context: context, delay: false, future: () async { - await widget.client.updateUserDeviceKeys(); - return widget.client - .userDeviceKeys[widget.client.userID!]! + await client.updateUserDeviceKeys(); + return client.userDeviceKeys[client.userID!]! .startVerification(); }, ); if (req.error != null) return; await KeyVerificationDialog(request: req.result!) .show(context); - Navigator.of(context, rootNavigator: false).pop(); }, ), const SizedBox(height: 16), @@ -446,8 +484,7 @@ class BootstrapDialogState extends State { body = const Icon(Icons.error_outline, color: Colors.red, size: 80); buttons.add( ElevatedButton( - onPressed: () => - Navigator.of(context, rootNavigator: false).pop(false), + onPressed: () => _goBackAction(false), child: Text(L10n.of(context).close), ), ); @@ -472,8 +509,7 @@ class BootstrapDialogState extends State { ); buttons.add( ElevatedButton( - onPressed: () => - Navigator.of(context, rootNavigator: false).pop(false), + onPressed: () => _goBackAction(false), child: Text(L10n.of(context).close), ), ); @@ -481,14 +517,9 @@ class BootstrapDialogState extends State { } } - return Scaffold( + return LoginScaffold( appBar: AppBar( - leading: Center( - child: CloseButton( - onPressed: () => - Navigator.of(context, rootNavigator: false).pop(true), - ), - ), + leading: CloseButton(onPressed: _cancelAction), title: Text(titleText ?? L10n.of(context).loadingPleaseWait), ), body: Center( diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 12ef8f112..833291068 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/setting_keys.dart'; @@ -17,7 +18,6 @@ import '../../../config/app_config.dart'; import '../../../utils/event_checkbox_extension.dart'; import '../../../utils/platform_infos.dart'; import '../../../utils/url_launcher.dart'; -import '../../bootstrap/bootstrap_dialog.dart'; import 'audio_player.dart'; import 'cute_events.dart'; import 'html_message.dart'; @@ -59,9 +59,7 @@ class MessageContent extends StatelessWidget { } final client = Matrix.of(context).client; if (client.isUnknownSession && client.encryption!.crossSigning.enabled) { - final success = await BootstrapDialog( - client: Matrix.of(context).client, - ).show(context); + final success = await context.push('/backup'); if (success != true) return; } event.requestKey(); diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 4f96aff6d..cd1d542a4 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -28,7 +28,6 @@ import '../../../utils/account_bundles.dart'; import '../../config/setting_keys.dart'; import '../../utils/url_launcher.dart'; import '../../widgets/matrix.dart'; -import '../bootstrap/bootstrap_dialog.dart'; enum PopupMenuAction { settings, @@ -754,15 +753,6 @@ class ChatListController extends State setState(() { waitForFirstSync = true; }); - - // Display first login bootstrap if enabled - if (client.encryption?.keyManager.enabled == true) { - if (await client.encryption?.keyManager.isCached() == false || - await client.encryption?.crossSigning.isCached() == false || - client.isUnknownSession && !mounted) { - await BootstrapDialog(client: client).show(context); - } - } } if (!mounted) return; setState(() { diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index bb8fa9a10..c386b18d3 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; +import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; @@ -14,7 +15,6 @@ import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog. import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../widgets/matrix.dart'; -import '../bootstrap/bootstrap_dialog.dart'; import 'settings_view.dart'; class Settings extends StatefulWidget { @@ -197,9 +197,7 @@ class SettingsController extends State { ); return; } - await BootstrapDialog( - client: Matrix.of(context).client, - ).show(context); + await context.push('/backup'); checkBootstrap(); } diff --git a/lib/pages/settings_security/settings_security.dart b/lib/pages/settings_security/settings_security.dart index 2ec3747c0..db12783f9 100644 --- a/lib/pages/settings_security/settings_security.dart +++ b/lib/pages/settings_security/settings_security.dart @@ -9,7 +9,6 @@ import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart' import 'package:fluffychat/widgets/app_lock.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import '../bootstrap/bootstrap_dialog.dart'; import 'settings_security_view.dart'; class SettingsSecurity extends StatefulWidget { @@ -101,12 +100,6 @@ class SettingsSecurityController extends State { ); } - void showBootstrapDialog(BuildContext context) async { - await BootstrapDialog( - client: Matrix.of(context).client, - ).show(context); - } - Future dehydrateAction() => Matrix.of(context).dehydrateAction(context); void changeShareKeysWith(ShareKeysWith? shareKeysWith) async { diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 55fd267ad..63b7ee117 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -170,7 +170,7 @@ class MatrixState extends State with WidgetsBindingObserver { ); _registerSubs(_loginClientCandidate!.clientName); _loginClientCandidate = null; - FluffyChatApp.router.go('/rooms'); + FluffyChatApp.router.go('/backup'); }); if (widget.clients.isEmpty) widget.clients.add(candidate); return candidate; @@ -283,7 +283,7 @@ class MatrixState extends State with WidgetsBindingObserver { } } else { FluffyChatApp.router - .go(state == LoginState.loggedIn ? '/rooms' : '/home'); + .go(state == LoginState.loggedIn ? '/backup' : '/home'); } }); onUiaRequest[name] ??= c.onUiaRequest.stream.listen(uiaRequestHandler);