From 4512c9ba142992ba77b388d5ec9e184770616761 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 29 Oct 2025 09:57:01 +0700 Subject: [PATCH 1/7] TF-4053 Add `Move folder content` action in mailbox content menu --- assets/images/ic_move_folder_content.svg | 5 +++++ .../presentation/resources/image_paths.dart | 1 + .../presentation/mailbox_controller.dart | 2 ++ .../mixin/mailbox_widget_mixin.dart | 9 +++++++-- .../presentation/model/mailbox_actions.dart | 18 ++++++------------ lib/l10n/intl_messages.arb | 6 ++++++ lib/main/localizations/app_localizations.dart | 7 +++++++ 7 files changed, 34 insertions(+), 14 deletions(-) create mode 100644 assets/images/ic_move_folder_content.svg diff --git a/assets/images/ic_move_folder_content.svg b/assets/images/ic_move_folder_content.svg new file mode 100644 index 0000000000..b3fd99d9ae --- /dev/null +++ b/assets/images/ic_move_folder_content.svg @@ -0,0 +1,5 @@ + + + diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 2a5d2cb7e9..0cfd8d6374 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -255,6 +255,7 @@ class ImagePaths { String get icMessage => _getImagePath('ic_message.svg'); String get icNavigation => _getImagePath('ic_navigation.svg'); String get icReading => _getImagePath('ic_reading.svg'); + String get icMoveFolderContent => _getImagePath('ic_move_folder_content.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 752867eeae..9cbf0b7713 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -1265,6 +1265,8 @@ class MailboxController extends BaseMailboxController case MailboxActions.recoverDeletedMessages: mailboxDashBoardController.gotoEmailRecovery(); break; + case MailboxActions.moveFolderContent: + break; default: break; } diff --git a/lib/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart b/lib/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart index a29a2e3713..842eb7d064 100644 --- a/lib/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart +++ b/lib/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart @@ -21,7 +21,6 @@ mixin MailboxWidgetMixin { bool spamReportEnabled, bool deletedMessageVaultSupported ) { - return [ if (PlatformInfo.isWeb) MailboxActions.openInNewTab, @@ -30,18 +29,23 @@ mixin MailboxWidgetMixin { MailboxActions.createFilter, if (mailbox.isTrash) ...[ + MailboxActions.moveFolderContent, MailboxActions.emptyTrash, if (deletedMessageVaultSupported) MailboxActions.recoverDeletedMessages, ] else if (mailbox.isSpam) ...[ + MailboxActions.moveFolderContent, _mailboxActionForSpam(spamReportEnabled), MailboxActions.confirmMailSpam, MailboxActions.emptySpam ] else if (mailbox.countUnReadEmailsAsString.isNotEmpty) - MailboxActions.markAsRead + ...[ + MailboxActions.markAsRead, + MailboxActions.moveFolderContent, + ] ]; } @@ -54,6 +58,7 @@ mixin MailboxWidgetMixin { if (mailbox.countUnReadEmailsAsString.isNotEmpty) MailboxActions.markAsRead, MailboxActions.move, + MailboxActions.moveFolderContent, MailboxActions.rename, if (subaddressingSupported) ...[ if (mailbox.isSubaddressingAllowed) diff --git a/lib/features/mailbox/presentation/model/mailbox_actions.dart b/lib/features/mailbox/presentation/model/mailbox_actions.dart index ad53d139fb..38f56b02c4 100644 --- a/lib/features/mailbox/presentation/model/mailbox_actions.dart +++ b/lib/features/mailbox/presentation/model/mailbox_actions.dart @@ -10,6 +10,7 @@ enum MailboxActions { delete, rename, move, + moveFolderContent, markAsRead, selectForRuleAction, openInNewTab, @@ -63,6 +64,8 @@ extension MailboxActionsExtension on MailboxActions { return appLocalizations.mark_as_read; case MailboxActions.move: return appLocalizations.moveFolder; + case MailboxActions.moveFolderContent: + return appLocalizations.moveFolderContent; case MailboxActions.rename: return appLocalizations.renameFolder; case MailboxActions.delete: @@ -106,6 +109,8 @@ extension MailboxActionsExtension on MailboxActions { return imagePaths.icMarkAsRead; case MailboxActions.move: return imagePaths.icMoveMailbox; + case MailboxActions.moveFolderContent: + return imagePaths.icMoveFolderContent; case MailboxActions.rename: return imagePaths.icRenameMailbox; case MailboxActions.delete: @@ -172,6 +177,7 @@ extension MailboxActionsExtension on MailboxActions { case MailboxActions.create: case MailboxActions.moveEmail: case MailboxActions.move: + case MailboxActions.moveFolderContent: case MailboxActions.select: case MailboxActions.selectForRuleAction: return true; @@ -190,16 +196,4 @@ extension MailboxActionsExtension on MailboxActions { return false; } } - - bool canCollapseMailboxGroup() { - switch(this) { - case MailboxActions.moveEmail: - case MailboxActions.move: - case MailboxActions.select: - case MailboxActions.selectForRuleAction: - return false; - default: - return true; - } - } } \ No newline at end of file diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index ec8801bf9d..083dc48998 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -5051,5 +5051,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "moveFolderContent": "Move folder content", + "@moveFolderContent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 33def3be68..951bbad18a 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -5345,4 +5345,11 @@ class AppLocalizations { name: 'deleteMessage', ); } + + String get moveFolderContent { + return Intl.message( + 'Move folder content', + name: 'moveFolderContent', + ); + } } From 7293156c11726042712f47ffe322d7ec1b832219 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 29 Oct 2025 13:19:25 +0700 Subject: [PATCH 2/7] TF-4053 Add interactor/datasource/repository for move folder content action --- .../base/mixin/handle_error_mixin.dart | 2 +- lib/features/base/mixin/mail_api_mixin.dart | 304 ++++++++++++++++++ .../email/data/network/email_api.dart | 99 +----- .../data/datasource/mailbox_datasource.dart | 8 + .../mailbox_cache_datasource_impl.dart | 11 + .../mailbox_datasource_impl.dart | 31 ++ ...move_folder_content_isolate_arguments.dart | 41 +++ .../mailbox/data/network/mailbox_api.dart | 28 +- .../data/network/mailbox_isolate_worker.dart | 102 ++++++ .../repository/mailbox_repository_impl.dart | 16 + .../domain/exceptions/mailbox_exception.dart | 2 + .../model/move_folder_content_request.dart | 31 ++ .../domain/repository/mailbox_repository.dart | 8 + .../state/move_folder_content_state.dart | 38 +++ .../move_folder_content_interactor.dart | 37 +++ .../data/extensions/list_email_extension.dart | 3 + .../thread/data/network/thread_api.dart | 88 +---- .../extensions/list_email_id_extension.dart | 18 +- .../lib/extensions/mailbox_id_extension.dart | 16 +- 19 files changed, 695 insertions(+), 188 deletions(-) create mode 100644 lib/features/base/mixin/mail_api_mixin.dart create mode 100644 lib/features/mailbox/data/model/move_folder_content_isolate_arguments.dart create mode 100644 lib/features/mailbox/domain/model/move_folder_content_request.dart create mode 100644 lib/features/mailbox/domain/state/move_folder_content_state.dart create mode 100644 lib/features/mailbox/domain/usecases/move_folder_content_interactor.dart diff --git a/lib/features/base/mixin/handle_error_mixin.dart b/lib/features/base/mixin/handle_error_mixin.dart index 99e700c8ab..71053bbc79 100644 --- a/lib/features/base/mixin/handle_error_mixin.dart +++ b/lib/features/base/mixin/handle_error_mixin.dart @@ -1,4 +1,4 @@ -import 'package:core/core.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; diff --git a/lib/features/base/mixin/mail_api_mixin.dart b/lib/features/base/mixin/mail_api_mixin.dart new file mode 100644 index 0000000000..6c3122a95f --- /dev/null +++ b/lib/features/base/mixin/mail_api_mixin.dart @@ -0,0 +1,304 @@ +import 'dart:async'; +import 'dart:math' hide log; + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/http/http_client.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/capability/core_capability.dart'; +import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; +import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/request/reference_path.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.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/jmap_request.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_comparator.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_comparator_property.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_filter_condition.dart'; +import 'package:jmap_dart_client/jmap/mail/email/get/get_email_method.dart'; +import 'package:jmap_dart_client/jmap/mail/email/get/get_email_response.dart'; +import 'package:jmap_dart_client/jmap/mail/email/query/query_email_method.dart'; +import 'package:jmap_dart_client/jmap/mail/email/query/query_email_response.dart'; +import 'package:jmap_dart_client/jmap/mail/email/set/set_email_method.dart'; +import 'package:jmap_dart_client/jmap/mail/email/set/set_email_response.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/email/email_property.dart'; +import 'package:model/extensions/list_email_extension.dart'; +import 'package:model/extensions/list_email_id_extension.dart'; +import 'package:model/extensions/list_id_extension.dart'; +import 'package:model/extensions/session_extension.dart'; +import 'package:tmail_ui_user/features/base/mixin/handle_error_mixin.dart'; +import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/exceptions/mailbox_exception.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/move_folder_content_state.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:tmail_ui_user/features/thread/domain/model/email_response.dart'; +import 'package:tmail_ui_user/main/error/capability_validator.dart'; + +mixin MailAPIMixin on HandleSetErrorMixin { + int getMaxObjectsInSetMethod(Session session, AccountId accountId) { + final coreCapability = session.getCapabilityProperties( + accountId, + CapabilityIdentifier.jmapCore, + ); + final maxObjectsInSetMethod = + coreCapability?.maxObjectsInSet?.value.toInt() ?? + CapabilityIdentifierExtension.defaultMaxObjectsInSet; + + final minOfMaxObjectsInSetMethod = min( + maxObjectsInSetMethod, + CapabilityIdentifierExtension.defaultMaxObjectsInSet, + ); + log('$runtimeType::_getMaxObjectsInSetMethod:minOfMaxObjectsInSetMethod = $minOfMaxObjectsInSetMethod'); + return minOfMaxObjectsInSetMethod; + } + + Future<({List emailIdsSuccess, Map mapErrors})> + moveEmailsBetweenMailboxes({ + required HttpClient httpClient, + required Session session, + required AccountId accountId, + required List emailIds, + required MailboxId currentMailboxId, + required MailboxId destinationMailboxId, + bool markAsRead = false, + }) async { + final maxObjects = getMaxObjectsInSetMethod(session, accountId); + final totalEmails = emailIds.length; + final maxBatches = min(totalEmails, maxObjects); + + final List updatedEmailIds = List.empty(growable: true); + final Map mapErrors = {}; + + for (int start = 0; start < totalEmails; start += maxBatches) { + int end = + (start + maxBatches < totalEmails) ? start + maxBatches : totalEmails; + log('$runtimeType::moveEmailsBetweenMailboxes:emails from ${start + 1} to $end'); + + final currentEmailIds = emailIds.sublist(start, end); + + final moveProperties = currentEmailIds.generateMapUpdateObjectMoveToMailbox( + currentMailboxId: currentMailboxId, + destinationMailboxId: destinationMailboxId, + markAsRead: markAsRead, + ); + + final setEmailMethod = SetEmailMethod(accountId) + ..addUpdates(moveProperties); + + final requestBuilder = + JmapRequestBuilder(httpClient, ProcessingInvocation()); + + final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + + final response = + await (requestBuilder..usings(capabilities)).build().execute(); + + final setEmailResponse = response.parse( + setEmailInvocation.methodCallId, + SetEmailResponse.deserialize, + ); + + final listEmailIds = setEmailResponse?.updated?.keys.toEmailIds() ?? []; + final mapErrors = handleSetResponse([setEmailResponse]); + + updatedEmailIds.addAll(listEmailIds); + mapErrors.addAll(mapErrors); + } + + return (emailIdsSuccess: updatedEmailIds, mapErrors: mapErrors); + } + + Future fetchAllEmail({ + required HttpClient httpClient, + required Session session, + required AccountId accountId, + UnsignedInt? limit, + int? position, + Set? sort, + Filter? filter, + Properties? properties, + }) async { + final processingInvocation = ProcessingInvocation(); + + final jmapRequestBuilder = JmapRequestBuilder( + httpClient, + processingInvocation, + ); + + final queryEmailMethod = QueryEmailMethod(accountId); + + if (limit != null) queryEmailMethod.addLimit(limit); + + if (position != null) queryEmailMethod.addPosition(position); + + if (sort != null) queryEmailMethod.addSorts(sort); + + if (filter != null) queryEmailMethod.addFilters(filter); + + final queryEmailInvocation = + jmapRequestBuilder.invocation(queryEmailMethod); + + final getEmailMethod = GetEmailMethod(accountId); + + if (properties != null) getEmailMethod.addProperties(properties); + + getEmailMethod.addReferenceIds( + processingInvocation.createResultReference( + queryEmailInvocation.methodCallId, + ReferencePath.idsPath, + ), + ); + + final getEmailInvocation = jmapRequestBuilder.invocation(getEmailMethod); + + final capabilities = getEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + + final result = + await (jmapRequestBuilder..usings(capabilities)).build().execute(); + + final responseOfGetEmailMethod = result.parse( + getEmailInvocation.methodCallId, + GetEmailResponse.deserialize, + ); + + final responseOfQueryEmailMethod = result.parse( + queryEmailInvocation.methodCallId, + QueryEmailResponse.deserialize, + ); + + final emailList = sortEmails( + getEmailResponse: responseOfGetEmailMethod, + queryEmailResponse: responseOfQueryEmailMethod, + ); + + final notFoundEmailIds = + responseOfGetEmailMethod?.notFound?.toEmailIds().toList(); + log('$runtimeType::getAllEmail:notFoundEmailIds = ${notFoundEmailIds!.asListString.toString()} | NewState = ${responseOfGetEmailMethod?.state.value}'); + return EmailsResponse( + emailList: emailList, + notFoundEmailIds: notFoundEmailIds, + state: responseOfGetEmailMethod?.state, + ); + } + + List? sortEmails({ + GetEmailResponse? getEmailResponse, + QueryEmailResponse? queryEmailResponse, + }) { + final listEmails = getEmailResponse?.list; + final listIds = queryEmailResponse?.ids.toList(); + + if (listEmails?.isNotEmpty != true || listIds?.isNotEmpty != true) { + return listEmails; + } + + final listSortedEmails = listEmails!.sortEmailsById(listIds!); + + return listSortedEmails; + } + + Future moveAllEmailsBetweenFolders({ + required HttpClient httpClient, + required Session session, + required AccountId accountId, + required MailboxId currentMailboxId, + required MailboxId destinationMailboxId, + required MoveAction moveAction, + int totalEmails = 0, + bool markAsRead = false, + StreamController>? onProgressController, + }) async { + int countEmailsCompleted = 0; + bool hasEmails = true; + Email? lastEmail; + + while (hasEmails) { + final listEmails = await getLatestEmails( + httpClient: httpClient, + session: session, + accountId: accountId, + mailboxId: currentMailboxId, + lastEmail: lastEmail, + ); + log('$runtimeType::moveAllEmailsBetweenFolders(): Length of emails = ${listEmails.length}'); + if (listEmails.isEmpty) { + hasEmails = false; + } else { + hasEmails = true; + lastEmail = listEmails.last; + + final movedEmails = await moveEmailsBetweenMailboxes( + httpClient: httpClient, + session: session, + accountId: accountId, + emailIds: listEmails.listEmailIds, + currentMailboxId: currentMailboxId, + destinationMailboxId: destinationMailboxId, + markAsRead: markAsRead, + ); + + countEmailsCompleted += movedEmails.emailIdsSuccess.length; + + onProgressController?.add( + Right(MoveFolderContentProgressState( + currentMailboxId, + countEmailsCompleted, + totalEmails, + )), + ); + } + } + log('$runtimeType::moveAllEmailsBetweenFolders(): Total emails moved = $countEmailsCompleted'); + if (moveAction == MoveAction.moving && + countEmailsCompleted < totalEmails && + totalEmails > 0) { + throw CannotMoveAllEmailException(); + } + } + + Future> getLatestEmails({ + required HttpClient httpClient, + required Session session, + required AccountId accountId, + required MailboxId mailboxId, + Email? lastEmail, + }) async { + final response = await fetchAllEmail( + httpClient: httpClient, + session: session, + accountId: accountId, + sort: {}..add( + EmailComparator( + EmailComparatorProperty.receivedAt, + )..setIsAscending(false), + ), + filter: EmailFilterCondition( + inMailbox: mailboxId, + before: lastEmail?.receivedAt, + ), + properties: Properties({ + EmailProperty.id, + EmailProperty.receivedAt, + }), + ); + + if (lastEmail?.id != null) { + return response.emailList?.withoutEmailWithId(lastEmail!.id!) ?? []; + } else { + return response.emailList ?? []; + } + } +} diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index ddae14b9e8..c1982ecee5 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -14,8 +14,6 @@ import 'package:email_recovery/email_recovery/set/set_email_recovery_action_meth import 'package:email_recovery/email_recovery/set/set_email_recovery_action_response.dart'; import 'package:jmap_dart_client/http/http_client.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/capability/core_capability.dart'; import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/patch_object.dart'; @@ -55,8 +53,10 @@ import 'package:model/extensions/list_email_id_extension.dart'; import 'package:model/extensions/list_id_extension.dart'; import 'package:model/extensions/mailbox_id_extension.dart'; import 'package:model/extensions/session_extension.dart'; +import 'package:model/oidc/token_oidc.dart'; import 'package:path_provider/path_provider.dart'; import 'package:tmail_ui_user/features/base/mixin/handle_error_mixin.dart'; +import 'package:tmail_ui_user/features/base/mixin/mail_api_mixin.dart'; import 'package:tmail_ui_user/features/composer/domain/exceptions/set_method_exception.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; @@ -70,7 +70,7 @@ import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:uri/uri.dart'; import 'package:uuid/uuid.dart'; -class EmailAPI with HandleSetErrorMixin { +class EmailAPI with HandleSetErrorMixin, MailAPIMixin { final HttpClient _httpClient; final DownloadManager _downloadManager; @@ -247,7 +247,7 @@ class EmailAPI with HandleSetErrorMixin { List emailIds, ReadActions readActions, ) async { - final maxObjects = _getMaxObjectsInSetMethod(session, accountId); + final maxObjects = getMaxObjectsInSetMethod(session, accountId); final totalEmails = emailIds.length; final maxBatches = min(totalEmails, maxObjects); @@ -455,13 +455,14 @@ class EmailAPI with HandleSetErrorMixin { final currentMailboxId = listMailboxIds[i]; final listEmailIds = moveRequest.currentMailboxes[currentMailboxId]!; log('EmailAPI::moveToMailbox:from mailbox ${currentMailboxId.asString} with ${listEmailIds.length} emails to mailbox ${moveRequest.destinationMailboxId.asString}'); - final resultRecords = await _moveEmailsBetweenMailboxes( + final resultRecords = await moveEmailsBetweenMailboxes( + httpClient: _httpClient, session: session, accountId: accountId, emailIds: listEmailIds, currentMailboxId: currentMailboxId, destinationMailboxId: moveRequest.destinationMailboxId, - isMovingToSpam: moveRequest.isMovingToSpam, + markAsRead: moveRequest.isMovingToSpam, ); listEmailIdResult.addAll(resultRecords.emailIdsSuccess); @@ -471,88 +472,6 @@ class EmailAPI with HandleSetErrorMixin { return (emailIdsSuccess: listEmailIdResult, mapErrors: mapErrors); } - Future<({ - List emailIdsSuccess, - Map mapErrors, - })> _moveEmailsBetweenMailboxes({ - required Session session, - required AccountId accountId, - required List emailIds, - required MailboxId currentMailboxId, - required MailboxId destinationMailboxId, - bool isMovingToSpam = false, - }) async { - final maxObjects = _getMaxObjectsInSetMethod(session, accountId); - final totalEmails = emailIds.length; - final maxBatches = min(totalEmails, maxObjects); - - final List updatedEmailIds = List.empty(growable: true); - final Map mapErrors = {}; - - for (int start = 0; start < totalEmails; start += maxBatches) { - int end = (start + maxBatches < totalEmails) - ? start + maxBatches - : totalEmails; - log('EmailAPI::_moveEmailsBetweenMailboxes:emails from ${start + 1} to $end'); - - final currentEmailIds = emailIds.sublist(start, end); - - final moveProperties = isMovingToSpam - ? currentEmailIds.generateMapUpdateObjectMoveToSpam( - currentMailboxId, - destinationMailboxId, - ) - : currentEmailIds.generateMapUpdateObjectMoveToMailbox( - currentMailboxId, - destinationMailboxId, - ); - - final setEmailMethod = SetEmailMethod(accountId) - ..addUpdates(moveProperties); - - final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - - final setEmailInvocation = requestBuilder.invocation(setEmailMethod); - - final capabilities = setEmailMethod.requiredCapabilities - .toCapabilitiesSupportTeamMailboxes(session, accountId); - - final response = await (requestBuilder - ..usings(capabilities)) - .build() - .execute(); - - final setEmailResponse = response.parse( - setEmailInvocation.methodCallId, - SetEmailResponse.deserialize, - ); - - final listEmailIds = setEmailResponse?.updated?.keys.toEmailIds() ?? []; - final mapErrors = handleSetResponse([setEmailResponse]); - - updatedEmailIds.addAll(listEmailIds); - mapErrors.addAll(mapErrors); - } - - return (emailIdsSuccess: updatedEmailIds, mapErrors: mapErrors); - } - - int _getMaxObjectsInSetMethod(Session session, AccountId accountId) { - final coreCapability = session.getCapabilityProperties( - accountId, - CapabilityIdentifier.jmapCore, - ); - final maxObjectsInSetMethod = coreCapability?.maxObjectsInSet?.value.toInt() - ?? CapabilityIdentifierExtension.defaultMaxObjectsInSet; - - final minOfMaxObjectsInSetMethod = min( - maxObjectsInSetMethod, - CapabilityIdentifierExtension.defaultMaxObjectsInSet, - ); - log('EmailAPI::_getMaxObjectsInSetMethod:minOfMaxObjectsInSetMethod = $minOfMaxObjectsInSetMethod'); - return minOfMaxObjectsInSetMethod; - } - Future<({ List emailIdsSuccess, Map mapErrors, @@ -562,7 +481,7 @@ class EmailAPI with HandleSetErrorMixin { List emailIds, MarkStarAction markStarAction ) async { - final maxObjects = _getMaxObjectsInSetMethod(session, accountId); + final maxObjects = getMaxObjectsInSetMethod(session, accountId); final totalEmails = emailIds.length; final maxBatches = min(totalEmails, maxObjects); @@ -803,7 +722,7 @@ class EmailAPI with HandleSetErrorMixin { AccountId accountId, List emailIds ) async { - final maxObjects = _getMaxObjectsInSetMethod(session, accountId); + final maxObjects = getMaxObjectsInSetMethod(session, accountId); final totalEmails = emailIds.length; final maxBatches = min(totalEmails, maxObjects); diff --git a/lib/features/mailbox/data/datasource/mailbox_datasource.dart b/lib/features/mailbox/data/datasource/mailbox_datasource.dart index c50321e479..3c6218a98d 100644 --- a/lib/features/mailbox/data/datasource/mailbox_datasource.dart +++ b/lib/features/mailbox/data/datasource/mailbox_datasource.dart @@ -17,6 +17,7 @@ import 'package:tmail_ui_user/features/mailbox/data/model/mailbox_change_respons import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/get_mailbox_by_role_response.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/jmap_mailbox_response.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/move_folder_content_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/move_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/rename_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_right_request.dart'; @@ -40,6 +41,13 @@ abstract class MailboxDataSource { Future moveMailbox(Session session, AccountId accountId, MoveMailboxRequest request); + Future moveFolderContent({ + required Session session, + required AccountId accountId, + required MoveFolderContentRequest request, + StreamController>? onProgressController, + }); + Future> markAsMailboxRead( Session session, AccountId accountId, diff --git a/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart b/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart index 0956ac80f5..57f21ba6a9 100644 --- a/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart +++ b/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart @@ -21,6 +21,7 @@ import 'package:tmail_ui_user/features/mailbox/data/model/mailbox_change_respons import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/get_mailbox_by_role_response.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/jmap_mailbox_response.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/move_folder_content_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/move_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/rename_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_right_request.dart'; @@ -166,4 +167,14 @@ class MailboxCacheDataSourceImpl extends MailboxDataSource { Future clearMailbox(Session session, AccountId accountId, MailboxId mailboxId) { throw UnimplementedError(); } + + @override + Future> moveFolderContent({ + required Session session, + required AccountId accountId, + required MoveFolderContentRequest request, + StreamController>? onProgressController, + }) { + throw UnimplementedError(); + } } \ No newline at end of file diff --git a/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart b/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart index 23a3495c21..8f3672d901 100644 --- a/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart +++ b/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart' as dartz; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; @@ -20,6 +21,7 @@ import 'package:tmail_ui_user/features/mailbox/data/network/mailbox_isolate_work import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/get_mailbox_by_role_response.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/jmap_mailbox_response.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/move_folder_content_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/move_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/rename_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_right_request.dart'; @@ -161,4 +163,33 @@ class MailboxDataSourceImpl extends MailboxDataSource { return await mailboxAPI.clearMailbox(session, accountId, mailboxId); }).catchError(_exceptionThrower.throwException); } + + @override + Future moveFolderContent({ + required Session session, + required AccountId accountId, + required MoveFolderContentRequest request, + StreamController>? onProgressController, + }) { + return Future.sync(() async { + if (PlatformInfo.isWeb) { + return await mailboxAPI.moveFolderContent( + session: session, + accountId: accountId, + request: request, + onProgressController: onProgressController, + ); + } else { + return await _mailboxIsolateWorker.moveFolderContent( + session: session, + accountId: accountId, + request: request, + onProgressController: onProgressController, + ); + } + }).catchError((error, stackTrace) async { + await _exceptionThrower.throwException(error, stackTrace); + throw error; + }); + } } \ No newline at end of file diff --git a/lib/features/mailbox/data/model/move_folder_content_isolate_arguments.dart b/lib/features/mailbox/data/model/move_folder_content_isolate_arguments.dart new file mode 100644 index 0000000000..0b1fee9059 --- /dev/null +++ b/lib/features/mailbox/data/model/move_folder_content_isolate_arguments.dart @@ -0,0 +1,41 @@ +import 'package:equatable/equatable.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/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/base/isolate/background_isolate_binary_messenger/background_isolate_binary_messenger.dart'; +import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; +import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; + +class MoveFolderContentIsolateArguments with EquatableMixin { + final Session session; + final AccountId accountId; + final MailboxId currentMailboxId; + final MailboxId destinationMailboxId; + final bool markAsRead; + final ThreadAPI threadAPI; + final EmailAPI emailAPI; + final RootIsolateToken isolateToken; + + MoveFolderContentIsolateArguments({ + required this.session, + required this.threadAPI, + required this.emailAPI, + required this.accountId, + required this.currentMailboxId, + required this.destinationMailboxId, + required this.isolateToken, + this.markAsRead = false, + }); + + @override + List get props => [ + session, + accountId, + threadAPI, + emailAPI, + currentMailboxId, + destinationMailboxId, + isolateToken, + markAsRead, + ]; +} diff --git a/lib/features/mailbox/data/network/mailbox_api.dart b/lib/features/mailbox/data/network/mailbox_api.dart index 752a4ae49e..ca50b28653 100644 --- a/lib/features/mailbox/data/network/mailbox_api.dart +++ b/lib/features/mailbox/data/network/mailbox_api.dart @@ -1,6 +1,9 @@ import 'dart:async'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart' hide State; import 'package:flutter/material.dart' hide State; import 'package:get/get.dart'; import 'package:jmap_dart_client/http/http_client.dart'; @@ -32,6 +35,7 @@ import 'package:model/error_type_handler/set_method_error_handler_mixin.dart'; import 'package:model/mailbox/mailbox_constants.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/mixin/handle_error_mixin.dart'; +import 'package:tmail_ui_user/features/base/mixin/mail_api_mixin.dart'; import 'package:tmail_ui_user/features/composer/domain/exceptions/set_method_exception.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/mailbox_change_response.dart'; import 'package:tmail_ui_user/features/mailbox/domain/exceptions/mailbox_exception.dart'; @@ -41,17 +45,18 @@ import 'package:tmail_ui_user/features/mailbox/domain/extensions/role_extension. import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/get_mailbox_by_role_response.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/jmap_mailbox_response.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_right_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_subaddressing_action.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_subscribe_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/move_folder_content_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/move_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/rename_mailbox_request.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_right_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_multiple_mailbox_request.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:uuid/uuid.dart'; -class MailboxAPI with HandleSetErrorMixin { +class MailboxAPI with HandleSetErrorMixin, MailAPIMixin { final HttpClient httpClient; final Uuid _uuid; @@ -636,4 +641,23 @@ class MailboxAPI with HandleSetErrorMixin { throw NotFoundClearMailboxResponseException(); } } + + Future moveFolderContent({ + required Session session, + required AccountId accountId, + required MoveFolderContentRequest request, + StreamController>? onProgressController, + }) async { + return await moveAllEmailsBetweenFolders( + httpClient: httpClient, + accountId: accountId, + session: session, + moveAction: request.moveAction, + currentMailboxId: request.mailboxId, + destinationMailboxId: request.destinationMailboxId, + totalEmails: request.totalEmails, + markAsRead: request.markAsRead, + onProgressController: onProgressController, + ); + } } \ No newline at end of file diff --git a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart index 2c627d8382..c745247cd2 100644 --- a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart +++ b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart @@ -23,8 +23,13 @@ import 'package:model/extensions/list_email_extension.dart'; import 'package:tmail_ui_user/features/base/isolate/background_isolate_binary_messenger/background_isolate_binary_messenger.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; +import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/mailbox_mark_as_read_arguments.dart'; +import 'package:tmail_ui_user/features/mailbox/data/model/move_folder_content_isolate_arguments.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/exceptions/mailbox_exception.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/move_folder_content_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/move_folder_content_state.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; import 'package:tmail_ui_user/main/exceptions/isolate_exception.dart'; @@ -213,4 +218,101 @@ class MailboxIsolateWorker { log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): TOTAL_READ: ${emailIdsCompleted.length}'); return emailIdsCompleted; } + + Future moveFolderContent({ + required Session session, + required AccountId accountId, + required MoveFolderContentRequest request, + StreamController>? onProgressController, + }) async { + final rootIsolateToken = RootIsolateToken.instance; + if (rootIsolateToken == null) { + throw CanNotGetRootIsolateToken(); + } + + final countEmailsCompleted = await _isolateExecutor.execute( + arg1: MoveFolderContentIsolateArguments( + session: session, + accountId: accountId, + threadAPI: _threadApi, + emailAPI: _emailApi, + currentMailboxId: request.mailboxId, + destinationMailboxId: request.destinationMailboxId, + isolateToken: rootIsolateToken, + markAsRead: request.markAsRead, + ), + fun1: _moveFolderContentIsolateMethod, + notification: (value) { + if (value is int) { + log('$runtimeType::moveFolderContent(): Progress percent is ${value / request.totalEmails}'); + onProgressController?.add( + Right(MoveFolderContentProgressState( + request.mailboxId, + value, + request.totalEmails, + )), + ); + } + }, + ); + + if (request.moveAction == MoveAction.moving && + countEmailsCompleted < request.totalEmails && + request.totalEmails > 0) { + throw CannotMoveAllEmailException(); + } + } + + static Future _moveFolderContentIsolateMethod( + MoveFolderContentIsolateArguments args, + TypeSendPort sendPort, + ) async { + final rootIsolateToken = args.isolateToken; + BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken); + await HiveCacheConfig.instance.setUp(); + + int countEmailsCompleted = 0; + bool hasEmails = true; + Email? lastEmail; + + final threadAPI = args.threadAPI; + final httpClient = threadAPI.httpClient; + final session = args.session; + final accountId = args.accountId; + final currentMailboxId = args.currentMailboxId; + final destinationMailboxId = args.destinationMailboxId; + final markAsRead = args.markAsRead; + + while (hasEmails) { + final listEmails = await threadAPI.getLatestEmails( + httpClient: httpClient, + session: session, + accountId: accountId, + mailboxId: currentMailboxId, + lastEmail: lastEmail, + ); + log('MailboxIsolateWorker::_moveFolderContentIsolateMethod(): Length of emails = ${listEmails.length}'); + if (listEmails.isEmpty) { + hasEmails = false; + } else { + hasEmails = true; + lastEmail = listEmails.last; + + final movedEmails = await threadAPI.moveEmailsBetweenMailboxes( + httpClient: httpClient, + session: session, + accountId: accountId, + emailIds: listEmails.listEmailIds, + currentMailboxId: currentMailboxId, + destinationMailboxId: destinationMailboxId, + markAsRead: markAsRead, + ); + + countEmailsCompleted += movedEmails.emailIdsSuccess.length; + sendPort.send(countEmailsCompleted); + } + } + log('MailboxIsolateWorker::_moveFolderContentIsolateMethod(): Total emails moved = $countEmailsCompleted'); + return countEmailsCompleted; + } } diff --git a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart index f2fd114e3c..4e0b62c1d1 100644 --- a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart +++ b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart @@ -28,6 +28,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_r import 'package:tmail_ui_user/features/mailbox/domain/model/get_mailbox_by_role_response.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/jmap_mailbox_response.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_response.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/move_folder_content_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/move_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/rename_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_right_request.dart'; @@ -305,4 +306,19 @@ class MailboxRepositoryImpl extends MailboxRepository { Future clearMailbox(Session session, AccountId accountId, MailboxId mailboxId) { return mapDataSource[DataSourceType.network]!.clearMailbox(session, accountId, mailboxId); } + + @override + Future moveFolderContent({ + required Session session, + required AccountId accountId, + required MoveFolderContentRequest request, + StreamController>? onProgressController, + }) { + return mapDataSource[DataSourceType.network]!.moveFolderContent( + session: session, + accountId: accountId, + request: request, + onProgressController: onProgressController, + ); + } } \ No newline at end of file diff --git a/lib/features/mailbox/domain/exceptions/mailbox_exception.dart b/lib/features/mailbox/domain/exceptions/mailbox_exception.dart index 68c8ace281..de530621d2 100644 --- a/lib/features/mailbox/domain/exceptions/mailbox_exception.dart +++ b/lib/features/mailbox/domain/exceptions/mailbox_exception.dart @@ -4,3 +4,5 @@ class NotFoundInboxMailboxException implements Exception {} class NotFoundMailboxException implements Exception {} class NotFoundClearMailboxResponseException implements Exception {} + +class CannotMoveAllEmailException implements Exception {} diff --git a/lib/features/mailbox/domain/model/move_folder_content_request.dart b/lib/features/mailbox/domain/model/move_folder_content_request.dart new file mode 100644 index 0000000000..963b994a6d --- /dev/null +++ b/lib/features/mailbox/domain/model/move_folder_content_request.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; + +class MoveFolderContentRequest with EquatableMixin { + final MailboxId mailboxId; + final MoveAction moveAction; + final MailboxId destinationMailboxId; + final String destinationMailboxDisplayName; + final int totalEmails; + final bool markAsRead; + + MoveFolderContentRequest({ + required this.mailboxId, + required this.moveAction, + required this.destinationMailboxId, + required this.destinationMailboxDisplayName, + this.totalEmails = 0, + this.markAsRead = false, + }); + + @override + List get props => [ + mailboxId, + moveAction, + destinationMailboxId, + destinationMailboxDisplayName, + totalEmails, + markAsRead, + ]; +} diff --git a/lib/features/mailbox/domain/repository/mailbox_repository.dart b/lib/features/mailbox/domain/repository/mailbox_repository.dart index 4504c5fad5..8416e869ad 100644 --- a/lib/features/mailbox/domain/repository/mailbox_repository.dart +++ b/lib/features/mailbox/domain/repository/mailbox_repository.dart @@ -15,6 +15,7 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/get_mailbox_by_role_response.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_response.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/move_folder_content_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/move_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/rename_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_mailbox_request.dart'; @@ -41,6 +42,13 @@ abstract class MailboxRepository { Future moveMailbox(Session session, AccountId accountId, MoveMailboxRequest request); + Future moveFolderContent({ + required Session session, + required AccountId accountId, + required MoveFolderContentRequest request, + StreamController>? onProgressController, + }); + Future getMailboxState(Session session, AccountId accountId); Future subscribeMailbox(Session session, AccountId accountId, SubscribeMailboxRequest request); diff --git a/lib/features/mailbox/domain/state/move_folder_content_state.dart b/lib/features/mailbox/domain/state/move_folder_content_state.dart new file mode 100644 index 0000000000..b1084a87c7 --- /dev/null +++ b/lib/features/mailbox/domain/state/move_folder_content_state.dart @@ -0,0 +1,38 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/move_folder_content_request.dart'; + +class MovingFolderContent extends LoadingState {} + +class MoveFolderContentProgressState extends UIState { + final MailboxId mailboxId; + final int countEmailsCompleted; + final int totalEmails; + + MoveFolderContentProgressState( + this.mailboxId, + this.countEmailsCompleted, + this.totalEmails, + ); + + @override + List get props => [ + mailboxId, + countEmailsCompleted, + totalEmails, + ]; +} + +class MoveFolderContentSuccess extends UIState { + final MoveFolderContentRequest request; + + MoveFolderContentSuccess(this.request); + + @override + List get props => [request]; +} + +class MoveFolderContentFailure extends FeatureFailure { + MoveFolderContentFailure(dynamic exception) : super(exception: exception); +} diff --git a/lib/features/mailbox/domain/usecases/move_folder_content_interactor.dart b/lib/features/mailbox/domain/usecases/move_folder_content_interactor.dart new file mode 100644 index 0000000000..860f2f2932 --- /dev/null +++ b/lib/features/mailbox/domain/usecases/move_folder_content_interactor.dart @@ -0,0 +1,37 @@ +import 'dart:async'; + +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/session/session.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/move_folder_content_request.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/move_folder_content_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/move_mailbox_state.dart'; + +class MoveFolderContentInteractor { + final MailboxRepository _mailboxRepository; + + MoveFolderContentInteractor(this._mailboxRepository); + + Stream execute({ + required Session session, + required AccountId accountId, + required MoveFolderContentRequest request, + StreamController>? onProgressController, + }) async* { + try { + yield Right(MovingFolderContent()); + await _mailboxRepository.moveFolderContent( + session: session, + accountId: accountId, + request: request, + onProgressController: onProgressController, + ); + yield Right(MoveFolderContentSuccess(request)); + } catch (e) { + yield Left(MoveMailboxFailure(e)); + } + } +} diff --git a/lib/features/thread/data/extensions/list_email_extension.dart b/lib/features/thread/data/extensions/list_email_extension.dart index 05befc913d..57dadba708 100644 --- a/lib/features/thread/data/extensions/list_email_extension.dart +++ b/lib/features/thread/data/extensions/list_email_extension.dart @@ -61,4 +61,7 @@ extension ListEmailExtension on List { ), ).toList(); } + + List withoutEmailWithId(EmailId emailId) => + where((email) => email.id != emailId).toList(); } \ No newline at end of file diff --git a/lib/features/thread/data/network/thread_api.dart b/lib/features/thread/data/network/thread_api.dart index c9d3d2abb9..205f39acd1 100644 --- a/lib/features/thread/data/network/thread_api.dart +++ b/lib/features/thread/data/network/thread_api.dart @@ -21,7 +21,8 @@ import 'package:jmap_dart_client/jmap/mail/email/get/get_email_response.dart'; import 'package:jmap_dart_client/jmap/mail/email/query/query_email_method.dart'; import 'package:jmap_dart_client/jmap/mail/email/query/query_email_response.dart'; import 'package:model/extensions/list_id_extension.dart'; -import 'package:tmail_ui_user/features/thread/data/extensions/list_email_extension.dart'; +import 'package:tmail_ui_user/features/base/mixin/handle_error_mixin.dart'; +import 'package:tmail_ui_user/features/base/mixin/mail_api_mixin.dart'; import 'package:jmap_dart_client/jmap/mail/email/search_snippet/search_snippet.dart'; import 'package:jmap_dart_client/jmap/mail/email/search_snippet/search_snippet_get_method.dart'; import 'package:jmap_dart_client/jmap/mail/email/search_snippet/search_snippet_get_response.dart'; @@ -31,7 +32,7 @@ import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_emails_response.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; -class ThreadAPI { +class ThreadAPI with HandleSetErrorMixin, MailAPIMixin { final HttpClient httpClient; @@ -48,83 +49,18 @@ class ThreadAPI { Properties? properties } ) async { - final processingInvocation = ProcessingInvocation(); - - final jmapRequestBuilder = JmapRequestBuilder(httpClient, processingInvocation); - - final queryEmailMethod = QueryEmailMethod(accountId); - - if (limit != null) queryEmailMethod.addLimit(limit); - - if (position != null) queryEmailMethod.addPosition(position); - - if (sort != null) queryEmailMethod.addSorts(sort); - - if (filter != null) queryEmailMethod.addFilters(filter); - - final queryEmailInvocation = jmapRequestBuilder.invocation(queryEmailMethod); - - final getEmailMethod = GetEmailMethod(accountId); - - if (properties != null) getEmailMethod.addProperties(properties); - - getEmailMethod.addReferenceIds(processingInvocation.createResultReference( - queryEmailInvocation.methodCallId, - ReferencePath.idsPath)); - - final getEmailInvocation = jmapRequestBuilder.invocation(getEmailMethod); - - final capabilities = getEmailMethod.requiredCapabilities - .toCapabilitiesSupportTeamMailboxes(session, accountId); - - final result = await (jmapRequestBuilder - ..usings(capabilities)) - .build() - .execute(); - - final responseOfGetEmailMethod = result.parse( - getEmailInvocation.methodCallId, - GetEmailResponse.deserialize, - ); - - final responseOfQueryEmailMethod = result.parse( - queryEmailInvocation.methodCallId, - QueryEmailResponse.deserialize, - ); - - final emailList = sortEmails( - getEmailResponse: responseOfGetEmailMethod, - queryEmailResponse: responseOfQueryEmailMethod, - ); - - final notFoundEmailIds = responseOfGetEmailMethod - ?.notFound - ?.toEmailIds() - .toList(); - log('ThreadAPI::getAllEmail:notFoundEmailIds = ${notFoundEmailIds!.asListString.toString()} | NewState = ${responseOfGetEmailMethod?.state.value}'); - return EmailsResponse( - emailList: emailList, - notFoundEmailIds: notFoundEmailIds, - state: responseOfGetEmailMethod?.state, + return await fetchAllEmail( + httpClient: httpClient, + session: session, + accountId: accountId, + limit: limit, + position: position, + sort: sort, + filter: filter, + properties: properties, ); } - List? sortEmails({ - GetEmailResponse? getEmailResponse, - QueryEmailResponse? queryEmailResponse, - }) { - final listEmails = getEmailResponse?.list; - final listIds = queryEmailResponse?.ids.toList(); - - if (listEmails?.isNotEmpty != true || listIds?.isNotEmpty != true) { - return listEmails; - } - - final listSortedEmails = listEmails!.sortEmailsById(listIds!); - - return listSortedEmails; - } - Future searchEmails( Session session, AccountId accountId, diff --git a/model/lib/extensions/list_email_id_extension.dart b/model/lib/extensions/list_email_id_extension.dart index 26d72e4fe6..fbb844c996 100644 --- a/model/lib/extensions/list_email_id_extension.dart +++ b/model/lib/extensions/list_email_id_extension.dart @@ -16,10 +16,17 @@ extension ListEmailIdExtension on List { }; } - Map generateMapUpdateObjectMoveToMailbox(MailboxId currentMailboxId, MailboxId destinationMailboxId) { + Map generateMapUpdateObjectMoveToMailbox({ + required MailboxId currentMailboxId, + required MailboxId destinationMailboxId, + bool markAsRead = false, + }) { return { for (var emailId in this) - emailId.id: currentMailboxId.generateMoveToMailboxActionPath(destinationMailboxId) + emailId.id: currentMailboxId.generateMoveToMailboxActionPath( + destinationMailboxId: destinationMailboxId, + markAsRead: markAsRead, + ) }; } @@ -30,13 +37,6 @@ extension ListEmailIdExtension on List { }; } - Map generateMapUpdateObjectMoveToSpam(MailboxId currentMailboxId, MailboxId spamMailboxId) { - return { - for (var emailId in this) - emailId.id: currentMailboxId.generateMoveToSpamActionPath(currentMailboxId, spamMailboxId) - }; - } - Map generateMapUpdateObjectMarkAsAnswered() { return { for (var emailId in this) diff --git a/model/lib/extensions/mailbox_id_extension.dart b/model/lib/extensions/mailbox_id_extension.dart index 5cfd5e7a31..0b701caf59 100644 --- a/model/lib/extensions/mailbox_id_extension.dart +++ b/model/lib/extensions/mailbox_id_extension.dart @@ -13,10 +13,14 @@ extension MailboxIdExtension on MailboxId { } } - PatchObject generateMoveToMailboxActionPath(MailboxId destinationMailboxId) { + PatchObject generateMoveToMailboxActionPath({ + required MailboxId destinationMailboxId, + bool markAsRead = false, + }) { return PatchObject({ generatePath(): null, - destinationMailboxId.generatePath(): true + destinationMailboxId.generatePath(): true, + if (markAsRead) KeyWordIdentifier.emailSeen.generatePath(): true, }); } @@ -26,13 +30,5 @@ extension MailboxIdExtension on MailboxId { }); } - PatchObject generateMoveToSpamActionPath(MailboxId currentMailboxId, MailboxId spamMailboxId) { - return PatchObject({ - currentMailboxId.generatePath(): null, - spamMailboxId.generatePath(): true, - KeyWordIdentifier.emailSeen.generatePath(): true - }); - } - String get asString => id.value; } \ No newline at end of file From 9cd067b884edfe10029874fa3fc5e6093ba5f2c7 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 29 Oct 2025 13:23:12 +0700 Subject: [PATCH 3/7] TF-4053 Remove method is not used --- .../presentation/mailbox_controller.dart | 92 +------------------ 1 file changed, 2 insertions(+), 90 deletions(-) diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 9cbf0b7713..ad2c2d5575 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -21,7 +21,6 @@ import 'package:tmail_ui_user/features/base/extensions/handle_mailbox_action_typ import 'package:tmail_ui_user/features/base/mixin/contact_support_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/launcher_application_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/mailbox_action_handler_mixin.dart'; -import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_manager.dart'; import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; @@ -32,15 +31,15 @@ import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state. import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; import 'package:tmail_ui_user/features/mailbox/domain/constants/mailbox_constants.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/exceptions/set_mailbox_name_exception.dart'; import 'package:tmail_ui_user/features/mailbox/domain/exceptions/null_session_or_accountid_exception.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/exceptions/set_mailbox_name_exception.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_right_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_subaddressing_action.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_subscribe_action_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_subscribe_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/move_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/rename_mailbox_request.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_right_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_multiple_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_request.dart'; @@ -952,44 +951,6 @@ class MailboxController extends BaseMailboxController .toList(); } - void pressMailboxSelectionAction( - BuildContext context, - MailboxActions actions, - List selectedMailboxList - ) { - switch(actions) { - case MailboxActions.delete: - _openConfirmationDialogDeleteMultipleMailboxAction(context, selectedMailboxList); - break; - case MailboxActions.rename: - openDialogRenameMailboxAction( - context, - selectedMailboxList.first, - responsiveUtils, - onRenameMailboxAction: _renameMailboxAction - ); - break; - case MailboxActions.markAsRead: - markAsReadMailboxAction( - context, - selectedMailboxList.first, - mailboxDashBoardController, - onCallbackAction: closeMailboxScreen - ); - break; - case MailboxActions.move: - moveMailboxAction( - context, - selectedMailboxList.first, - mailboxDashBoardController, - onMovingMailboxAction: (mailboxSelected, destinationMailbox) => _invokeMovingMailboxAction(context, mailboxSelected, destinationMailbox) - ); - break; - default: - break; - } - } - void _deleteMailboxAction(PresentationMailbox presentationMailbox) { if (session != null && accountId != null) { final tupleMap = MailboxUtils.generateMapDescendantIdsAndMailboxIdList( @@ -1028,55 +989,6 @@ class MailboxController extends BaseMailboxController } } - void _openConfirmationDialogDeleteMultipleMailboxAction( - BuildContext context, - List selectedMailboxList - ) { - if (responsiveUtils.isLandscapeMobile(context) || - responsiveUtils.isPortraitMobile(context)) { - (ConfirmationDialogActionSheetBuilder(context) - ..messageText(AppLocalizations.of(context) - .messageConfirmationDialogDeleteMultipleFolder(selectedMailboxList.length)) - ..onCancelAction(AppLocalizations.of(context).cancel, () => - popBack()) - ..onConfirmAction(AppLocalizations.of(context).delete, () => - _deleteMultipleMailboxAction(selectedMailboxList))) - .show(); - } else { - MessageDialogActionManager().showConfirmDialogAction( - key: const Key('confirm_dialog_delete_multiple_mailbox'), - context, - title: AppLocalizations.of(context).deleteFolders, - AppLocalizations.of(context).messageConfirmationDialogDeleteMultipleFolder(selectedMailboxList.length), - cancelTitle: AppLocalizations.of(context).cancel, - AppLocalizations.of(context).delete, - onConfirmAction: () => _deleteMultipleMailboxAction(selectedMailboxList), - onCloseButtonAction: popBack, - ); - } - } - - void _deleteMultipleMailboxAction(List selectedMailboxList) { - if (session != null && accountId != null) { - final tupleMap = MailboxUtils.generateMapDescendantIdsAndMailboxIdList( - selectedMailboxList, - defaultMailboxTree.value, - personalMailboxTree.value); - final mapDescendantIds = tupleMap.value1; - final listMailboxId = tupleMap.value2; - consumeState(_deleteMultipleMailboxInteractor.execute( - session!, - accountId!, - mapDescendantIds, - listMailboxId, - )); - } else { - _deleteMailboxFailure(DeleteMultipleMailboxFailure(null)); - } - - popBack(); - } - void _switchBackToMailboxDefault() { final inboxMailbox = findMailboxNodeByRole(PresentationMailbox.roleInbox); mailboxDashBoardController.setSelectedMailbox(inboxMailbox?.item); From 46e155dc63d16cbeff6046c3ce59c75daae4d092 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 29 Oct 2025 14:56:07 +0700 Subject: [PATCH 4/7] TF-4053 Implement move folder content on mailbox list view --- .../base/base_mailbox_controller.dart | 33 ++++++ .../destination_picker_controller.dart | 12 +- .../presentation/destination_picker_view.dart | 6 +- ...on_picker_search_mailbox_item_builder.dart | 3 +- .../move_folder_content_interactor.dart | 6 +- .../handle_move_folder_content_extension.dart | 108 ++++++++++++++++++ .../presentation/mailbox_bindings.dart | 3 + .../presentation/mailbox_controller.dart | 22 +++- .../presentation/model/mailbox_actions.dart | 1 + .../utils/mailbox_action_reactor.dart | 29 +++++ .../mailbox_dashboard_controller.dart | 35 +++--- .../mark_mailbox_as_read_loading_banner.dart | 6 + .../thread/presentation/thread_view.dart | 8 ++ lib/l10n/intl_messages.arb | 6 + lib/main/bindings/core/core_bindings.dart | 2 +- lib/main/localizations/app_localizations.dart | 7 ++ lib/main/utils/toast_manager.dart | 45 +++++++- .../mailbox_dashboard_controller_test.dart | 5 + .../mailbox_dashboard_view_widget_test.dart | 4 + 19 files changed, 313 insertions(+), 28 deletions(-) create mode 100644 lib/features/mailbox/presentation/extensions/handle_move_folder_content_extension.dart create mode 100644 lib/features/mailbox/presentation/utils/mailbox_action_reactor.dart diff --git a/lib/features/base/base_mailbox_controller.dart b/lib/features/base/base_mailbox_controller.dart index b71e66b60f..570eaeca94 100644 --- a/lib/features/base/base_mailbox_controller.dart +++ b/lib/features/base/base_mailbox_controller.dart @@ -53,6 +53,10 @@ import 'package:tmail_ui_user/main/routes/route_navigation.dart'; typedef RenameMailboxActionCallback = void Function(PresentationMailbox mailbox, MailboxName newMailboxName); typedef MovingMailboxActionCallback = void Function(PresentationMailbox mailboxSelected, PresentationMailbox? destinationMailbox); +typedef OnMoveFolderContentActionCallback = void Function( + PresentationMailbox currentMailbox, + PresentationMailbox destinationMailbox, +); typedef DeleteMailboxActionCallback = void Function(PresentationMailbox mailbox); typedef AllowSubaddressingActionCallback = void Function(MailboxId, Map?>?, MailboxActions); @@ -637,4 +641,33 @@ abstract class BaseMailboxController extends BaseController triggerScrollWhenExpandFolder(newExpandMode, itemKey, scrollController); } } + + void moveFolderContentAction({ + required BuildContext context, + required AccountId accountId, + required Session session, + required PresentationMailbox mailboxSelected, + required OnMoveFolderContentActionCallback onMoveFolderContentAction, + }) async { + final arguments = DestinationPickerArguments( + accountId, + MailboxActions.moveFolderContent, + session, + mailboxIdSelected: mailboxSelected.id, + ); + + final destinationMailbox = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog( + routeName: AppRoutes.destinationPicker, + arguments: arguments, + ) + : await push(AppRoutes.destinationPicker, arguments: arguments); + if (destinationMailbox is PresentationMailbox) { + log('$runtimeType::moveFolderContentAction: DestinationMailbox is ${destinationMailbox.name?.name}'); + onMoveFolderContentAction( + mailboxSelected, + destinationMailbox, + ); + } + } } \ No newline at end of file diff --git a/lib/features/destination_picker/presentation/destination_picker_controller.dart b/lib/features/destination_picker/presentation/destination_picker_controller.dart index 6d80add262..1f67631d9f 100644 --- a/lib/features/destination_picker/presentation/destination_picker_controller.dart +++ b/lib/features/destination_picker/presentation/destination_picker_controller.dart @@ -103,7 +103,9 @@ class DestinationPickerController extends BaseMailboxController { void handleSuccessViewState(Success success) async { super.handleSuccessViewState(success); if (success is GetAllMailboxSuccess) { - if (mailboxAction.value == MailboxActions.move && mailboxIdSelected != null) { + if ((mailboxAction.value == MailboxActions.move || + mailboxAction.value == MailboxActions.moveFolderContent) && + mailboxIdSelected != null) { await buildTree( success.mailboxList.listSubscribedMailboxesAndDefaultMailboxes, mailboxIdSelected: mailboxIdSelected); @@ -249,9 +251,11 @@ class DestinationPickerController extends BaseMailboxController { void searchMailbox(BuildContext context, String value) { searchQuery.value = SearchQuery(value); - final searchableMailboxList = mailboxAction.value == MailboxActions.moveEmail - ? allMailboxes - : allMailboxes.listPersonalMailboxes; + final searchableMailboxList = + mailboxAction.value == MailboxActions.moveEmail || + mailboxAction.value == MailboxActions.moveFolderContent + ? allMailboxes + : allMailboxes.listPersonalMailboxes; final mailboxListWithDisplayName = searchableMailboxList .map((mailbox) => mailbox.withDisplayName(mailbox.getDisplayName(context))) diff --git a/lib/features/destination_picker/presentation/destination_picker_view.dart b/lib/features/destination_picker/presentation/destination_picker_view.dart index 8094faeca6..eec4ab2dcc 100644 --- a/lib/features/destination_picker/presentation/destination_picker_view.dart +++ b/lib/features/destination_picker/presentation/destination_picker_view.dart @@ -296,8 +296,10 @@ class DestinationPickerView extends GetWidget } }), Obx(() { - if (controller.teamMailboxesIsNotEmpty - && controller.mailboxAction.value == MailboxActions.moveEmail) { + if (controller.teamMailboxesIsNotEmpty && + (controller.mailboxAction.value == MailboxActions.moveEmail || + controller.mailboxAction.value == + MailboxActions.moveFolderContent)) { return Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/features/destination_picker/presentation/widgets/destination_picker_search_mailbox_item_builder.dart b/lib/features/destination_picker/presentation/widgets/destination_picker_search_mailbox_item_builder.dart index a7c8a9af3b..4173720533 100644 --- a/lib/features/destination_picker/presentation/widgets/destination_picker_search_mailbox_item_builder.dart +++ b/lib/features/destination_picker/presentation/widgets/destination_picker_search_mailbox_item_builder.dart @@ -158,6 +158,7 @@ class DestinationPickerSearchMailboxItemBuilder extends StatelessWidget { ( mailboxActions == MailboxActions.select || mailboxActions == MailboxActions.create || - mailboxActions == MailboxActions.moveEmail + mailboxActions == MailboxActions.moveEmail || + mailboxActions == MailboxActions.moveFolderContent ); } \ No newline at end of file diff --git a/lib/features/mailbox/domain/usecases/move_folder_content_interactor.dart b/lib/features/mailbox/domain/usecases/move_folder_content_interactor.dart index 860f2f2932..1bfb7d7ddc 100644 --- a/lib/features/mailbox/domain/usecases/move_folder_content_interactor.dart +++ b/lib/features/mailbox/domain/usecases/move_folder_content_interactor.dart @@ -8,14 +8,13 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/move_folder_content_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/move_folder_content_state.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/state/move_mailbox_state.dart'; class MoveFolderContentInteractor { final MailboxRepository _mailboxRepository; MoveFolderContentInteractor(this._mailboxRepository); - Stream execute({ + Stream> execute({ required Session session, required AccountId accountId, required MoveFolderContentRequest request, @@ -23,6 +22,7 @@ class MoveFolderContentInteractor { }) async* { try { yield Right(MovingFolderContent()); + onProgressController?.add(Right(MovingFolderContent())); await _mailboxRepository.moveFolderContent( session: session, accountId: accountId, @@ -31,7 +31,7 @@ class MoveFolderContentInteractor { ); yield Right(MoveFolderContentSuccess(request)); } catch (e) { - yield Left(MoveMailboxFailure(e)); + yield Left(MoveFolderContentFailure(e)); } } } diff --git a/lib/features/mailbox/presentation/extensions/handle_move_folder_content_extension.dart b/lib/features/mailbox/presentation/extensions/handle_move_folder_content_extension.dart new file mode 100644 index 0000000000..f6bb1fffed --- /dev/null +++ b/lib/features/mailbox/presentation/extensions/handle_move_folder_content_extension.dart @@ -0,0 +1,108 @@ +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; +import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/move_folder_content_request.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/move_folder_content_state.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/mailbox_controller.dart'; + +extension HandleMoveFolderContentExtension on MailboxController { + void performMoveFolderContent({ + required BuildContext context, + required PresentationMailbox mailboxSelected, + }) { + final accountId = mailboxDashBoardController.accountId.value; + final session = mailboxDashBoardController.sessionCurrent; + + if (accountId == null || session == null) { + consumeState( + Stream.value( + Left(MoveFolderContentFailure(NotFoundAccountIdException())), + ), + ); + return; + } + + moveFolderContentAction( + context: context, + accountId: accountId, + session: session, + mailboxSelected: mailboxSelected, + onMoveFolderContentAction: (currentMailbox, destinationMailbox) { + consumeState(mailboxActionReactor.moveFolderContent( + session: session, + accountId: accountId, + moveRequest: MoveFolderContentRequest( + moveAction: MoveAction.moving, + mailboxId: currentMailbox.id, + destinationMailboxId: destinationMailbox.id, + destinationMailboxDisplayName: + destinationMailbox.getDisplayName(context), + markAsRead: destinationMailbox.isSpam, + totalEmails: currentMailbox.countTotalEmails, + ), + onProgressController: + mailboxDashBoardController.progressStateController, + )); + }, + ); + } + + void handleMoveFolderContentSuccess(MoveFolderContentSuccess success) { + mailboxDashBoardController.syncViewStateMailboxActionProgress( + newState: Right(UIState.idle), + ); + final moveFolderRequest = success.request; + + if (moveFolderRequest.moveAction == MoveAction.moving) { + toastManager.showMessageSuccessWithAction( + success: success, + onActionCallback: () { + _undoMoveFolderContentAction( + newMoveRequest: MoveFolderContentRequest( + moveAction: MoveAction.undo, + mailboxId: moveFolderRequest.destinationMailboxId, + destinationMailboxId: moveFolderRequest.mailboxId, + destinationMailboxDisplayName: '', + totalEmails: moveFolderRequest.totalEmails, + ), + ); + }, + ); + } + } + + void _undoMoveFolderContentAction({ + required MoveFolderContentRequest newMoveRequest, + }) { + final accountId = mailboxDashBoardController.accountId.value; + final session = mailboxDashBoardController.sessionCurrent; + + if (accountId == null || session == null) { + consumeState( + Stream.value( + Left(MoveFolderContentFailure(NotFoundAccountIdException())), + ), + ); + return; + } + + consumeState(mailboxActionReactor.moveFolderContent( + session: session, + accountId: accountId, + moveRequest: newMoveRequest, + onProgressController: mailboxDashBoardController.progressStateController, + )); + } + + void handleMoveFolderContentFailure(MoveFolderContentFailure failure) { + mailboxDashBoardController.syncViewStateMailboxActionProgress( + newState: Right(UIState.idle), + ); + toastManager.showMessageFailure(failure); + } +} diff --git a/lib/features/mailbox/presentation/mailbox_bindings.dart b/lib/features/mailbox/presentation/mailbox_bindings.dart index 4af5019990..152112a371 100644 --- a/lib/features/mailbox/presentation/mailbox_bindings.dart +++ b/lib/features/mailbox/presentation/mailbox_bindings.dart @@ -20,6 +20,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/usecases/create_new_defaul import 'package:tmail_ui_user/features/mailbox/domain/usecases/create_new_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/delete_multiple_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/move_folder_content_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/move_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/refresh_all_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/rename_mailbox_interactor.dart'; @@ -60,6 +61,7 @@ class MailboxBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), Get.find(), Get.find(), Get.find(), @@ -111,6 +113,7 @@ class MailboxBindings extends BaseBindings { Get.lazyPut(() => SubscribeMultipleMailboxInteractor(Get.find())); Get.lazyPut(() => SubaddressingInteractor(Get.find())); Get.lazyPut(() => CreateDefaultMailboxInteractor(Get.find())); + Get.lazyPut(() => MoveFolderContentInteractor(Get.find())); } @override diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index ad2c2d5575..e32019be06 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -49,6 +49,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/state/create_new_mailbox_s import 'package:tmail_ui_user/features/mailbox/domain/state/delete_multiple_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/get_all_mailboxes_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/move_folder_content_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/move_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/refresh_all_mailboxes_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/refresh_changes_all_mailboxes_state.dart'; @@ -60,6 +61,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/usecases/create_new_defaul import 'package:tmail_ui_user/features/mailbox/domain/usecases/create_new_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/delete_multiple_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/move_folder_content_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/move_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/refresh_all_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/rename_mailbox_interactor.dart'; @@ -67,6 +69,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/usecases/subaddressing_int import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_multiple_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_action.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/handle_move_folder_content_extension.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; @@ -74,6 +77,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_action_reactor.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'; @@ -116,8 +120,10 @@ class MailboxController extends BaseMailboxController final SubscribeMultipleMailboxInteractor _subscribeMultipleMailboxInteractor; final SubaddressingInteractor _subaddressingInteractor; final CreateDefaultMailboxInteractor _createDefaultMailboxInteractor; + final MoveFolderContentInteractor _moveFolderContentInteractor; IOSSharingManager? _iosSharingManager; + late MailboxActionReactor mailboxActionReactor; final _activeScrollTop = RxBool(false); final _activeScrollBottom = RxBool(true); @@ -147,6 +153,7 @@ class MailboxController extends BaseMailboxController this._subscribeMultipleMailboxInteractor, this._subaddressingInteractor, this._createDefaultMailboxInteractor, + this._moveFolderContentInteractor, TreeBuilder treeBuilder, VerifyNameInteractor verifyNameInteractor, GetAllMailboxInteractor getAllMailboxInteractor, @@ -162,6 +169,9 @@ class MailboxController extends BaseMailboxController void onInit() { _registerObxStreamListener(); _initWebSocketQueueHandler(); + mailboxActionReactor = MailboxActionReactor( + _moveFolderContentInteractor, + ); super.onInit(); } @@ -186,7 +196,6 @@ class MailboxController extends BaseMailboxController @override void handleSuccessViewState(Success success) { - super.handleSuccessViewState(success); if (success is GetAllMailboxSuccess) { _handleGetAllMailboxSuccess(success); } else if (success is CreateNewMailboxSuccess) { @@ -209,12 +218,15 @@ class MailboxController extends BaseMailboxController handleSubAddressingSuccess(success); } else if (success is CreateDefaultMailboxAllSuccess) { _handleCreateDefaultFolderIfMissingSuccess(success); + } else if (success is MoveFolderContentSuccess) { + handleMoveFolderContentSuccess(success); + } else { + super.handleSuccessViewState(success); } } @override void handleFailureViewState(Failure failure) { - super.handleFailureViewState(failure); if (failure is CreateNewMailboxFailure) { _createNewMailboxFailure(failure); } else if (failure is RenameMailboxFailure) { @@ -223,6 +235,10 @@ class MailboxController extends BaseMailboxController _deleteMailboxFailure(failure); } else if (failure is SubaddressingFailure) { handleSubAddressingFailure(failure); + } else if (failure is MoveFolderContentFailure) { + handleMoveFolderContentFailure(failure); + } else { + super.handleFailureViewState(failure); } } @@ -1178,6 +1194,8 @@ class MailboxController extends BaseMailboxController mailboxDashBoardController.gotoEmailRecovery(); break; case MailboxActions.moveFolderContent: + performMoveFolderContent(context: context, mailboxSelected: mailbox); + mailboxDashBoardController.closeMailboxMenuDrawer(); break; default: break; diff --git a/lib/features/mailbox/presentation/model/mailbox_actions.dart b/lib/features/mailbox/presentation/model/mailbox_actions.dart index 38f56b02c4..27e78c6772 100644 --- a/lib/features/mailbox/presentation/model/mailbox_actions.dart +++ b/lib/features/mailbox/presentation/model/mailbox_actions.dart @@ -37,6 +37,7 @@ extension MailboxActionsExtension on MailboxActions { return AppLocalizations.of(context).selectParentFolder; case MailboxActions.moveEmail: case MailboxActions.move: + case MailboxActions.moveFolderContent: return AppLocalizations.of(context).moveTo; case MailboxActions.select: case MailboxActions.selectForRuleAction: diff --git a/lib/features/mailbox/presentation/utils/mailbox_action_reactor.dart b/lib/features/mailbox/presentation/utils/mailbox_action_reactor.dart new file mode 100644 index 0000000000..4571b433ea --- /dev/null +++ b/lib/features/mailbox/presentation/utils/mailbox_action_reactor.dart @@ -0,0 +1,29 @@ +import 'dart:async'; + +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/session/session.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/move_folder_content_request.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/move_folder_content_interactor.dart'; + +class MailboxActionReactor { + MailboxActionReactor(this._moveFolderContentInteractor); + + final MoveFolderContentInteractor _moveFolderContentInteractor; + + Stream> moveFolderContent({ + required Session session, + required AccountId accountId, + required MoveFolderContentRequest moveRequest, + StreamController>? onProgressController, + }) { + return _moveFolderContentInteractor.execute( + session: session, + accountId: accountId, + request: moveRequest, + onProgressController: onProgressController, + ); + } +} 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 17a8ea63e3..61c1695e7c 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -317,9 +317,9 @@ class MailboxDashBoardController extends ReloadableController Worker? searchInputFocusWorker; Worker? _downloadUIActionWorker; - final StreamController> _progressStateController = + final StreamController> progressStateController = StreamController>.broadcast(); - Stream> get progressState => _progressStateController.stream; + Stream> get progressState => progressStateController.stream; final StreamController _refreshActionEventController = StreamController.broadcast(); @@ -743,7 +743,8 @@ class MailboxDashBoardController extends ReloadableController void _registerStreamListener() { progressState.listen((state) { - viewStateMailboxActionProgress.value = state; + log('$runtimeType::_registerStreamListener: ViewStateMailboxActionProgress = ${state.runtimeType}'); + syncViewStateMailboxActionProgress(newState: state); }); _refreshActionEventController.stream @@ -1760,13 +1761,19 @@ class MailboxDashBoardController extends ReloadableController accountId, trashMailboxId, totalEmailsInTrash, - _progressStateController, + progressStateController, )); } } + void syncViewStateMailboxActionProgress({ + required Either newState, + }) { + viewStateMailboxActionProgress.value = newState; + } + void _emptyTrashFolderSuccess(EmptyTrashFolderSuccess success) { - viewStateMailboxActionProgress.value = Right(UIState.idle); + syncViewStateMailboxActionProgress(newState: Right(UIState.idle)); handleDeleteEmailsInMailbox( emailIds: success.emailIds, @@ -1902,11 +1909,11 @@ class MailboxDashBoardController extends ReloadableController mailboxId, mailboxDisplayName, totalEmailsUnread, - _progressStateController)); + progressStateController)); } void _markAsReadMailboxSuccess(Success success) { - viewStateMailboxActionProgress.value = Right(UIState.idle); + syncViewStateMailboxActionProgress(newState: Right(UIState.idle)); if (success is MarkAsMailboxReadAllSuccess) { if (currentContext != null && currentOverlayContext != null) { @@ -1926,7 +1933,7 @@ class MailboxDashBoardController extends ReloadableController } void _markAsReadMailboxFailure(MarkAsMailboxReadFailure failure) { - viewStateMailboxActionProgress.value = Right(UIState.idle); + syncViewStateMailboxActionProgress(newState: Right(UIState.idle)); if (currentOverlayContext != null && currentContext != null) { appToast.showToastErrorMessage( currentOverlayContext!, @@ -1938,7 +1945,7 @@ class MailboxDashBoardController extends ReloadableController } void _markAsReadMailboxAllFailure(MarkAsMailboxReadAllFailure failure) { - viewStateMailboxActionProgress.value = Right(UIState.idle); + syncViewStateMailboxActionProgress(newState: Right(UIState.idle)); if (currentOverlayContext != null && currentContext != null) { appToast.showToastErrorMessage( currentOverlayContext!, @@ -2694,13 +2701,13 @@ class MailboxDashBoardController extends ReloadableController accountId, spamFolderId, totalEmails, - _progressStateController, + progressStateController, )); } } void _emptySpamFolderSuccess(EmptySpamFolderSuccess success) { - viewStateMailboxActionProgress.value = Right(UIState.idle); + syncViewStateMailboxActionProgress(newState: Right(UIState.idle)); handleDeleteEmailsInMailbox( emailIds: success.emailIds, @@ -3108,13 +3115,13 @@ class MailboxDashBoardController extends ReloadableController } void _handleEmptySpamFolderFailure(EmptySpamFolderFailure failure) { - viewStateMailboxActionProgress.value = Right(UIState.idle); + syncViewStateMailboxActionProgress(newState: Right(UIState.idle)); toastManager.showMessageFailure(failure); } void _handleEmptyTrashFolderFailure(EmptyTrashFolderFailure failure) { - viewStateMailboxActionProgress.value = Right(UIState.idle); + syncViewStateMailboxActionProgress(newState: Right(UIState.idle)); toastManager.showMessageFailure(failure); } @@ -3325,7 +3332,7 @@ class MailboxDashBoardController extends ReloadableController _emailReceiveManager.closeEmailReceiveManagerStream(); _deepLinkDataStreamSubscription?.cancel(); } - _progressStateController.close(); + progressStateController.close(); _refreshActionEventController.close(); _notificationManager.closeStream(); _fcmService.closeStream(); diff --git a/lib/features/mailbox_dashboard/presentation/widgets/mark_mailbox_as_read_loading_banner.dart b/lib/features/mailbox_dashboard/presentation/widgets/mark_mailbox_as_read_loading_banner.dart index f8f1667825..90f4a45813 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/mark_mailbox_as_read_loading_banner.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/mark_mailbox_as_read_loading_banner.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/clear_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/move_folder_content_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/mark_mailbox_as_read_loading_banner_style.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; @@ -29,6 +30,11 @@ class MarkMailboxAsReadLoadingBanner extends StatelessWidget with AppLoaderMixin return _buildProgressBanner(success.countRead, success.totalUnread); } else if (success is EmptyingFolderState) { return _buildProgressBanner(success.countEmailsDeleted, success.totalEmails); + } else if (success is MoveFolderContentProgressState) { + return _buildProgressBanner( + success.countEmailsCompleted, + success.totalEmails, + ); } else { return const SizedBox.shrink(); } diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index 35ca8832a7..285c2f80bf 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -18,6 +18,7 @@ import 'package:tmail_ui_user/features/email/presentation/model/context_item_ema import 'package:tmail_ui_user/features/email/presentation/model/popup_menu_item_email_action.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/clear_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/move_folder_content_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/spam_report_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_open_context_menu_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/open_and_close_composer_extension.dart'; @@ -793,6 +794,7 @@ class _MailboxActionProgressBanner extends StatelessWidget with AppLoaderMixin { if (success is MarkAsMailboxReadLoading || success is EmptySpamFolderLoading || success is EmptyTrashFolderLoading || + success is MovingFolderContent || success is ClearingMailbox) { return Padding( padding: EdgeInsets.only( @@ -815,6 +817,12 @@ class _MailboxActionProgressBanner extends StatelessWidget with AppLoaderMixin { success.countEmailsDeleted, success.totalEmails, ); + } else if (success is MoveFolderContentProgressState) { + return _buildProgressBanner( + context, + success.countEmailsCompleted, + success.totalEmails, + ); } return const SizedBox.shrink(); }, diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 083dc48998..9dd41e721a 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -5057,5 +5057,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "moveFolderContentToastMessage": "Failed to move all emails from this folder to another folder.", + "@moveFolderContentToastMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/bindings/core/core_bindings.dart b/lib/main/bindings/core/core_bindings.dart index 639af730e8..b0cab9cbe6 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -54,7 +54,7 @@ class CoreBindings extends Bindings { void _bindingToast() { Get.put(AppToast()); - Get.put(ToastManager(Get.find())); + Get.put(ToastManager(Get.find(), Get.find())); } void _bindingDeviceManager() { diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 951bbad18a..89158e39f2 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -5352,4 +5352,11 @@ class AppLocalizations { name: 'moveFolderContent', ); } + + String get moveFolderContentToastMessage { + return Intl.message( + 'Failed to move all emails from this folder to another folder.', + name: 'moveFolderContentToastMessage', + ); + } } diff --git a/lib/main/utils/toast_manager.dart b/lib/main/utils/toast_manager.dart index 6fb0ac5257..3d97c0996c 100644 --- a/lib/main/utils/toast_manager.dart +++ b/lib/main/utils/toast_manager.dart @@ -2,12 +2,16 @@ import 'dart:io'; import 'package:core/domain/exceptions/file_exception.dart'; import 'package:core/domain/exceptions/web_session_exception.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_appauth_web/authorization_exception.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart'; import 'package:jmap_dart_client/jmap/core/error/method/exception/error_method_response_exception.dart'; import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; @@ -27,6 +31,7 @@ import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/oauth_authorization_error.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/clear_mailbox_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/move_folder_content_state.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/create_new_rule_filter_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/delete_recipient_in_forwarding_state.dart'; @@ -45,8 +50,9 @@ import 'package:tmail_ui_user/main/routes/route_navigation.dart'; class ToastManager { final AppToast appToast; + final ImagePaths imagePaths; - ToastManager(this.appToast); + ToastManager(this.appToast, this.imagePaths); String getDefaultMessageByException( AppLocalizations appLocalizations, @@ -187,6 +193,9 @@ class ToastManager { } else if (failure is CalendarEventReplyFailure) { message = message ?? appLocalizations.eventReplyWasSentUnsuccessfully; + } else if (failure is MoveFolderContentFailure) { + message = message ?? + appLocalizations.moveFolderContentToastMessage; } log('ToastManager::showMessageFailure: Message: $message'); if (message?.trim().isNotEmpty == true) { @@ -256,4 +265,38 @@ class ToastManager { appToast.showToastSuccessMessage(overlayContext, message!); } } + + void showMessageSuccessWithAction({ + required Success success, + required VoidCallback onActionCallback, + }) { + final context = currentContext; + final overlayContext = currentOverlayContext; + if (context == null || overlayContext == null) { + logError('$runtimeType::showMessageSuccessWithAction: Context or OverlayContext is null'); + return; + } + + String? message; + final appLocalizations = AppLocalizations.of(context); + if (success is MoveFolderContentSuccess) { + message = appLocalizations.movedToFolder( + success.request.destinationMailboxDisplayName, + ); + } + log('$runtimeType::showMessageSuccessWithAction: Message: $message'); + if (message?.trim().isNotEmpty == true) { + appToast.showToastMessage( + overlayContext, + message!, + actionName: appLocalizations.undo, + onActionClick: onActionCallback, + leadingSVGIcon: imagePaths.icFolderMailbox, + leadingSVGIconColor: Colors.white, + backgroundColor: AppColor.toastSuccessBackgroundColor, + textColor: Colors.white, + actionIcon: SvgPicture.asset(imagePaths.icUndo), + ); + } + } } diff --git a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart index bcd2152ad2..0d5cebfedf 100644 --- a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart +++ b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart @@ -49,6 +49,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/usecases/create_new_mailbo import 'package:tmail_ui_user/features/mailbox/domain/usecases/delete_multiple_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/move_folder_content_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/move_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/refresh_all_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/rename_mailbox_interactor.dart'; @@ -165,6 +166,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), MockSpec(), MockSpec(), MockSpec(), @@ -280,6 +282,7 @@ void main() { final subscribeMultipleMailboxInteractor = MockSubscribeMultipleMailboxInteractor(); final subaddressingInteractor = MockSubaddressingInteractor(); final createDefaultMailboxInteractor = MockCreateDefaultMailboxInteractor(); + final moveFolderContentInteractor = MockMoveFolderContentInteractor(); final treeBuilder = MockTreeBuilder(); final verifyNameInteractor = MockVerifyNameInteractor(); final getAllMailboxInteractor = MockGetAllMailboxInteractor(); @@ -411,6 +414,7 @@ void main() { subscribeMultipleMailboxInteractor, subaddressingInteractor, createDefaultMailboxInteractor, + moveFolderContentInteractor, treeBuilder, verifyNameInteractor, getAllMailboxInteractor, @@ -647,6 +651,7 @@ void main() { subscribeMultipleMailboxInteractor, subaddressingInteractor, createDefaultMailboxInteractor, + moveFolderContentInteractor, treeBuilder, verifyNameInteractor, getAllMailboxInteractor, diff --git a/test/features/mailbox_dashboard/presentation/view/mailbox_dashboard_view_widget_test.dart b/test/features/mailbox_dashboard/presentation/view/mailbox_dashboard_view_widget_test.dart index 997c2a9a1a..4177c2492e 100644 --- a/test/features/mailbox_dashboard/presentation/view/mailbox_dashboard_view_widget_test.dart +++ b/test/features/mailbox_dashboard/presentation/view/mailbox_dashboard_view_widget_test.dart @@ -46,6 +46,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/usecases/create_new_mailbo import 'package:tmail_ui_user/features/mailbox/domain/usecases/delete_multiple_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/move_folder_content_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/move_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/refresh_all_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/rename_mailbox_interactor.dart'; @@ -166,6 +167,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), MockSpec(), MockSpec(), MockSpec(), @@ -266,6 +268,7 @@ void main() { final subscribeMultipleMailboxInteractor = MockSubscribeMultipleMailboxInteractor(); final subaddressingInteractor = MockSubaddressingInteractor(); final createDefaultMailboxInteractor = MockCreateDefaultMailboxInteractor(); + final moveFolderContentInteractor = MockMoveFolderContentInteractor(); final treeBuilder = MockTreeBuilder(); final verifyNameInteractor = MockVerifyNameInteractor(); final getAllMailboxInteractor = MockGetAllMailboxInteractor(); @@ -400,6 +403,7 @@ void main() { subscribeMultipleMailboxInteractor, subaddressingInteractor, createDefaultMailboxInteractor, + moveFolderContentInteractor, treeBuilder, verifyNameInteractor, getAllMailboxInteractor, From 6cd4c821a9db63adf591b93312fbea7daa3d0c6c Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 29 Oct 2025 15:19:16 +0700 Subject: [PATCH 5/7] TF-4053 Implement move folder content on mailbox search view --- .../mixin/mailbox_action_handler_mixin.dart | 125 ++++++++++++++++++ .../handle_move_folder_content_extension.dart | 108 --------------- .../presentation/mailbox_controller.dart | 22 ++- .../mark_mailbox_as_read_loading_banner.dart | 4 +- .../presentation/search_mailbox_bindings.dart | 2 + .../search_mailbox_controller.dart | 29 ++++ 6 files changed, 177 insertions(+), 113 deletions(-) delete mode 100644 lib/features/mailbox/presentation/extensions/handle_move_folder_content_extension.dart diff --git a/lib/features/base/mixin/mailbox_action_handler_mixin.dart b/lib/features/base/mixin/mailbox_action_handler_mixin.dart index ea394c01c7..523b2367d8 100644 --- a/lib/features/base/mixin/mailbox_action_handler_mixin.dart +++ b/lib/features/base/mixin/mailbox_action_handler_mixin.dart @@ -1,17 +1,26 @@ +import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/views/bottom_popup/confirmation_dialog_action_sheet_builder.dart'; +import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/base/base_mailbox_controller.dart'; import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_manager.dart'; +import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; +import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/move_folder_content_request.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/move_folder_content_state.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_action_reactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; +import 'package:tmail_ui_user/main/utils/toast_manager.dart'; mixin MailboxActionHandlerMixin { @@ -149,4 +158,120 @@ mixin MailboxActionHandlerMixin { ); } } + + void performMoveFolderContent({ + required BuildContext context, + required PresentationMailbox mailboxSelected, + required MailboxDashBoardController dashboardController, + required BaseMailboxController baseMailboxController, + required MailboxActionReactor mailboxActionReactor, + }) { + final accountId = dashboardController.accountId.value; + final session = dashboardController.sessionCurrent; + + if (accountId == null || session == null) { + baseMailboxController.consumeState( + Stream.value( + Left(MoveFolderContentFailure(NotFoundAccountIdException())), + ), + ); + return; + } + + baseMailboxController.moveFolderContentAction( + context: context, + accountId: accountId, + session: session, + mailboxSelected: mailboxSelected, + onMoveFolderContentAction: (currentMailbox, destinationMailbox) { + baseMailboxController.consumeState( + mailboxActionReactor.moveFolderContent( + session: session, + accountId: accountId, + moveRequest: MoveFolderContentRequest( + moveAction: MoveAction.moving, + mailboxId: currentMailbox.id, + destinationMailboxId: destinationMailbox.id, + destinationMailboxDisplayName: + destinationMailbox.getDisplayName(context), + markAsRead: destinationMailbox.isSpam, + totalEmails: currentMailbox.countTotalEmails, + ), + onProgressController: dashboardController.progressStateController, + ), + ); + }, + ); + } + + void handleMoveFolderContentSuccess({ + required MoveFolderContentSuccess success, + required MailboxDashBoardController dashboardController, + required BaseMailboxController baseMailboxController, + required MailboxActionReactor mailboxActionReactor, + }) { + dashboardController.syncViewStateMailboxActionProgress( + newState: Right(UIState.idle), + ); + final moveFolderRequest = success.request; + + if (moveFolderRequest.moveAction == MoveAction.moving) { + baseMailboxController.toastManager.showMessageSuccessWithAction( + success: success, + onActionCallback: () { + _undoMoveFolderContentAction( + dashboardController: dashboardController, + baseMailboxController: baseMailboxController, + mailboxActionReactor: mailboxActionReactor, + newMoveRequest: MoveFolderContentRequest( + moveAction: MoveAction.undo, + mailboxId: moveFolderRequest.destinationMailboxId, + destinationMailboxId: moveFolderRequest.mailboxId, + destinationMailboxDisplayName: '', + totalEmails: moveFolderRequest.totalEmails, + ), + ); + }, + ); + } + } + + void _undoMoveFolderContentAction({ + required MoveFolderContentRequest newMoveRequest, + required MailboxDashBoardController dashboardController, + required BaseMailboxController baseMailboxController, + required MailboxActionReactor mailboxActionReactor, + }) { + final accountId = dashboardController.accountId.value; + final session = dashboardController.sessionCurrent; + + if (accountId == null || session == null) { + baseMailboxController.consumeState( + Stream.value( + Left(MoveFolderContentFailure(NotFoundAccountIdException())), + ), + ); + return; + } + + baseMailboxController.consumeState( + mailboxActionReactor.moveFolderContent( + session: session, + accountId: accountId, + moveRequest: newMoveRequest, + onProgressController: dashboardController.progressStateController, + ), + ); + } + + void handleMoveFolderContentFailure({ + required MoveFolderContentFailure failure, + required MailboxDashBoardController dashboardController, + required ToastManager toastManager, + }) { + dashboardController.syncViewStateMailboxActionProgress( + newState: Right(UIState.idle), + ); + toastManager.showMessageFailure(failure); + } } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/extensions/handle_move_folder_content_extension.dart b/lib/features/mailbox/presentation/extensions/handle_move_folder_content_extension.dart deleted file mode 100644 index f6bb1fffed..0000000000 --- a/lib/features/mailbox/presentation/extensions/handle_move_folder_content_extension.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:core/presentation/state/success.dart'; -import 'package:dartz/dartz.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:model/extensions/presentation_mailbox_extension.dart'; -import 'package:model/mailbox/presentation_mailbox.dart'; -import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; -import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/model/move_folder_content_request.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/state/move_folder_content_state.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/mailbox_controller.dart'; - -extension HandleMoveFolderContentExtension on MailboxController { - void performMoveFolderContent({ - required BuildContext context, - required PresentationMailbox mailboxSelected, - }) { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; - - if (accountId == null || session == null) { - consumeState( - Stream.value( - Left(MoveFolderContentFailure(NotFoundAccountIdException())), - ), - ); - return; - } - - moveFolderContentAction( - context: context, - accountId: accountId, - session: session, - mailboxSelected: mailboxSelected, - onMoveFolderContentAction: (currentMailbox, destinationMailbox) { - consumeState(mailboxActionReactor.moveFolderContent( - session: session, - accountId: accountId, - moveRequest: MoveFolderContentRequest( - moveAction: MoveAction.moving, - mailboxId: currentMailbox.id, - destinationMailboxId: destinationMailbox.id, - destinationMailboxDisplayName: - destinationMailbox.getDisplayName(context), - markAsRead: destinationMailbox.isSpam, - totalEmails: currentMailbox.countTotalEmails, - ), - onProgressController: - mailboxDashBoardController.progressStateController, - )); - }, - ); - } - - void handleMoveFolderContentSuccess(MoveFolderContentSuccess success) { - mailboxDashBoardController.syncViewStateMailboxActionProgress( - newState: Right(UIState.idle), - ); - final moveFolderRequest = success.request; - - if (moveFolderRequest.moveAction == MoveAction.moving) { - toastManager.showMessageSuccessWithAction( - success: success, - onActionCallback: () { - _undoMoveFolderContentAction( - newMoveRequest: MoveFolderContentRequest( - moveAction: MoveAction.undo, - mailboxId: moveFolderRequest.destinationMailboxId, - destinationMailboxId: moveFolderRequest.mailboxId, - destinationMailboxDisplayName: '', - totalEmails: moveFolderRequest.totalEmails, - ), - ); - }, - ); - } - } - - void _undoMoveFolderContentAction({ - required MoveFolderContentRequest newMoveRequest, - }) { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; - - if (accountId == null || session == null) { - consumeState( - Stream.value( - Left(MoveFolderContentFailure(NotFoundAccountIdException())), - ), - ); - return; - } - - consumeState(mailboxActionReactor.moveFolderContent( - session: session, - accountId: accountId, - moveRequest: newMoveRequest, - onProgressController: mailboxDashBoardController.progressStateController, - )); - } - - void handleMoveFolderContentFailure(MoveFolderContentFailure failure) { - mailboxDashBoardController.syncViewStateMailboxActionProgress( - newState: Right(UIState.idle), - ); - toastManager.showMessageFailure(failure); - } -} diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index e32019be06..363499f67a 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -69,7 +69,6 @@ import 'package:tmail_ui_user/features/mailbox/domain/usecases/subaddressing_int import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_multiple_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_action.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/extensions/handle_move_folder_content_extension.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; @@ -219,7 +218,12 @@ class MailboxController extends BaseMailboxController } else if (success is CreateDefaultMailboxAllSuccess) { _handleCreateDefaultFolderIfMissingSuccess(success); } else if (success is MoveFolderContentSuccess) { - handleMoveFolderContentSuccess(success); + handleMoveFolderContentSuccess( + success: success, + mailboxActionReactor: mailboxActionReactor, + dashboardController: mailboxDashBoardController, + baseMailboxController: this, + ); } else { super.handleSuccessViewState(success); } @@ -236,7 +240,11 @@ class MailboxController extends BaseMailboxController } else if (failure is SubaddressingFailure) { handleSubAddressingFailure(failure); } else if (failure is MoveFolderContentFailure) { - handleMoveFolderContentFailure(failure); + handleMoveFolderContentFailure( + failure: failure, + dashboardController: mailboxDashBoardController, + toastManager: toastManager, + ); } else { super.handleFailureViewState(failure); } @@ -1194,7 +1202,13 @@ class MailboxController extends BaseMailboxController mailboxDashBoardController.gotoEmailRecovery(); break; case MailboxActions.moveFolderContent: - performMoveFolderContent(context: context, mailboxSelected: mailbox); + performMoveFolderContent( + context: context, + mailboxSelected: mailbox, + mailboxActionReactor: mailboxActionReactor, + dashboardController: mailboxDashBoardController, + baseMailboxController: this, + ); mailboxDashBoardController.closeMailboxMenuDrawer(); break; default: diff --git a/lib/features/mailbox_dashboard/presentation/widgets/mark_mailbox_as_read_loading_banner.dart b/lib/features/mailbox_dashboard/presentation/widgets/mark_mailbox_as_read_loading_banner.dart index 90f4a45813..2f4f9f8ca5 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/mark_mailbox_as_read_loading_banner.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/mark_mailbox_as_read_loading_banner.dart @@ -22,7 +22,9 @@ class MarkMailboxAsReadLoadingBanner extends StatelessWidget with AppLoaderMixin return viewState.fold( (failure) => const SizedBox.shrink(), (success) { - if (success is MarkAsMailboxReadLoading || success is ClearingMailbox) { + if (success is MarkAsMailboxReadLoading || + success is ClearingMailbox || + success is MovingFolderContent) { return Padding( padding: MarkMailboxAsReadLoadingBannerStyle.bannerMargin, child: horizontalLoadingWidget); diff --git a/lib/features/search/mailbox/presentation/search_mailbox_bindings.dart b/lib/features/search/mailbox/presentation/search_mailbox_bindings.dart index df17880d8d..62e6170457 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_bindings.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_bindings.dart @@ -16,6 +16,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_reposit import 'package:tmail_ui_user/features/mailbox/domain/usecases/create_new_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/delete_multiple_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/move_folder_content_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/move_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/refresh_all_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/rename_mailbox_interactor.dart'; @@ -53,6 +54,7 @@ class SearchMailboxBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), Get.find(), Get.find(), Get.find(), diff --git a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart index 148f717735..2711920638 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart @@ -38,6 +38,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/state/create_new_mailbox_s import 'package:tmail_ui_user/features/mailbox/domain/state/delete_multiple_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/get_all_mailboxes_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/move_folder_content_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/move_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/refresh_changes_all_mailboxes_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/rename_mailbox_state.dart'; @@ -48,6 +49,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/state/subscribe_multiple_m import 'package:tmail_ui_user/features/mailbox/domain/usecases/create_new_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/delete_multiple_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/move_folder_content_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/move_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/refresh_all_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/rename_mailbox_interactor.dart'; @@ -59,6 +61,7 @@ import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_ac import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree_builder.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_action_reactor.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'; @@ -84,6 +87,7 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa final SubscribeMultipleMailboxInteractor _subscribeMultipleMailboxInteractor; final CreateNewMailboxInteractor _createNewMailboxInteractor; final SubaddressingInteractor _subAddressingInteractor; + final MoveFolderContentInteractor _moveFolderContentInteractor; final dashboardController = Get.find(); @@ -92,6 +96,7 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa final textInputSearchController = TextEditingController(); late Debouncer _deBouncerTime; FocusNode? searchFocusNode; + late MailboxActionReactor mailboxActionReactor; PresentationMailbox? get selectedMailbox => dashboardController.selectedMailbox.value; @@ -110,6 +115,7 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa this._subscribeMultipleMailboxInteractor, this._createNewMailboxInteractor, this._subAddressingInteractor, + this._moveFolderContentInteractor, TreeBuilder treeBuilder, VerifyNameInteractor verifyNameInteractor, GetAllMailboxInteractor getAllMailboxInteractor, @@ -124,6 +130,7 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa @override void onInit() { super.onInit(); + mailboxActionReactor = MailboxActionReactor(_moveFolderContentInteractor); _initializeDebounceTimeTextSearchChange(); _registerObxStreamListener(); if (PlatformInfo.isWeb) { @@ -142,6 +149,12 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa _renameMailboxFailure(failure); } else if (failure is SubaddressingFailure) { handleSubAddressingFailure(failure); + } else if (failure is MoveFolderContentFailure) { + handleMoveFolderContentFailure( + failure: failure, + dashboardController: dashboardController, + toastManager: toastManager, + ); } else { super.handleFailureViewState(failure); } @@ -182,6 +195,13 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa _createNewMailboxSuccess(success); } else if (success is SubaddressingSuccess) { handleSubAddressingSuccess(success); + } else if (success is MoveFolderContentSuccess) { + handleMoveFolderContentSuccess( + success: success, + mailboxActionReactor: mailboxActionReactor, + dashboardController: dashboardController, + baseMailboxController: this, + ); } else { super.handleSuccessViewState(success); } @@ -416,6 +436,15 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa case MailboxActions.disallowSubaddressing: _handleSubAddressingAction(mailbox.id, mailbox.rights, actions); break; + case MailboxActions.moveFolderContent: + performMoveFolderContent( + context: context, + mailboxSelected: mailbox, + mailboxActionReactor: mailboxActionReactor, + dashboardController: dashboardController, + baseMailboxController: this, + ); + break; default: break; } From 5ad006b7917ae7353c63eb1581071f5d8721830f Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 29 Oct 2025 16:20:10 +0700 Subject: [PATCH 6/7] TF-4053 Add E2E test for test case move folder content --- .../robots/mailbox_menu_robot.dart | 8 ++ .../mailbox/move_folder_content_scenario.dart | 83 +++++++++++++++++++ .../mailbox/move_folder_content_test.dart | 9 ++ .../base/base_mailbox_controller.dart | 4 +- .../mixin/mailbox_action_handler_mixin.dart | 7 +- .../presentation_mailbox_extension.dart | 27 ++++++ 6 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 integration_test/scenarios/mailbox/move_folder_content_scenario.dart create mode 100644 integration_test/tests/mailbox/move_folder_content_test.dart diff --git a/integration_test/robots/mailbox_menu_robot.dart b/integration_test/robots/mailbox_menu_robot.dart index 4f2e2d9da3..2a1330dde9 100644 --- a/integration_test/robots/mailbox_menu_robot.dart +++ b/integration_test/robots/mailbox_menu_robot.dart @@ -180,4 +180,12 @@ class MailboxMenuRobot extends CoreRobot { await $(AppLocalizations().mark_as_read).tap(); await $.pumpAndSettle(); } + + Future tapMoveFolderContentAction(String mailboxName) async { + await $(AppLocalizations().moveFolderContent).tap(); + await $.pumpAndTrySettle(); + + await $(mailboxName).tap(); + await $.pumpAndTrySettle(); + } } \ No newline at end of file diff --git a/integration_test/scenarios/mailbox/move_folder_content_scenario.dart b/integration_test/scenarios/mailbox/move_folder_content_scenario.dart new file mode 100644 index 0000000000..64b41a15cc --- /dev/null +++ b/integration_test/scenarios/mailbox/move_folder_content_scenario.dart @@ -0,0 +1,83 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/trailing_mailbox_item_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +import '../../base/base_test_scenario.dart'; +import '../../models/provisioning_email.dart'; +import '../../robots/mailbox_menu_robot.dart'; +import '../../robots/thread_robot.dart'; + +class MoveFolderContentScenario extends BaseTestScenario { + const MoveFolderContentScenario(super.$); + + @override + Future runTestLogic() async { + const email = String.fromEnvironment('BASIC_AUTH_EMAIL'); + const emailSubject = 'Move folder content'; + + final threadRobot = ThreadRobot($); + final mailboxMenuRobot = MailboxMenuRobot($); + final appLocalizations = AppLocalizations(); + + final listEmails = List.generate( + 40, + (_) => ProvisioningEmail( + toEmail: email, + subject: emailSubject, + content: '', + ), + ); + + await provisionEmail(listEmails); + await $.pumpAndTrySettle(duration: const Duration(seconds: 2)); + + await threadRobot.openMailbox(); + await $.pumpAndTrySettle(); + _expectInboxUnreadCountVisible(); + + await mailboxMenuRobot.longPressMailboxWithName( + appLocalizations.inboxMailboxDisplayName, + ); + await mailboxMenuRobot.tapMoveFolderContentAction( + appLocalizations.templatesMailboxDisplayName, + ); + await $.pumpAndTrySettle(duration: const Duration(seconds: 3)); + + await threadRobot.openMailbox(); + await $.pumpAndTrySettle(); + await mailboxMenuRobot.openFolderByName( + appLocalizations.templatesMailboxDisplayName, + ); + await $.pumpAndTrySettle(); + await _expectEmailWithSubjectVisible(emailSubject); + + await threadRobot.openMailbox(); + await $.pumpAndTrySettle(); + await mailboxMenuRobot.openFolderByName( + appLocalizations.inboxMailboxDisplayName, + ); + await $.pumpAndTrySettle(); + await _expectEmailWithSubjectInVisible(emailSubject); + } + + Future _expectEmailWithSubjectVisible(String subject) async { + await expectViewVisible($(subject)); + } + + Future _expectEmailWithSubjectInVisible(String subject) async { + await expectViewInvisible($(subject)); + } + + void _expectInboxUnreadCountVisible() { + expect( + $(TrailingMailboxItemWidget).which((widget) { + final mailbox = widget.mailboxNode.item; + return mailbox.role == PresentationMailbox.roleInbox && + mailbox.countUnreadEmails > 0; + }), + findsOneWidget, + ); + } +} diff --git a/integration_test/tests/mailbox/move_folder_content_test.dart b/integration_test/tests/mailbox/move_folder_content_test.dart new file mode 100644 index 0000000000..a6e7fe903f --- /dev/null +++ b/integration_test/tests/mailbox/move_folder_content_test.dart @@ -0,0 +1,9 @@ +import '../../base/test_base.dart'; +import '../../scenarios/mailbox/move_folder_content_scenario.dart'; + +void main() { + TestBase().runPatrolTest( + description: 'Should see all Inbox emails in the Templates folder when perform move folder content action successfully', + scenarioBuilder: ($) => MoveFolderContentScenario($), + ); +} diff --git a/lib/features/base/base_mailbox_controller.dart b/lib/features/base/base_mailbox_controller.dart index 570eaeca94..6b108457cc 100644 --- a/lib/features/base/base_mailbox_controller.dart +++ b/lib/features/base/base_mailbox_controller.dart @@ -56,6 +56,7 @@ typedef MovingMailboxActionCallback = void Function(PresentationMailbox mailboxS typedef OnMoveFolderContentActionCallback = void Function( PresentationMailbox currentMailbox, PresentationMailbox destinationMailbox, + String destinationMailboxName, ); typedef DeleteMailboxActionCallback = void Function(PresentationMailbox mailbox); typedef AllowSubaddressingActionCallback = void Function(MailboxId, Map?>?, MailboxActions); @@ -643,7 +644,7 @@ abstract class BaseMailboxController extends BaseController } void moveFolderContentAction({ - required BuildContext context, + required AppLocalizations appLocalizations, required AccountId accountId, required Session session, required PresentationMailbox mailboxSelected, @@ -667,6 +668,7 @@ abstract class BaseMailboxController extends BaseController onMoveFolderContentAction( mailboxSelected, destinationMailbox, + destinationMailbox.getDisplayNameWithoutContext(appLocalizations), ); } } diff --git a/lib/features/base/mixin/mailbox_action_handler_mixin.dart b/lib/features/base/mixin/mailbox_action_handler_mixin.dart index 523b2367d8..92b5dfd43f 100644 --- a/lib/features/base/mixin/mailbox_action_handler_mixin.dart +++ b/lib/features/base/mixin/mailbox_action_handler_mixin.dart @@ -179,11 +179,11 @@ mixin MailboxActionHandlerMixin { } baseMailboxController.moveFolderContentAction( - context: context, + appLocalizations: AppLocalizations.of(context), accountId: accountId, session: session, mailboxSelected: mailboxSelected, - onMoveFolderContentAction: (currentMailbox, destinationMailbox) { + onMoveFolderContentAction: (currentMailbox, destinationMailbox, appLocalizations) { baseMailboxController.consumeState( mailboxActionReactor.moveFolderContent( session: session, @@ -192,8 +192,7 @@ mixin MailboxActionHandlerMixin { moveAction: MoveAction.moving, mailboxId: currentMailbox.id, destinationMailboxId: destinationMailbox.id, - destinationMailboxDisplayName: - destinationMailbox.getDisplayName(context), + destinationMailboxDisplayName: appLocalizations, markAsRead: destinationMailbox.isSpam, totalEmails: currentMailbox.countTotalEmails, ), diff --git a/lib/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart b/lib/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart index 01f7b45cca..cd7260b5ab 100644 --- a/lib/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart +++ b/lib/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart @@ -37,6 +37,33 @@ extension PresentationMailboxExtension on PresentationMailbox { return name?.name ?? ''; } + String getDisplayNameWithoutContext(AppLocalizations appLocalizations) { + if (isDefault) { + switch(role!.value.toLowerCase()) { + case PresentationMailbox.inboxRole: + return appLocalizations.inboxMailboxDisplayName; + case PresentationMailbox.archiveRole: + return appLocalizations.archiveMailboxDisplayName; + case PresentationMailbox.draftsRole: + return appLocalizations.draftsMailboxDisplayName; + case PresentationMailbox.sentRole: + return appLocalizations.sentMailboxDisplayName; + case PresentationMailbox.outboxRole: + return appLocalizations.outboxMailboxDisplayName; + case PresentationMailbox.trashRole: + return appLocalizations.trashMailboxDisplayName; + case PresentationMailbox.spamRole: + case PresentationMailbox.junkRole: + return appLocalizations.spamMailboxDisplayName; + case PresentationMailbox.templatesRole: + return appLocalizations.templatesMailboxDisplayName; + case PresentationMailbox.recoveredRole: + return appLocalizations.recoveredMailboxDisplayName; + } + } + return name?.name ?? ''; + } + String getMailboxIcon(ImagePaths imagePaths) { if (hasRole()) { switch(role!.value) { From c92269490ba8a43dcacbade8b6eb30d251488aeb Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 4 Nov 2025 12:12:55 +0700 Subject: [PATCH 7/7] TF-4053 Verify `empty widget` in Inbox when perform E2E for move folder content --- .../mailbox/move_folder_content_scenario.dart | 21 +++++++------------ .../email/data/network/email_api.dart | 2 -- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/integration_test/scenarios/mailbox/move_folder_content_scenario.dart b/integration_test/scenarios/mailbox/move_folder_content_scenario.dart index 64b41a15cc..c5450b96e8 100644 --- a/integration_test/scenarios/mailbox/move_folder_content_scenario.dart +++ b/integration_test/scenarios/mailbox/move_folder_content_scenario.dart @@ -1,7 +1,4 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:model/extensions/presentation_mailbox_extension.dart'; -import 'package:model/mailbox/presentation_mailbox.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/widgets/trailing_mailbox_item_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import '../../base/base_test_scenario.dart'; @@ -32,10 +29,10 @@ class MoveFolderContentScenario extends BaseTestScenario { await provisionEmail(listEmails); await $.pumpAndTrySettle(duration: const Duration(seconds: 2)); + await _expectEmptyViewInVisibleInInboxFolder(); await threadRobot.openMailbox(); await $.pumpAndTrySettle(); - _expectInboxUnreadCountVisible(); await mailboxMenuRobot.longPressMailboxWithName( appLocalizations.inboxMailboxDisplayName, @@ -60,6 +57,7 @@ class MoveFolderContentScenario extends BaseTestScenario { ); await $.pumpAndTrySettle(); await _expectEmailWithSubjectInVisible(emailSubject); + await _expectEmptyViewVisibleInInboxFolder(); } Future _expectEmailWithSubjectVisible(String subject) async { @@ -70,14 +68,11 @@ class MoveFolderContentScenario extends BaseTestScenario { await expectViewInvisible($(subject)); } - void _expectInboxUnreadCountVisible() { - expect( - $(TrailingMailboxItemWidget).which((widget) { - final mailbox = widget.mailboxNode.item; - return mailbox.role == PresentationMailbox.roleInbox && - mailbox.countUnreadEmails > 0; - }), - findsOneWidget, - ); + Future _expectEmptyViewVisibleInInboxFolder() async { + await expectViewVisible($(#empty_thread_view)); + } + + Future _expectEmptyViewInVisibleInInboxFolder() async { + await expectViewInvisible($(#empty_thread_view)); } } diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index c1982ecee5..166f576f62 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -52,8 +52,6 @@ import 'package:model/extensions/keyword_identifier_extension.dart'; import 'package:model/extensions/list_email_id_extension.dart'; import 'package:model/extensions/list_id_extension.dart'; import 'package:model/extensions/mailbox_id_extension.dart'; -import 'package:model/extensions/session_extension.dart'; -import 'package:model/oidc/token_oidc.dart'; import 'package:path_provider/path_provider.dart'; import 'package:tmail_ui_user/features/base/mixin/handle_error_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/mail_api_mixin.dart';