From ad32e1f621fd47ba6929b872d4c03ec818b17f25 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Wed, 10 Apr 2024 22:09:22 +0700 Subject: [PATCH] Draft --- lib/pages/bootstrap/bootstrap_dialog.dart | 2 - .../linear_progress_indicator_widget.dart | 63 ++++ lib/pages/bootstrap/tom_bootstrap_dialog.dart | 351 +++++++++++------- lib/pages/chat_list/chat_list.dart | 77 ++-- lib/pages/connect/connect_page_mixin.dart | 2 +- .../model/client_login_state_event.dart | 26 ++ lib/utils/dialog/stream_dialog.dart | 167 +++++---- lib/utils/dialog/twake_dialog.dart | 8 +- lib/widgets/matrix.dart | 37 +- 9 files changed, 436 insertions(+), 297 deletions(-) create mode 100644 lib/pages/bootstrap/linear_progress_indicator_widget.dart create mode 100644 lib/presentation/model/client_login_state_event.dart diff --git a/lib/pages/bootstrap/bootstrap_dialog.dart b/lib/pages/bootstrap/bootstrap_dialog.dart index bca0fa0442..2abd7f931a 100644 --- a/lib/pages/bootstrap/bootstrap_dialog.dart +++ b/lib/pages/bootstrap/bootstrap_dialog.dart @@ -372,8 +372,6 @@ class BootstrapDialogState extends State { isDestructiveAction: true, )) { await TomBootstrapDialog( - wipe: true, - wipeRecovery: true, client: widget.client, ).show().then( (value) => Navigator.of( diff --git a/lib/pages/bootstrap/linear_progress_indicator_widget.dart b/lib/pages/bootstrap/linear_progress_indicator_widget.dart new file mode 100644 index 0000000000..7fde08604e --- /dev/null +++ b/lib/pages/bootstrap/linear_progress_indicator_widget.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; + +class TomBootstrapProgressItem extends StatelessWidget { + final String titleProgress; + final String? semanticsLabel; + final bool isCompleted; + final bool isProgress; + + const TomBootstrapProgressItem({ + super.key, + required this.titleProgress, + this.semanticsLabel, + this.isCompleted = false, + this.isProgress = false, + }); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.check_circle_outline, + size: 30, + color: isCompleted == true + ? Theme.of(context).colorScheme.primary + : LinagoraSysColors.material().tertiary, + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only( + bottom: 8, + ), + child: Text( + titleProgress, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: LinagoraSysColors.material().tertiary, + ), + ), + ), + LinearProgressIndicator( + value: isProgress == true + ? null + : isCompleted + ? 1 + : 0, + minHeight: 4, + borderRadius: BorderRadius.circular(11), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages/bootstrap/tom_bootstrap_dialog.dart b/lib/pages/bootstrap/tom_bootstrap_dialog.dart index 02f327f93a..0e6d56c709 100644 --- a/lib/pages/bootstrap/tom_bootstrap_dialog.dart +++ b/lib/pages/bootstrap/tom_bootstrap_dialog.dart @@ -1,11 +1,12 @@ +import 'package:fluffychat/di/global/dio_cache_interceptor_for_client.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/model/recovery_words/recovery_words.dart'; import 'package:fluffychat/domain/usecase/recovery/delete_recovery_words_interactor.dart'; +import 'package:fluffychat/domain/usecase/recovery/get_recovery_words_interactor.dart'; import 'package:fluffychat/domain/usecase/recovery/save_recovery_words_interactor.dart'; +import 'package:fluffychat/pages/bootstrap/linear_progress_indicator_widget.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; -import 'package:fluffychat/widgets/adaptive_flat_button.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/encryption.dart'; import 'package:matrix/encryption/utils/bootstrap.dart'; import 'package:matrix/matrix.dart'; @@ -13,100 +14,143 @@ import 'package:matrix/matrix.dart'; import 'bootstrap_dialog.dart'; class TomBootstrapDialog extends StatefulWidget { - final bool wipe; - final bool wipeRecovery; final Client client; - final RecoveryWords? recoveryWords; const TomBootstrapDialog({ Key? key, - this.recoveryWords, - this.wipe = false, - this.wipeRecovery = false, required this.client, }) : super(key: key); Future show() => TwakeDialog.showDialogFullScreen( builder: () => this, + barrierColor: Colors.white, ); @override TomBootstrapDialogState createState() => TomBootstrapDialogState(); } -class TomBootstrapDialogState extends State { +class TomBootstrapDialogState extends State + with TickerProviderStateMixin { final _saveRecoveryWordsInteractor = getIt.get(); + + final _getRecoveryWordsInteractor = getIt.get(); + final _deleteRecoveryWordsInteractor = getIt.get(); Bootstrap? bootstrap; - String? titleText; - Widget? body; - final buttons = []; - UploadRecoveryKeyState _uploadRecoveryKeyState = - UploadRecoveryKeyState.initial; + UploadRecoveryKeyState.dataLoading; - bool? _wipe; + bool _wipe = false; + RecoveryWords? _recoveryWords; @override void initState() { super.initState(); - _createBootstrap(widget.wipe); + _createBootstrap(); } - void _createBootstrap(bool wipe) async { - _wipe = wipe; - titleText = null; - _uploadRecoveryKeyState = _initializeRecoveryKeyState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - bootstrap = - widget.client.encryption!.bootstrap(onUpdate: (_) => setState(() {})); + void _createBootstrap() async { + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _loadingData(); }); } - UploadRecoveryKeyState _initializeRecoveryKeyState() { - if (widget.wipeRecovery) { - return UploadRecoveryKeyState.wipeRecovery; + Future setupAdditionalDioCacheOption(String userId) async { + Logs().d('TomBootstrapDialog::setupAdditionalDioCacheOption: $userId'); + DioCacheInterceptorForClient(userId).setup(getIt); + } + + Future _getRecoveryWords() async { + return await _getRecoveryWordsInteractor.execute().then( + (either) => either.fold( + (failure) => null, + (success) => success.words, + ), + ); + } + + Future _loadingData() async { + _uploadRecoveryKeyState = UploadRecoveryKeyState.dataLoading; + await widget.client.roomsLoading; + await widget.client.accountDataLoading; + if (widget.client.userID != null) { + await setupAdditionalDioCacheOption(widget.client.userID!); } + setState(() { + _uploadRecoveryKeyState = UploadRecoveryKeyState.checkingRecoveryWork; + }); + await _getRecoveryKeyState(); + bootstrap = + widget.client.encryption!.bootstrap(onUpdate: (_) => setState(() {})); + } - if (widget.recoveryWords != null) { - return UploadRecoveryKeyState.useExisting; + Future _getRecoveryKeyState() async { + await widget.client.onSync.stream.first; + await widget.client.initCompleter?.future; + + // Display first login bootstrap if enabled + if (widget.client.encryption?.keyManager.enabled == true) { + Logs().d( + 'TomBootstrapDialog::_initializeRecoveryKeyState: Showing bootstrap dialog when encryption is enabled', + ); + if (await widget.client.encryption?.keyManager.isCached() == false || + await widget.client.encryption?.crossSigning.isCached() == false || + widget.client.isUnknownSession && mounted) { + final recoveryWords = await _getRecoveryWords(); + if (recoveryWords != null) { + _recoveryWords = recoveryWords; + _uploadRecoveryKeyState = UploadRecoveryKeyState.useExisting; + } else { + Logs().d( + 'TomBootstrapDialog::_initializeRecoveryKeyState(): no recovery existed then call bootstrap', + ); + await BootstrapDialog(client: widget.client).show(); + } + } + } else { + Logs().d( + 'TomBootstrapDialog::_initializeRecoveryKeyState(): encryption is not enabled', + ); + final recoveryWords = await _getRecoveryWords(); + _wipe = recoveryWords != null; + if (recoveryWords != null) { + _uploadRecoveryKeyState = UploadRecoveryKeyState.wipeRecovery; + } } - return UploadRecoveryKeyState.initial; + setState(() {}); } + bool get isDataLoadingState => + _uploadRecoveryKeyState == UploadRecoveryKeyState.dataLoading; + + bool get isCheckingRecoveryWorkState => + _uploadRecoveryKeyState == UploadRecoveryKeyState.checkingRecoveryWork; + @override Widget build(BuildContext context) { Logs().d( 'TomBootstrapDialogState::build(): BootstrapState = ${bootstrap?.state}', ); - _wipe ??= widget.wipe; - body = _loadingContent(context); + + Logs().d( + 'TomBootstrapDialogState::build(): RecoveryKeyState = $_uploadRecoveryKeyState', + ); switch (_uploadRecoveryKeyState) { + case UploadRecoveryKeyState.dataLoading: + break; + case UploadRecoveryKeyState.checkingRecoveryWork: + break; case UploadRecoveryKeyState.wipeRecovery: WidgetsBinding.instance.addPostFrameCallback((_) { _wipeRecoveryWord(); }); break; case UploadRecoveryKeyState.wipeRecoveryFailed: - titleText = L10n.of(context)!.chatBackup; - body = Text( - L10n.of(context)!.cannotEnableKeyBackup, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ); - buttons.clear(); - buttons.add( - AdaptiveFlatButton( - label: L10n.of(context)!.close, - onPressed: () => - Navigator.of(context, rootNavigator: false).pop(false), - ), - ); break; case UploadRecoveryKeyState.created: if (_createNewRecoveryKeySuccess()) { @@ -118,7 +162,7 @@ class TomBootstrapDialogState extends State { Logs().d( 'TomBootstrapDialogState::build(): check if key is already in TOM = ${_existedRecoveryWordsInTom( key, - )} - ${widget.recoveryWords?.words}', + )} - ${_recoveryWords?.words}', ); if (_existedRecoveryWordsInTom(key)) { _uploadRecoveryKeyState = UploadRecoveryKeyState.uploaded; @@ -135,69 +179,111 @@ class TomBootstrapDialogState extends State { _handleBootstrapState(); break; case UploadRecoveryKeyState.unlockError: - titleText = L10n.of(context)!.chatBackup; - body = Text( - L10n.of(context)!.cannotUnlockBackupKey, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ); - buttons.clear(); - buttons - ..add( - AdaptiveFlatButton( - label: L10n.of(context)!.close, - onPressed: () => - Navigator.of(context, rootNavigator: false).pop(false), - ), - ) - ..add( - AdaptiveFlatButton( - label: L10n.of(context)!.next, - onPressed: () async { - await BootstrapDialog(client: widget.client).show().then( - (value) => Navigator.of(context, rootNavigator: false) - .pop(false), - ); - }, - ), - ); + // titleText = L10n.of(context)!.chatBackup; + // body = Text( + // L10n.of(context)!.cannotUnlockBackupKey, + // style: Theme.of(context).textTheme.bodyMedium?.copyWith( + // color: Theme.of(context).colorScheme.onSurfaceVariant, + // ), + // ); + // buttons.clear(); + // buttons + // ..add( + // AdaptiveFlatButton( + // label: L10n.of(context)!.close, + // onPressed: () => + // Navigator.of(context, rootNavigator: false).pop(false), + // ), + // ) + // ..add( + // AdaptiveFlatButton( + // label: L10n.of(context)!.next, + // onPressed: () async { + // await BootstrapDialog(client: widget.client).show().then( + // (value) => Navigator.of(context, rootNavigator: false) + // .pop(false), + // ); + // }, + // ), + // ); break; case UploadRecoveryKeyState.uploadError: Logs().e('TomBootstrapDialogState::build(): upload recovery key error'); - titleText = L10n.of(context)!.chatBackup; - body = Text( - L10n.of(context)!.cannotUploadKey, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ); - buttons.clear(); - buttons.add( - AdaptiveFlatButton( - label: L10n.of(context)!.close, - onPressed: () => - Navigator.of(context, rootNavigator: false).pop(false), - ), - ); + // titleText = L10n.of(context)!.chatBackup; + // body = Text( + // L10n.of(context)!.cannotUploadKey, + // style: Theme.of(context).textTheme.bodyMedium?.copyWith( + // color: Theme.of(context).colorScheme.onSurfaceVariant, + // ), + // ); + // buttons.clear(); + // buttons.add( + // AdaptiveFlatButton( + // label: L10n.of(context)!.close, + // onPressed: () => + // Navigator.of(context, rootNavigator: false).pop(false), + // ), + // ); break; default: _handleBootstrapState(); break; } - return AlertDialog( - title: titleText != null ? Text(titleText!) : null, - content: body, - actions: buttons, + return Scaffold( + backgroundColor: Colors.transparent, + body: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 56, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Setting up your Twake', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Setting up requires extra time so, please, be patient.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + TomBootstrapProgressItem( + titleProgress: 'Data loading ...', + isCompleted: !isDataLoadingState, + isProgress: isDataLoadingState, + ), + const SizedBox(height: 24), + TomBootstrapProgressItem( + titleProgress: 'Recovery work ...', + isCompleted: !isCheckingRecoveryWorkState && !isDataLoadingState, + isProgress: isCheckingRecoveryWorkState, + ), + const SizedBox(height: 24), + TomBootstrapProgressItem( + titleProgress: 'Backup ...', + isProgress: !isCheckingRecoveryWorkState && !isDataLoadingState, + isCompleted: + bootstrap != null && bootstrap!.state == BootstrapState.done, + ), + ], + ), + ), ); } bool _existedRecoveryWordsInTom(String? key) { - if (key == null && widget.recoveryWords != null) { + if (key == null && _recoveryWords != null) { return true; } - return widget.recoveryWords != null && widget.recoveryWords!.words == key; + return _recoveryWords != null && _recoveryWords!.words == key; } bool _createNewRecoveryKeySuccess() { @@ -205,14 +291,18 @@ class TomBootstrapDialogState extends State { _uploadRecoveryKeyState == UploadRecoveryKeyState.created; } + bool get _setUpSuccess => + _uploadRecoveryKeyState != UploadRecoveryKeyState.dataLoading && + _uploadRecoveryKeyState != UploadRecoveryKeyState.checkingRecoveryWork; + void _handleBootstrapState() { - if (bootstrap != null) { + if (bootstrap != null && _setUpSuccess) { switch (bootstrap!.state) { case BootstrapState.loading: break; case BootstrapState.askWipeSsss: WidgetsBinding.instance.addPostFrameCallback( - (_) => bootstrap?.wipeSsss(_wipe!), + (_) => bootstrap?.wipeSsss(_wipe), ); break; case BootstrapState.askBadSsss: @@ -223,7 +313,7 @@ class TomBootstrapDialogState extends State { case BootstrapState.askUseExistingSsss: _uploadRecoveryKeyState = UploadRecoveryKeyState.useExisting; WidgetsBinding.instance.addPostFrameCallback( - (_) => bootstrap?.useExistingSsss(!_wipe!), + (_) => bootstrap?.useExistingSsss(!_wipe), ); break; case BootstrapState.askUnlockSsss: @@ -246,7 +336,7 @@ class TomBootstrapDialogState extends State { break; case BootstrapState.askWipeCrossSigning: WidgetsBinding.instance.addPostFrameCallback( - (_) => bootstrap?.wipeCrossSigning(_wipe!), + (_) => bootstrap?.wipeCrossSigning(_wipe), ); break; case BootstrapState.askSetupCrossSigning: @@ -262,7 +352,7 @@ class TomBootstrapDialogState extends State { break; case BootstrapState.askWipeOnlineKeyBackup: WidgetsBinding.instance.addPostFrameCallback( - (_) => bootstrap?.wipeOnlineKeyBackup(_wipe!), + (_) => bootstrap?.wipeOnlineKeyBackup(_wipe), ); break; case BootstrapState.askSetupOnlineKeyBackup: @@ -271,33 +361,22 @@ class TomBootstrapDialogState extends State { ); break; case BootstrapState.error: - titleText = L10n.of(context)!.oopsSomethingWentWrong; - body = const Icon(Icons.error_outline, color: Colors.red, size: 40); - buttons.clear(); - buttons.add( - AdaptiveFlatButton( - label: L10n.of(context)!.close, - onPressed: () => - Navigator.of(context, rootNavigator: false).pop(false), - ), - ); + // titleText = L10n.of(context)!.oopsSomethingWentWrong; + // body = const Icon(Icons.error_outline, color: Colors.red, size: 40); + // buttons.clear(); + // buttons.add( + // AdaptiveFlatButton( + // label: L10n.of(context)!.close, + // onPressed: () => + // Navigator.of(context, rootNavigator: false).pop(false), + // ), + // ); break; case BootstrapState.done: - titleText = L10n.of(context)!.everythingReady; - body = Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset('assets/backup.png', fit: BoxFit.contain), - Text(L10n.of(context)!.yourChatBackupHasBeenSetUp), - ], - ); - buttons.clear(); - buttons.add( - AdaptiveFlatButton( - label: L10n.of(context)!.close, - onPressed: () => - Navigator.of(context, rootNavigator: false).pop(false), - ), + WidgetsBinding.instance.addPostFrameCallback( + (_) { + Navigator.of(context, rootNavigator: false).pop(true); + }, ); break; } @@ -351,7 +430,7 @@ class TomBootstrapDialogState extends State { } Future _unlockBackUp() async { - final recoveryWords = widget.recoveryWords; + final recoveryWords = _recoveryWords; if (recoveryWords == null) { Logs().e('TomBootstrapDialogState::_unlockBackUp(): recoveryWords null'); setState(() { @@ -383,27 +462,11 @@ class TomBootstrapDialogState extends State { setState(() {}); } } - - Widget _loadingContent(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding( - padding: EdgeInsets.only(right: 16.0), - child: CircularProgressIndicator.adaptive(), - ), - Expanded( - child: Text( - L10n.of(context)!.loadingPleaseWait, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ); - } } enum UploadRecoveryKeyState { + dataLoading, + checkingRecoveryWork, initial, wipeRecovery, wipeRecoveryFailed, diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index cbb0720a55..007e56303e 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -6,12 +6,9 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/first_column_inner_routes.dart'; import 'package:fluffychat/di/global/dio_cache_interceptor_for_client.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; -import 'package:fluffychat/domain/model/recovery_words/recovery_words.dart'; import 'package:fluffychat/domain/model/room/room_extension.dart'; -import 'package:fluffychat/domain/usecase/recovery/get_recovery_words_interactor.dart'; import 'package:fluffychat/pages/multiple_accounts/multiple_accounts_picker.dart'; import 'package:fluffychat/presentation/mixins/comparable_presentation_contact_mixin.dart'; -import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; import 'package:fluffychat/pages/bootstrap/tom_bootstrap_dialog.dart'; import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_security/settings_security.dart'; @@ -27,6 +24,7 @@ import 'package:fluffychat/utils/tor_stub.dart' if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:fluffychat/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart'; +import 'package:fluffychat/widgets/layouts/agruments/logged_in_body_args.dart'; import 'package:fluffychat/widgets/layouts/agruments/logged_in_other_account_body_args.dart'; import 'package:fluffychat/widgets/mixins/popup_context_menu_action_mixin.dart'; import 'package:fluffychat/widgets/mixins/popup_menu_widget_mixin.dart'; @@ -74,8 +72,6 @@ class ChatListController extends State PopupMenuWidgetMixin, GoToGroupChatMixin, TwakeContextMenuMixin { - final _getRecoveryWordsInteractor = getIt.get(); - final responsive = getIt.get(); final ValueNotifier expandRoomsForAllNotifier = ValueNotifier(true); @@ -416,48 +412,34 @@ class ChatListController extends State DioCacheInterceptorForClient(userId).setup(getIt); } + Future _trySync() async { + if (widget.adaptiveScaffoldBodyArgs is LoggedInBodyArgs || + widget.adaptiveScaffoldBodyArgs is LoggedInOtherAccountBodyArgs) { + _waitForFirstSyncAfterLogin(); + } else { + _waitForFirstSync(); + } + } + + Future _waitForFirstSyncAfterLogin() async { + WidgetsBinding.instance.addPostFrameCallback((_) async { + await TomBootstrapDialog( + client: activeClient, + ).show(); + }); + + if (!mounted) return; + setState(() { + waitForFirstSync = true; + }); + } + Future _waitForFirstSync() async { await activeClient.roomsLoading; await activeClient.accountDataLoading; if (activeClient.userID != null) { await setupAdditionalDioCacheOption(activeClient.userID!); } - if (activeClient.prevBatch == null) { - await activeClient.onSync.stream.first; - await activeClient.initCompleter?.future; - - // Display first login bootstrap if enabled - if (activeClient.encryption?.keyManager.enabled == true) { - Logs().d( - 'ChatList::_waitForFirstSync: Showing bootstrap dialog when encryption is enabled', - ); - if (await activeClient.encryption?.keyManager.isCached() == false || - await activeClient.encryption?.crossSigning.isCached() == false || - activeClient.isUnknownSession && mounted) { - final recoveryWords = await _getRecoveryWords(); - if (recoveryWords != null) { - await TomBootstrapDialog( - client: activeClient, - recoveryWords: recoveryWords, - ).show(); - } else { - Logs().d( - 'ChatListController::_waitForFirstSync(): no recovery existed then call bootstrap', - ); - await BootstrapDialog(client: activeClient).show(); - } - } - } else { - Logs().d( - 'ChatListController::_waitForFirstSync(): encryption is not enabled', - ); - final recoveryWords = await _getRecoveryWords(); - await TomBootstrapDialog( - client: activeClient, - wipeRecovery: recoveryWords != null, - ).show(); - } - } if (!mounted) return; setState(() { waitForFirstSync = true; @@ -713,15 +695,6 @@ class ChatListController extends State isTorBrowser = isTor; } - Future _getRecoveryWords() async { - return await _getRecoveryWordsInteractor.execute().then( - (either) => either.fold( - (failure) => null, - (success) => success.words, - ), - ); - } - Future dehydrate() => SettingsSecurityController.dehydrateDevice(context); @@ -776,7 +749,7 @@ class ChatListController extends State Logs().d( "ChatList::_handleAnotherAccountAdded(): Handle recovery data for another account", ); - _waitForFirstSync(); + _trySync(); } } @@ -806,7 +779,7 @@ class ChatListController extends State } activeRoomIdNotifier.value = widget.activeRoomIdNotifier.value; scrollController.addListener(_onScroll); - _waitForFirstSync(); + _trySync(); _hackyWebRTCFixForWeb(); _getCurrentProfile(activeClient); // TODO: 28Dec2023 Disable callkeep for util we support audio/video calls diff --git a/lib/pages/connect/connect_page_mixin.dart b/lib/pages/connect/connect_page_mixin.dart index f28b16ed9c..b68a1a2613 100644 --- a/lib/pages/connect/connect_page_mixin.dart +++ b/lib/pages/connect/connect_page_mixin.dart @@ -128,7 +128,7 @@ mixin ConnectPageMixin { final token = Uri.parse(result).queryParameters['loginToken']; if (token?.isEmpty ?? false) return; Matrix.of(context).loginType = LoginType.mLoginToken; - await TwakeDialog.showFutureLoadingDialogFullScreen( + await TwakeDialog.showStreamDialogFullScreen( future: () => Matrix.of(context) .getLoginClient() .login( diff --git a/lib/presentation/model/client_login_state_event.dart b/lib/presentation/model/client_login_state_event.dart new file mode 100644 index 0000000000..49e1a9b5cd --- /dev/null +++ b/lib/presentation/model/client_login_state_event.dart @@ -0,0 +1,26 @@ +import 'package:equatable/equatable.dart'; +import 'package:matrix/matrix.dart'; + +enum MultiLoginState { + primaryAccount, + secondaryAccount, +} + +class ClientLoginStateEvent with EquatableMixin { + final Client client; + final LoginState loginState; + final MultiLoginState multiLoginState; + + ClientLoginStateEvent({ + required this.client, + required this.loginState, + required this.multiLoginState, + }); + + @override + List get props => [ + client, + loginState, + multiLoginState, + ]; +} diff --git a/lib/utils/dialog/stream_dialog.dart b/lib/utils/dialog/stream_dialog.dart index 0e241077b3..8d82859aa3 100644 --- a/lib/utils/dialog/stream_dialog.dart +++ b/lib/utils/dialog/stream_dialog.dart @@ -1,20 +1,18 @@ import 'dart:async'; +import 'package:fluffychat/presentation/model/client_login_state_event.dart'; +import 'package:fluffychat/widgets/layouts/agruments/logged_in_body_args.dart'; +import 'package:fluffychat/widgets/layouts/agruments/logged_in_other_account_body_args.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/twake_app.dart'; import 'package:flutter/material.dart'; - -enum StreamDialogState { - loginSSOSuccess, - backupSuccess, - recoveringSuccess, -} +import 'package:matrix/matrix.dart'; class StreamDialogBuilder extends StatefulWidget { final Future Function() future; - final Function(StreamDialogState) listen; const StreamDialogBuilder({ super.key, required this.future, - required this.listen, }); @override @@ -23,92 +21,106 @@ class StreamDialogBuilder extends StatefulWidget { class _StreamDialogBuilderState extends State with TickerProviderStateMixin { - final StreamController streamController = - StreamController.broadcast(); - late AnimationController loginSSOProgressController; - late AnimationController backupProgressController; - late AnimationController recoveringProgressController; static const String loginSSOProgress = 'loginSSOProgress'; - static const String backupProgress = 'backupProgress'; - static const String recoveringProgress = 'recoveringProgress'; - bool _isCompletedFunc = false; + Client? _clientFirstLoggedIn; + + Client? _clientAddAnotherAccount; @override void initState() { _initial(); - streamController.stream.listen((event) { - widget.listen(event); - }); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - _startLoginSSOProgress(); - await widget.future().then((value) { - _isCompletedFunc = true; - }); - }); + Matrix.of(context).onClientLoginStateChanged.stream.listen( + _listenClientLoginStateChanged, + ); + WidgetsBinding.instance.addPostFrameCallback( + (_) async { + _startLoginSSOProgress(); + await widget + .future() + .then( + (_) => _handleFunctionOnDone(), + ) + .onError( + (error, _) => _handleFunctionOnError(error), + ); + }, + ); super.initState(); } + void _listenClientLoginStateChanged(ClientLoginStateEvent event) { + Logs().i( + 'StreamDialogBuilder::_listenClientLoginStateChanged - ${event.multiLoginState}', + ); + if (event.multiLoginState == MultiLoginState.primaryAccount) { + _clientFirstLoggedIn = event.client; + return; + } + + if (event.multiLoginState == MultiLoginState.secondaryAccount) { + _clientAddAnotherAccount = event.client; + return; + } + } + + void _handleFunctionOnDone() async { + Logs().i('StreamDialogBuilder::_handleFunctionOnDone'); + Navigator.of(context, rootNavigator: false).pop(); + if (_clientFirstLoggedIn != null) { + _handleFirstLoggedIn(_clientFirstLoggedIn!); + return; + } + + if (_clientAddAnotherAccount != null) { + _handleAddAnotherAccount(_clientAddAnotherAccount!); + return; + } + } + + void _handleFunctionOnError(Object? error) { + Logs().i('StreamDialogBuilder::_handleFunctionOnError - $error'); + Navigator.pop(context); + } + + void _handleFirstLoggedIn(Client client) { + TwakeApp.router.go( + '/rooms', + extra: LoggedInBodyArgs( + newActiveClient: client, + ), + ); + } + + void _handleAddAnotherAccount(Client client) { + TwakeApp.router.go( + '/rooms', + extra: LoggedInOtherAccountBodyArgs( + newActiveClient: client, + ), + ); + } + void _initial() { loginSSOProgressController = AnimationController( vsync: this, duration: const Duration(seconds: 2), ); - backupProgressController = AnimationController( - vsync: this, - duration: const Duration(seconds: 2), - ); - recoveringProgressController = AnimationController( - vsync: this, - duration: const Duration(seconds: 1), - ); } void _startLoginSSOProgress() { loginSSOProgressController.addListener(() { setState(() {}); - if (loginSSOProgressController.isCompleted) { - streamController.add(StreamDialogState.loginSSOSuccess); - _startBackupProgress(); - } - }); - loginSSOProgressController.forward(from: 0); - } - - void _startBackupProgress() { - backupProgressController.addListener(() { - setState(() {}); - if (backupProgressController.isCompleted) { - streamController.add(StreamDialogState.backupSuccess); - _startRecoveringProgress(); - } - }); - backupProgressController.forward(from: 0); - } - - void _startRecoveringProgress() { - recoveringProgressController.addListener(() { - setState(() {}); - if (_isCompletedFunc) { - streamController.add(StreamDialogState.recoveringSuccess); - recoveringProgressController.stop(); - Navigator.of(context).pop(); - } }); - - recoveringProgressController.repeat(); + loginSSOProgressController.repeat(); } @override void dispose() { loginSSOProgressController.dispose(); - backupProgressController.dispose(); - recoveringProgressController.dispose(); - streamController.close(); super.dispose(); } @@ -116,15 +128,19 @@ class _StreamDialogBuilderState extends State Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.transparent, - body: Center( + body: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 56, + ), child: Column( - mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Setting up your Twake', style: Theme.of(context).textTheme.titleLarge?.copyWith( color: Theme.of(context).colorScheme.onBackground, ), + textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( @@ -132,21 +148,14 @@ class _StreamDialogBuilderState extends State style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onBackground, ), + textAlign: TextAlign.center, ), - const SizedBox(height: 8), + const SizedBox(height: 16), LinearProgressIndicator( value: loginSSOProgressController.value, semanticsLabel: loginSSOProgress, - ), - const SizedBox(height: 8), - LinearProgressIndicator( - value: backupProgressController.value, - semanticsLabel: backupProgress, - ), - const SizedBox(height: 8), - LinearProgressIndicator( - value: recoveringProgressController.value, - semanticsLabel: recoveringProgress, + minHeight: 4, + borderRadius: BorderRadius.circular(11), ), ], ), diff --git a/lib/utils/dialog/twake_dialog.dart b/lib/utils/dialog/twake_dialog.dart index 86ac33cf73..fa77596c93 100644 --- a/lib/utils/dialog/twake_dialog.dart +++ b/lib/utils/dialog/twake_dialog.dart @@ -57,9 +57,8 @@ class TwakeDialog { ); } - static void showStreamDialogFullScreen({ + static Future showStreamDialogFullScreen({ required Future Function() future, - required Function(StreamDialogState) listen, }) async { final twakeContext = TwakeApp.routerKey.currentContext; if (twakeContext == null) { @@ -67,11 +66,10 @@ class TwakeDialog { 'TwakeLoadingDialog()::showStreamDialogFullScreen - Twake context is null', ); } - await showDialog( + return await showDialog( context: twakeContext!, builder: (context) => StreamDialogBuilder( future: future, - listen: listen, ), barrierDismissible: true, barrierColor: Colors.white, @@ -82,6 +80,7 @@ class TwakeDialog { static Future showDialogFullScreen({ required Widget Function() builder, bool barrierDismissible = true, + Color? barrierColor, }) { final twakeContext = TwakeApp.routerKey.currentContext; if (twakeContext == null) { @@ -93,6 +92,7 @@ class TwakeDialog { return showDialog( context: twakeContext, builder: (context) => builder(), + barrierColor: barrierColor, barrierDismissible: barrierDismissible, useRootNavigator: false, ); diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index be2e502699..d3dd89db61 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:fluffychat/presentation/model/client_login_state_event.dart'; +import 'package:fluffychat/widgets/layouts/agruments/logout_body_args.dart'; import 'package:universal_html/html.dart' as html hide File; import 'package:adaptive_dialog/adaptive_dialog.dart'; @@ -25,9 +27,6 @@ import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:fluffychat/utils/uia_request_manager.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/utils/voip_plugin.dart'; -import 'package:fluffychat/widgets/layouts/agruments/logged_in_body_args.dart'; -import 'package:fluffychat/widgets/layouts/agruments/logged_in_other_account_body_args.dart'; -import 'package:fluffychat/widgets/layouts/agruments/logout_body_args.dart'; import 'package:fluffychat/widgets/set_active_client_state.dart'; import 'package:fluffychat/widgets/twake_app.dart'; import 'package:flutter/foundation.dart'; @@ -190,7 +189,7 @@ class MatrixState extends State .stream .where((l) => l == LoginState.loggedIn) .first - .then((_) => _handleAddAnotherAccount()); + .then((state) => _handleAddAnotherAccount(state)); return candidate; } @@ -248,6 +247,8 @@ class MatrixState extends State final onNotification = {}; final onLoginStateChanged = >{}; final onUiaRequest = >{}; + final StreamController onClientLoginStateChanged = + StreamController.broadcast(); StreamSubscription? onFocusSub; StreamSubscription? onBlurSub; @@ -396,7 +397,7 @@ class MatrixState extends State } else { if (state == LoginState.loggedIn) { Logs().v('[MATRIX]:_listenLoginStateChanged:: First Log in successful'); - _handleFirstLoggedIn(client); + _handleFirstLoggedIn(client, state); } else { Logs().v('[MATRIX]:_listenLoginStateChanged:: Log out successful'); if (PlatformInfos.isMobile) { @@ -434,18 +435,22 @@ class MatrixState extends State } } - void _handleFirstLoggedIn(Client newActiveClient) { + void _handleFirstLoggedIn( + Client newActiveClient, + LoginState loginState, + ) { setUpToMServicesInLogin(newActiveClient); _storePersistActiveAccount(newActiveClient); - TwakeApp.router.go( - '/rooms', - extra: LoggedInBodyArgs( - newActiveClient: newActiveClient, + onClientLoginStateChanged.add( + ClientLoginStateEvent( + client: client, + loginState: loginState, + multiLoginState: MultiLoginState.primaryAccount, ), ); } - Future _handleAddAnotherAccount() async { + Future _handleAddAnotherAccount(LoginState loginState) async { Logs().d( 'MatrixState::_handleAddAnotherAccount() - Add another account successful', ); @@ -465,10 +470,11 @@ class MatrixState extends State setUpToMServicesInLogin(activeClient); final result = await setActiveClient(activeClient); if (result.isSuccess) { - TwakeApp.router.go( - '/rooms', - extra: LoggedInOtherAccountBodyArgs( - newActiveClient: activeClient, + onClientLoginStateChanged.add( + ClientLoginStateEvent( + client: client, + loginState: loginState, + multiLoginState: MultiLoginState.secondaryAccount, ), ); _loginClientCandidate = null; @@ -852,6 +858,7 @@ class MatrixState extends State onKeyVerificationRequestSub.values.map((s) => s.cancel()); onLoginStateChanged.values.map((s) => s.cancel()); onNotification.values.map((s) => s.cancel()); + onClientLoginStateChanged.close(); client.httpClient.close(); onFocusSub?.cancel(); onBlurSub?.cancel();