diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 739ac510e..55f78d935 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2550,6 +2550,7 @@ "wrongRecoveryKey": "Sorry... this does not seem to be the correct recovery key.", "startConversation": "Start conversation", "commandHint_sendraw": "Send raw json", - "databaseMigrationTitle": "Database is optimized", - "databaseMigrationBody": "Please wait. This may take a moment." + "databaseMigrationTitle": "Optimizing your chats... This can take a few moments!", + "loadingChats": "Loading your chats...", + "reportError": "Report error" } diff --git a/lib/main.dart b/lib/main.dart index 877f80da3..2efbf0d0f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:collection/collection.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:matrix/matrix.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -22,15 +21,23 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); Logs().nativeColors = !PlatformInfos.isIOS; + + // Do not send online presences when app is in background fetch mode. final store = await SharedPreferences.getInstance(); - final clients = await ClientManager.getClients(store: store); + var isMigratingDatabase = false; + final clientsFuture = ClientManager.getClients( + store: store, + onMigration: () { + isMigratingDatabase = true; + }, + ); // If the app starts in detached mode, we assume that it is in // background fetch mode for processing push notifications. This is // currently only supported on Android. if (PlatformInfos.isAndroid && AppLifecycleState.detached == WidgetsBinding.instance.lifecycleState) { - // Do not send online presences when app is in background fetch mode. + final clients = await clientsFuture; for (final client in clients) { client.syncPresence = PresenceType.offline; } @@ -46,15 +53,26 @@ void main() async { return; } + if (!isMigratingDatabase) { + final clients = await clientsFuture; + // Preload first client + final firstClient = clients.firstOrNull; + await firstClient?.roomsLoading; + await firstClient?.accountDataLoading; + } + // Started in foreground mode. Logs().i( '${AppConfig.applicationName} started in foreground mode. Rendering GUI...', ); - await startGui(clients, store); + await startGui(clientsFuture, store); } /// Fetch the pincode for the applock and start the flutter engine. -Future startGui(List clients, SharedPreferences store) async { +Future startGui( + Future> clientsFuture, + SharedPreferences store, +) async { // Fetch the pin for the applock if existing for mobile applications. String? pin; if (PlatformInfos.isMobile) { @@ -66,13 +84,14 @@ Future startGui(List clients, SharedPreferences store) async { } } - // Preload first client - final firstClient = clients.firstOrNull; - await firstClient?.roomsLoading; - await firstClient?.accountDataLoading; - ErrorWidget.builder = (details) => FluffyChatErrorWidget(details); - runApp(FluffyChatApp(clients: clients, pincode: pin, store: store)); + runApp( + FluffyChatApp( + pincode: pin, + clientsFuture: clientsFuture, + store: store, + ), + ); } /// Watches the lifecycle changes to start the application when it @@ -96,7 +115,7 @@ class AppStarter with WidgetsBindingObserver { for (final client in clients) { client.syncPresence = PresenceType.online; } - startGui(clients, store); + startGui(Future.value(clients), store); // We must make sure that the GUI is only started once. guiStarted = true; } diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 335706ea8..02dcc918c 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -1,19 +1,11 @@ -import 'dart:io'; -import 'dart:ui'; - import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:matrix/encryption/utils/key_verification.dart'; import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:universal_html/html.dart' as html; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/custom_http_client.dart'; import 'package:fluffychat/utils/custom_image_resizer.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart'; @@ -23,8 +15,8 @@ import 'matrix_sdk_extensions/flutter_matrix_sdk_database_builder.dart'; abstract class ClientManager { static const String clientNamespace = 'im.fluffychat.store.clients'; static Future> getClients({ - bool initialize = true, required SharedPreferences store, + void Function()? onMigration, }) async { if (PlatformInfos.isLinux) { Hive.init((await getApplicationSupportDirectory()).path); @@ -44,28 +36,20 @@ abstract class ClientManager { await store.setStringList(clientNamespace, clientNames.toList()); } final clients = clientNames.map(createClient).toList(); - if (initialize) { - FlutterLocalNotificationsPlugin? flutterLocalNotificationsPlugin; - await Future.wait( - clients.map( - (client) => client - .init( - waitForFirstSync: false, - waitUntilLoadCompletedLoaded: false, - onMigration: () { - sendMigrationNotification( - flutterLocalNotificationsPlugin ??= - FlutterLocalNotificationsPlugin(), - ); - }, - ) - .catchError( - (e, s) => Logs().e('Unable to initialize client', e, s), - ), - ), - ); - flutterLocalNotificationsPlugin?.cancel(0); - } + await Future.wait( + clients.map( + (client) => client + .init( + waitForFirstSync: false, + waitUntilLoadCompletedLoaded: false, + onMigration: onMigration, + ) + .catchError( + (e, s) => Logs().e('Unable to initialize client', e, s), + ), + ), + ); + if (clients.length > 1 && clients.any((c) => !c.isLogged())) { final loggedOutClients = clients.where((c) => !c.isLogged()).toList(); for (final client in loggedOutClients) { @@ -130,42 +114,4 @@ abstract class ClientManager { enableDehydratedDevices: true, ); } - - static void sendMigrationNotification( - FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin, - ) async { - final l10n = lookupL10n(Locale(Platform.localeName)); - - if (kIsWeb) { - html.Notification( - l10n.databaseMigrationTitle, - body: l10n.databaseMigrationBody, - ); - } - - await flutterLocalNotificationsPlugin.initialize( - const InitializationSettings( - android: AndroidInitializationSettings('notifications_icon'), - iOS: DarwinInitializationSettings(), - ), - ); - - flutterLocalNotificationsPlugin.show( - 0, - l10n.databaseMigrationTitle, - l10n.databaseMigrationBody, - const NotificationDetails( - android: AndroidNotificationDetails( - AppConfig.pushNotificationsChannelId, - AppConfig.pushNotificationsChannelName, - channelDescription: AppConfig.pushNotificationsChannelDescription, - importance: Importance.max, - priority: Priority.max, - fullScreenIntent: true, // To show notification popup - showProgress: true, - ), - iOS: DarwinNotificationDetails(), - ), - ); - } } diff --git a/lib/utils/error_reporter.dart b/lib/utils/error_reporter.dart index 3f8a003d4..d2334a7e1 100644 --- a/lib/utils/error_reporter.dart +++ b/lib/utils/error_reporter.dart @@ -18,9 +18,13 @@ class ErrorReporter { const ErrorReporter(this.context, [this.message]); - void onErrorCallback(Object error, [StackTrace? stackTrace]) async { + void onErrorCallback( + Object error, [ + StackTrace? stackTrace, + OkCancelResult? consent, + ]) async { Logs().e(message ?? 'Error caught', error, stackTrace); - final consent = await showOkCancelAlertDialog( + consent ??= await showOkCancelAlertDialog( context: context, title: error.toLocalizedString(context), message: L10n.of(context)!.reportErrorDescription, diff --git a/lib/utils/push_helper.dart b/lib/utils/push_helper.dart index 2b4b561bf..9854bcd15 100644 --- a/lib/utils/push_helper.dart +++ b/lib/utils/push_helper.dart @@ -101,7 +101,6 @@ Future _tryPushHelper( ); client ??= (await ClientManager.getClients( - initialize: false, store: await SharedPreferences.getInstance(), )) .first; diff --git a/lib/widgets/fluffy_chat_app.dart b/lib/widgets/fluffy_chat_app.dart index d9d2f042a..70866457a 100644 --- a/lib/widgets/fluffy_chat_app.dart +++ b/lib/widgets/fluffy_chat_app.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; @@ -7,24 +8,25 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:fluffychat/config/routes.dart'; import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/widgets/app_lock.dart'; import 'package:fluffychat/widgets/theme_builder.dart'; import '../config/app_config.dart'; import '../utils/custom_scroll_behaviour.dart'; import 'matrix.dart'; -class FluffyChatApp extends StatelessWidget { +class FluffyChatApp extends StatefulWidget { final Widget? testWidget; - final List clients; final String? pincode; + final Future> clientsFuture; final SharedPreferences store; const FluffyChatApp({ super.key, this.testWidget, - required this.clients, - required this.store, this.pincode, + required this.clientsFuture, + required this.store, }); /// getInitialLink may rereturn the value multiple times if this view is @@ -36,6 +38,18 @@ class FluffyChatApp extends StatelessWidget { // the current path. static final GoRouter router = GoRouter(routes: AppRoutes.routes); + @override + State createState() => _FluffyChatAppState(); +} + +class _FluffyChatAppState extends State { + List? clients; + bool isMigratingDatabase = false; + + Future> loadClient() async { + return clients ??= await widget.clientsFuture; + } + @override Widget build(BuildContext context) { return ThemeBuilder( @@ -48,21 +62,83 @@ class FluffyChatApp extends StatelessWidget { scrollBehavior: CustomScrollBehavior(), localizationsDelegates: L10n.localizationsDelegates, supportedLocales: L10n.supportedLocales, - routerConfig: router, - builder: (context, child) => AppLockWidget( - pincode: pincode, - clients: clients, - // Need a navigator above the Matrix widget for - // displaying dialogs - child: Navigator( - onGenerateRoute: (_) => MaterialPageRoute( - builder: (_) => Matrix( - clients: clients, - store: store, - child: testWidget ?? child, + routerConfig: FluffyChatApp.router, + builder: (context, child) => FutureBuilder( + future: loadClient(), + builder: (context, snapshot) { + final data = snapshot.data; + if (data == null) { + final error = snapshot.error; + final label = error != null + ? L10n.of(context)!.reportErrorDescription + : L10n.of(context)!.databaseMigrationTitle; + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (error == null) + Image.asset( + 'assets/logo.png', + width: 64, + height: 64, + ) + else + const Text('😭', style: TextStyle(fontSize: 100)), + Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + width: FluffyThemes.columnWidth, + child: Text( + label, + style: TextStyle( + fontSize: error == null ? 20 : 16, + color: error == null + ? null + : Theme.of(context).colorScheme.error, + ), + textAlign: TextAlign.center, + ), + ), + ), + if (error != null) + OutlinedButton.icon( + onPressed: () => + ErrorReporter(context, 'INITIALIZATION ERROR') + .onErrorCallback( + error, + snapshot.stackTrace, + OkCancelResult.ok, + ), + label: Text(L10n.of(context)!.reportError), + icon: const Icon(Icons.favorite), + ), + if (error == null) + const CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ], + ), + ), + ); + } + return AppLockWidget( + pincode: widget.pincode, + clients: data, + // Need a navigator above the Matrix widget for + // displaying dialogs + child: Navigator( + onGenerateRoute: (_) => MaterialPageRoute( + builder: (_) => Matrix( + clients: data, + store: widget.store, + child: widget.testWidget ?? child, + ), + ), ), - ), - ), + ); + }, ), ), );