diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b16780217..b4863e5301 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -279,7 +279,7 @@ - \#1792 Add option 'Empty Trash' in folder menu ### Changed -- \#1755 Use TupleKey('ObjectId\|AccountId\|UserName') store data to cache +- \#1755 Use TupleKey('AccountId\|UserName\|ObjectId') store data to cache ### Fixed - \#1385 Fix \[Email rule\] Icon edit and delete might be seen as disable diff --git a/assets/images/ic_logo_twake_horizontal.svg b/assets/images/ic_logo_twake_horizontal.svg new file mode 100644 index 0000000000..b4c397d8ce --- /dev/null +++ b/assets/images/ic_logo_twake_horizontal.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/ic_switch_account.svg b/assets/images/ic_switch_account.svg new file mode 100644 index 0000000000..8d71fee1ae --- /dev/null +++ b/assets/images/ic_switch_account.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index e62d020a99..b697145b7f 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -211,6 +211,8 @@ class ImagePaths { String get icLogoTwakeWelcome => _getImagePath('ic_logo_twake_welcome.svg'); String get icCheckboxOn => _getImagePath('ic_checkbox_on.svg'); String get icCheckboxOff => _getImagePath('ic_checkbox_off.svg'); + String get icLogoTwakeHorizontal => _getImagePath('ic_logo_twake_horizontal.svg'); + String get icSwitchAccount => _getImagePath('ic_switch_account.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; diff --git a/core/lib/presentation/utils/theme_utils.dart b/core/lib/presentation/utils/theme_utils.dart index 83058a04af..15c909bcf5 100644 --- a/core/lib/presentation/utils/theme_utils.dart +++ b/core/lib/presentation/utils/theme_utils.dart @@ -22,21 +22,21 @@ class ThemeUtils { static TextTheme get _textTheme { return const TextTheme( - bodyLarge: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 17, - letterSpacing: -0.15, - ), titleLarge: TextStyle( color: Colors.black, fontWeight: FontWeight.bold, fontSize: 28 ), + bodyLarge: TextStyle( + fontSize: 17 + ), bodyMedium: TextStyle( fontWeight: FontWeight.w500, + fontSize: 15 ), bodySmall: TextStyle( - fontWeight: FontWeight.w500 + fontWeight: FontWeight.normal, + fontSize: 15 ), headlineLarge: TextStyle( fontWeight: FontWeight.w600, @@ -44,6 +44,10 @@ class ThemeUtils { ), labelSmall: TextStyle( fontWeight: FontWeight.normal + ), + labelLarge: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16 ) ); } diff --git a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart index 9f32053589..84e38e4c2c 100644 --- a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart +++ b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart @@ -23,12 +23,12 @@ class ConfirmDialogBuilder { TextStyle? _styleContent; double? _radiusButton; double? heightButton; - EdgeInsets? _paddingTitle; - EdgeInsets? _paddingContent; - EdgeInsets? _paddingButton; + EdgeInsetsGeometry? _paddingTitle; + EdgeInsetsGeometry? _paddingContent; + EdgeInsetsGeometry? _paddingButton; EdgeInsets? _outsideDialogPadding; - EdgeInsets? _marginIcon; - EdgeInsets? _margin; + EdgeInsetsGeometry? _marginIcon; + EdgeInsetsGeometry? _margin; double? _widthDialog; double? _heightDialog; double maxWith; @@ -95,23 +95,23 @@ class ConfirmDialogBuilder { _radiusButton = radius; } - void paddingTitle(EdgeInsets? value) { + void paddingTitle(EdgeInsetsGeometry? value) { _paddingTitle = value; } - void paddingContent(EdgeInsets? value) { + void paddingContent(EdgeInsetsGeometry? value) { _paddingContent = value; } - void paddingButton(EdgeInsets? value) { + void paddingButton(EdgeInsetsGeometry? value) { _paddingButton = value; } - void marginIcon(EdgeInsets? value) { + void marginIcon(EdgeInsetsGeometry? value) { _marginIcon = value; } - void margin(EdgeInsets? value) { + void margin(EdgeInsetsGeometry? value) { _margin = value; } diff --git a/docs/adr/0027-use-tuplekey-store-data-cache.md b/docs/adr/0027-use-tuplekey-store-data-to-hive-database.md similarity index 56% rename from docs/adr/0027-use-tuplekey-store-data-cache.md rename to docs/adr/0027-use-tuplekey-store-data-to-hive-database.md index 86924533ff..1170d5b4af 100644 --- a/docs/adr/0027-use-tuplekey-store-data-cache.md +++ b/docs/adr/0027-use-tuplekey-store-data-to-hive-database.md @@ -1,4 +1,4 @@ -# 27. Use TupleKey store data cache +# 27. Use TupleKey store data to hive database Date: 2023-04-27 @@ -9,13 +9,14 @@ Accepted ## Context - Multiple accounts login at the same time in the same browser. The accounts will use the same database (`IndexDatabase`). +- To support multiple accounts ## Decision -- Use unique parameters (`AccountId`, `UserName`, `ObjectId(MailboxId/EmailId/StateType`) to form a unique `key` for storage (called `TupleKey`). -- TupleKey has the format: `ObjectId | AccountId | User`; +- Use unique parameters (`AccountId`, `UserName`, `ObjectId(MailboxId/EmailId/StateType/...)`) to form a unique `key` for storage (called `TupleKey`). +- TupleKey has the format: `AccountId | UserName | [ObjectId]`; - `HiveDatabase` includes many `Box`. Each box is a `Map` with `key=TupleKey`. ## Consequences -- The correct `mailbox` and `email` lists are obtained for each account +- Each account will manage its own data storage boxes diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index 581a0a45e1..b17a691a40 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -18,6 +18,7 @@ import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/authentication_type.dart'; import 'package:model/account/personal_account.dart'; import 'package:rule_filter/rule_filter/capability_rule_filter.dart'; @@ -25,13 +26,17 @@ import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.da import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixin.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; import 'package:tmail_ui_user/features/email/presentation/bindings/mdn_interactor_bindings.dart'; +import 'package:tmail_ui_user/features/login/data/extensions/token_oidc_extension.dart'; import 'package:tmail_ui_user/features/login/data/network/config/authorization_interceptors.dart'; import 'package:tmail_ui_user/features/login/domain/state/logout_current_account_basic_auth_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/logout_current_account_oidc_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/logout_current_account_state.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/logout_current_account_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/set_current_active_account_interactor.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/model/login_arguments.dart'; +import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_arguments.dart'; +import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/bindings/contact_autocomplete_bindings.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/bindings/tmail_autocomplete_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; @@ -40,7 +45,6 @@ import 'package:tmail_ui_user/features/manage_account/presentation/forward/bindi import 'package:tmail_ui_user/features/manage_account/presentation/vacation/vacation_interactors_bindings.dart'; import 'package:tmail_ui_user/features/push_notification/domain/exceptions/fcm_exception.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/destroy_firebase_registration_state.dart'; -import 'package:tmail_ui_user/features/push_notification/domain/state/get_stored_firebase_registration_state.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/remove_firebase_registration_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/config/fcm_configuration.dart'; @@ -58,6 +62,7 @@ import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:tmail_ui_user/main/utils/app_store.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; +import 'package:tmail_ui_user/main/utils/authenticated_account_manager.dart'; import 'package:uuid/uuid.dart'; abstract class BaseController extends GetxController @@ -75,6 +80,8 @@ abstract class BaseController extends GetxController final ResponsiveUtils responsiveUtils = Get.find(); final Uuid uuid = Get.find(); final AppStore appStore = Get.find(); + final SetCurrentActiveAccountInteractor _setCurrentActiveAccountInteractor = Get.find(); + final AuthenticatedAccountManager authenticatedAccountManager = Get.find(); final _fcmReceiver = FcmReceiver.instance; bool _isFcmEnabled = false; @@ -173,7 +180,7 @@ abstract class BaseController extends GetxController return; } if (exception is BadCredentialsException || exception is ConnectionError) { - clearDataAndGoToLoginPage(); + _handleBadCredentials(); } } @@ -182,19 +189,18 @@ abstract class BaseController extends GetxController if (failure is LogoutCurrentAccountOidcFailure || failure is LogoutCurrentAccountBasicAuthFailure || failure is LogoutCurrentAccountFailure) { - await _handleLogoutCurrentAccountFailure(failure); - } else if (failure is GetStoredFirebaseRegistrationFailure || - failure is DestroyFirebaseRegistrationFailure) { - await clearDataAndGoToLoginPage(); + _handleLogoutCurrentAccountFailure(failure); + } else if (failure is DestroyFirebaseRegistrationFailure) { + _handleDestroyFirebaseRegistrationFailure(failure); } } void handleSuccessViewState(Success success) async { log('BaseController::handleSuccessViewState(): ${success.runtimeType}'); if (success is LogoutCurrentAccountOidcSuccess || success is LogoutCurrentAccountBasicAuthSuccess) { - await _handleLogoutCurrentAccountSuccess(success); + _handleLogoutCurrentAccountSuccess(success); } else if (success is DestroyFirebaseRegistrationSuccess) { - await clearDataAndGoToLoginPage(); + _handleDestroyFirebaseRegistrationSuccess(success); } } @@ -274,20 +280,9 @@ abstract class BaseController extends GetxController bool _isFcmActivated(Session session, AccountId accountId) => FirebaseCapability.fcmIdentifier.isSupported(session, accountId) && AppConfig.fcmAvailable; - void goToLogin({LoginArguments? arguments}) { - if (PlatformInfo.isMobile) { - navigateToTwakeIdPage(); - } else { - navigateToLoginPage(arguments: arguments); - } - } - void removeAllPageAndGoToLogin({LoginArguments? arguments}) { - if (PlatformInfo.isMobile) { - pushAndPopAll(AppRoutes.twakeId); - } else { - navigateToLoginPage(arguments: arguments); - } + void removeAllRouteAndNavigateToTwakeIdPage() { + pushAndPopAll(AppRoutes.twakeId); } void navigateToTwakeIdPage() { @@ -302,48 +297,109 @@ abstract class BaseController extends GetxController } void logout({ + required BuildContext context, required Session session, required AccountId accountId }) async { _isFcmEnabled = _isFcmActivated(session, accountId); - consumeState(_logoutCurrentAccountInteractor.execute()); + + if (PlatformInfo.isMobile) { + _showConfirmDialogLogout( + context: context, + userName: session.username + ); + } else { + consumeState(_logoutCurrentAccountInteractor.execute()); + } + } + + void _showConfirmDialogLogout({ + required BuildContext context, + required UserName userName + }) { + showConfirmDialogAction( + context: context, + actionName: AppLocalizations.of(currentContext!).yesLogout, + title: AppLocalizations.of(currentContext!).logoutConfirmation, + alignCenter: true, + titlePadding: const EdgeInsetsDirectional.only(top: 24), + messageStyle: Theme.of(context).textTheme.bodySmall?.copyWith( + color: AppColor.colorTextBody, + fontSize: 15 + ), + titleStyle: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 20 + ), + actionButtonColor: AppColor.primaryColor, + actionStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.white, + fontSize: 16 + ), + cancelStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: AppColor.primaryColor, + fontSize: 16 + ), + listTextSpan: [ + TextSpan(text: AppLocalizations.of(context).messageConfirmationLogout), + TextSpan( + text: ' ${userName.value}', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: AppColor.colorTextBody, + fontWeight: FontWeight.bold, + fontSize: 15 + ), + ), + const TextSpan(text: '?'), + ], + onConfirmAction: () { + consumeState(_logoutCurrentAccountInteractor.execute()); + }, + ); } void _removeFirebaseRegistration(PersonalAccount deletedAccount) async { _removeFirebaseRegistrationInteractor = getBinding(); - if (_removeFirebaseRegistrationInteractor != null && - deletedAccount.accountId != null && - deletedAccount.userName != null) { - consumeState(_removeFirebaseRegistrationInteractor!.execute( - deletedAccount.accountId!, - deletedAccount.userName!)); + if (_removeFirebaseRegistrationInteractor != null) { + consumeState(_removeFirebaseRegistrationInteractor!.execute(deletedAccount)); } else { - await clearDataAndGoToLoginPage(); + if (PlatformInfo.isMobile) { + await clearDataByAccount(deletedAccount); + _handleNavigationRouteAfterLogoutCurrentAccountSuccess(); + } else { + await clearAllDataAndBackToLogin(); + } } } - Future clearDataAndGoToLoginPage() async { - log('BaseController::clearDataAndGoToLoginPage:'); + Future clearAllDataAndBackToLogin() async { + log('BaseController::clearAllDataAndBackToLogin:'); await clearAllData(); - removeAllPageAndGoToLogin(arguments: LoginArguments( - PlatformInfo.isWeb - ? LoginFormType.none - : LoginFormType.dnsLookupForm - )); + if (PlatformInfo.isMobile) { + removeAllRouteAndNavigateToTwakeIdPage(); + } else { + navigateToLoginPage(arguments: LoginArguments(LoginFormType.none)); + } } Future clearAllData() async { + log('BaseController::clearAllData:'); try { + authorizationInterceptors.clear(); + authorizationIsolateInterceptors.clear(); + await Future.wait([ cachingManager.clearAll(), languageCacheManager.removeLanguage(), ]); + + await cachingManager.closeHive(); + if (PlatformInfo.isMobile) { - await cachingManager.clearAllFileInStorage(); + await cachingManager.clearAllFolderInStorage(); } - authorizationInterceptors.clear(); - authorizationIsolateInterceptors.clear(); - await cachingManager.closeHive(); + if (_isFcmEnabled) { await _fcmReceiver.deleteFcmToken(); } @@ -352,7 +408,22 @@ abstract class BaseController extends GetxController } } - Future _handleLogoutCurrentAccountSuccess(Success success) async { + Future clearDataByAccount(PersonalAccount currentAccount) async { + log('BaseController::clearDataByAccount:currentAccount: $currentAccount'); + try { + authorizationInterceptors.clear(); + authorizationIsolateInterceptors.clear(); + + await cachingManager.clearCacheByAccount(currentAccount); + await cachingManager.closeHive(); + + await cachingManager.clearFolderStorageByAccount(currentAccount); + } catch (e, s) { + logError('BaseController::clearAllDataByAccount: Exception: $e | Stack: $s'); + } + } + + void _handleLogoutCurrentAccountSuccess(Success success) async { PersonalAccount? deletedAccount; if (success is LogoutCurrentAccountOidcSuccess) { @@ -361,14 +432,24 @@ abstract class BaseController extends GetxController deletedAccount = success.deletedAccount; } - if (_isFcmEnabled && deletedAccount != null) { + if (deletedAccount == null) { + await clearAllDataAndBackToLogin(); + return; + } + + if (_isFcmEnabled) { _removeFirebaseRegistration(deletedAccount); } else { - await clearDataAndGoToLoginPage(); + if (PlatformInfo.isMobile) { + await clearDataByAccount(deletedAccount); + _handleNavigationRouteAfterLogoutCurrentAccountSuccess(); + } else { + await clearAllDataAndBackToLogin(); + } } } - Future _handleLogoutCurrentAccountFailure(Failure failure) async { + void _handleLogoutCurrentAccountFailure(Failure failure) async { PersonalAccount? deletedAccount; if (failure is LogoutCurrentAccountOidcFailure) { @@ -379,10 +460,90 @@ abstract class BaseController extends GetxController deletedAccount = failure.deletedAccount; } - if (_isFcmEnabled && deletedAccount != null) { + if (deletedAccount == null) { + await clearAllDataAndBackToLogin(); + return; + } + + if (_isFcmEnabled) { _removeFirebaseRegistration(deletedAccount); } else { - await clearDataAndGoToLoginPage(); + if (PlatformInfo.isMobile) { + await clearDataByAccount(deletedAccount); + _handleNavigationRouteAfterLogoutCurrentAccountSuccess(); + } else { + await clearAllDataAndBackToLogin(); + } + } + } + + void setUpInterceptors(PersonalAccount personalAccount) { + dynamicUrlInterceptors.setJmapUrl(personalAccount.baseUrl); + dynamicUrlInterceptors.changeBaseUrl(personalAccount.baseUrl); + + switch(personalAccount.authType) { + case AuthenticationType.oidc: + authorizationInterceptors.setTokenAndAuthorityOidc( + newToken: personalAccount.tokenOidc, + newConfig: personalAccount.tokenOidc!.oidcConfiguration + ); + authorizationIsolateInterceptors.setTokenAndAuthorityOidc( + newToken: personalAccount.tokenOidc, + newConfig: personalAccount.tokenOidc!.oidcConfiguration + ); + break; + case AuthenticationType.basic: + authorizationInterceptors.setBasicAuthorization( + personalAccount.basicAuth!.userName, + personalAccount.basicAuth!.password, + ); + authorizationIsolateInterceptors.setBasicAuthorization( + personalAccount.basicAuth!.userName, + personalAccount.basicAuth!.password, + ); + break; + default: + break; + } + } + + void setCurrentActiveAccount(PersonalAccount activeAccount) { + consumeState(_setCurrentActiveAccountInteractor.execute(activeAccount)); + } + + void _handleBadCredentials() async { + await clearAllDataAndBackToLogin(); + } + + void _handleDestroyFirebaseRegistrationFailure(DestroyFirebaseRegistrationFailure failure) async { + if (PlatformInfo.isMobile) { + await clearDataByAccount(failure.currentAccount); + _handleNavigationRouteAfterLogoutCurrentAccountSuccess(); + } else { + await clearAllDataAndBackToLogin(); + } + } + + void _handleDestroyFirebaseRegistrationSuccess(DestroyFirebaseRegistrationSuccess success) async { + if (PlatformInfo.isMobile) { + await clearDataByAccount(success.currentAccount); + _handleNavigationRouteAfterLogoutCurrentAccountSuccess(); + } else { + await clearAllDataAndBackToLogin(); + } + } + + void _handleNavigationRouteAfterLogoutCurrentAccountSuccess() async { + log('BaseController::_handleNavigationRouteAfterLogoutCurrentAccountSuccess:'); + final listAccounts = await authenticatedAccountManager.getAllPersonalAccount(); + if (listAccounts.isEmpty) { + removeAllRouteAndNavigateToTwakeIdPage(); + } else { + pushAndPopAll( + AppRoutes.home, + arguments: LoginNavigateArguments( + navigateType: LoginNavigateType.selectActiveAccount + )); } } } diff --git a/lib/features/base/mixin/message_dialog_action_mixin.dart b/lib/features/base/mixin/message_dialog_action_mixin.dart index 1b93d13be9..e5d0230b82 100644 --- a/lib/features/base/mixin/message_dialog_action_mixin.dart +++ b/lib/features/base/mixin/message_dialog_action_mixin.dart @@ -8,28 +8,27 @@ import 'package:tmail_ui_user/main/routes/route_navigation.dart'; mixin MessageDialogActionMixin { - Future showConfirmDialogAction( - BuildContext context, - String message, - String actionName, - { - Function? onConfirmAction, - Function? onCancelAction, - String? title, - String? cancelTitle, - bool hasCancelButton = true, - bool showAsBottomSheet = false, - bool alignCenter = false, - List? listTextSpan, - Widget? icon, - TextStyle? titleStyle, - TextStyle? messageStyle, - TextStyle? actionStyle, - TextStyle? cancelStyle, - Color? actionButtonColor, - Color? cancelButtonColor, - } - ) async { + Future showConfirmDialogAction({ + required BuildContext context, + String? message, + String? actionName, + Function? onConfirmAction, + Function? onCancelAction, + String? title, + String? cancelTitle, + bool hasCancelButton = true, + bool showAsBottomSheet = false, + bool alignCenter = false, + List? listTextSpan, + Widget? icon, + TextStyle? titleStyle, + TextStyle? messageStyle, + TextStyle? actionStyle, + TextStyle? cancelStyle, + Color? actionButtonColor, + Color? cancelButtonColor, + EdgeInsetsGeometry? titlePadding, + }) async { final responsiveUtils = Get.find(); final imagePaths = Get.find(); @@ -39,20 +38,20 @@ mixin MessageDialogActionMixin { child: (ConfirmDialogBuilder(imagePaths, listTextSpan: listTextSpan, heightButton: 44) ..key(const Key('confirm_dialog_action')) ..title(title ?? '') - ..content(message) + ..content(message ?? '') ..addIcon(icon) ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) ..marginIcon(icon != null ? const EdgeInsets.only(top: 24) : null) - ..paddingTitle(icon != null ? const EdgeInsets.only(top: 24) : EdgeInsets.zero) + ..paddingTitle(icon != null ? const EdgeInsets.only(top: 24) : titlePadding ?? EdgeInsets.zero) ..radiusButton(12) - ..paddingContent(const EdgeInsets.only(left: 24, right: 24, bottom: 24, top: 12)) - ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 24, left: 24, right: 24)) + ..paddingContent(const EdgeInsetsDirectional.only(start: 24, end: 24, bottom: 24, top: 12)) + ..paddingButton(hasCancelButton ? null : const EdgeInsetsDirectional.only(bottom: 24, start: 24, end: 24)) ..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)) ..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) - ..onConfirmButtonAction(actionName, () { + ..onConfirmButtonAction(actionName ?? AppLocalizations.of(context).yes, () { popBack(); onConfirmAction?.call(); }) @@ -80,21 +79,21 @@ mixin MessageDialogActionMixin { ) ..key(const Key('confirm_dialog_action')) ..title(title ?? '') - ..content(message) + ..content(message ?? '') ..addIcon(icon) ..margin(const EdgeInsets.only(bottom: 42)) ..widthDialog(responsiveUtils.getSizeScreenWidth(context)) ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) - ..paddingTitle(icon != null ? const EdgeInsets.only(top: 24) : EdgeInsets.zero) + ..paddingTitle(icon != null ? const EdgeInsets.only(top: 24) : titlePadding ?? EdgeInsets.zero) ..marginIcon(EdgeInsets.zero) - ..paddingContent(const EdgeInsets.only(left: 44, right: 44, bottom: 24, top: 12)) - ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44)) + ..paddingContent(const EdgeInsetsDirectional.only(start: 44, end: 44, bottom: 24, top: 12)) + ..paddingButton(hasCancelButton ? null : const EdgeInsetsDirectional.only(bottom: 16, start: 44, end: 44)) ..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)) ..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) - ..onConfirmButtonAction(actionName, () { + ..onConfirmButtonAction(actionName ?? AppLocalizations.of(context).yes, () { popBack(); onConfirmAction?.call(); }) @@ -117,7 +116,7 @@ mixin MessageDialogActionMixin { ); } else { return (ConfirmationDialogActionSheetBuilder(context, listTextSpan: listTextSpan) - ..messageText(message) + ..messageText(message ?? '') ..styleConfirmButton(const TextStyle(fontSize: 20, fontWeight: FontWeight.normal, color: Colors.black)) ..styleMessage(messageStyle) ..styleCancelButton(cancelStyle) @@ -128,7 +127,7 @@ mixin MessageDialogActionMixin { onCancelAction?.call(); } ) - ..onConfirmAction(actionName, () { + ..onConfirmAction(actionName ?? AppLocalizations.of(context).yes, () { popBack(); onConfirmAction?.call(); })).show(); @@ -139,20 +138,20 @@ mixin MessageDialogActionMixin { child: (ConfirmDialogBuilder(imagePaths, listTextSpan: listTextSpan) ..key(const Key('confirm_dialog_action')) ..title(title ?? '') - ..content(message) + ..content(message ?? '') ..addIcon(icon) ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) ..marginIcon(icon != null ? const EdgeInsets.only(top: 24) : null) - ..paddingTitle(icon != null ? const EdgeInsets.only(top: 24) : EdgeInsets.zero) + ..paddingTitle(icon != null ? const EdgeInsets.only(top: 24) : titlePadding ?? EdgeInsets.zero) ..marginIcon(EdgeInsets.zero) - ..paddingContent(const EdgeInsets.only(left: 44, right: 44, bottom: 24, top: 12)) + ..paddingContent(const EdgeInsetsDirectional.only(start: 44, end: 44, bottom: 24, top: 12)) ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44)) ..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)) ..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) - ..onConfirmButtonAction(actionName, () { + ..onConfirmButtonAction(actionName ?? AppLocalizations.of(context).yes, () { popBack(); onConfirmAction?.call(); }) diff --git a/lib/features/base/reloadable/reloadable_controller.dart b/lib/features/base/reloadable/reloadable_controller.dart index fc00649c3c..b9109e26a4 100644 --- a/lib/features/base/reloadable/reloadable_controller.dart +++ b/lib/features/base/reloadable/reloadable_controller.dart @@ -1,31 +1,34 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/platform_info.dart'; +import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; -import 'package:model/account/authentication_type.dart'; import 'package:model/account/personal_account.dart'; import 'package:model/extensions/session_extension.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/home/domain/state/get_session_state.dart'; import 'package:tmail_ui_user/features/home/domain/usecases/get_session_interactor.dart'; -import 'package:tmail_ui_user/features/login/data/extensions/token_oidc_extension.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_authenticated_account_state.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/add_account_id_to_active_account_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/model/login_arguments.dart'; +import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_arguments.dart'; +import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_type.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; +import 'package:tmail_ui_user/main/utils/authenticated_account_manager.dart'; import 'package:tmail_ui_user/main/utils/message_toast_utils.dart'; abstract class ReloadableController extends BaseController { - final GetSessionInteractor _getSessionInteractor = Get.find(); + final GetSessionInteractor getSessionInteractor = Get.find(); final GetAuthenticatedAccountInteractor _getAuthenticatedAccountInteractor = Get.find(); - final UpdateAuthenticationAccountInteractor _updateAuthenticationAccountInteractor = Get.find(); + final AddAccountIdToActiveAccountInteractor _addAccountIdToActiveAccountInteractor = Get.find(); @override void handleFailureViewState(Failure failure) { @@ -33,13 +36,11 @@ abstract class ReloadableController extends BaseController { if (failure is GetSessionFailure) { _handleGetSessionFailure(failure.exception); } else if (failure is GetAuthenticatedAccountFailure) { - goToLogin( - arguments: LoginArguments( - PlatformInfo.isMobile - ? LoginFormType.dnsLookupForm - : LoginFormType.none - ) - ); + if (PlatformInfo.isMobile) { + navigateToTwakeIdPage(); + } else { + navigateToLoginPage(arguments: LoginArguments(LoginFormType.none)); + } } } @@ -68,38 +69,8 @@ abstract class ReloadableController extends BaseController { consumeState(_getAuthenticatedAccountInteractor.execute()); } - void setUpInterceptors(PersonalAccount personalAccount) { - dynamicUrlInterceptors.setJmapUrl(personalAccount.baseUrl); - dynamicUrlInterceptors.changeBaseUrl(personalAccount.baseUrl); - - switch(personalAccount.authType) { - case AuthenticationType.oidc: - authorizationInterceptors.setTokenAndAuthorityOidc( - newToken: personalAccount.tokenOidc, - newConfig: personalAccount.tokenOidc!.oidcConfiguration - ); - authorizationIsolateInterceptors.setTokenAndAuthorityOidc( - newToken: personalAccount.tokenOidc, - newConfig: personalAccount.tokenOidc!.oidcConfiguration - ); - break; - case AuthenticationType.basic: - authorizationInterceptors.setBasicAuthorization( - personalAccount.basicAuth!.userName, - personalAccount.basicAuth!.password, - ); - authorizationIsolateInterceptors.setBasicAuthorization( - personalAccount.basicAuth!.userName, - personalAccount.basicAuth!.password, - ); - break; - default: - break; - } - } - void getSessionAction({AccountId? accountId, UserName? userName}) { - consumeState(_getSessionInteractor.execute( + consumeState(getSessionInteractor.execute( accountId: accountId, userName: userName )); @@ -112,7 +83,7 @@ abstract class ReloadableController extends BaseController { MessageToastUtils.getMessageByException(currentContext!, exception) ?? AppLocalizations.of(currentContext!).unknownError ); } - clearDataAndGoToLoginPage(); + clearAllDataAndBackToLogin(); } void _handleGetSessionSuccess(GetSessionSuccess success) { @@ -121,19 +92,65 @@ abstract class ReloadableController extends BaseController { final apiUrl = session.getQualifiedApiUrl(baseUrl: dynamicUrlInterceptors.jmapUrl); if (apiUrl.isNotEmpty) { dynamicUrlInterceptors.changeBaseUrl(apiUrl); - updateAuthenticationAccount(session, personalAccount.accountId, session.username); + _addAccountIdToActiveAccount( + personalAccount.accountId, + session.username, + apiUrl + ); handleReloaded(session); } else { - clearDataAndGoToLoginPage(); + clearAllDataAndBackToLogin(); } } void handleReloaded(Session session) {} - void updateAuthenticationAccount(Session session, AccountId accountId, UserName userName) { - final apiUrl = session.getQualifiedApiUrl(baseUrl: dynamicUrlInterceptors.jmapUrl); - if (apiUrl.isNotEmpty) { - consumeState(_updateAuthenticationAccountInteractor.execute(accountId, apiUrl, userName)); - } + void _addAccountIdToActiveAccount( + AccountId accountId, + UserName userName, + String apiUrl, + ) { + consumeState(_addAccountIdToActiveAccountInteractor.execute( + accountId, + apiUrl, + userName + )); + } + + void switchActiveAccount({ + required PersonalAccount currentAccount, + required PersonalAccount nextAccount, + required Session sessionCurrentAccount, + }) async { + await pushAndPopAll( + AppRoutes.home, + arguments: LoginNavigateArguments( + navigateType: LoginNavigateType.switchActiveAccount, + currentAccount: currentAccount, + sessionCurrentAccount: sessionCurrentAccount, + nextActiveAccount: nextAccount, + )); + } + + void _addAnotherAccount(PersonalAccount? currentAccount) async { + await pushAndPopAll( + AppRoutes.twakeId, + arguments: LoginNavigateArguments( + navigateType: LoginNavigateType.addAnotherAccount, + currentAccount: currentAccount + )); + } + + Future showAccountPicker({ + required BuildContext context, + VoidCallback? goToSettingAction, + OnSwitchActiveAccountAction? onSwitchActiveAccountAction + }) async { + await authenticatedAccountManager.showAccountsBottomSheetModal( + context: context, + onGoToManageAccount: goToSettingAction, + onAddAnotherAccountAction: _addAnotherAccount, + onSwitchActiveAccountAction: onSwitchActiveAccountAction + ); } } \ No newline at end of file diff --git a/lib/features/base/upgradeable/upgrade_hive_database_steps.dart b/lib/features/base/upgradeable/upgrade_hive_database_steps.dart deleted file mode 100644 index 197120fd13..0000000000 --- a/lib/features/base/upgradeable/upgrade_hive_database_steps.dart +++ /dev/null @@ -1,17 +0,0 @@ - -import 'package:tmail_ui_user/features/base/upgradeable/upgrade_database_steps.dart'; -import 'package:tmail_ui_user/features/caching/caching_manager.dart'; - -class UpgradeHiveDatabaseSteps extends UpgradeDatabaseSteps { - - final CachingManager _cachingManager; - - UpgradeHiveDatabaseSteps(this._cachingManager); - - @override - Future onUpgrade(int oldVersion, int newVersion) async { - if (oldVersion != newVersion) { - await _cachingManager.clearData(); - } - } -} \ No newline at end of file diff --git a/lib/features/caching/caching_manager.dart b/lib/features/caching/caching_manager.dart index ffe3b306c9..dae83d2068 100644 --- a/lib/features/caching/caching_manager.dart +++ b/lib/features/caching/caching_manager.dart @@ -2,7 +2,9 @@ import 'package:core/utils/app_logger.dart'; import 'package:core/utils/file_utils.dart'; import 'package:core/utils/platform_info.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/account/personal_account.dart'; +import 'package:model/extensions/account_id_extensions.dart'; import 'package:tmail_ui_user/features/caching/clients/account_cache_client.dart'; import 'package:tmail_ui_user/features/caching/clients/email_cache_client.dart'; import 'package:tmail_ui_user/features/caching/clients/fcm_cache_client.dart'; @@ -15,6 +17,7 @@ import 'package:tmail_ui_user/features/caching/clients/recent_search_cache_clien import 'package:tmail_ui_user/features/caching/clients/session_hive_cache_client.dart'; import 'package:tmail_ui_user/features/caching/clients/state_cache_client.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/state_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/local/local_spam_report_manager.dart'; @@ -79,27 +82,30 @@ class CachingManager { ], eagerError: true); } - Future clearData() async { + Future clearCacheByAccount(PersonalAccount currentAccount) async { + final accountKey = TupleKey( + currentAccount.accountId!.asString, + currentAccount.userName!.value + ).encodeKey; + await Future.wait([ - _stateCacheClient.clearAllData(), - _mailboxCacheClient.clearAllData(), - _emailCacheClient.clearAllData(), - _fcmCacheClient.clearAllData(), - _firebaseRegistrationCacheClient.clearAllData(), - _recentSearchCacheClient.clearAllData(), - _localSpamReportManager.clear(), - if (PlatformInfo.isMobile) - ...[ - _newEmailHiveCacheClient.clearAllData(), - _openedEmailHiveCacheClient.clearAllData(), - _clearSendingEmailCache(), - ] + _sessionHiveCacheClient.clearAllDataContainKey(accountKey), + _mailboxCacheClient.clearAllDataContainKey(accountKey), + _emailCacheClient.clearAllDataContainKey(accountKey), + _stateCacheClient.clearAllDataContainKey(accountKey), + _fcmCacheClient.clearAllDataContainKey(accountKey), + _firebaseRegistrationCacheClient.clearAllDataContainKey(accountKey), + _newEmailHiveCacheClient.clearAllDataContainKey(accountKey), + _openedEmailHiveCacheClient.clearAllDataContainKey(accountKey), + _clearSendingEmailCacheByAccount(currentAccount), + if (PlatformInfo.isIOS) + _keychainSharingManager.delete(accountId: currentAccount.accountId?.asString) ], eagerError: true); } - Future clearEmailCacheAndStateCacheByTupleKey(AccountId accountId, Session session) { + Future clearEmailCacheAndStateCacheByTupleKey(AccountId accountId, UserName userName) { return Future.wait([ - _stateCacheClient.deleteItem(StateType.email.getTupleKeyStored(accountId, session.username)), + _stateCacheClient.deleteItem(StateType.email.getTupleKeyStored(accountId, userName)), _emailCacheClient.clearAllData(), ], eagerError: true); } @@ -124,13 +130,29 @@ class CachingManager { return await HiveCacheConfig().closeHive(); } - Future clearAllFileInStorage() async { + Future clearAllFolderInStorage() async { await Future.wait([ _fileUtils.removeFolder(CachingConstants.newEmailsContentFolderName), _fileUtils.removeFolder(CachingConstants.openedEmailContentFolderName), ]); } + Future clearFolderStorageByAccount(PersonalAccount currentAccount) async { + final folderKey = TupleKey( + currentAccount.accountId!.asString, + currentAccount.userName!.value + ).encodeKey; + + await Future.wait([ + _fileUtils.removeFolder( + '${CachingConstants.newEmailsContentFolderName}/$folderKey' + ), + _fileUtils.removeFolder( + '${CachingConstants.openedEmailContentFolderName}/$folderKey' + ), + ]); + } + Future _clearSendingEmailCache() async { final listSendingEmails = await _sendingEmailCacheManager.getAllSendingEmails(); final sendingIds = listSendingEmails.map((sendingEmail) => sendingEmail.sendingId).toSet().toList(); @@ -142,4 +164,20 @@ class CachingManager { await _sendingEmailCacheManager.clearAllSendingEmails(); } } + + Future _clearSendingEmailCacheByAccount(PersonalAccount currentAccount) async { + final listSendingEmails = await _sendingEmailCacheManager.getAllSendingEmailsByAccount( + currentAccount.accountId!, + currentAccount.userName!); + + final sendingIds = listSendingEmails.map((sendingEmail) => sendingEmail.sendingId).toSet().toList(); + if (sendingIds.isNotEmpty) { + await Future.wait( + sendingIds.map(WorkManagerController().cancelByUniqueId), + eagerError: true + ); + + await _sendingEmailCacheManager.clearAllSendingEmailsByAccount(currentAccount); + } + } } diff --git a/lib/features/caching/config/hive_cache_client.dart b/lib/features/caching/config/hive_cache_client.dart index 89ca70241b..3e6c1a04ca 100644 --- a/lib/features/caching/config/hive_cache_client.dart +++ b/lib/features/caching/config/hive_cache_client.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'package:core/presentation/extensions/map_extensions.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:hive/hive.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; @@ -80,13 +81,15 @@ abstract class HiveCacheClient { }); } - Future> getListByTupleKey(String accountId, String userName) { + Future> getListByNestedKey(String nestedKey) { return Future.sync(() async { final boxItem = encryption ? await openBoxEncryption() : await openBox(); - return boxItem.toMap() - .where((key, value) => _matchedKey(key, accountId, userName)) + final listItem = boxItem.toMap() + .where((key, value) => _matchedNestedKey(key, nestedKey)) .values .toList(); + log('HiveCacheClient::getListByNestedKey:listItem: ${listItem.length}'); + return listItem; }).catchError((error) { throw error; }); @@ -104,12 +107,6 @@ abstract class HiveCacheClient { }); } - bool _matchedKey(String key, String accountId, String userName) { - final keyDecoded = CacheUtils.decodeKey(key); - final tupleKey = TupleKey.fromString(keyDecoded); - return tupleKey.parts.length >= 3 && tupleKey.parts[1] == accountId && tupleKey.parts[2] == userName; - } - Future updateItem(String key, T newObject) { return Future.sync(() async { final boxItem = encryption ? await openBoxEncryption() : await openBox(); @@ -182,6 +179,25 @@ abstract class HiveCacheClient { }); } + Future clearAllDataContainKey(String nestedKey) { + return Future.sync(() async { + final boxItem = encryption ? await openBoxEncryption() : await openBox(); + + final mapItemNotContainNestedKey = boxItem.toMap() + .where((key, value) => !_matchedNestedKey(key, nestedKey)); + log('HiveCacheClient::clearAllDataContainKey:mapItemNotContainNestedKey: ${mapItemNotContainNestedKey.length}'); + return boxItem.putAll(mapItemNotContainNestedKey); + }).catchError((error) { + throw error; + }); + } + + bool _matchedNestedKey(String key, String nestedKey) { + final decodedKey = CacheUtils.decodeKey(key); + final decodedNestedKey = CacheUtils.decodeKey(nestedKey); + return decodedKey.contains(decodedNestedKey); + } + Future closeBox() async { if (Hive.isBoxOpen(tableName)) { await Hive.box(tableName).close(); diff --git a/lib/features/caching/config/hive_cache_config.dart b/lib/features/caching/config/hive_cache_config.dart index d4fdb849b1..fd69c23546 100644 --- a/lib/features/caching/config/hive_cache_config.dart +++ b/lib/features/caching/config/hive_cache_config.dart @@ -6,7 +6,6 @@ import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:hive/hive.dart'; import 'package:path_provider/path_provider.dart' as path_provider; -import 'package:tmail_ui_user/features/base/upgradeable/upgrade_hive_database_steps.dart'; import 'package:tmail_ui_user/features/base/upgradeable/upgrade_hive_database_steps_v10.dart'; import 'package:tmail_ui_user/features/base/upgradeable/upgrade_hive_database_steps_v7.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; @@ -58,7 +57,6 @@ class HiveCacheConfig { const newVersion = CacheVersion.hiveDBVersion; log('HiveCacheConfig::onUpgradeDatabase():oldVersion: $oldVersion | newVersion: $newVersion'); - await UpgradeHiveDatabaseSteps(cachingManager).onUpgrade(oldVersion, newVersion); await UpgradeHiveDatabaseStepsV7(cachingManager).onUpgrade(oldVersion, newVersion); await UpgradeHiveDatabaseStepsV10(cachingManager).onUpgrade(oldVersion, newVersion); diff --git a/lib/features/caching/utils/cache_utils.dart b/lib/features/caching/utils/cache_utils.dart index 954395754a..8000a51a16 100644 --- a/lib/features/caching/utils/cache_utils.dart +++ b/lib/features/caching/utils/cache_utils.dart @@ -12,14 +12,14 @@ class TupleKey { TupleKey( String key1, + String key2, [ - String? key2, String? key3, String? key4, ] ) : parts = [ key1, - if (key2 != null) key2, + key2, if (key3 != null) key3, if (key4 != null) key4, ]; diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 3037a84479..161a7b4266 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -832,9 +832,10 @@ class ComposerController extends BaseController { } if (!isEnableEmailSendButton.value) { - showConfirmDialogAction(context, - AppLocalizations.of(context).message_dialog_send_email_without_recipient, - AppLocalizations.of(context).add_recipients, + showConfirmDialogAction( + context: context, + message: AppLocalizations.of(context).message_dialog_send_email_without_recipient, + actionName: AppLocalizations.of(context).add_recipients, onConfirmAction: () => isSendEmailLoading.value = false, title: AppLocalizations.of(context).sending_failed, icon: SvgPicture.asset(imagePaths.icSendToastError, fit: BoxFit.fill), @@ -849,9 +850,10 @@ class ComposerController extends BaseController { .where((emailAddress) => !GetUtils.isEmail(emailAddress.emailAddress)) .toList(); if (listEmailAddressInvalid.isNotEmpty) { - showConfirmDialogAction(context, - AppLocalizations.of(context).message_dialog_send_email_with_email_address_invalid, - AppLocalizations.of(context).fix_email_addresses, + showConfirmDialogAction( + context: context, + message: AppLocalizations.of(context).message_dialog_send_email_with_email_address_invalid, + actionName: AppLocalizations.of(context).fix_email_addresses, onConfirmAction: () { toAddressExpandMode.value = ExpandMode.EXPAND; ccAddressExpandMode.value = ExpandMode.EXPAND; @@ -867,9 +869,10 @@ class ComposerController extends BaseController { } if (subjectEmail.value == null || subjectEmail.isEmpty == true) { - showConfirmDialogAction(context, - AppLocalizations.of(context).message_dialog_send_email_without_a_subject, - AppLocalizations.of(context).send_anyway, + showConfirmDialogAction( + context: context, + message: AppLocalizations.of(context).message_dialog_send_email_without_a_subject, + actionName: AppLocalizations.of(context).send_anyway, onConfirmAction: () => _handleSendMessages(context), onCancelAction: () => isSendEmailLoading.value = false, title: AppLocalizations.of(context).empty_subject, @@ -881,9 +884,9 @@ class ComposerController extends BaseController { if (!uploadController.allUploadAttachmentsCompleted) { showConfirmDialogAction( - context, - AppLocalizations.of(context).messageDialogSendEmailUploadingAttachment, - AppLocalizations.of(context).got_it, + context: context, + message: AppLocalizations.of(context).messageDialogSendEmailUploadingAttachment, + actionName: AppLocalizations.of(context).got_it, onConfirmAction: () => isSendEmailLoading.value = false, title: AppLocalizations.of(context).sending_failed, showAsBottomSheet: true, @@ -895,10 +898,10 @@ class ComposerController extends BaseController { if (!uploadController.hasEnoughMaxAttachmentSize()) { showConfirmDialogAction( - context, - AppLocalizations.of(context).message_dialog_send_email_exceeds_maximum_size( + context: context, + message: AppLocalizations.of(context).message_dialog_send_email_exceeds_maximum_size( filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(context).got_it, + actionName: AppLocalizations.of(context).got_it, onConfirmAction: () => isSendEmailLoading.value = false, title: AppLocalizations.of(context).sending_failed, icon: SvgPicture.asset(imagePaths.icSendToastError, fit: BoxFit.fill), @@ -990,11 +993,11 @@ class ComposerController extends BaseController { void _showConfirmDialogStoreSendingEmail(BuildContext context) { showConfirmDialogAction( - context, - PlatformInfo.isIOS + context: context, + message: PlatformInfo.isIOS ? AppLocalizations.of(context).messageDialogOfflineModeOnIOS : '', - AppLocalizations.of(context).proceed, + actionName: AppLocalizations.of(context).proceed, onConfirmAction: () async { final sendingArgs = await _createSendingEmailArguments(context); _closeComposerAction( @@ -1155,10 +1158,10 @@ class ComposerController extends BaseController { } else { if (currentContext != null) { showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( + context: currentContext!, + message: AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, + actionName: AppLocalizations.of(currentContext!).got_it, onConfirmAction: () => {isSendEmailLoading.value = false}, title: AppLocalizations.of(currentContext!).maximum_files_size, hasCancelButton: false); @@ -1401,10 +1404,10 @@ class ComposerController extends BaseController { } else { if (currentContext != null) { showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( + context: currentContext!, + message: AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, + actionName: AppLocalizations.of(currentContext!).got_it, title: AppLocalizations.of(currentContext!).maximum_files_size, hasCancelButton: false, ); @@ -1888,10 +1891,10 @@ class ComposerController extends BaseController { } else { if (currentContext != null) { showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( + context: currentContext!, + message: AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, + actionName: AppLocalizations.of(currentContext!).got_it, onConfirmAction: () => {isSendEmailLoading.value = false}, title: AppLocalizations.of(currentContext!).maximum_files_size, hasCancelButton: false); @@ -2080,10 +2083,10 @@ class ComposerController extends BaseController { } else { if (currentContext != null) { showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( + context: currentContext!, + message: AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, + actionName: AppLocalizations.of(currentContext!).got_it, title: AppLocalizations.of(currentContext!).maximum_files_size, hasCancelButton: false, ); @@ -2189,10 +2192,10 @@ class ComposerController extends BaseController { } else { if (currentContext != null) { showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( + context: currentContext!, + message: AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, + actionName: AppLocalizations.of(currentContext!).got_it, title: AppLocalizations.of(currentContext!).maximum_files_size, hasCancelButton: false, ); diff --git a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart index fb43275879..8ed2922eda 100644 --- a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart @@ -139,7 +139,7 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { final fileSaved = await _fileUtils.saveToFile( nameFile: detailedEmail.emailId.asString, content: detailedEmail.htmlEmailContent ?? '', - folderPath: detailedEmail.newEmailFolderPath + folderPath: detailedEmail.getNewEmailFolderPath(accountId, session.username) ); final detailedEmailSaved = detailedEmail.fromEmailContentPath(fileSaved.path); @@ -182,7 +182,7 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { final fileSaved = await _fileUtils.saveToFile( nameFile: detailedEmail.emailId.asString, content: detailedEmail.htmlEmailContent ?? '', - folderPath: detailedEmail.openedEmailFolderPath + folderPath: detailedEmail.getOpenedEmailFolderPath(accountId, session.username) ); final detailedEmailSaved = detailedEmail.fromEmailContentPath(fileSaved.path); @@ -247,7 +247,7 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { @override Future> getAllSendingEmails(AccountId accountId, UserName userName) { return Future.sync(() async { - final sendingEmailsCache = await _sendingEmailCacheManager.getAllSendingEmailsByTupleKey(accountId, userName); + final sendingEmailsCache = await _sendingEmailCacheManager.getAllSendingEmailsByAccount(accountId, userName); return sendingEmailsCache.toSendingEmails(); }).catchError(_exceptionThrower.throwException); } diff --git a/lib/features/email/domain/extensions/detailed_email_extension.dart b/lib/features/email/domain/extensions/detailed_email_extension.dart index c7b9396db9..6b331b9b47 100644 --- a/lib/features/email/domain/extensions/detailed_email_extension.dart +++ b/lib/features/email/domain/extensions/detailed_email_extension.dart @@ -1,5 +1,9 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/extensions/account_id_extensions.dart'; import 'package:model/extensions/email_id_extensions.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; import 'package:tmail_ui_user/features/email/domain/extensions/list_attachments_extension.dart'; import 'package:tmail_ui_user/features/email/domain/extensions/list_email_header_extension.dart'; @@ -21,9 +25,15 @@ extension DetailedEmailExtension on DetailedEmail { ); } - String get newEmailFolderPath => CachingConstants.newEmailsContentFolderName; + String getNewEmailFolderPath(AccountId accountId, UserName userName) { + final folderKey = TupleKey(accountId.asString, userName.value).encodeKey; + return '${CachingConstants.newEmailsContentFolderName}/$folderKey'; + } - String get openedEmailFolderPath => CachingConstants.openedEmailContentFolderName; + String getOpenedEmailFolderPath(AccountId accountId, UserName userName) { + final folderKey = TupleKey(accountId.asString, userName.value).encodeKey; + return '${CachingConstants.openedEmailContentFolderName}/$folderKey'; + } DetailedEmail fromEmailContentPath(String path) { return DetailedEmail( diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 85570a7831..96e71a470e 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -531,9 +531,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _handleReadReceipt() { if (currentContext != null) { - showConfirmDialogAction(currentContext!, - AppLocalizations.of(currentContext!).subTitleReadReceiptRequestNotificationMessage, - AppLocalizations.of(currentContext!).yes, + showConfirmDialogAction( + context: currentContext!, + message: AppLocalizations.of(currentContext!).subTitleReadReceiptRequestNotificationMessage, + actionName: AppLocalizations.of(currentContext!).yes, onConfirmAction: () => _handleSendReceiptToSenderAction(currentContext!), showAsBottomSheet: true, title: AppLocalizations.of(currentContext!).titleReadReceiptRequestNotificationMessage, @@ -1416,9 +1417,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _unsubscribeEmail(BuildContext context, PresentationEmail presentationEmail) { showConfirmDialogAction( - context, - '', - AppLocalizations.of(context).unsubscribe, + context: context, + actionName: AppLocalizations.of(context).unsubscribe, onConfirmAction: () { if (emailUnsubscribe.value?.httpLinks.isNotEmpty == true) { _handleUnsubscribeMailByHttpsLink( diff --git a/lib/features/home/presentation/home_controller.dart b/lib/features/home/presentation/home_controller.dart index e3b3d7b788..f093348600 100644 --- a/lib/features/home/presentation/home_controller.dart +++ b/lib/features/home/presentation/home_controller.dart @@ -1,4 +1,7 @@ +import 'dart:async'; + import 'package:core/presentation/utils/theme_utils.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; @@ -22,8 +25,15 @@ import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_email_cac import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_login_url_cache_interactor.dart'; import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_login_username_interactor.dart'; import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_search_cache_interactor.dart'; +import 'package:tmail_ui_user/features/home/domain/state/get_session_state.dart'; +import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_arguments.dart'; +import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/preview_email_arguments.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/restore_active_account_arguments.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/select_active_account_arguments.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/switch_active_account_arguments.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_receiver.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_utils.dart'; @@ -45,8 +55,8 @@ class HomeController extends ReloadableController { this._cleanupRecentLoginUsernameCacheInteractor, ); - PersonalAccount? currentAccount; EmailId? _emailIdPreview; + StreamSubscription? _sessionStreamSubscription; @override void onInit() { @@ -68,6 +78,12 @@ class HomeController extends ReloadableController { super.onReady(); } + @override + void onClose() { + _sessionStreamSubscription?.cancel(); + super.onClose(); + } + @override void handleReloaded(Session session) { if (_emailIdPreview != null) { @@ -87,20 +103,26 @@ class HomeController extends ReloadableController { } void _initFlutterDownloader() { - FlutterDownloader - .initialize(debug: kDebugMode) - .then((_) => FlutterDownloader.registerCallback(downloadCallback)); + if (!FlutterDownloader.initialized) { + FlutterDownloader + .initialize(debug: kDebugMode) + .then((_) => FlutterDownloader.registerCallback(downloadCallback)); + } } static void downloadCallback(String id, DownloadTaskStatus status, int progress) {} void _handleNavigateToScreen() async { if (PlatformInfo.isMobile) { - final firstTimeAppLaunch = await appStore.getItemBoolean(AppConfig.firstTimeAppLaunchKey); - if (firstTimeAppLaunch) { - await _cleanupCache(); + if (Get.arguments is LoginNavigateArguments) { + _handleLoginNavigateArguments(Get.arguments); } else { - _navigateToTwakeWelcomePage(); + final firstTimeAppLaunch = await appStore.getItemBoolean(AppConfig.firstTimeAppLaunchKey); + if (firstTimeAppLaunch) { + await _cleanupCache(); + } else { + _navigateToTwakeWelcomePage(); + } } } else { await _cleanupCache(); @@ -151,4 +173,174 @@ class HomeController extends ReloadableController { } } } + + void _handleLoginNavigateArguments(LoginNavigateArguments navigateArguments) async { + switch (navigateArguments.navigateType) { + case LoginNavigateType.switchActiveAccount: + _switchActiveAccount( + navigateArguments.currentAccount!, + navigateArguments.sessionCurrentAccount!, + navigateArguments.nextActiveAccount!); + break; + case LoginNavigateType.selectActiveAccount: + _showListAccountPicker(); + break; + default: + await _cleanupCache(); + break; + } + } + + void _switchActiveAccount( + PersonalAccount currentAccount, + Session sessionCurrentAccount, + PersonalAccount nextAccount + ) { + setUpInterceptors(nextAccount); + + _sessionStreamSubscription = getSessionInteractor.execute( + accountId: nextAccount.accountId, + userName: nextAccount.userName + ).listen( + (viewState) { + viewState.fold( + (failure) => _handleGetSessionFailureWhenSwitchActiveAccount( + currentActiveAccount: currentAccount, + session: sessionCurrentAccount, + exception: failure), + (success) => success is GetSessionSuccess + ? _handleGetSessionSuccessWhenSwitchActiveAccount( + currentAccount, + nextAccount, + sessionCurrentAccount, + success.session) + : null, + ); + }, + onError: (error, stack) { + logError('HomeController::_switchActiveAccount:Exception: $error | Stack: $stack'); + _handleGetSessionFailureWhenSwitchActiveAccount( + currentActiveAccount: currentAccount, + session: sessionCurrentAccount, + exception: error); + } + ); + } + + void _handleGetSessionSuccessWhenSwitchActiveAccount( + PersonalAccount currentAccount, + PersonalAccount nextAccount, + Session sessionCurrentAccount, + Session sessionNextAccount, + ) async { + log('HomeController::_handleGetSessionSuccessWhenSwitchActiveAccount:sessionNextAccount: $sessionNextAccount'); + await popAndPush( + RouteUtils.generateNavigationRoute(AppRoutes.dashboard), + arguments: SwitchActiveAccountArguments( + sessionCurrentAccount: sessionCurrentAccount, + sessionNextAccount: sessionNextAccount, + currentAccount: currentAccount, + nextAccount: nextAccount, + ) + ); + } + + void _restoreActiveAccount({ + required PersonalAccount currentActiveAccount, + required Session session, + dynamic exception + }) async { + logError('HomeController::_restoreActiveAccount:currentActiveAccount: $currentActiveAccount'); + await popAndPush( + RouteUtils.generateNavigationRoute(AppRoutes.dashboard), + arguments: RestoreActiveAccountArguments( + currentAccount: currentActiveAccount, + session: session, + exception: exception + ) + ); + } + + void _handleGetSessionFailureWhenSwitchActiveAccount({ + required PersonalAccount currentActiveAccount, + required Session session, + dynamic exception + }) async { + logError('HomeController::_handleGetSessionFailureWhenSwitchActiveAccount:exception: $exception'); + _restoreActiveAccount( + currentActiveAccount: currentActiveAccount, + session: session, + exception: exception + ); + } + + void _showListAccountPicker() { + if (currentContext == null) { + logError('HomeController::_showListAccountPicker: context is null'); + return; + } + + authenticatedAccountManager.showAccountsBottomSheetModal( + context: currentContext!, + onSelectActiveAccountAction: _handleSelectActiveAccount, + onAddAnotherAccountAction: (_) => _handleAddOtherAccount() + ); + } + + void _handleSelectActiveAccount(PersonalAccount activeAccount) { + setUpInterceptors(activeAccount); + + _sessionStreamSubscription = getSessionInteractor.execute( + accountId: activeAccount.accountId, + userName: activeAccount.userName + ).listen( + (viewState) { + viewState.fold( + (failure) => _handleGetSessionFailureWhenSelectActiveAccount( + activeAccount: activeAccount, + exception: failure), + (success) => success is GetSessionSuccess + ? _handleGetSessionSuccessWhenSelectActiveAccount(activeAccount, success.session) + : null, + ); + }, + onError: (error, stack) { + _handleGetSessionFailureWhenSelectActiveAccount( + activeAccount: activeAccount, + exception: error); + } + ); + } + + void _handleGetSessionSuccessWhenSelectActiveAccount( + PersonalAccount activeAccount, + Session sessionActiveAccount + ) async { + log('HomeController::_handleGetSessionSuccessWhenSelectActiveAccount:sessionActiveAccount: $sessionActiveAccount'); + await popAndPush( + RouteUtils.generateNavigationRoute(AppRoutes.dashboard), + arguments: SelectActiveAccountArguments( + session: sessionActiveAccount, + activeAccount: activeAccount, + ) + ); + } + + void _handleGetSessionFailureWhenSelectActiveAccount({ + required PersonalAccount activeAccount, + dynamic exception + }) async { + logError('HomeController::_handleGetSessionFailureWhenSelectActiveAccount:exception: $exception'); + if (currentOverlayContext != null && currentContext != null) { + appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).unableToLogInToAccount(activeAccount.userName?.value ?? '')); + } + + _showListAccountPicker(); + } + + void _handleAddOtherAccount() { + navigateToTwakeIdPage(); + } } \ No newline at end of file diff --git a/lib/features/login/data/datasource/account_datasource.dart b/lib/features/login/data/datasource/account_datasource.dart index 1f145075bd..a1b0f8beef 100644 --- a/lib/features/login/data/datasource/account_datasource.dart +++ b/lib/features/login/data/datasource/account_datasource.dart @@ -6,4 +6,8 @@ abstract class AccountDatasource { Future setCurrentAccount(PersonalAccount newCurrentAccount); Future deleteCurrentAccount(String accountId); + + Future> getAllAccount(); + + Future setCurrentActiveAccount(PersonalAccount activeAccount); } \ No newline at end of file diff --git a/lib/features/login/data/datasource_impl/hive_account_datasource_impl.dart b/lib/features/login/data/datasource_impl/hive_account_datasource_impl.dart index d6c1606976..fc12216cf2 100644 --- a/lib/features/login/data/datasource_impl/hive_account_datasource_impl.dart +++ b/lib/features/login/data/datasource_impl/hive_account_datasource_impl.dart @@ -41,4 +41,18 @@ class HiveAccountDatasourceImpl extends AccountDatasource { return await _accountCacheManager.deleteCurrentAccount(accountId); }).catchError(_exceptionThrower.throwException); } + + @override + Future> getAllAccount() { + return Future.sync(() async { + return await _accountCacheManager.getAllAccount(); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future setCurrentActiveAccount(PersonalAccount activeAccount) { + return Future.sync(() async { + return await _accountCacheManager.setCurrentActiveAccount(activeAccount); + }).catchError(_exceptionThrower.throwException); + } } \ No newline at end of file diff --git a/lib/features/login/data/extensions/personal_account_extension.dart b/lib/features/login/data/extensions/personal_account_extension.dart index 0106ca2aa8..76fbd6fb4c 100644 --- a/lib/features/login/data/extensions/personal_account_extension.dart +++ b/lib/features/login/data/extensions/personal_account_extension.dart @@ -8,11 +8,11 @@ import 'package:tmail_ui_user/features/login/data/extensions/token_oidc_extensio import 'package:tmail_ui_user/features/login/data/model/account_cache.dart'; extension PersonalAccountExtension on PersonalAccount { - AccountCache toCache() { + AccountCache toCache({bool? isSelected}) { return AccountCache( id: id, authType: authType.name, - isSelected: isSelected, + isSelected: isSelected ?? this.isSelected, baseUrl: baseUrl, accountId: accountId?.id.value, apiUrl: apiUrl, @@ -36,7 +36,7 @@ extension PersonalAccountExtension on PersonalAccount { ); } - PersonalAccount updateAccountId({ + PersonalAccount addAccountId({ required AccountId accountId, required String apiUrl, required UserName userName, diff --git a/lib/features/login/data/local/account_cache_manager.dart b/lib/features/login/data/local/account_cache_manager.dart index 3d3d6a43e4..7135fa7cae 100644 --- a/lib/features/login/data/local/account_cache_manager.dart +++ b/lib/features/login/data/local/account_cache_manager.dart @@ -20,7 +20,7 @@ class AccountCacheManager { if (accountCache != null) { return accountCache.toAccount(); } else { - throw NotFoundAuthenticatedAccountException(); + throw NotFoundActiveAccountException(); } } @@ -47,4 +47,28 @@ class AccountCacheManager { log('AccountCacheManager::deleteCurrentAccount(): $hashId'); return _accountCacheClient.deleteItem(hashId); } + + Future> getAllAccount() async { + final allAccounts = await _accountCacheClient.getAll(); + log('AccountCacheManager::getAllAccount::allAccounts(): $allAccounts'); + if (allAccounts.isNotEmpty) { + return allAccounts.map((account) => account.toAccount()).toList(); + } else { + throw NotFoundAuthenticatedAccountException(); + } + } + + Future setCurrentActiveAccount(PersonalAccount activeAccount) async { + log('AccountCacheManager::setCurrentActiveAccount(): $activeAccount'); + final newAccountCache = activeAccount.toCache(isSelected: true); + final allAccounts = await _accountCacheClient.getAll(); + log('AccountCacheManager::setCurrentActiveAccount::allAccounts(): $allAccounts'); + if (allAccounts.isNotEmpty) { + final newAllAccounts = allAccounts.unselected().toList(); + if (newAllAccounts.isNotEmpty) { + await _accountCacheClient.updateMultipleItem(newAllAccounts.toMap()); + } + } + return _accountCacheClient.insertItem(newAccountCache.id, newAccountCache); + } } \ No newline at end of file diff --git a/lib/features/login/data/repository/account_repository_impl.dart b/lib/features/login/data/repository/account_repository_impl.dart index 0df1f5dedc..e9a35af0ad 100644 --- a/lib/features/login/data/repository/account_repository_impl.dart +++ b/lib/features/login/data/repository/account_repository_impl.dart @@ -22,4 +22,14 @@ class AccountRepositoryImpl extends AccountRepository { Future deleteCurrentAccount(String hashId) { return _accountDatasource.deleteCurrentAccount(hashId); } + + @override + Future> getAllAccount() { + return _accountDatasource.getAllAccount(); + } + + @override + Future setCurrentActiveAccount(PersonalAccount activeAccount) { + return _accountDatasource.setCurrentActiveAccount(activeAccount); + } } \ No newline at end of file diff --git a/lib/features/login/domain/exceptions/authentication_exception.dart b/lib/features/login/domain/exceptions/authentication_exception.dart index 926d23f34b..aab6e7911d 100644 --- a/lib/features/login/domain/exceptions/authentication_exception.dart +++ b/lib/features/login/domain/exceptions/authentication_exception.dart @@ -25,6 +25,8 @@ class BadGateway extends AuthenticationException { class NotFoundAuthenticatedAccountException implements Exception {} +class NotFoundActiveAccountException implements Exception {} + class InvalidBaseUrl extends AuthenticationException { InvalidBaseUrl() : super(AuthenticationException.invalidBaseUrl); diff --git a/lib/features/login/domain/repository/account_repository.dart b/lib/features/login/domain/repository/account_repository.dart index d1d656e72f..7c957a5cae 100644 --- a/lib/features/login/domain/repository/account_repository.dart +++ b/lib/features/login/domain/repository/account_repository.dart @@ -7,4 +7,8 @@ abstract class AccountRepository { Future setCurrentAccount(PersonalAccount newCurrentAccount); Future deleteCurrentAccount(String hashId); + + Future> getAllAccount(); + + Future setCurrentActiveAccount(PersonalAccount activeAccount); } \ No newline at end of file diff --git a/lib/features/login/domain/state/add_account_id_to_active_account_state.dart b/lib/features/login/domain/state/add_account_id_to_active_account_state.dart new file mode 100644 index 0000000000..1dfde78274 --- /dev/null +++ b/lib/features/login/domain/state/add_account_id_to_active_account_state.dart @@ -0,0 +1,11 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class AddAccountIdToActiveAccountLoading extends LoadingState {} + +class AddAccountIdToActiveAccountSuccess extends UIState {} + +class AddAccountIdToActiveAccountFailure extends FeatureFailure { + + AddAccountIdToActiveAccountFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/login/domain/state/get_all_authenticated_account_state.dart b/lib/features/login/domain/state/get_all_authenticated_account_state.dart new file mode 100644 index 0000000000..3e99fd595f --- /dev/null +++ b/lib/features/login/domain/state/get_all_authenticated_account_state.dart @@ -0,0 +1,17 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/account/personal_account.dart'; + +class GetAllAuthenticatedAccountSuccess extends UIState { + final List listAccount; + + GetAllAuthenticatedAccountSuccess(this.listAccount); + + @override + List get props => [listAccount]; +} + +class GetAllAuthenticatedAccountFailure extends FeatureFailure { + + GetAllAuthenticatedAccountFailure(dynamic exception) : super(exception: exception); +} diff --git a/lib/features/login/domain/state/set_current_account_active_state.dart b/lib/features/login/domain/state/set_current_account_active_state.dart new file mode 100644 index 0000000000..774ef99d31 --- /dev/null +++ b/lib/features/login/domain/state/set_current_account_active_state.dart @@ -0,0 +1,11 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class SetCurrentActiveAccountLoading extends LoadingState {} + +class SetCurrentActiveAccountSuccess extends UIState {} + +class SetCurrentActiveAccountFailure extends FeatureFailure { + + SetCurrentActiveAccountFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/login/domain/state/update_authentication_account_state.dart b/lib/features/login/domain/state/update_authentication_account_state.dart deleted file mode 100644 index c5d1931edf..0000000000 --- a/lib/features/login/domain/state/update_authentication_account_state.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:core/presentation/state/failure.dart'; -import 'package:core/presentation/state/success.dart'; - -class UpdateAuthenticationAccountLoading extends LoadingState {} - -class UpdateAuthenticationAccountSuccess extends UIState {} - -class UpdateAuthenticationAccountFailure extends FeatureFailure { - - UpdateAuthenticationAccountFailure(dynamic exception) : super(exception: exception); -} \ No newline at end of file diff --git a/lib/features/login/domain/usecases/update_authentication_account_interactor.dart b/lib/features/login/domain/usecases/add_account_id_to_active_account_interactor.dart similarity index 67% rename from lib/features/login/domain/usecases/update_authentication_account_interactor.dart rename to lib/features/login/domain/usecases/add_account_id_to_active_account_interactor.dart index b7e0df51d4..bdcf085ed7 100644 --- a/lib/features/login/domain/usecases/update_authentication_account_interactor.dart +++ b/lib/features/login/domain/usecases/add_account_id_to_active_account_interactor.dart @@ -5,27 +5,27 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/login/data/extensions/personal_account_extension.dart'; import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/state/update_authentication_account_state.dart'; +import 'package:tmail_ui_user/features/login/domain/state/add_account_id_to_active_account_state.dart'; -class UpdateAuthenticationAccountInteractor { +class AddAccountIdToActiveAccountInteractor { final AccountRepository _accountRepository; - UpdateAuthenticationAccountInteractor(this._accountRepository); + AddAccountIdToActiveAccountInteractor(this._accountRepository); Stream> execute(AccountId accountId, String apiUrl, UserName userName) async* { try{ - yield Right(UpdateAuthenticationAccountLoading()); + yield Right(AddAccountIdToActiveAccountLoading()); final currentAccount = await _accountRepository.getCurrentAccount(); await _accountRepository.setCurrentAccount( - currentAccount.updateAccountId( + currentAccount.addAccountId( accountId: accountId, apiUrl: apiUrl, userName: userName ) ); - yield Right(UpdateAuthenticationAccountSuccess()); + yield Right(AddAccountIdToActiveAccountSuccess()); } catch(e) { - yield Left(UpdateAuthenticationAccountFailure(e)); + yield Left(AddAccountIdToActiveAccountFailure(e)); } } } \ No newline at end of file diff --git a/lib/features/login/domain/usecases/get_all_authenticated_account_interactor.dart b/lib/features/login/domain/usecases/get_all_authenticated_account_interactor.dart new file mode 100644 index 0000000000..49def607d5 --- /dev/null +++ b/lib/features/login/domain/usecases/get_all_authenticated_account_interactor.dart @@ -0,0 +1,20 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/state/get_all_authenticated_account_state.dart'; + +class GetAllAuthenticatedAccountInteractor { + final AccountRepository _accountRepository; + + GetAllAuthenticatedAccountInteractor(this._accountRepository); + + Future> execute() async { + try { + final listAccount = await _accountRepository.getAllAccount(); + return Right(GetAllAuthenticatedAccountSuccess(listAccount)); + } catch (e) { + return Left(GetAllAuthenticatedAccountFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/login/domain/usecases/set_current_active_account_interactor.dart b/lib/features/login/domain/usecases/set_current_active_account_interactor.dart new file mode 100644 index 0000000000..57075a469c --- /dev/null +++ b/lib/features/login/domain/usecases/set_current_active_account_interactor.dart @@ -0,0 +1,22 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:model/account/personal_account.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/state/set_current_account_active_state.dart'; + +class SetCurrentActiveAccountInteractor { + final AccountRepository _accountRepository; + + SetCurrentActiveAccountInteractor(this._accountRepository); + + Stream> execute(PersonalAccount activeAccount) async* { + try{ + yield Right(SetCurrentActiveAccountLoading()); + await _accountRepository.setCurrentActiveAccount(activeAccount); + yield Right(SetCurrentActiveAccountSuccess()); + } catch(e) { + yield Left(SetCurrentActiveAccountFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/login/presentation/login_controller.dart b/lib/features/login/presentation/login_controller.dart index 93ce51ae6c..60e698c156 100644 --- a/lib/features/login/presentation/login_controller.dart +++ b/lib/features/login/presentation/login_controller.dart @@ -45,6 +45,8 @@ import 'package:tmail_ui_user/features/login/domain/usecases/save_login_url_on_m import 'package:tmail_ui_user/features/login/domain/usecases/save_login_username_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/model/login_arguments.dart'; +import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_arguments.dart'; +import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_type.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_utils.dart'; @@ -77,6 +79,7 @@ class LoginController extends ReloadableController { UserName? _username; Password? _password; Uri? _baseUri; + LoginNavigateArguments? _navigateArguments; LoginController( this._authenticationInteractor, @@ -113,6 +116,9 @@ class LoginController extends ReloadableController { if (PlatformInfo.isWeb) { _checkOIDCIsAvailable(); } + } else if (PlatformInfo.isMobile && arguments is LoginNavigateArguments) { + loginFormType.value = LoginFormType.dnsLookupForm; + _navigateArguments = arguments; } else if (PlatformInfo.isWeb) { _handleAuthenticationSSOBrowserCallbackAction(); } @@ -190,12 +196,20 @@ class LoginController extends ReloadableController { @override void handleReloaded(Session session) { - popAndPush( - RouteUtils.generateNavigationRoute(AppRoutes.dashboard), - arguments: session - ); + if (PlatformInfo.isMobile && _isAddAnotherAccount) { + pushAndPopAll( + RouteUtils.generateNavigationRoute(AppRoutes.dashboard), + arguments: session); + } else { + popAndPush( + RouteUtils.generateNavigationRoute(AppRoutes.dashboard), + arguments: session); + } } + bool get _isAddAnotherAccount => _navigateArguments != null && + _navigateArguments?.navigateType == LoginNavigateType.addAnotherAccount; + void _handleAuthenticationSSOBrowserCallbackAction() { consumeState(_getAuthResponseUrlBrowserInteractor.execute()); } @@ -226,7 +240,11 @@ class LoginController extends ReloadableController { switch(loginFormType.value) { case LoginFormType.dnsLookupForm: case LoginFormType.baseUrlForm: - navigateToTwakeIdPage(); + if (PlatformInfo.isMobile && _isAddAnotherAccount) { + popBack(); + } else { + navigateToTwakeIdPage(); + } break; case LoginFormType.passwordForm: _password = null; diff --git a/lib/features/login/presentation/model/login_navigate_arguments.dart b/lib/features/login/presentation/model/login_navigate_arguments.dart new file mode 100644 index 0000000000..5d2509d601 --- /dev/null +++ b/lib/features/login/presentation/model/login_navigate_arguments.dart @@ -0,0 +1,28 @@ + +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:model/account/personal_account.dart'; +import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_type.dart'; +import 'package:tmail_ui_user/main/routes/router_arguments.dart'; + +class LoginNavigateArguments extends RouterArguments { + + final LoginNavigateType navigateType; + final PersonalAccount? currentAccount; + final Session? sessionCurrentAccount; + final PersonalAccount? nextActiveAccount; + + LoginNavigateArguments({ + required this.navigateType, + this.currentAccount, + this.sessionCurrentAccount, + this.nextActiveAccount, + }); + + @override + List get props => [ + navigateType, + currentAccount, + sessionCurrentAccount, + nextActiveAccount, + ]; +} \ No newline at end of file diff --git a/lib/features/login/presentation/model/login_navigate_type.dart b/lib/features/login/presentation/model/login_navigate_type.dart new file mode 100644 index 0000000000..ee49799693 --- /dev/null +++ b/lib/features/login/presentation/model/login_navigate_type.dart @@ -0,0 +1,7 @@ + +enum LoginNavigateType { + signIn, + addAnotherAccount, + switchActiveAccount, + selectActiveAccount; +} \ No newline at end of file diff --git a/lib/features/login/presentation/model/twake_mail_presentation_account.dart b/lib/features/login/presentation/model/twake_mail_presentation_account.dart new file mode 100644 index 0000000000..c78ad71109 --- /dev/null +++ b/lib/features/login/presentation/model/twake_mail_presentation_account.dart @@ -0,0 +1,31 @@ + +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/multiple_account/models/twake_presentation_account.dart'; +import 'package:model/account/personal_account.dart'; + +class TwakeMailPresentationAccount extends TwakePresentationAccount { + + final PersonalAccount personalAccount; + + const TwakeMailPresentationAccount({ + required this.personalAccount, + required String accountName, + required String accountId, + required Widget avatar, + required AccountActiveStatus accountActiveStatus, + }) : super( + accountName: accountName, + accountId: accountId, + avatar: avatar, + accountActiveStatus: accountActiveStatus, + ); + + @override + List get props => [ + personalAccount, + accountName, + accountId, + avatar, + accountActiveStatus, + ]; +} \ No newline at end of file diff --git a/lib/features/mailbox/data/extensions/list_mailbox_extension.dart b/lib/features/mailbox/data/extensions/list_mailbox_extension.dart index 5ddabf3b4c..031af8eeaf 100644 --- a/lib/features/mailbox/data/extensions/list_mailbox_extension.dart +++ b/lib/features/mailbox/data/extensions/list_mailbox_extension.dart @@ -12,7 +12,7 @@ extension ListMailboxExtension on List { Map toMapCache(AccountId accountId, UserName userName) { return { for (var mailbox in this) - TupleKey(mailbox.id!.asString, accountId.asString, userName.value).encodeKey : mailbox.toMailboxCache() + TupleKey(accountId.asString, userName.value, mailbox.id!.asString).encodeKey : mailbox.toMailboxCache() }; } } \ No newline at end of file diff --git a/lib/features/mailbox/data/extensions/list_mailbox_id_extension.dart b/lib/features/mailbox/data/extensions/list_mailbox_id_extension.dart index 4f3e083c12..4164f4a16a 100644 --- a/lib/features/mailbox/data/extensions/list_mailbox_id_extension.dart +++ b/lib/features/mailbox/data/extensions/list_mailbox_id_extension.dart @@ -8,5 +8,5 @@ import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; extension ListMailboxIdExtension on List { List toCacheKeyList(AccountId accountId, UserName userName) => - map((id) => TupleKey(id.asString, accountId.asString, userName.value).encodeKey).toList(); + map((id) => TupleKey(accountId.asString, userName.value, id.asString).encodeKey).toList(); } \ No newline at end of file diff --git a/lib/features/mailbox/data/local/mailbox_cache_manager.dart b/lib/features/mailbox/data/local/mailbox_cache_manager.dart index f41c87e5a2..4af5270903 100644 --- a/lib/features/mailbox/data/local/mailbox_cache_manager.dart +++ b/lib/features/mailbox/data/local/mailbox_cache_manager.dart @@ -1,10 +1,12 @@ +import 'package:collection/collection.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/extensions/account_id_extensions.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/caching/clients/mailbox_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; import 'package:tmail_ui_user/features/mailbox/data/extensions/list_mailbox_cache_extension.dart'; import 'package:tmail_ui_user/features/mailbox/data/extensions/list_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox/data/extensions/list_mailbox_id_extension.dart'; @@ -17,7 +19,8 @@ class MailboxCacheManager { MailboxCacheManager(this._mailboxCacheClient); Future> getAllMailbox(AccountId accountId, UserName userName) async { - final mailboxCacheList = await _mailboxCacheClient.getListByTupleKey(accountId.asString, userName.value); + final nestedKey = TupleKey(accountId.asString, userName.value).encodeKey; + final mailboxCacheList = await _mailboxCacheClient.getListByNestedKey(nestedKey); return mailboxCacheList.toMailboxList(); } @@ -47,14 +50,11 @@ class MailboxCacheManager { } Future getSpamMailbox(AccountId accountId, UserName userName) async { - final mailboxCachedList = await _mailboxCacheClient.getListByTupleKey(accountId.asString, userName.value); - final listSpamMailboxCached = mailboxCachedList - .toMailboxList() - .where((mailbox) => mailbox.role == PresentationMailbox.roleSpam) - .toList(); - - if (listSpamMailboxCached.isNotEmpty) { - return listSpamMailboxCached.first; + final mailboxList = await getAllMailbox(accountId, userName); + final spamMailbox = mailboxList.firstWhereOrNull((mailbox) => mailbox.role == PresentationMailbox.roleSpam); + + if (spamMailbox != null) { + return spamMailbox; } else { throw NotFoundSpamMailboxCachedException(); } diff --git a/lib/features/mailbox/data/local/state_cache_manager.dart b/lib/features/mailbox/data/local/state_cache_manager.dart index 22066f4d58..dfe7f32697 100644 --- a/lib/features/mailbox/data/local/state_cache_manager.dart +++ b/lib/features/mailbox/data/local/state_cache_manager.dart @@ -15,14 +15,14 @@ class StateCacheManager { StateCacheManager(this._stateCacheClient); Future getState(AccountId accountId, UserName userName, StateType stateType) async { - final stateKey = TupleKey(stateType.name, accountId.asString, userName.value).encodeKey; + final stateKey = TupleKey(accountId.asString, userName.value, stateType.name).encodeKey; final stateCache = await _stateCacheClient.getItem(stateKey); return stateCache?.toState(); } Future saveState(AccountId accountId, UserName userName, StateCache stateCache) async { final stateCacheExist = await _stateCacheClient.isExistTable(); - final stateKey = TupleKey(stateCache.type.name, accountId.asString, userName.value).encodeKey; + final stateKey = TupleKey(accountId.asString, userName.value, stateCache.type.name).encodeKey; if (stateCacheExist) { return await _stateCacheClient.updateItem(stateKey, stateCache); } else { diff --git a/lib/features/mailbox/data/model/state_type.dart b/lib/features/mailbox/data/model/state_type.dart index b7786c0d22..a96426a154 100644 --- a/lib/features/mailbox/data/model/state_type.dart +++ b/lib/features/mailbox/data/model/state_type.dart @@ -18,6 +18,6 @@ enum StateType { email; String getTupleKeyStored(AccountId accountId, UserName userName) { - return TupleKey(name, accountId.asString, userName.value).encodeKey; + return TupleKey(accountId.asString, userName.value, name).encodeKey; } } \ No newline at end of file diff --git a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart index 24148e2949..48f50d8d46 100644 --- a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart +++ b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart @@ -52,7 +52,7 @@ class MailboxRepositoryImpl extends MailboxRepository { if (localMailboxResponse.hasData()) { bool hasMoreChanges = true; - State? sinceState = localMailboxResponse.state!; + State? sinceState = localMailboxResponse.state; while(hasMoreChanges && sinceState != null) { final changesResponse = await mapDataSource[DataSourceType.network]!.getChanges(session, accountId, sinceState, properties: properties); @@ -76,6 +76,15 @@ class MailboxRepositoryImpl extends MailboxRepository { stateDataSource.saveState(accountId, session.username, changesResponse.newStateMailbox!.toStateCache(StateType.mailbox)), ]); } + + final newMailboxResponse = await Future.wait([ + mapDataSource[DataSourceType.local]!.getAllMailboxCache(accountId, session.username), + stateDataSource.getState(accountId, session.username, StateType.mailbox) + ]).then((List response) { + return MailboxResponse(mailboxes: response.first, state: response.last); + }); + + yield newMailboxResponse; } else { final mailboxResponse = await mapDataSource[DataSourceType.network]!.getAllMailbox(session, accountId); @@ -84,16 +93,9 @@ class MailboxRepositoryImpl extends MailboxRepository { if (mailboxResponse.state != null) stateDataSource.saveState(accountId, session.username, mailboxResponse.state!.toStateCache(StateType.mailbox)), ]); - } - final newMailboxResponse = await Future.wait([ - mapDataSource[DataSourceType.local]!.getAllMailboxCache(accountId, session.username), - stateDataSource.getState(accountId, session.username, StateType.mailbox) - ]).then((List response) { - return MailboxResponse(mailboxes: response.first, state: response.last); - }); - - yield newMailboxResponse; + yield mailboxResponse; + } } Future?> _combineMailboxCache({ @@ -159,7 +161,11 @@ class MailboxRepositoryImpl extends MailboxRepository { return MailboxResponse(mailboxes: response.first, state: response.last); }); - yield newMailboxResponse; + if (newMailboxResponse.hasData() == true) { + yield newMailboxResponse; + } else { + yield* getAllMailbox(session, accountId); + } } @override diff --git a/lib/features/mailbox/domain/model/mailbox_response.dart b/lib/features/mailbox/domain/model/mailbox_response.dart index 8b07a0e5e2..d113f28a13 100644 --- a/lib/features/mailbox/domain/model/mailbox_response.dart +++ b/lib/features/mailbox/domain/model/mailbox_response.dart @@ -12,11 +12,7 @@ class MailboxResponse with EquatableMixin { this.state }); - bool hasData() { - return mailboxes != null - && mailboxes!.isNotEmpty - && state != null; - } + bool hasData() => mailboxes?.isNotEmpty == true; @override List get props => [mailboxes, state]; diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 42d277d407..bea3a45bf1 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -96,9 +96,12 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/down import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/preview_email_arguments.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/refresh_action_view_event.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/restore_active_account_arguments.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/select_active_account_arguments.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/switch_active_account_arguments.dart'; import 'package:tmail_ui_user/features/mailto/presentation/model/mailto_arguments.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_vacation_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/update_vacation_state.dart'; @@ -484,13 +487,19 @@ class MailboxDashBoardController extends ReloadableController { void _handleArguments() { final arguments = Get.arguments; - log('MailboxDashBoardController::_getSessionCurrent(): arguments = $arguments'); + log('MailboxDashBoardController::_handleArguments(): arguments = $arguments'); if (arguments is Session) { _handleSession(arguments); } else if (arguments is MailtoArguments) { _handleMailtoURL(arguments); } else if (arguments is PreviewEmailArguments) { _handleOpenEmailAction(arguments); + } else if (arguments is SwitchActiveAccountArguments) { + _handleSwitchActiveAccountAction(arguments); + } else if (arguments is SelectActiveAccountArguments) { + _handleSelectActiveAccountAction(arguments); + } else if (arguments is RestoreActiveAccountArguments) { + _handleRestorePreviousActiveAccountAction(arguments); } else { dispatchRoute(DashboardRoutes.thread); reload(); @@ -521,13 +530,7 @@ class MailboxDashBoardController extends ReloadableController { void _handleSession(Session session) { log('MailboxDashBoardController::_handleSession:'); - _setUpComponentsFromSession(session); - - updateAuthenticationAccount( - sessionCurrent!, - accountId.value!, - sessionCurrent!.username - ); + _setUpComponentsFromSession(session, saveSession: true); if (PlatformInfo.isMobile && !_notificationManager.isNotificationClickedOnTerminate) { _handleClickLocalNotificationOnTerminated(); @@ -536,7 +539,7 @@ class MailboxDashBoardController extends ReloadableController { } } - void _setUpComponentsFromSession(Session session) { + void _setUpComponentsFromSession(Session session, {bool saveSession = false}) { sessionCurrent = session; accountId.value = sessionCurrent!.personalAccount.accountId; userProfile.value = UserProfile(sessionCurrent!.username.value); @@ -551,11 +554,14 @@ class MailboxDashBoardController extends ReloadableController { if (PlatformInfo.isMobile) { getAllSendingEmails(); - _storeSessionAction( - sessionCurrent!, - accountId.value!, - sessionCurrent!.username - ); + + if (saveSession) { + _storeSessionAction( + sessionCurrent!, + accountId.value!, + sessionCurrent!.username + ); + } } } @@ -1317,7 +1323,7 @@ class MailboxDashBoardController extends ReloadableController { void handleReloaded(Session session) { log('MailboxDashBoardController::handleReloaded():'); _getRouteParameters(); - _setUpComponentsFromSession(session); + _setUpComponentsFromSession(session, saveSession: true); if (PlatformInfo.isWeb) { _handleComposerCache(); } @@ -2510,7 +2516,7 @@ class MailboxDashBoardController extends ReloadableController { leadingSVGIcon: imagePaths.icRecoverDeletedMessages, leadingSVGIconColor: Colors.white, backgroundColor: AppColor.primaryColor, - textColor: Colors.white, + textColor: Colors.white, ); } } @@ -2522,7 +2528,7 @@ class MailboxDashBoardController extends ReloadableController { if (currentAccountId != null && currentSession != null) { final arguments = EmailRecoveryArguments(currentAccountId, currentSession); - final result = PlatformInfo.isWeb + final result = PlatformInfo.isWeb ? await DialogRouter.pushGeneralDialog( routeName: AppRoutes.emailRecovery, arguments: arguments, @@ -2545,6 +2551,95 @@ class MailboxDashBoardController extends ReloadableController { isRecoveringDeletedMessage.value = true; } + void _loadActiveAccount({ + required PersonalAccount activeAccount, + required Session sessionActiveAccount + }) { + dispatchRoute(DashboardRoutes.waiting); + + setCurrentActiveAccount(activeAccount); + + dynamicUrlInterceptors.changeBaseUrl(activeAccount.apiUrl); + + _setUpComponentsFromSession(sessionActiveAccount); + + dispatchRoute(DashboardRoutes.thread); + } + + void _handleSwitchActiveAccountAction(SwitchActiveAccountArguments arguments) { + log('MailboxDashBoardController::_handleSwitchActiveAccountAction:arguments: $arguments'); + _loadActiveAccount( + activeAccount: arguments.nextAccount, + sessionActiveAccount: arguments.sessionNextAccount + ); + + _showToastMessageSwitchActiveAccountSuccess( + previousAccount: arguments.currentAccount, + currentAccount: arguments.nextAccount, + sessionPreviousAccount: arguments.sessionCurrentAccount, + ); + } + + void _handleSelectActiveAccountAction(SelectActiveAccountArguments arguments) { + log('MailboxDashBoardController::_handleSelectActiveAccountAction:arguments: $arguments'); + _loadActiveAccount( + activeAccount: arguments.activeAccount, + sessionActiveAccount: arguments.session + ); + } + + void _showToastMessageSwitchActiveAccountSuccess({ + required PersonalAccount previousAccount, + required PersonalAccount currentAccount, + required Session sessionPreviousAccount, + }) { + if (currentContext != null && currentOverlayContext != null) { + appToast.showToastMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).toastMessageSuccessWhenSwitchActiveAccount(currentAccount.userName?.value ?? ''), + actionName: AppLocalizations.of(currentContext!).undo, + onActionClick: () => _undoSwitchActiveAccountAction(previousAccount, sessionPreviousAccount), + leadingSVGIcon: imagePaths.icToastSuccessMessage, + leadingSVGIconColor: Colors.white, + backgroundColor: AppColor.toastSuccessBackgroundColor, + textColor: Colors.white, + actionIcon: SvgPicture.asset(imagePaths.icUndo)); + } + } + + void _handleRestorePreviousActiveAccountAction(RestoreActiveAccountArguments arguments) { + log('MailboxDashBoardController::_handleRestorePreviousActiveAccountAction:arguments: $arguments'); + if (arguments.exception != null && + currentContext != null && + currentOverlayContext != null) { + appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).toastMessageFailureWhenSwitchActiveAccount); + } + + _loadActiveAccount( + activeAccount: arguments.currentAccount, + sessionActiveAccount: arguments.session + ); + } + + void _undoSwitchActiveAccountAction( + PersonalAccount previousAccount, + Session sessionPreviousAccount + ) { + setUpInterceptors(previousAccount); + + dispatchRoute(DashboardRoutes.waiting); + + setCurrentActiveAccount(previousAccount); + + dynamicUrlInterceptors.changeBaseUrl(previousAccount.apiUrl); + + _setUpComponentsFromSession(sessionPreviousAccount); + + dispatchRoute(DashboardRoutes.thread); + } + @override void onClose() { _emailReceiveManager.closeEmailReceiveManagerStream(); diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index 0040177e6d..9f1af184a2 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -365,6 +365,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { if (controller.sessionCurrent != null && controller.accountId.value != null) { controller.logout( + context: context, session: controller.sessionCurrent!, accountId: controller.accountId.value! ); diff --git a/lib/features/mailbox_dashboard/presentation/model/restore_active_account_arguments.dart b/lib/features/mailbox_dashboard/presentation/model/restore_active_account_arguments.dart new file mode 100644 index 0000000000..a92539cd1a --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/model/restore_active_account_arguments.dart @@ -0,0 +1,24 @@ + +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:model/account/personal_account.dart'; +import 'package:tmail_ui_user/main/routes/router_arguments.dart'; + +class RestoreActiveAccountArguments extends RouterArguments { + + final PersonalAccount currentAccount; + final Session session; + final dynamic exception; + + RestoreActiveAccountArguments({ + required this.currentAccount, + required this.session, + this.exception, + }); + + @override + List get props => [ + currentAccount, + session, + exception, + ]; +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/model/select_active_account_arguments.dart b/lib/features/mailbox_dashboard/presentation/model/select_active_account_arguments.dart new file mode 100644 index 0000000000..70a02b587e --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/model/select_active_account_arguments.dart @@ -0,0 +1,21 @@ + +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:model/account/personal_account.dart'; +import 'package:tmail_ui_user/main/routes/router_arguments.dart'; + +class SelectActiveAccountArguments extends RouterArguments { + + final Session session; + final PersonalAccount activeAccount; + + SelectActiveAccountArguments({ + required this.session, + required this.activeAccount, + }); + + @override + List get props => [ + session, + activeAccount, + ]; +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/model/switch_active_account_arguments.dart b/lib/features/mailbox_dashboard/presentation/model/switch_active_account_arguments.dart new file mode 100644 index 0000000000..df6452d2d1 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/model/switch_active_account_arguments.dart @@ -0,0 +1,27 @@ + +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:model/account/personal_account.dart'; +import 'package:tmail_ui_user/main/routes/router_arguments.dart'; + +class SwitchActiveAccountArguments extends RouterArguments { + + final Session sessionCurrentAccount; + final Session sessionNextAccount; + final PersonalAccount currentAccount; + final PersonalAccount nextAccount; + + SwitchActiveAccountArguments({ + required this.sessionCurrentAccount, + required this.sessionNextAccount, + required this.currentAccount, + required this.nextAccount, + }); + + @override + List get props => [ + sessionCurrentAccount, + sessionNextAccount, + currentAccount, + nextAccount, + ]; +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/forward/forward_controller.dart b/lib/features/manage_account/presentation/forward/forward_controller.dart index c24b5d9f29..c52d1e0377 100644 --- a/lib/features/manage_account/presentation/forward/forward_controller.dart +++ b/lib/features/manage_account/presentation/forward/forward_controller.dart @@ -8,6 +8,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:forward/forward/tmail_forward.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:model/mailbox/select_mode.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; @@ -16,7 +17,6 @@ import 'package:tmail_ui_user/features/manage_account/domain/model/delete_recipi import 'package:tmail_ui_user/features/manage_account/domain/model/edit_local_copy_in_forwarding_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/add_recipient_in_forwarding_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/delete_recipient_in_forwarding_state.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/edit_local_copy_in_forwarding_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_forward_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/add_recipients_in_forwarding_interactor.dart'; @@ -110,10 +110,11 @@ class ForwardController extends BaseController { } void deleteRecipients(BuildContext context, String emailAddress) { - showConfirmDialogAction(context, + showConfirmDialogAction( + context: context, title: AppLocalizations.of(context).deleteRecipient, - AppLocalizations.of(context).messageConfirmationDialogDeleteRecipientForward(emailAddress), - AppLocalizations.of(context).remove, + message: AppLocalizations.of(context).messageConfirmationDialogDeleteRecipientForward(emailAddress), + actionName: AppLocalizations.of(context).remove, onConfirmAction: () => _handleDeleteRecipientAction({emailAddress}), showAsBottomSheet: true, icon: SvgPicture.asset(imagePaths.icDeleteDialogRecipients, fit: BoxFit.fill), @@ -195,10 +196,11 @@ class ForwardController extends BaseController { } void deleteMultipleRecipients(BuildContext context, Set listEmailAddress) { - showConfirmDialogAction(currentContext!, + showConfirmDialogAction( + context: currentContext!, title: AppLocalizations.of(context).deleteRecipient, - AppLocalizations.of(context).messageConfirmationDialogDeleteAllRecipientForward, - AppLocalizations.of(currentContext!).remove, + message: AppLocalizations.of(context).messageConfirmationDialogDeleteAllRecipientForward, + actionName: AppLocalizations.of(currentContext!).remove, onConfirmAction: () => _handleDeleteRecipientAction(listEmailAddress), showAsBottomSheet: true, icon: SvgPicture.asset(imagePaths.icDeleteDialogRecipients, fit: BoxFit.fill), diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart index 97460b0bb2..d6ffb8f235 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart @@ -154,6 +154,7 @@ class ManageAccountDashBoardView extends GetWidget { if (controller.dashBoardController.sessionCurrent != null && controller.dashBoardController.accountId.value != null) { controller.dashBoardController.logout( + context: context, session: controller.dashBoardController.sessionCurrent!, accountId: controller.dashBoardController.accountId.value! ); diff --git a/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart b/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart index c6e0e88c74..f5cc03d35e 100644 --- a/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart +++ b/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart @@ -3,9 +3,9 @@ import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/widgets/user_information_widget.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings/settings_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings_utils.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/menu/widgets/account_profile_widget.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/widgets/setting_first_level_tile_builder.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/account_menu_item.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -18,16 +18,24 @@ class SettingsFirstLevelView extends GetWidget { final child = SingleChildScrollView( controller: PlatformInfo.isMobile ? null : controller.settingScrollController, child: Column(children: [ - Obx(() => UserInformationWidget( + Obx(() => AccountProfileWidget( + imagePaths: controller.imagePaths, userProfile: controller.manageAccountDashboardController.userProfile.value, padding: SettingsUtils.getPaddingInFirstLevel(context, controller.responsiveUtils), - titlePadding: const EdgeInsetsDirectional.only(start: 16))), - Divider( - color: AppColor.colorDividerHorizontal, - height: 1, - indent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils), - endIndent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils) - ), + onOpenAccountPicker: () async { + await controller.manageAccountDashboardController.showAccountPicker( + context: context, + onSwitchActiveAccountAction: (currentAccount, nextAccount) { + controller.manageAccountDashboardController.switchActiveAccount( + currentAccount: currentAccount, + nextAccount: nextAccount, + sessionCurrentAccount: controller.manageAccountDashboardController.sessionCurrent! + ); + } + ); + }, + )), + const Divider(), SettingFirstLevelTileBuilder( AppLocalizations.of(context).profiles, AccountMenuItem.profiles.getIcon(controller.imagePaths), @@ -37,8 +45,7 @@ class SettingsFirstLevelView extends GetWidget { Divider( color: AppColor.colorDividerHorizontal, height: 1, - indent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils), - endIndent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils) + indent: SettingsUtils.getDividerHorizontalPadding(context, controller.responsiveUtils) ), Obx(() { if (controller.manageAccountDashboardController.isRuleFilterCapabilitySupported) { @@ -52,8 +59,7 @@ class SettingsFirstLevelView extends GetWidget { Divider( color: AppColor.colorDividerHorizontal, height: 1, - indent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils), - endIndent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils) + indent: SettingsUtils.getDividerHorizontalPadding(context, controller.responsiveUtils) ), ]); } else { @@ -72,8 +78,7 @@ class SettingsFirstLevelView extends GetWidget { Divider( color: AppColor.colorDividerHorizontal, height: 1, - indent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils), - endIndent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils) + indent: SettingsUtils.getDividerHorizontalPadding(context, controller.responsiveUtils) ), ]); } else { @@ -92,8 +97,7 @@ class SettingsFirstLevelView extends GetWidget { Divider( color: AppColor.colorDividerHorizontal, height: 1, - indent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils), - endIndent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils) + indent: SettingsUtils.getDividerHorizontalPadding(context, controller.responsiveUtils) ), ]); } else { @@ -110,8 +114,7 @@ class SettingsFirstLevelView extends GetWidget { Divider( color: AppColor.colorDividerHorizontal, height: 1, - indent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils), - endIndent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils) + indent: SettingsUtils.getDividerHorizontalPadding(context, controller.responsiveUtils) ), ]), SettingFirstLevelTileBuilder( @@ -122,8 +125,7 @@ class SettingsFirstLevelView extends GetWidget { Divider( color: AppColor.colorDividerHorizontal, height: 1, - indent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils), - endIndent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils) + indent: SettingsUtils.getDividerHorizontalPadding(context, controller.responsiveUtils) ), SettingFirstLevelTileBuilder( AppLocalizations.of(context).sign_out, @@ -132,6 +134,7 @@ class SettingsFirstLevelView extends GetWidget { if (controller.manageAccountDashboardController.sessionCurrent != null && controller.manageAccountDashboardController.accountId.value != null) { controller.manageAccountDashboardController.logout( + context: context, session: controller.manageAccountDashboardController.sessionCurrent!, accountId: controller.manageAccountDashboardController.accountId.value! ); diff --git a/lib/features/manage_account/presentation/menu/settings/settings_view.dart b/lib/features/manage_account/presentation/menu/settings/settings_view.dart index d6f83f8b1e..0d4c5a64df 100644 --- a/lib/features/manage_account/presentation/menu/settings/settings_view.dart +++ b/lib/features/manage_account/presentation/menu/settings/settings_view.dart @@ -1,8 +1,5 @@ -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/utils/style_utils.dart'; -import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:core/core.dart'; import 'package:core/utils/direction_utils.dart'; -import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; @@ -41,7 +38,7 @@ class SettingsView extends GetWidget { child: Padding( padding: SettingsUtils.getPaddingAppBar(context, controller.responsiveUtils), child: _buildAppbar(context))), - const Divider(color: AppColor.colorDividerComposer, height: 1), + const Divider(), Obx(() { if (controller.manageAccountDashboardController.vacationResponse.value?.vacationResponderIsValid == true) { return VacationNotificationMessageWidget( @@ -98,16 +95,18 @@ class SettingsView extends GetWidget { children: [ Positioned(left: 0,child: _buildCloseSettingButton(context)), Padding( - padding: const EdgeInsets.only(left: 50, right: 50), + padding: const EdgeInsets.symmetric(horizontal: 50), child: Text( AppLocalizations.of(context).settings, maxLines: 1, softWrap: CommonTextStyle.defaultSoftWrap, overflow: CommonTextStyle.defaultTextOverFlow, - style: const TextStyle( + style: Theme.of(context).textTheme.bodyLarge?.copyWith( fontSize: 20, - color: AppColor.colorNameEmail, - fontWeight: FontWeight.w700)) + fontWeight: FontWeight.bold, + color: Colors.black + ) + ) ) ] ); diff --git a/lib/features/manage_account/presentation/menu/settings_utils.dart b/lib/features/manage_account/presentation/menu/settings_utils.dart index 6e76620bcc..daca98adf1 100644 --- a/lib/features/manage_account/presentation/menu/settings_utils.dart +++ b/lib/features/manage_account/presentation/menu/settings_utils.dart @@ -13,11 +13,19 @@ class SettingsUtils { } } + static double getDividerHorizontalPadding(BuildContext context, ResponsiveUtils responsiveUtils) { + if (responsiveUtils.isMobile(context)) { + return 50; + } else { + return 66; + } + } + static EdgeInsets getPaddingInFirstLevel(BuildContext context, ResponsiveUtils responsiveUtils) { if (responsiveUtils.isMobile(context)) { - return const EdgeInsets.only(left: 16, top: 12, bottom: 12, right: 16); + return const EdgeInsets.only(left: 16, top: 16, bottom: 12, right: 16); } else { - return const EdgeInsets.only(left: 32, top: 12, bottom: 12, right: 32); + return const EdgeInsets.only(left: 32, top: 16, bottom: 12, right: 32); } } diff --git a/lib/features/manage_account/presentation/menu/widgets/account_profile_widget.dart b/lib/features/manage_account/presentation/menu/widgets/account_profile_widget.dart new file mode 100644 index 0000000000..c781d627ef --- /dev/null +++ b/lib/features/manage_account/presentation/menu/widgets/account_profile_widget.dart @@ -0,0 +1,75 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/presentation/views/image/avatar_builder.dart'; +import 'package:core/presentation/views/text/text_overflow_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:model/user/user_profile.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class AccountProfileWidget extends StatelessWidget { + final ImagePaths imagePaths; + final UserProfile? userProfile; + final VoidCallback? onOpenAccountPicker; + final EdgeInsetsGeometry? padding; + + const AccountProfileWidget({ + Key? key, + required this.imagePaths, + this.userProfile, + this.onOpenAccountPicker, + this.padding, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: padding ?? const EdgeInsetsDirectional.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AvatarBuilder( + text: userProfile?.getAvatarText() ?? '', + size: 56, + textColor: Colors.black, + bgColor: Colors.white, + boxShadows: const [ + BoxShadow( + color: AppColor.colorShadowBgContentEmail, + spreadRadius: 1, + blurRadius: 1, + offset: Offset(0, 0.5) + ) + ] + ), + Expanded(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(start: 16), + child: TextOverflowBuilder( + userProfile?.email ?? '', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + ) + ), + ), + TMailButtonWidget( + text: AppLocalizations.of(context).switchAccounts, + icon: imagePaths.icSwitchAccount, + onTapActionCallback: onOpenAccountPicker, + mainAxisSize: MainAxisSize.min, + margin: const EdgeInsetsDirectional.only(start: 8), + backgroundColor: Colors.transparent, + textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 16, + color: AppColor.primaryColor + )) + ])) + ] + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/profiles/identities/widgets/delete_identity_dialog_builder.dart b/lib/features/manage_account/presentation/profiles/identities/widgets/delete_identity_dialog_builder.dart index a6e6e9fe34..9c14c48be6 100644 --- a/lib/features/manage_account/presentation/profiles/identities/widgets/delete_identity_dialog_builder.dart +++ b/lib/features/manage_account/presentation/profiles/identities/widgets/delete_identity_dialog_builder.dart @@ -26,7 +26,7 @@ class DeleteIdentityDialogBuilder extends StatelessWidget { responsiveUtils: responsiveUtils, mobile: (_buildDeleteDialog(context) ..alignment(Alignment.bottomCenter) - ..outsideDialogPadding(const EdgeInsets.only(left: 0, right: 0, bottom: PlatformInfo.isWeb ? 42 : 16)) + ..outsideDialogPadding(const EdgeInsets.only(bottom: PlatformInfo.isWeb ? 42 : 16)) ..widthDialog(MediaQuery.of(context).size.width - 16) ..heightDialog(280)) .build(), diff --git a/lib/features/offline_mode/manager/new_email_cache_manager.dart b/lib/features/offline_mode/manager/new_email_cache_manager.dart index 83bf38b535..d169d8bbe4 100644 --- a/lib/features/offline_mode/manager/new_email_cache_manager.dart +++ b/lib/features/offline_mode/manager/new_email_cache_manager.dart @@ -46,7 +46,7 @@ class NewEmailCacheManager { UserName userName, DetailedEmailHiveCache detailedEmailCache ) { - final keyCache = TupleKey(detailedEmailCache.emailId, accountId.asString, userName.value).encodeKey; + final keyCache = TupleKey(accountId.asString, userName.value, detailedEmailCache.emailId).encodeKey; return _cacheClient.insertItem(keyCache, detailedEmailCache); } @@ -55,12 +55,13 @@ class NewEmailCacheManager { UserName userName, String emailId ) { - final keyCache = TupleKey(emailId, accountId.asString, userName.value).encodeKey; + final keyCache = TupleKey(accountId.asString, userName.value, emailId).encodeKey; return _cacheClient.deleteItem(keyCache); } Future> getAllDetailedEmails(AccountId accountId, UserName userName) async { - final detailedEmailCacheList = await _cacheClient.getListByTupleKey(accountId.asString, userName.value); + final nestedKey = TupleKey(accountId.asString, userName.value).encodeKey; + final detailedEmailCacheList = await _cacheClient.getListByNestedKey(nestedKey); detailedEmailCacheList.sortByLatestTime(); return detailedEmailCacheList; } @@ -74,7 +75,7 @@ class NewEmailCacheManager { UserName userName, EmailId emailId ) async { - final keyCache = TupleKey(emailId.asString, accountId.asString, userName.value).encodeKey; + final keyCache = TupleKey(accountId.asString, userName.value, emailId.asString).encodeKey; final detailedEmailCache = await _cacheClient.getItem(keyCache, needToReopen: true); if (detailedEmailCache != null) { return detailedEmailCache; diff --git a/lib/features/offline_mode/manager/opened_email_cache_manager.dart b/lib/features/offline_mode/manager/opened_email_cache_manager.dart index b6f791ab11..439b8fadbd 100644 --- a/lib/features/offline_mode/manager/opened_email_cache_manager.dart +++ b/lib/features/offline_mode/manager/opened_email_cache_manager.dart @@ -24,7 +24,7 @@ class OpenedEmailCacheManager { UserName userName, DetailedEmailHiveCache detailedEmailCache ) { - final keyCache = TupleKey(detailedEmailCache.emailId, accountId.asString, userName.value).encodeKey; + final keyCache = TupleKey(accountId.asString, userName.value, detailedEmailCache.emailId).encodeKey; log('OpenedEmailCacheManager::insertDetailedEmail(): $keyCache'); return _cacheClient.insertItem(keyCache, detailedEmailCache); } @@ -34,13 +34,14 @@ class OpenedEmailCacheManager { UserName userName, String emailId ) { - final keyCache = TupleKey(emailId, accountId.asString, userName.value).encodeKey; + final keyCache = TupleKey(accountId.asString, userName.value, emailId).encodeKey; log('OpenedEmailCacheManager::removeDetailedEmail(): $keyCache'); return _cacheClient.deleteItem(keyCache); } Future> getAllDetailedEmails(AccountId accountId, UserName userName) async { - final detailedEmailCacheList = await _cacheClient.getListByTupleKey(accountId.asString, userName.value); + final nestedKey = TupleKey(accountId.asString, userName.value).encodeKey; + final detailedEmailCacheList = await _cacheClient.getListByNestedKey(nestedKey); detailedEmailCacheList.sortByLatestTime(); log('OpenedEmailCacheManager::getAllDetailedEmails():SIZE: ${detailedEmailCacheList.length}'); return detailedEmailCacheList; @@ -72,7 +73,7 @@ class OpenedEmailCacheManager { UserName userName, EmailId emailId ) async { - final keyCache = TupleKey(emailId.asString, accountId.asString, userName.value).encodeKey; + final keyCache = TupleKey(accountId.asString, userName.value, emailId.asString).encodeKey; final detailedEmailCache = await _cacheClient.getItem(keyCache, needToReopen: true); if (detailedEmailCache != null) { return detailedEmailCache; diff --git a/lib/features/offline_mode/manager/sending_email_cache_manager.dart b/lib/features/offline_mode/manager/sending_email_cache_manager.dart index 34c6fecb51..5bee6c6cc6 100644 --- a/lib/features/offline_mode/manager/sending_email_cache_manager.dart +++ b/lib/features/offline_mode/manager/sending_email_cache_manager.dart @@ -1,6 +1,7 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/account/personal_account.dart'; import 'package:model/extensions/account_id_extensions.dart'; import 'package:tmail_ui_user/features/caching/clients/sending_email_hive_cache_client.dart'; import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; @@ -19,7 +20,7 @@ class SendingEmailCacheManager { UserName userName, SendingEmailHiveCache sendingEmailHiveCache ) async { - final keyCache = TupleKey(sendingEmailHiveCache.sendingId, accountId.asString, userName.value).encodeKey; + final keyCache = TupleKey(accountId.asString, userName.value, sendingEmailHiveCache.sendingId).encodeKey; await _hiveCacheClient.insertItem(keyCache, sendingEmailHiveCache); final newSendingEmailHiveCache = await _hiveCacheClient.getItem(keyCache); if (newSendingEmailHiveCache != null) { @@ -29,14 +30,15 @@ class SendingEmailCacheManager { } } - Future> getAllSendingEmailsByTupleKey(AccountId accountId, UserName userName) async { - final sendingEmailsCache = await _hiveCacheClient.getListByTupleKey(accountId.asString, userName.value); + Future> getAllSendingEmailsByAccount(AccountId accountId, UserName userName) async { + final nestedKey = TupleKey(accountId.asString, userName.value).encodeKey; + final sendingEmailsCache = await _hiveCacheClient.getListByNestedKey(nestedKey); sendingEmailsCache.sortByLatestTime(); return sendingEmailsCache; } Future deleteSendingEmail(AccountId accountId, UserName userName, String sendingId) async { - final keyCache = TupleKey(sendingId, accountId.asString, userName.value).encodeKey; + final keyCache = TupleKey(accountId.asString, userName.value, sendingId).encodeKey; await _hiveCacheClient.deleteItem(keyCache); final storedSendingEmail = await _hiveCacheClient.getItem(keyCache); if (storedSendingEmail != null) { @@ -52,12 +54,21 @@ class SendingEmailCacheManager { Future clearAllSendingEmails() => _hiveCacheClient.clearAllData(); + Future clearAllSendingEmailsByAccount(PersonalAccount currentAccount) async { + final nestedKey = TupleKey( + currentAccount.accountId!.asString, + currentAccount.userName!.value + ).encodeKey; + + await _hiveCacheClient.clearAllDataContainKey(nestedKey); + } + Future updateSendingEmail( AccountId accountId, UserName userName, SendingEmailHiveCache sendingEmailHiveCache ) async { - final keyCache = TupleKey(sendingEmailHiveCache.sendingId, accountId.asString, userName.value).encodeKey; + final keyCache = TupleKey(accountId.asString, userName.value, sendingEmailHiveCache.sendingId).encodeKey; await _hiveCacheClient.updateItem(keyCache, sendingEmailHiveCache); final newSendingEmailHiveCache = await _hiveCacheClient.getItem(keyCache); if (newSendingEmailHiveCache != null) { @@ -74,7 +85,7 @@ class SendingEmailCacheManager { ) async { final mapSendingEmailCache = { for (var sendingEmailCache in listSendingEmailHiveCache) - TupleKey(sendingEmailCache.sendingId, accountId.asString, userName.value).encodeKey: sendingEmailCache + TupleKey(accountId.asString, userName.value, sendingEmailCache.sendingId).encodeKey: sendingEmailCache }; await _hiveCacheClient.updateMultipleItem(mapSendingEmailCache); final newListSendingEmailCache = await _hiveCacheClient.getValuesByListKey(mapSendingEmailCache.keys.toList()); @@ -82,7 +93,7 @@ class SendingEmailCacheManager { } Future deleteMultipleSendingEmail(AccountId accountId, UserName userName, List sendingIds) async { - final listTupleKey = sendingIds.map((sendingId) => TupleKey(sendingId, accountId.asString, userName.value).encodeKey).toList(); + final listTupleKey = sendingIds.map((sendingId) => TupleKey(accountId.asString, userName.value, sendingId).encodeKey).toList(); await _hiveCacheClient.deleteMultipleItem(listTupleKey); final newListSendingEmailCache = await _hiveCacheClient.getValuesByListKey(listTupleKey); if (newListSendingEmailCache.isNotEmpty) { @@ -91,7 +102,7 @@ class SendingEmailCacheManager { } Future getStoredSendingEmail(AccountId accountId, UserName userName, String sendingId) async { - final keyCache = TupleKey(sendingId, accountId.asString, userName.value).encodeKey; + final keyCache = TupleKey(accountId.asString, userName.value, sendingId).encodeKey; final storedSendingEmail = await _hiveCacheClient.getItem(keyCache); if (storedSendingEmail != null) { return storedSendingEmail; diff --git a/lib/features/push_notification/data/local/fcm_cache_manager.dart b/lib/features/push_notification/data/local/fcm_cache_manager.dart index 7656b94cae..797a78b724 100644 --- a/lib/features/push_notification/data/local/fcm_cache_manager.dart +++ b/lib/features/push_notification/data/local/fcm_cache_manager.dart @@ -16,12 +16,12 @@ class FCMCacheManager { FCMCacheManager(this._fcmCacheClient,this._firebaseRegistrationCacheClient); Future storeStateToRefresh(AccountId accountId, UserName userName, TypeName typeName, jmap.State newState) { - final stateKeyCache = TupleKey(typeName.value, accountId.asString, userName.value).encodeKey; + final stateKeyCache = TupleKey(accountId.asString, userName.value, typeName.value).encodeKey; return _fcmCacheClient.insertItem(stateKeyCache, newState.value); } Future getStateToRefresh(AccountId accountId, UserName userName, TypeName typeName) async { - final stateKeyCache = TupleKey(typeName.value, accountId.asString, userName.value).encodeKey; + final stateKeyCache = TupleKey(accountId.asString, userName.value, typeName.value).encodeKey; final stateValue = await _fcmCacheClient.getItem(stateKeyCache); if (stateValue != null) { return jmap.State(stateValue); @@ -35,7 +35,7 @@ class FCMCacheManager { } Future deleteStateToRefresh(AccountId accountId, UserName userName, TypeName typeName) { - final stateKeyCache = TupleKey(typeName.value, accountId.asString, userName.value).encodeKey; + final stateKeyCache = TupleKey(accountId.asString, userName.value, typeName.value).encodeKey; return _fcmCacheClient.deleteItem(stateKeyCache); } diff --git a/lib/features/push_notification/domain/state/destroy_firebase_registration_state.dart b/lib/features/push_notification/domain/state/destroy_firebase_registration_state.dart index d661ea4c80..c9ea64407d 100644 --- a/lib/features/push_notification/domain/state/destroy_firebase_registration_state.dart +++ b/lib/features/push_notification/domain/state/destroy_firebase_registration_state.dart @@ -1,11 +1,24 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:model/account/personal_account.dart'; class DestroyFirebaseRegistrationLoading extends LoadingState {} -class DestroyFirebaseRegistrationSuccess extends UIState {} +class DestroyFirebaseRegistrationSuccess extends UIState { + + final PersonalAccount currentAccount; + + DestroyFirebaseRegistrationSuccess(this.currentAccount); + + @override + List get props => [currentAccount]; +} class DestroyFirebaseRegistrationFailure extends FeatureFailure { + final PersonalAccount currentAccount; + + DestroyFirebaseRegistrationFailure({dynamic exception, required this.currentAccount}) : super(exception: exception); - DestroyFirebaseRegistrationFailure(dynamic exception) : super(exception: exception); + @override + List get props => [...super.props, currentAccount]; } \ No newline at end of file diff --git a/lib/features/push_notification/domain/usecases/remove_firebase_registration_interactor.dart b/lib/features/push_notification/domain/usecases/remove_firebase_registration_interactor.dart index 817ca1c43e..1c02f38cf5 100644 --- a/lib/features/push_notification/domain/usecases/remove_firebase_registration_interactor.dart +++ b/lib/features/push_notification/domain/usecases/remove_firebase_registration_interactor.dart @@ -1,8 +1,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; -import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/account/personal_account.dart'; import 'package:tmail_ui_user/features/push_notification/domain/repository/fcm_repository.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/destroy_firebase_registration_state.dart'; @@ -12,17 +11,25 @@ class RemoveFirebaseRegistrationInteractor { RemoveFirebaseRegistrationInteractor(this._fcmRepository); - Stream> execute(AccountId accountId, UserName userName) async* { + Stream> execute(PersonalAccount currentAccount) async* { try { yield Right(DestroyFirebaseRegistrationLoading()); - final registration = await _fcmRepository.getStoredFirebaseRegistration(accountId, userName); + final registration = await _fcmRepository.getStoredFirebaseRegistration( + currentAccount.accountId!, + currentAccount.userName!); + await Future.wait([ _fcmRepository.destroyFirebaseRegistration(registration.id!), - _fcmRepository.deleteFirebaseRegistrationCache(accountId, userName), + _fcmRepository.deleteFirebaseRegistrationCache( + currentAccount.accountId!, + currentAccount.userName!), ], eagerError: true); - yield Right(DestroyFirebaseRegistrationSuccess()); + + yield Right(DestroyFirebaseRegistrationSuccess(currentAccount)); } catch (e) { - yield Left(DestroyFirebaseRegistrationFailure(e)); + yield Left(DestroyFirebaseRegistrationFailure( + exception: e, + currentAccount: currentAccount)); } } } \ No newline at end of file diff --git a/lib/features/sending_queue/presentation/sending_queue_controller.dart b/lib/features/sending_queue/presentation/sending_queue_controller.dart index 6eed34be2b..6be065d9d5 100644 --- a/lib/features/sending_queue/presentation/sending_queue_controller.dart +++ b/lib/features/sending_queue/presentation/sending_queue_controller.dart @@ -202,9 +202,9 @@ class SendingQueueController extends BaseController with MessageDialogActionMixi void _deleteListSendingEmailAction(BuildContext context, List listSendingEmails) { showConfirmDialogAction( - context, - AppLocalizations.of(context).messageDialogDeleteSendingEmail, - AppLocalizations.of(currentContext!).delete, + context: context, + message: AppLocalizations.of(context).messageDialogDeleteSendingEmail, + actionName: AppLocalizations.of(currentContext!).delete, title: AppLocalizations.of(currentContext!).deleteOfflineEmail, icon: SvgPicture.asset(imagePaths.icDeleteDialogRecipients), alignCenter: true, diff --git a/lib/features/starting_page/presentation/twake_id/twake_id_controller.dart b/lib/features/starting_page/presentation/twake_id/twake_id_controller.dart index 838805be5d..7c05d6bc30 100644 --- a/lib/features/starting_page/presentation/twake_id/twake_id_controller.dart +++ b/lib/features/starting_page/presentation/twake_id/twake_id_controller.dart @@ -1,22 +1,48 @@ +import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/theme_utils.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/model/login_arguments.dart'; +import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_arguments.dart'; +import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_type.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; class TwakeIdController extends GetxController { + final ImagePaths imagePaths = Get.find(); + + final navigateArguments = Rxn(); + @override void onInit() { ThemeUtils.setPreferredPortraitOrientations(); super.onInit(); } + @override + void onReady() { + super.onReady(); + navigateArguments.value = Get.arguments; + } + void handleUseCompanyServer() { - popAndPush( - AppRoutes.login, - arguments: LoginArguments(LoginFormType.dnsLookupForm)); + if (isAddAnotherAccount) { + push( + AppRoutes.login, + arguments: navigateArguments.value); + } else { + popAndPush( + AppRoutes.login, + arguments: LoginArguments(LoginFormType.dnsLookupForm)); + } + } + + void backToHomeView() { + popAndPush(AppRoutes.home); } + + bool get isAddAnotherAccount => navigateArguments.value != null && + navigateArguments.value?.navigateType == LoginNavigateType.addAnotherAccount; } \ No newline at end of file diff --git a/lib/features/starting_page/presentation/twake_id/twake_id_view.dart b/lib/features/starting_page/presentation/twake_id/twake_id_view.dart index cd8fbdddc0..80d675b60d 100644 --- a/lib/features/starting_page/presentation/twake_id/twake_id_view.dart +++ b/lib/features/starting_page/presentation/twake_id/twake_id_view.dart @@ -1,3 +1,4 @@ +import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; @@ -10,16 +11,26 @@ class TwakeIdView extends GetWidget { @override Widget build(BuildContext context) { - return TwakeIdScreen( - focusColor: Colors.transparent, - hoverColor: Colors.transparent, - highlightColor: Colors.transparent, - overlayColor: MaterialStateProperty.all(Colors.transparent), - signInTitle: AppLocalizations.of(context).signIn.capitalizeFirst ?? '', - createTwakeIdTitle: AppLocalizations.of(context).createTwakeId, - useCompanyServerTitle: AppLocalizations.of(context).useCompanyServer, - description: AppLocalizations.of(context).descriptionTwakeId, - onUseCompanyServerOnTap: controller.handleUseCompanyServer, - ); + return Obx(() { + return TwakeIdScreen( + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + highlightColor: Colors.transparent, + overlayColor: MaterialStateProperty.all(Colors.transparent), + signInTitle: AppLocalizations.of(context).signIn.capitalizeFirst ?? '', + createTwakeIdTitle: AppLocalizations.of(context).createTwakeId, + useCompanyServerTitle: AppLocalizations.of(context).useCompanyServer, + description: AppLocalizations.of(context).descriptionTwakeId, + onUseCompanyServerOnTap: controller.handleUseCompanyServer, + backButton: controller.isAddAnotherAccount + ? TMailButtonWidget.fromIcon( + icon: controller.imagePaths.icArrowLeft, + backgroundColor: Colors.transparent, + margin: const EdgeInsetsDirectional.only(start: 8), + iconColor: Colors.black, + onTapActionCallback: controller.backToHomeView) + : null, + ); + }); } } \ No newline at end of file diff --git a/lib/features/thread/data/extensions/list_email_extension.dart b/lib/features/thread/data/extensions/list_email_extension.dart index 9640dffee6..d5d05ce812 100644 --- a/lib/features/thread/data/extensions/list_email_extension.dart +++ b/lib/features/thread/data/extensions/list_email_extension.dart @@ -12,7 +12,7 @@ extension ListEmailExtension on List { Map toMapCache(AccountId accountId, UserName userName) { return { for (var email in this) - TupleKey(email.id!.asString, accountId.asString, userName.value).encodeKey : email.toEmailCache() + TupleKey(accountId.asString, userName.value, email.id!.asString).encodeKey : email.toEmailCache() }; } } \ No newline at end of file diff --git a/lib/features/thread/data/extensions/list_email_id_extension.dart b/lib/features/thread/data/extensions/list_email_id_extension.dart index f164824263..2bb6445a2e 100644 --- a/lib/features/thread/data/extensions/list_email_id_extension.dart +++ b/lib/features/thread/data/extensions/list_email_id_extension.dart @@ -8,5 +8,5 @@ import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; extension ListEmailIdExtension on List { List toCacheKeyList(AccountId accountId, UserName userName) => - map((id) => TupleKey(id.asString, accountId.asString, userName.value).encodeKey).toList(); + map((id) => TupleKey(accountId.asString, userName.value, id.asString).encodeKey).toList(); } \ No newline at end of file diff --git a/lib/features/thread/data/local/email_cache_manager.dart b/lib/features/thread/data/local/email_cache_manager.dart index 0e37fe9d51..b4fce7c020 100644 --- a/lib/features/thread/data/local/email_cache_manager.dart +++ b/lib/features/thread/data/local/email_cache_manager.dart @@ -1,9 +1,10 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; -import 'package:model/model.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/model.dart'; import 'package:tmail_ui_user/features/caching/clients/email_cache_client.dart'; import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; import 'package:tmail_ui_user/features/cleanup/domain/model/email_cleanup_rule.dart'; @@ -13,7 +14,6 @@ import 'package:tmail_ui_user/features/thread/data/extensions/email_extension.da import 'package:tmail_ui_user/features/thread/data/extensions/list_email_cache_extension.dart'; import 'package:tmail_ui_user/features/thread/data/extensions/list_email_extension.dart'; import 'package:tmail_ui_user/features/thread/data/extensions/list_email_id_extension.dart'; -import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:tmail_ui_user/features/thread/data/model/email_cache.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; @@ -31,7 +31,8 @@ class EmailCacheManager { UnsignedInt? limit, FilterMessageOption filterOption = FilterMessageOption.all }) async { - final emailCacheList = await _emailCacheClient.getListByTupleKey(accountId.asString, userName.value); + final nestedKey = TupleKey(accountId.asString, userName.value).encodeKey; + final emailCacheList = await _emailCacheClient.getListByNestedKey(nestedKey); final emailList = emailCacheList .toEmailList() .where((email) => _filterEmailByMailbox(email, filterOption, inMailboxId)) @@ -94,12 +95,12 @@ class EmailCacheManager { } Future storeEmail(AccountId accountId, UserName userName, EmailCache emailCache) { - final keyCache = TupleKey(emailCache.id, accountId.asString, userName.value).encodeKey; + final keyCache = TupleKey(accountId.asString, userName.value, emailCache.id).encodeKey; return _emailCacheClient.insertItem(keyCache, emailCache); } Future getStoredEmail(AccountId accountId, UserName userName, EmailId emailId) async { - final keyCache = TupleKey(emailId.asString, accountId.asString, userName.value).encodeKey; + final keyCache = TupleKey(accountId.asString, userName.value, emailId.asString).encodeKey; final emailCache = await _emailCacheClient.getItem(keyCache, needToReopen: true); if (emailCache != null) { return emailCache; diff --git a/lib/features/thread/data/repository/thread_repository_impl.dart b/lib/features/thread/data/repository/thread_repository_impl.dart index a3721f83e4..0b58b2ae73 100644 --- a/lib/features/thread/data/repository/thread_repository_impl.dart +++ b/lib/features/thread/data/repository/thread_repository_impl.dart @@ -87,8 +87,8 @@ class ThreadRepositoryImpl extends ThreadRepository { yield localEmailResponse; } - if (networkEmailResponse != null) { - await _updateEmailCache(accountId, session.username, newCreated: networkEmailResponse.emailList); + if (networkEmailResponse?.hasEmails() == true) { + await _updateEmailCache(accountId, session.username, newCreated: networkEmailResponse!.emailList); } if (localEmailResponse.hasState()) { @@ -101,11 +101,9 @@ class ThreadRepositoryImpl extends ThreadRepository { propertiesUpdated: propertiesUpdated ); } else { - if (networkEmailResponse != null) { - log('ThreadRepositoryImpl::getAllEmail(): filter = ${emailFilter?.mailboxId} no local state -> update from network: ${networkEmailResponse.state}'); - if (networkEmailResponse.state != null) { - await _updateState(accountId, session.username, networkEmailResponse.state!); - } + if (networkEmailResponse?.hasState() == true) { + log('ThreadRepositoryImpl::getAllEmail(): filter = ${emailFilter?.mailboxId} no local state -> update from network: ${networkEmailResponse?.state}'); + await _updateState(accountId, session.username, networkEmailResponse!.state!); } } @@ -122,7 +120,11 @@ class ThreadRepositoryImpl extends ThreadRepository { return EmailsResponse(emailList: response.first, state: response.last); }); - yield newEmailResponse; + if (newEmailResponse.hasEmails() == true) { + yield newEmailResponse; + } else { + yield networkEmailResponse ?? newEmailResponse; + } } bool _isApproveFilterOption(FilterMessageOption? filterOption, List? listEmailResponse) { diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 6e4274cd72..634d88b214 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -83,7 +83,7 @@ typedef EndRangeSelection = int; class ThreadController extends BaseController with EmailActionController { - final networkConnectionController = Get.find(); + final NetworkConnectionController networkConnectionController = Get.find(); final GetEmailsInMailboxInteractor _getEmailsInMailboxInteractor; final RefreshChangesEmailsInMailboxInteractor _refreshChangesEmailsInMailboxInteractor; @@ -367,7 +367,7 @@ class ThreadController extends BaseController with EmailActionController { logError('ThreadController::_handleErrorGetAllOrRefreshChangesEmail():Error: $error'); if (error is CannotCalculateChangesMethodResponseException) { if (_accountId != null && _session != null) { - await cachingManager.clearEmailCacheAndStateCacheByTupleKey(_accountId!, _session!); + await cachingManager.clearEmailCacheAndStateCacheByTupleKey(_accountId!, _session!.username); } else { await cachingManager.clearEmailCacheAndAllStateCache(); } diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index 9db6e2a333..19be0fcfbf 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -101,7 +101,20 @@ class ThreadView extends GetWidget (option) => controller.filterMessagesAction(context, option) ) ) - : null + : null, + onOpenAccountPickerAction: () async { + await controller.mailboxDashBoardController.showAccountPicker( + context: context, + goToSettingAction: controller.mailboxDashBoardController.goToSettings, + onSwitchActiveAccountAction: (currentAccount, nextAccount) { + controller.mailboxDashBoardController.switchActiveAccount( + currentAccount: currentAccount, + nextAccount: nextAccount, + sessionCurrentAccount: controller.mailboxDashBoardController.sessionCurrent! + ); + } + ); + }, ); }), if (PlatformInfo.isMobile) diff --git a/lib/features/thread/presentation/widgets/app_bar/app_bar_thread_widget.dart b/lib/features/thread/presentation/widgets/app_bar/app_bar_thread_widget.dart index f2d518ef97..7bf080e58c 100644 --- a/lib/features/thread/presentation/widgets/app_bar/app_bar_thread_widget.dart +++ b/lib/features/thread/presentation/widgets/app_bar/app_bar_thread_widget.dart @@ -16,6 +16,7 @@ typedef OnEditThreadAction = void Function(); typedef OnOpenMailboxMenuActionClick = void Function(); typedef OnCancelEditThreadAction = void Function(); typedef OnEmailSelectionAction = void Function(EmailActionType, List); +typedef OnOpenAccountPickerAction = void Function(); class AppBarThreadWidget extends StatelessWidget { final OnPopupMenuFilterEmailAction? onPopupMenuFilterEmailAction; @@ -29,6 +30,7 @@ class AppBarThreadWidget extends StatelessWidget { final SelectMode selectMode; final FilterMessageOption filterOption; final UserProfile? userProfile; + final OnOpenAccountPickerAction? onOpenAccountPickerAction; const AppBarThreadWidget({ Key? key, @@ -43,6 +45,7 @@ class AppBarThreadWidget extends StatelessWidget { required this.userProfile, this.onPopupMenuFilterEmailAction, this.onContextMenuFilterEmailAction, + this.onOpenAccountPickerAction, }) : super(key: key); @override @@ -70,8 +73,8 @@ class AppBarThreadWidget extends StatelessWidget { selectMode: selectMode, filterOption: filterOption, openMailboxAction: openMailboxAction, - editThreadAction: editThreadAction, cancelEditThreadAction: cancelEditThreadAction, + onOpenAccountPickerAction: onOpenAccountPickerAction, ); } } diff --git a/lib/features/thread/presentation/widgets/app_bar/mobile_app_bar_thread_widget.dart b/lib/features/thread/presentation/widgets/app_bar/mobile_app_bar_thread_widget.dart index 1399bdd3f4..b35226d537 100644 --- a/lib/features/thread/presentation/widgets/app_bar/mobile_app_bar_thread_widget.dart +++ b/lib/features/thread/presentation/widgets/app_bar/mobile_app_bar_thread_widget.dart @@ -27,8 +27,8 @@ class MobileAppBarThreadWidget extends StatelessWidget { final SelectMode selectMode; final FilterMessageOption filterOption; final OnOpenMailboxMenuActionClick openMailboxAction; - final OnEditThreadAction editThreadAction; final OnCancelEditThreadAction cancelEditThreadAction; + final OnOpenAccountPickerAction? onOpenAccountPickerAction; MobileAppBarThreadWidget({ super.key, @@ -38,8 +38,8 @@ class MobileAppBarThreadWidget extends StatelessWidget { required this.selectMode, required this.filterOption, required this.openMailboxAction, - required this.editThreadAction, required this.cancelEditThreadAction, + required this.onOpenAccountPickerAction, }); @override @@ -97,7 +97,8 @@ class MobileAppBarThreadWidget extends StatelessWidget { blurRadius: 1, offset: Offset(0, 0.5) ) - ] + ], + onTapAction: onOpenAccountPickerAction, ), ], ); diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 95ff6fc154..d77e50ea4a 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-01-07T14:01:28.226469", + "@@last_modified": "2024-01-16T15:38:46.103746", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3665,5 +3665,67 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "addAnotherAccount": "Add another account", + "@addAnotherAccount": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageFailureWhenSwitchActiveAccount": "Switch active account failure", + "@toastMessageFailureWhenSwitchActiveAccount": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "unableToLogInToAccount": "Unable to log in to account {userName}", + "@unableToLogInToAccount": { + "type": "text", + "placeholders_order": [ + "userName" + ], + "placeholders": { + "userName": {} + } + }, + "toastMessageSuccessWhenSwitchActiveAccount": "You’ve been switched to your {userName} account.", + "@toastMessageSuccessWhenSwitchActiveAccount": { + "type": "text", + "placeholders_order": [ + "userName" + ], + "placeholders": { + "userName": {} + } + }, + "switchAccounts": "Switch Accounts", + "@switchAccounts": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "accountSettings": "Account settings", + "@accountSettings": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "logoutConfirmation": "Logout Confirmation", + "@logoutConfirmation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "yesLogout": "Yes, Log out", + "@yesLogout": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageConfirmationLogout": "Do you want to log out of", + "@messageConfirmationLogout": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/bindings/credential/credential_bindings.dart b/lib/main/bindings/credential/credential_bindings.dart index de04145d42..3ca7dade5c 100644 --- a/lib/main/bindings/credential/credential_bindings.dart +++ b/lib/main/bindings/credential/credential_bindings.dart @@ -15,12 +15,14 @@ import 'package:tmail_ui_user/features/login/data/repository/authentication_repo import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/authentication_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/add_account_id_to_active_account_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/authentication_user_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_all_authenticated_account_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/logout_current_account_basic_auth_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/logout_current_account_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/logout_current_account_oidc_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/set_current_active_account_interactor.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart'; @@ -39,11 +41,13 @@ class CredentialBindings extends InteractorsBindings { Get.find(), )); Get.put(GetAuthenticatedAccountInteractor(Get.find())); + Get.put(GetAllAuthenticatedAccountInteractor(Get.find())); Get.put(AuthenticationInteractor( Get.find(), Get.find() )); - Get.put(UpdateAuthenticationAccountInteractor(Get.find())); + Get.put(AddAccountIdToActiveAccountInteractor(Get.find())); + Get.put(SetCurrentActiveAccountInteractor(Get.find())); } @override diff --git a/lib/main/bindings/main_bindings.dart b/lib/main/bindings/main_bindings.dart index 358a6df72f..eb7640d178 100644 --- a/lib/main/bindings/main_bindings.dart +++ b/lib/main/bindings/main_bindings.dart @@ -3,6 +3,7 @@ import 'package:tmail_ui_user/main/bindings/core/core_bindings.dart'; import 'package:tmail_ui_user/main/bindings/credential/credential_bindings.dart'; import 'package:tmail_ui_user/main/bindings/local/local_bindings.dart'; import 'package:tmail_ui_user/main/bindings/local/local_isolate_bindings.dart'; +import 'package:tmail_ui_user/main/bindings/manager/manager_bindings.dart'; import 'package:tmail_ui_user/main/bindings/network/network_bindings.dart'; import 'package:tmail_ui_user/main/bindings/network/network_isolate_binding.dart'; import 'package:tmail_ui_user/main/bindings/network_connection/network_connection_bindings.dart'; @@ -19,5 +20,6 @@ class MainBindings extends Bindings { CredentialBindings().dependencies(); SessionBindings().dependencies(); NetWorkConnectionBindings().dependencies(); + ManagerBindings().dependencies(); } } \ No newline at end of file diff --git a/lib/main/bindings/manager/manager_bindings.dart b/lib/main/bindings/manager/manager_bindings.dart new file mode 100644 index 0000000000..a9f4618111 --- /dev/null +++ b/lib/main/bindings/manager/manager_bindings.dart @@ -0,0 +1,16 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_all_authenticated_account_interactor.dart'; +import 'package:tmail_ui_user/main/utils/authenticated_account_manager.dart'; + +class ManagerBindings extends Bindings { + + @override + void dependencies() { + Get.put(AuthenticatedAccountManager( + Get.find(), + Get.find(), + )); + } +} \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 271d6f05c4..9bb13c704d 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3821,4 +3821,65 @@ class AppLocalizations { name: 'descriptionTwakeId', ); } + + String get addAnotherAccount { + return Intl.message( + 'Add another account', + name: 'addAnotherAccount', + ); + } + + String get toastMessageFailureWhenSwitchActiveAccount { + return Intl.message( + 'Switch active account failure', + name: 'toastMessageFailureWhenSwitchActiveAccount', + ); + } + + String unableToLogInToAccount(String userName) { + return Intl.message( + 'Unable to log in to account $userName', + name: 'unableToLogInToAccount', + args: [userName] + ); + } + + String toastMessageSuccessWhenSwitchActiveAccount(String userName) { + return Intl.message( + 'You’ve been switched to your $userName account.', + name: 'toastMessageSuccessWhenSwitchActiveAccount', + args: [userName] + ); + } + + String get switchAccounts { + return Intl.message( + 'Switch Accounts', + name: 'switchAccounts', + ); + } + + String get accountSettings { + return Intl.message( + 'Account settings', + name: 'accountSettings'); + } + + String get logoutConfirmation { + return Intl.message( + 'Logout Confirmation', + name: 'logoutConfirmation'); + } + + String get yesLogout { + return Intl.message( + 'Yes, Log out', + name: 'yesLogout'); + } + + String get messageConfirmationLogout { + return Intl.message( + 'Do you want to log out of', + name: 'messageConfirmationLogout'); + } } \ No newline at end of file diff --git a/lib/main/utils/authenticated_account_manager.dart b/lib/main/utils/authenticated_account_manager.dart new file mode 100644 index 0000000000..e8eba78d73 --- /dev/null +++ b/lib/main/utils/authenticated_account_manager.dart @@ -0,0 +1,158 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/extensions/string_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/presentation/views/image/avatar_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; +import 'package:linagora_design_flutter/multiple_account/models/twake_presentation_account.dart'; +import 'package:linagora_design_flutter/multiple_account/multiple_account_picker.dart'; +import 'package:model/account/personal_account.dart'; +import 'package:tmail_ui_user/features/login/domain/state/get_all_authenticated_account_state.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_all_authenticated_account_interactor.dart'; +import 'package:tmail_ui_user/features/login/presentation/model/twake_mail_presentation_account.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +typedef OnAddAnotherAccountAction = Function(PersonalAccount? currentAccount); +typedef OnSwitchActiveAccountAction = Function( + PersonalAccount currentActiveAccount, + PersonalAccount nextActiveAccount); +typedef OnSelectActiveAccountAction = Function(PersonalAccount activeAccount); + +class AuthenticatedAccountManager { + + final GetAllAuthenticatedAccountInteractor _getAllAuthenticatedAccountInteractor; + final ImagePaths _imagePaths; + + AuthenticatedAccountManager( + this._getAllAuthenticatedAccountInteractor, + this._imagePaths, + ); + + Future> getAllPersonalAccount() { + return _getAllAuthenticatedAccountInteractor + .execute() + .then((result) => result.fold( + (failure) => [], + (success) => success is GetAllAuthenticatedAccountSuccess + ? success.listAccount + : [] + ) + ); + } + + Future> _getAllTwakeMailPresentationAccount() { + return _getAllAuthenticatedAccountInteractor + .execute() + .then((result) => result.fold( + (failure) => [], + (success) => success is GetAllAuthenticatedAccountSuccess + ? _generateTwakePresentationAccount(success.listAccount) + : [] + ) + ); + } + + List _generateTwakePresentationAccount(List listAccount) { + final listPresentationAccount = listAccount + .map((account) => TwakeMailPresentationAccount( + personalAccount: account, + accountId: account.userName?.value ?? '', + accountName: account.userName?.value ?? '', + accountActiveStatus: account.isSelected + ? AccountActiveStatus.active + : AccountActiveStatus.inactive, + avatar: AvatarBuilder( + text: account.userName?.value.isNotEmpty == true + ? account.userName!.value.firstLetterToUpperCase + : '', + size: 56, + textColor: Colors.black, + bgColor: Colors.white, + boxShadows: const [ + BoxShadow( + color: AppColor.colorShadowBgContentEmail, + spreadRadius: 1, + blurRadius: 1, + offset: Offset(0, 0.5) + ) + ] + ) + )) + .toList(); + + listPresentationAccount.sort((pre, next) => pre.accountActiveStatus.index.compareTo(next.accountActiveStatus.index)); + + return listPresentationAccount; + } + + Future showAccountsBottomSheetModal({ + required BuildContext context, + VoidCallback? onGoToManageAccount, + OnAddAnotherAccountAction? onAddAnotherAccountAction, + OnSwitchActiveAccountAction? onSwitchActiveAccountAction, + OnSelectActiveAccountAction? onSelectActiveAccountAction, + }) async { + final listPresentationAccount = await _getAllTwakeMailPresentationAccount(); + final currentActiveAccount = listPresentationAccount + .firstWhereOrNull((presentationAccount) => presentationAccount.isActive); + + if (context.mounted) { + await MultipleAccountPicker.showMultipleAccountPicker( + accounts: listPresentationAccount, + context: context, + titleAddAnotherAccount: AppLocalizations.of(context).addAnotherAccount, + titleAccountSettings: AppLocalizations.of(context).accountSettings, + logoApp: Stack( + children: [ + Center(child: Padding( + padding: const EdgeInsetsDirectional.symmetric(vertical: 16), + child: SvgPicture.asset(_imagePaths.icLogoTwakeHorizontal), + )), + Align( + alignment: AlignmentDirectional.centerEnd, + child: TMailButtonWidget.fromIcon( + icon: _imagePaths.icComposerClose, + iconColor: Colors.black, + margin: const EdgeInsetsDirectional.only(top: 8, end: 8), + backgroundColor: Colors.transparent, + onTapActionCallback: popBack, + ), + ) + ] + ), + accountNameStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: LinagoraSysColors.material().onSurface + ), + accountIdStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: LinagoraRefColors.material().tertiary[20], + ), + addAnotherAccountStyle: Theme.of(context).textTheme.labelLarge!.copyWith( + color: LinagoraSysColors.material().onPrimary, + ), + titleAccountSettingsStyle: Theme.of(context).textTheme.labelLarge!.copyWith( + color: LinagoraSysColors.material().primary, + ), + onAddAnotherAccount: () => onAddAnotherAccountAction?.call(currentActiveAccount?.personalAccount), + onGoToAccountSettings: () => onGoToManageAccount?.call(), + onSetAccountAsActive: (presentationAccount) { + if (presentationAccount is TwakeMailPresentationAccount) { + if (currentActiveAccount != null) { + onSwitchActiveAccountAction?.call( + currentActiveAccount.personalAccount, + presentationAccount.personalAccount); + } else { + onSelectActiveAccountAction?.call(presentationAccount.personalAccount); + } + } + }, + ); + } + } +} \ No newline at end of file