refactor: Always open Chat Backup as page right after login

This commit is contained in:
Christian Kußowski 2025-11-22 14:58:07 +01:00
commit 31a204f1ea
No known key found for this signature in database
GPG key ID: E067ECD60F1A0652
8 changed files with 95 additions and 69 deletions

View file

@ -7,6 +7,7 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/archive/archive.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/chat.dart';
import 'package:fluffychat/pages/chat_access_settings/chat_access_settings_controller.dart'; import 'package:fluffychat/pages/chat_access_settings/chat_access_settings_controller.dart';
import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart';
@ -101,6 +102,17 @@ abstract class AppRoutes {
const ConfigViewer(), const ConfigViewer(),
), ),
), ),
GoRoute(
path: '/backup',
redirect: loggedOutRedirect,
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
BootstrapDialog(
wipe: state.uri.queryParameters['wipe'] == 'true',
),
),
),
ShellRoute( ShellRoute(
// Never use a transition on the shell route. Changing the PageBuilder // Never use a transition on the shell route. Changing the PageBuilder
// here based on a MediaQuery causes the child to briefly be rendered // here based on a MediaQuery causes the child to briefly be rendered

View file

@ -502,7 +502,7 @@
"type": "String", "type": "String",
"placeholders": {} "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": { "@chatBackupDescription": {
"type": "String", "type": "String",
"placeholders": {} "placeholders": {}
@ -3460,5 +3460,9 @@
"stickerPackNameAlreadyExists": "Sticker pack name already exists", "stickerPackNameAlreadyExists": "Sticker pack name already exists",
"newStickerPack": "New sticker pack", "newStickerPack": "New sticker pack",
"stickerPackName": "Sticker pack name", "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"
} }

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.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/encryption.dart';
import 'package:matrix/matrix.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/fluffy_share.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/platform_infos.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/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/future_loading_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'; import '../key_verification/key_verification_dialog.dart';
class BootstrapDialog extends StatefulWidget { class BootstrapDialog extends StatefulWidget {
final bool wipe; final bool wipe;
final Client client;
const BootstrapDialog({ const BootstrapDialog({
super.key, super.key,
this.wipe = false, this.wipe = false,
required this.client,
}); });
Future<bool?> show(BuildContext context) => showAdaptiveBottomSheet(
context: context,
builder: (context) => this,
);
@override @override
BootstrapDialogState createState() => BootstrapDialogState(); BootstrapDialogState createState() => BootstrapDialogState();
} }
@ -38,7 +34,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
final TextEditingController _recoveryKeyTextEditingController = final TextEditingController _recoveryKeyTextEditingController =
TextEditingController(); TextEditingController();
late Bootstrap bootstrap; Bootstrap? bootstrap;
String? _recoveryKeyInputError; String? _recoveryKeyInputError;
@ -54,7 +50,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
bool? _wipe; bool? _wipe;
String get _secureStorageKey => String get _secureStorageKey =>
'ssss_recovery_key_${bootstrap.client.userID}'; 'ssss_recovery_key_${bootstrap!.client.userID}';
bool get _supportsSecureStorage => bool get _supportsSecureStorage =>
PlatformInfos.isMobile || PlatformInfos.isDesktop; PlatformInfos.isMobile || PlatformInfos.isDesktop;
@ -69,18 +65,42 @@ class BootstrapDialogState extends State<BootstrapDialog> {
return L10n.of(context).storeSecurlyOnThisDevice; return L10n.of(context).storeSecurlyOnThisDevice;
} }
late final Client client;
@override @override
void initState() { void initState() {
_createBootstrap(widget.wipe);
super.initState(); 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 { 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; _wipe = wipe;
titleText = null; titleText = null;
_recoveryKeyStored = false; _recoveryKeyStored = false;
bootstrap = bootstrap = client.encryption!.bootstrap(onUpdate: (_) => setState(() {}));
widget.client.encryption!.bootstrap(onUpdate: (_) => setState(() {}));
final key = await const FlutterSecureStorage().read(key: _secureStorageKey); final key = await const FlutterSecureStorage().read(key: _secureStorageKey);
if (key == null) return; if (key == null) return;
_recoveryKeyTextEditingController.text = key; _recoveryKeyTextEditingController.text = key;
@ -89,22 +109,45 @@ class BootstrapDialogState extends State<BootstrapDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(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; _wipe ??= widget.wipe;
final buttons = <Widget>[]; final buttons = <Widget>[];
Widget body = const CircularProgressIndicator.adaptive(); Widget body = const Center(child: CircularProgressIndicator.adaptive());
titleText = L10n.of(context).loadingPleaseWait; titleText = L10n.of(context).loadingPleaseWait;
if (bootstrap.newSsssKey?.recoveryKey != null && if (bootstrap.newSsssKey?.recoveryKey != null &&
_recoveryKeyStored == false) { _recoveryKeyStored == false) {
final key = bootstrap.newSsssKey!.recoveryKey; final key = bootstrap.newSsssKey!.recoveryKey;
titleText = L10n.of(context).recoveryKey; titleText = L10n.of(context).recoveryKey;
return Scaffold( return LoginScaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
leading: IconButton( leading: CloseButton(onPressed: _cancelAction),
icon: const Icon(Icons.close),
onPressed: Navigator.of(context).pop,
),
title: Text(L10n.of(context).recoveryKey), title: Text(L10n.of(context).recoveryKey),
), ),
body: Center( body: Center(
@ -220,14 +263,11 @@ class BootstrapDialogState extends State<BootstrapDialog> {
break; break;
case BootstrapState.openExistingSsss: case BootstrapState.openExistingSsss:
_recoveryKeyStored = true; _recoveryKeyStored = true;
return Scaffold( return LoginScaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
leading: IconButton( leading: CloseButton(onPressed: _cancelAction),
icon: const Icon(Icons.close), title: Text(L10n.of(context).setupChatBackup),
onPressed: Navigator.of(context).pop,
),
title: Text(L10n.of(context).chatBackup),
), ),
body: Center( body: Center(
child: ConstrainedBox( child: ConstrainedBox(
@ -373,16 +413,14 @@ class BootstrapDialogState extends State<BootstrapDialog> {
context: context, context: context,
delay: false, delay: false,
future: () async { future: () async {
await widget.client.updateUserDeviceKeys(); await client.updateUserDeviceKeys();
return widget.client return client.userDeviceKeys[client.userID!]!
.userDeviceKeys[widget.client.userID!]!
.startVerification(); .startVerification();
}, },
); );
if (req.error != null) return; if (req.error != null) return;
await KeyVerificationDialog(request: req.result!) await KeyVerificationDialog(request: req.result!)
.show(context); .show(context);
Navigator.of(context, rootNavigator: false).pop();
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@ -446,8 +484,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
body = const Icon(Icons.error_outline, color: Colors.red, size: 80); body = const Icon(Icons.error_outline, color: Colors.red, size: 80);
buttons.add( buttons.add(
ElevatedButton( ElevatedButton(
onPressed: () => onPressed: () => _goBackAction(false),
Navigator.of(context, rootNavigator: false).pop<bool>(false),
child: Text(L10n.of(context).close), child: Text(L10n.of(context).close),
), ),
); );
@ -472,8 +509,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
); );
buttons.add( buttons.add(
ElevatedButton( ElevatedButton(
onPressed: () => onPressed: () => _goBackAction(false),
Navigator.of(context, rootNavigator: false).pop<bool>(false),
child: Text(L10n.of(context).close), child: Text(L10n.of(context).close),
), ),
); );
@ -481,14 +517,9 @@ class BootstrapDialogState extends State<BootstrapDialog> {
} }
} }
return Scaffold( return LoginScaffold(
appBar: AppBar( appBar: AppBar(
leading: Center( leading: CloseButton(onPressed: _cancelAction),
child: CloseButton(
onPressed: () =>
Navigator.of(context, rootNavigator: false).pop<bool>(true),
),
),
title: Text(titleText ?? L10n.of(context).loadingPleaseWait), title: Text(titleText ?? L10n.of(context).loadingPleaseWait),
), ),
body: Center( body: Center(

View file

@ -2,6 +2,7 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/setting_keys.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/event_checkbox_extension.dart';
import '../../../utils/platform_infos.dart'; import '../../../utils/platform_infos.dart';
import '../../../utils/url_launcher.dart'; import '../../../utils/url_launcher.dart';
import '../../bootstrap/bootstrap_dialog.dart';
import 'audio_player.dart'; import 'audio_player.dart';
import 'cute_events.dart'; import 'cute_events.dart';
import 'html_message.dart'; import 'html_message.dart';
@ -59,9 +59,7 @@ class MessageContent extends StatelessWidget {
} }
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
if (client.isUnknownSession && client.encryption!.crossSigning.enabled) { if (client.isUnknownSession && client.encryption!.crossSigning.enabled) {
final success = await BootstrapDialog( final success = await context.push('/backup');
client: Matrix.of(context).client,
).show(context);
if (success != true) return; if (success != true) return;
} }
event.requestKey(); event.requestKey();

View file

@ -28,7 +28,6 @@ import '../../../utils/account_bundles.dart';
import '../../config/setting_keys.dart'; import '../../config/setting_keys.dart';
import '../../utils/url_launcher.dart'; import '../../utils/url_launcher.dart';
import '../../widgets/matrix.dart'; import '../../widgets/matrix.dart';
import '../bootstrap/bootstrap_dialog.dart';
enum PopupMenuAction { enum PopupMenuAction {
settings, settings,
@ -754,15 +753,6 @@ class ChatListController extends State<ChatList>
setState(() { setState(() {
waitForFirstSync = true; 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; if (!mounted) return;
setState(() { setState(() {

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:matrix/matrix.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/adaptive_dialogs/show_text_input_dialog.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart';
import '../../widgets/matrix.dart'; import '../../widgets/matrix.dart';
import '../bootstrap/bootstrap_dialog.dart';
import 'settings_view.dart'; import 'settings_view.dart';
class Settings extends StatefulWidget { class Settings extends StatefulWidget {
@ -197,9 +197,7 @@ class SettingsController extends State<Settings> {
); );
return; return;
} }
await BootstrapDialog( await context.push('/backup');
client: Matrix.of(context).client,
).show(context);
checkBootstrap(); checkBootstrap();
} }

View file

@ -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/app_lock.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import '../bootstrap/bootstrap_dialog.dart';
import 'settings_security_view.dart'; import 'settings_security_view.dart';
class SettingsSecurity extends StatefulWidget { class SettingsSecurity extends StatefulWidget {
@ -101,12 +100,6 @@ class SettingsSecurityController extends State<SettingsSecurity> {
); );
} }
void showBootstrapDialog(BuildContext context) async {
await BootstrapDialog(
client: Matrix.of(context).client,
).show(context);
}
Future<void> dehydrateAction() => Matrix.of(context).dehydrateAction(context); Future<void> dehydrateAction() => Matrix.of(context).dehydrateAction(context);
void changeShareKeysWith(ShareKeysWith? shareKeysWith) async { void changeShareKeysWith(ShareKeysWith? shareKeysWith) async {

View file

@ -170,7 +170,7 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
); );
_registerSubs(_loginClientCandidate!.clientName); _registerSubs(_loginClientCandidate!.clientName);
_loginClientCandidate = null; _loginClientCandidate = null;
FluffyChatApp.router.go('/rooms'); FluffyChatApp.router.go('/backup');
}); });
if (widget.clients.isEmpty) widget.clients.add(candidate); if (widget.clients.isEmpty) widget.clients.add(candidate);
return candidate; return candidate;
@ -283,7 +283,7 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
} }
} else { } else {
FluffyChatApp.router FluffyChatApp.router
.go(state == LoginState.loggedIn ? '/rooms' : '/home'); .go(state == LoginState.loggedIn ? '/backup' : '/home');
} }
}); });
onUiaRequest[name] ??= c.onUiaRequest.stream.listen(uiaRequestHandler); onUiaRequest[name] ??= c.onUiaRequest.stream.listen(uiaRequestHandler);