diff --git a/core/lib/core.dart b/core/lib/core.dart index bfe0664e63..1d92e3eef8 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -16,6 +16,7 @@ export 'presentation/extensions/string_extension.dart'; export 'presentation/extensions/tap_down_details_extension.dart'; export 'domain/extensions/media_type_extension.dart'; export 'presentation/extensions/map_extensions.dart'; +export 'presentation/extensions/either_view_state_extension.dart'; // Exceptions export 'domain/exceptions/download_file_exception.dart'; diff --git a/core/lib/presentation/extensions/either_view_state_extension.dart b/core/lib/presentation/extensions/either_view_state_extension.dart new file mode 100644 index 0000000000..76faf2f419 --- /dev/null +++ b/core/lib/presentation/extensions/either_view_state_extension.dart @@ -0,0 +1,12 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; + +extension EitherViewStateExtension on Either { + dynamic foldSuccessWithResult() { + return fold( + (failure) => failure, + (success) => success is T ? success as T : null, + ); + } +} \ No newline at end of file diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index 1c8f904e8a..9c8ca16689 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -143,20 +143,7 @@ abstract class BaseController extends GetxController void onData(Either newState) { viewState.value = newState; - viewState.value.fold( - (failure) { - if (failure is FeatureFailure) { - final isUrgentException = validateUrgentException(failure.exception); - if (isUrgentException) { - handleUrgentException(failure: failure, exception: failure.exception); - } else { - handleFailureViewState(failure); - } - } else { - handleFailureViewState(failure); - } - }, - handleSuccessViewState); + viewState.value.fold(onDataFailureViewState, handleSuccessViewState); } void onError(dynamic error, StackTrace stackTrace) { @@ -272,6 +259,19 @@ abstract class BaseController extends GetxController } } + void onDataFailureViewState(Failure failure) { + if (failure is FeatureFailure) { + final isUrgentException = validateUrgentException(failure.exception); + if (isUrgentException) { + handleUrgentException(failure: failure, exception: failure.exception); + } else { + handleFailureViewState(failure); + } + } else { + handleFailureViewState(failure); + } + } + void handleFailureViewState(Failure failure) async { logError('$runtimeType::handleFailureViewState():Failure = $failure'); if (failure is LogoutOidcFailure) { diff --git a/lib/features/base/base_mailbox_controller.dart b/lib/features/base/base_mailbox_controller.dart index 854eca61a7..aa62a5145a 100644 --- a/lib/features/base/base_mailbox_controller.dart +++ b/lib/features/base/base_mailbox_controller.dart @@ -104,8 +104,7 @@ abstract class BaseMailboxController extends BaseController { teamMailboxesTree.value = tupleTree.value3; } - Future syncAllMailboxWithDisplayName(BuildContext context) async { - log("BaseMailboxController::syncAllMailboxWithDisplayName"); + void syncAllMailboxWithDisplayName(BuildContext context) { final syncedMailbox = allMailboxes .map((mailbox) => mailbox.withDisplayName(mailbox.getDisplayName(context))) .toList(); diff --git a/lib/features/destination_picker/presentation/destination_picker_controller.dart b/lib/features/destination_picker/presentation/destination_picker_controller.dart index eb77e50a25..28d6ede527 100644 --- a/lib/features/destination_picker/presentation/destination_picker_controller.dart +++ b/lib/features/destination_picker/presentation/destination_picker_controller.dart @@ -112,12 +112,12 @@ class DestinationPickerController extends BaseMailboxController { await buildTree(success.mailboxList.listSubscribedMailboxesAndDefaultMailboxes); } if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); + syncAllMailboxWithDisplayName(currentContext!); } } else if (success is RefreshChangesAllMailboxSuccess) { await refreshTree(success.mailboxList.listSubscribedMailboxesAndDefaultMailboxes); if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); + syncAllMailboxWithDisplayName(currentContext!); } } else if (success is SearchMailboxSuccess) { _searchMailboxSuccess(success); diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index c4173a30b3..b803bbe7bf 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -1697,57 +1697,54 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _rejectCalendarEventAction(EmailId emailId) { if (_rejectCalendarEventInteractor == null || _displayingEventBlobId == null - || mailboxDashBoardController.accountId.value == null - || mailboxDashBoardController.sessionCurrent == null - || mailboxDashBoardController.sessionCurrent - !.validateCalendarEventCapability(mailboxDashBoardController.accountId.value!) - .isAvailable == false + || accountId == null + || session == null + || session!.validateCalendarEventCapability(accountId!).isAvailable == false ) { consumeState(Stream.value(Left(CalendarEventRejectFailure()))); } else { consumeState(_rejectCalendarEventInteractor!.execute( - mailboxDashBoardController.accountId.value!, + accountId!, {_displayingEventBlobId!}, emailId, - mailboxDashBoardController.sessionCurrent!.getLanguageForCalendarEvent( + session!.getLanguageForCalendarEvent( LocalizationService.getLocaleFromLanguage(), - mailboxDashBoardController.accountId.value!))); + accountId!, + ), + )); } } void _maybeCalendarEventAction(EmailId emailId) { if (_maybeCalendarEventInteractor == null || _displayingEventBlobId == null - || mailboxDashBoardController.accountId.value == null - || mailboxDashBoardController.sessionCurrent == null - || mailboxDashBoardController.sessionCurrent - !.validateCalendarEventCapability(mailboxDashBoardController.accountId.value!) - .isAvailable == false + || accountId == null + || session == null + || session!.validateCalendarEventCapability(accountId!).isAvailable == false ) { consumeState(Stream.value(Left(CalendarEventMaybeFailure()))); } else { consumeState(_maybeCalendarEventInteractor!.execute( - mailboxDashBoardController.accountId.value!, + accountId!, {_displayingEventBlobId!}, emailId, - mailboxDashBoardController.sessionCurrent!.getLanguageForCalendarEvent( + session!.getLanguageForCalendarEvent( LocalizationService.getLocaleFromLanguage(), - mailboxDashBoardController.accountId.value!))); + accountId!, + ), + )); } } void calendarEventSuccess(CalendarEventReplySuccess success) { - final session = mailboxDashBoardController.sessionCurrent; - final accountId = mailboxDashBoardController.accountId.value; - if (session == null || accountId == null) { consumeState(Stream.value(Left(StoreEventAttendanceStatusFailure(exception: NotFoundSessionException())))); return; } consumeState(_storeEventAttendanceStatusInteractor.execute( - session, - accountId, + session!, + accountId!, success.emailId, success.getEventActionType() )); @@ -1825,16 +1822,15 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } Future previewPDFFileAction(BuildContext context, Attachment attachment) async { - final accountId = mailboxDashBoardController.accountId.value; - final downloadUrl = mailboxDashBoardController.sessionCurrent - ?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl); - - if (accountId == null || downloadUrl == null) { + if (accountId == null || session == null) { appToast.showToastErrorMessage( context, AppLocalizations.of(context).noPreviewAvailable); return; } + final downloadUrl = session!.getDownloadUrl( + jmapUrl: dynamicUrlInterceptors.jmapUrl, + ); await Get.generalDialog( barrierColor: Colors.black.withOpacity(0.8), @@ -1842,7 +1838,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { return PointerInterceptor( child: PDFViewer( attachment: attachment, - accountId: accountId, + accountId: accountId!, downloadUrl: downloadUrl, downloadAction: _downloadPDFFile, printAction: _printPDFFile, @@ -1862,7 +1858,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } final listEmailAddressMailTo = listEmailAddressAttendees - .where((emailAddress) => emailAddress.emailAddress.isNotEmpty && emailAddress.emailAddress != mailboxDashBoardController.sessionCurrent?.username.value) + .where((emailAddress) { + return emailAddress.emailAddress.isNotEmpty && + emailAddress.emailAddress != session?.username.value; + }) .toSet() .toList(); diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index d6ac440311..3c7ca467f6 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -57,6 +57,7 @@ import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_catego import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree_builder.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/open_mailbox_view_event.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_state_manager.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_utils.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/mailbox_creator_arguments.dart'; @@ -97,6 +98,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM final _openMailboxEventController = StreamController(); final mailboxListScrollController = ScrollController(); + final _mailboxStateManager = MailboxStateManager(); PresentationMailbox? get selectedMailbox => mailboxDashBoardController.selectedMailbox.value; @@ -145,6 +147,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM void onClose() { _openMailboxEventController.close(); mailboxListScrollController.dispose(); + _mailboxStateManager.dispose(); super.onClose(); } @@ -153,8 +156,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM super.handleSuccessViewState(success); if (success is GetAllMailboxSuccess) { _handleGetAllMailboxSuccess(success); - } else if (success is RefreshChangesAllMailboxSuccess) { - _handleRefreshChangesAllMailboxSuccess(success); } else if (success is CreateNewMailboxSuccess) { _createNewMailboxSuccess(success); } else if (success is DeleteMultipleMailboxAllSuccess) { @@ -181,8 +182,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _renameMailboxFailure(failure); } else if (failure is DeleteMultipleMailboxFailure) { _deleteMailboxFailure(failure); - } else if (failure is RefreshChangesAllMailboxFailure) { - _clearNewFolderId(); } } @@ -204,13 +203,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM if (PlatformInfo.isIOS) { _updateMailboxIdsBlockNotificationToKeychain(success.mailboxList); } - } else if (success is RefreshChangesAllMailboxSuccess) { - _selectSelectedMailboxDefault(); - mailboxDashBoardController.refreshSpamReportBanner(); - - if (_newFolderId != null) { - _redirectToNewFolder(); - } } }); } @@ -283,17 +275,87 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM if (accountId == null || session == null || currentMailboxState == null || - newState == currentMailboxState) { + newState == null) { _newFolderId = null; return; } - refreshMailboxChanges( - session!, - accountId!, - currentMailboxState!, - properties: MailboxConstants.propertiesDefault, - ); + _mailboxStateManager.addState(newState); + + if (!_mailboxStateManager.isProcessing) { + _processMailboxStateQueue( + session!, + accountId!, + ); + } + } + + Future _processMailboxStateQueue( + Session session, + AccountId accountId, + ) async { + if (_mailboxStateManager.isProcessing) return; + + _mailboxStateManager.startProcessing(); + + while (_mailboxStateManager.isQueueNotEmpty()) { + final nextState = _mailboxStateManager.getFirstState(); + + if (nextState == currentMailboxState) { + log('MailboxController::_processMailboxStateQueue:Skipping redundant state: $nextState'); + continue; + } + + log('MailboxController::_processMailboxStateQueue:Processing new state: $nextState & current state = $currentMailboxState'); + try { + final refreshViewState = await refreshAllMailboxInteractor!.execute( + session, + accountId, + currentMailboxState!, + properties: MailboxConstants.propertiesDefault, + ).last; + + final refreshState = refreshViewState + .foldSuccessWithResult(); + + if (refreshState is RefreshChangesAllMailboxSuccess) { + await _handleRefreshChangeMailboxSuccess(refreshState); + if (currentMailboxState != null) { + _mailboxStateManager.removeStatesUpToCurrent(currentMailboxState!); + } + } else { + _clearNewFolderId(); + onDataFailureViewState(refreshState); + } + } catch (e, stackTrace) { + logError('MailboxController::_processMailboxStateQueue:Error processing state: $e'); + onError(e, stackTrace); + } + } + + _mailboxStateManager.stopProcessing(); + } + + Future _handleRefreshChangeMailboxSuccess(RefreshChangesAllMailboxSuccess success) async { + currentMailboxState = success.currentMailboxState; + log('MailboxController::_handleRefreshChangeMailboxSuccess:currentMailboxState: $currentMailboxState'); + final listMailboxDisplayed = success + .mailboxList + .listSubscribedMailboxesAndDefaultMailboxes; + + await refreshTree(listMailboxDisplayed); + + if (currentContext != null) { + syncAllMailboxWithDisplayName(currentContext!); + } + _setMapMailbox(); + _setOutboxMailbox(); + _selectSelectedMailboxDefault(); + mailboxDashBoardController.refreshSpamReportBanner(); + + if (_newFolderId != null) { + _redirectToNewFolder(); + } } void _setMapMailbox() { @@ -1078,7 +1140,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM final listMailboxDisplayed = success.mailboxList.listSubscribedMailboxesAndDefaultMailboxes; await buildTree(listMailboxDisplayed); if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); + syncAllMailboxWithDisplayName(currentContext!); } _setMapMailbox(); _setOutboxMailbox(); @@ -1105,18 +1167,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM mailboxIds: mailboxIdsBlockNotification); } - void _handleRefreshChangesAllMailboxSuccess(RefreshChangesAllMailboxSuccess success) async { - currentMailboxState = success.currentMailboxState; - log('MailboxController::_handleRefreshChangesAllMailboxSuccess:currentMailboxState: $currentMailboxState'); - final listMailboxDisplayed = success.mailboxList.listSubscribedMailboxesAndDefaultMailboxes; - await refreshTree(listMailboxDisplayed); - if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); - } - _setMapMailbox(); - _setOutboxMailbox(); - } - void _unsubscribeMailboxAction(MailboxId mailboxId) { if (session != null && accountId != null) { final subscribeRequest = generateSubscribeRequest( diff --git a/lib/features/mailbox/presentation/utils/mailbox_state_manager.dart b/lib/features/mailbox/presentation/utils/mailbox_state_manager.dart new file mode 100644 index 0000000000..a0ac814ca6 --- /dev/null +++ b/lib/features/mailbox/presentation/utils/mailbox_state_manager.dart @@ -0,0 +1,44 @@ + +import 'dart:collection'; +import 'package:core/utils/app_logger.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; + +class MailboxStateManager { + final Queue _stateQueue = Queue(); + bool _isProcessing = false; + + void addState(jmap.State newState) { + _stateQueue.add(newState); + } + + Queue get stateQueue => _stateQueue; + + bool get isProcessing => _isProcessing; + + void startProcessing() => _isProcessing = true; + + void stopProcessing() => _isProcessing = false; + + bool isQueueNotEmpty() => _stateQueue.isNotEmpty; + + jmap.State getFirstState() => _stateQueue.removeFirst(); + + void removeStatesUpToCurrent(jmap.State currentState) { + if (!stateQueue.contains(currentState)) { + log('MailboxStateManager::removeStatesUpToCurrent:Current state $currentState not found in the queue.'); + return; + } + while (stateQueue.isNotEmpty) { + final removedState = stateQueue.removeFirst(); + if (removedState == currentState) { + break; + } + } + log('MailboxStateManager::removeStatesUpToCurrent:Updated Queue: $stateQueue'); + } + + void dispose() { + _isProcessing = false; + _stateQueue.clear(); + } +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_controller.dart b/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_controller.dart index ceb7b0669d..d405b92fa6 100644 --- a/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_controller.dart +++ b/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_controller.dart @@ -73,7 +73,7 @@ class MailboxVisibilityController extends BaseMailboxController { currentMailboxState = success.currentMailboxState; await refreshTree(success.mailboxList); if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); + syncAllMailboxWithDisplayName(currentContext!); } } else if (success is SubscribeMailboxSuccess) { _subscribeMailboxSuccess(success); @@ -99,7 +99,7 @@ class MailboxVisibilityController extends BaseMailboxController { await buildTree(mailboxList); dispatchState(Right(BuildTreeMailboxVisibilitySuccess())); if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); + syncAllMailboxWithDisplayName(currentContext!); } } diff --git a/lib/features/rules_filter_creator/presentation/rules_filter_creator_controller.dart b/lib/features/rules_filter_creator/presentation/rules_filter_creator_controller.dart index e9f535deaf..bea0723471 100644 --- a/lib/features/rules_filter_creator/presentation/rules_filter_creator_controller.dart +++ b/lib/features/rules_filter_creator/presentation/rules_filter_creator_controller.dart @@ -127,7 +127,7 @@ class RulesFilterCreatorController extends BaseMailboxController { if (success is GetAllMailboxSuccess) { await buildTree(success.mailboxList); if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); + syncAllMailboxWithDisplayName(currentContext!); } } else if (success is GetAllRulesSuccess) { log('RulesFilterCreatorController::handleSuccessViewState():GetAllRulesSuccess: ${success.rules}'); diff --git a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart index 3447b9ff56..2ae4bc4455 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart @@ -136,13 +136,13 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa currentMailboxState = success.currentMailboxState; await buildTree(success.mailboxList); if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); + syncAllMailboxWithDisplayName(currentContext!); } } else if (success is RefreshChangesAllMailboxSuccess) { currentMailboxState = success.currentMailboxState; await refreshTree(success.mailboxList); if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); + syncAllMailboxWithDisplayName(currentContext!); } searchMailboxAction(); } else if (success is SearchMailboxSuccess) { diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 8aac45a658..d8cd28bcd4 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -513,17 +513,23 @@ class ThreadController extends BaseController with EmailActionController { return limit; } + @visibleForTesting + void setCurrentEmailState({jmap.State? newState}) { + _currentEmailState = newState; + } + void _refreshEmailChanges({jmap.State? newState}) { log('ThreadController::_refreshEmailChanges(): newState: $newState'); + if (_currentEmailState == null || + _currentEmailState == newState || + _session == null || + _accountId == null) { + return; + } + if (searchController.isSearchEmailRunning) { _searchEmail(limit: limitEmailFetched, needRefreshSearchState: true); } else { - if (_currentEmailState == null || - _currentEmailState == newState || - _session == null || - _accountId == null) { - return; - } consumeState(_refreshChangesEmailsInMailboxInteractor.execute( _session!, _accountId!, @@ -1168,12 +1174,10 @@ class ThreadController extends BaseController with EmailActionController { } void goToCreateEmailRuleView() async { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; - if (accountId != null && session != null) { + if (_accountId != null && _session != null) { final arguments = RulesFilterCreatorArguments( - accountId, - session, + _accountId!, + _session!, mailboxDestination: selectedMailbox ); @@ -1182,7 +1186,7 @@ class ThreadController extends BaseController with EmailActionController { : await push(AppRoutes.rulesFilterCreator, arguments: arguments); if (newRuleFilterRequest is CreateNewEmailRuleFilterRequest) { - _createNewRuleFilterAction(accountId, newRuleFilterRequest); + _createNewRuleFilterAction(_accountId!, newRuleFilterRequest); } } else { logError('ThreadController::goToCreateEmailRuleView: Account or Session is NULL'); diff --git a/test/features/thread/presentation/controller/thread_controller_test.dart b/test/features/thread/presentation/controller/thread_controller_test.dart index 7f8ad860d8..442c570623 100644 --- a/test/features/thread/presentation/controller/thread_controller_test.dart +++ b/test/features/thread/presentation/controller/thread_controller_test.dart @@ -304,6 +304,7 @@ void main() { // Act threadController.onInit(); + threadController.setCurrentEmailState(newState: State('old-state')); mockMailboxDashBoardController.emailUIAction.value = RefreshChangeEmailAction(State('new-state'));