diff --git a/lib/features/base/action/update_mailbox_properties_action/update_mailbox_name_action.dart b/lib/features/base/action/update_mailbox_properties_action/update_mailbox_name_action.dart new file mode 100644 index 0000000000..5327133a20 --- /dev/null +++ b/lib/features/base/action/update_mailbox_properties_action/update_mailbox_name_action.dart @@ -0,0 +1,18 @@ +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/base/action/update_mailbox_properties_action/update_mailbox_properties_action.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree.dart'; + +class UpdateMailboxNameAction extends UpdateMailboxPropertiesAction { + const UpdateMailboxNameAction({ + required super.mailboxTrees, + required super.mailboxId, + required this.mailboxName, + }); + + final MailboxName mailboxName; + + @override + bool updateProperty(MailboxTree mailboxTree) { + return mailboxTree.updateMailboxNameById(mailboxId, mailboxName); + } +} \ No newline at end of file diff --git a/lib/features/base/action/update_mailbox_properties_action/update_mailbox_properties_action.dart b/lib/features/base/action/update_mailbox_properties_action/update_mailbox_properties_action.dart new file mode 100644 index 0000000000..ed37f10c20 --- /dev/null +++ b/lib/features/base/action/update_mailbox_properties_action/update_mailbox_properties_action.dart @@ -0,0 +1,24 @@ +import 'package:get/get_rx/get_rx.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree.dart'; + +abstract class UpdateMailboxPropertiesAction { + const UpdateMailboxPropertiesAction({ + required this.mailboxTrees, + required this.mailboxId, + }); + + final List> mailboxTrees; + final MailboxId mailboxId; + + bool updateProperty(MailboxTree mailboxTree); + + void execute() { + for (var mailboxTree in mailboxTrees) { + if (updateProperty(mailboxTree.value)) { + mailboxTree.refresh(); + break; + } + } + } +} \ No newline at end of file diff --git a/lib/features/base/action/update_mailbox_properties_action/update_mailbox_total_emails_count_action.dart b/lib/features/base/action/update_mailbox_properties_action/update_mailbox_total_emails_count_action.dart new file mode 100644 index 0000000000..35c3e69c00 --- /dev/null +++ b/lib/features/base/action/update_mailbox_properties_action/update_mailbox_total_emails_count_action.dart @@ -0,0 +1,20 @@ +import 'package:tmail_ui_user/features/base/action/update_mailbox_properties_action/update_mailbox_properties_action.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree.dart'; + +class UpdateMailboxTotalEmailsCountAction extends UpdateMailboxPropertiesAction { + const UpdateMailboxTotalEmailsCountAction({ + required super.mailboxTrees, + required super.mailboxId, + required this.totalEmailsCountChanged, + }); + + final int totalEmailsCountChanged; + + @override + bool updateProperty(MailboxTree mailboxTree) { + return mailboxTree.updateMailboxTotalEmailsCountById( + mailboxId, + totalEmailsCountChanged, + ); + } +} \ No newline at end of file diff --git a/lib/features/base/action/update_mailbox_properties_action/update_mailbox_unread_count_action.dart b/lib/features/base/action/update_mailbox_properties_action/update_mailbox_unread_count_action.dart new file mode 100644 index 0000000000..5651d24b4f --- /dev/null +++ b/lib/features/base/action/update_mailbox_properties_action/update_mailbox_unread_count_action.dart @@ -0,0 +1,17 @@ +import 'package:tmail_ui_user/features/base/action/update_mailbox_properties_action/update_mailbox_properties_action.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree.dart'; + +class UpdateMailboxUnreadCountAction extends UpdateMailboxPropertiesAction { + const UpdateMailboxUnreadCountAction({ + required super.mailboxTrees, + required super.mailboxId, + required this.unreadChanges, + }); + + final int unreadChanges; + + @override + bool updateProperty(MailboxTree mailboxTree) { + return mailboxTree.updateMailboxUnreadCountById(mailboxId, unreadChanges); + } +} \ No newline at end of file diff --git a/lib/features/base/base_mailbox_controller.dart b/lib/features/base/base_mailbox_controller.dart index aa62a5145a..ee92dc783d 100644 --- a/lib/features/base/base_mailbox_controller.dart +++ b/lib/features/base/base_mailbox_controller.dart @@ -20,6 +20,9 @@ import 'package:model/mailbox/expand_mode.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:model/mailbox/select_mode.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/base/action/update_mailbox_properties_action/update_mailbox_name_action.dart'; +import 'package:tmail_ui_user/features/base/action/update_mailbox_properties_action/update_mailbox_total_emails_count_action.dart'; +import 'package:tmail_ui_user/features/base/action/update_mailbox_properties_action/update_mailbox_unread_count_action.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_subscribe_action_state.dart'; @@ -553,4 +556,50 @@ abstract class BaseMailboxController extends BaseController { mailboxNode = personalMailboxTree.value.findNodeOnFirstLevel((node) => node.item.name?.name.toLowerCase() == name); return mailboxNode; } + + void updateMailboxNameById(MailboxId mailboxId, MailboxName mailboxName) { + UpdateMailboxNameAction( + mailboxTrees: [defaultMailboxTree, personalMailboxTree, teamMailboxesTree], + mailboxId: mailboxId, + mailboxName: mailboxName, + ).execute(); + } + + void updateUnreadCountOfMailboxById( + MailboxId mailboxId, { + required int unreadChanges, + }) { + UpdateMailboxUnreadCountAction( + mailboxTrees: [defaultMailboxTree, personalMailboxTree, teamMailboxesTree], + mailboxId: mailboxId, + unreadChanges: unreadChanges, + ).execute(); + } + + void clearUnreadCount(MailboxId mailboxId) { + final mailboxTrees = [ + defaultMailboxTree, + personalMailboxTree, + teamMailboxesTree, + ]; + + for (var mailboxTree in mailboxTrees) { + final selectedNode = mailboxTree.value.findNode((node) => node.item.id == mailboxId); + if (selectedNode == null) continue; + final currentUnreadCount = selectedNode.item.unreadEmails?.value.value.toInt(); + mailboxTree.value.updateMailboxUnreadCountById( + mailboxId, + -(currentUnreadCount ?? 0)); + mailboxTree.refresh(); + break; + } + } + + void updateMailboxTotalEmailsCountById(MailboxId mailboxId, int totalEmails) { + UpdateMailboxTotalEmailsCountAction( + mailboxTrees: [defaultMailboxTree, personalMailboxTree, teamMailboxesTree], + mailboxId: mailboxId, + totalEmailsCountChanged: totalEmails, + ).execute(); + } } \ No newline at end of file diff --git a/lib/features/composer/domain/state/save_email_as_drafts_state.dart b/lib/features/composer/domain/state/save_email_as_drafts_state.dart index 1eddfdeeae..e9e9f7682b 100644 --- a/lib/features/composer/domain/state/save_email_as_drafts_state.dart +++ b/lib/features/composer/domain/state/save_email_as_drafts_state.dart @@ -1,17 +1,19 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; class SaveEmailAsDraftsLoading extends LoadingState {} class SaveEmailAsDraftsSuccess extends UIState { final EmailId emailId; + final MailboxId? draftMailboxId; - SaveEmailAsDraftsSuccess(this.emailId); + SaveEmailAsDraftsSuccess(this.emailId, this.draftMailboxId); @override - List get props => [emailId, ...super.props]; + List get props => [emailId, draftMailboxId, ...super.props]; } class SaveEmailAsDraftsFailure extends FeatureFailure { diff --git a/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart b/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart index ac651fc187..65a3bd226f 100644 --- a/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart +++ b/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart @@ -44,7 +44,10 @@ class CreateNewAndSaveEmailToDraftsInteractor { ); yield dartz.Right( - SaveEmailAsDraftsSuccess(emailDraftSaved.id!) + SaveEmailAsDraftsSuccess( + emailDraftSaved.id!, + createEmailRequest.draftsMailboxId, + ) ); } else { yield dartz.Right(UpdatingEmailDrafts()); diff --git a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart index 9e96e02936..c607b59bb0 100644 --- a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart @@ -18,11 +18,13 @@ import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:model/account/account_request.dart'; import 'package:model/download/download_task_id.dart'; import 'package:model/email/attachment.dart'; import 'package:model/email/mark_star_action.dart'; import 'package:model/email/read_actions.dart'; +import 'package:model/extensions/email_extension.dart'; import 'package:model/extensions/email_id_extensions.dart'; import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; @@ -79,8 +81,13 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { AccountId accountId, EmailId emailId, {CancelToken? cancelToken} - ) { - throw UnimplementedError(); + ) async { + await _emailCacheManager.update( + accountId, + session.username, + destroyed: [emailId], + ); + return true; } @override @@ -91,8 +98,16 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { Session session, AccountId accountId, List emailIds, - ) { - throw UnimplementedError(); + ) async { + await _emailCacheManager.update( + accountId, + session.username, + destroyed: emailIds, + ); + return ( + emailIdsSuccess: emailIds, + mapErrors: {} + ); } @override @@ -137,8 +152,29 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { AccountId accountId, List emailIds, ReadActions readActions, - ) { - throw UnimplementedError(); + ) async { + final cacheEmails = await _emailCacheManager.getMultipleStoredEmails( + accountId, + session.username, + emailIds, + ); + final storedEmails = cacheEmails.map((emailCache) => emailCache.toEmail()).toList(); + for (var email in storedEmails) { + if (readActions == ReadActions.markAsUnread) { + email.keywords?.remove(KeyWordIdentifier.emailSeen); + } else { + email.keywords?[KeyWordIdentifier.emailSeen] = true; + } + } + await _emailCacheManager.storeMultipleEmails( + accountId, + session.username, + storedEmails.map((email) => email.toEmailCache()).toList(), + ); + return ( + emailIdsSuccess: emailIds, + mapErrors: {} + ); } @override @@ -150,8 +186,29 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { AccountId accountId, List emailIds, MarkStarAction markStarAction, - ) { - throw UnimplementedError(); + ) async { + final cacheEmails = await _emailCacheManager.getMultipleStoredEmails( + accountId, + session.username, + emailIds, + ); + final storedEmails = cacheEmails.map((emailCache) => emailCache.toEmail()).toList(); + for (var email in storedEmails) { + if (markStarAction == MarkStarAction.unMarkStar) { + email.keywords?.remove(KeyWordIdentifier.emailFlagged); + } else { + email.keywords?[KeyWordIdentifier.emailFlagged] = true; + } + } + await _emailCacheManager.storeMultipleEmails( + accountId, + session.username, + storedEmails.map((email) => email.toEmailCache()).toList(), + ); + return ( + emailIdsSuccess: emailIds, + mapErrors: {} + ); } @override @@ -162,8 +219,32 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { Session session, AccountId accountId, MoveToMailboxRequest moveRequest, - ) { - throw UnimplementedError(); + ) async { + final emailIds = moveRequest + .currentMailboxes + .values + .expand((emails) => emails) + .toList(); + final cacheEmails = await _emailCacheManager.getMultipleStoredEmails( + accountId, + session.username, + emailIds, + ); + final storedEmails = cacheEmails.map((emailCache) => emailCache.toEmail()).toList(); + for (int i = 0; i < storedEmails.length; i++) { + storedEmails[i] = storedEmails[i].updatedEmail( + newMailboxIds: {moveRequest.destinationMailboxId: true}, + ); + } + await _emailCacheManager.storeMultipleEmails( + accountId, + session.username, + storedEmails.map((email) => email.toEmailCache()).toList(), + ); + return ( + emailIdsSuccess: emailIds, + mapErrors: {} + ); } @override @@ -172,8 +253,13 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { AccountId accountId, EmailId emailId, {CancelToken? cancelToken} - ) { - throw UnimplementedError(); + ) async { + await _emailCacheManager.update( + accountId, + session.username, + destroyed: [emailId], + ); + return true; } @override @@ -182,8 +268,9 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { AccountId accountId, Email email, {CancelToken? cancelToken} - ) { - throw UnimplementedError(); + ) async { + await _emailCacheManager.update(accountId, session.username, created: [email]); + return email; } @override @@ -232,8 +319,13 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { Email newEmail, EmailId oldEmailId, {CancelToken? cancelToken} - ) { - throw UnimplementedError(); + ) async { + await _emailCacheManager.update( + accountId, + session.username, + updated: [newEmail], + ); + return newEmail; } @override diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index c849e3b22c..5b19266ebd 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -6,6 +6,7 @@ import 'package:core/data/network/download/downloaded_response.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; @@ -93,13 +94,26 @@ class EmailRepositoryImpl extends EmailRepository { AccountId accountId, List emailIds, ReadActions readActions, - ) { - return emailDataSource[DataSourceType.network]!.markAsRead( + ) async { + final result = await emailDataSource[DataSourceType.network]!.markAsRead( session, accountId, emailIds, readActions, ); + + try { + await emailDataSource[DataSourceType.hiveCache]!.markAsRead( + session, + accountId, + result.emailIdsSuccess, + readActions, + ); + } catch (e) { + logError('EmailRepositoryImpl::markAsRead:exception $e'); + } + + return result; } @override @@ -136,12 +150,38 @@ class EmailRepositoryImpl extends EmailRepository { Session session, AccountId accountId, MoveToMailboxRequest moveRequest, - ) { - return emailDataSource[DataSourceType.network]!.moveToMailbox( + ) async { + final result = await emailDataSource[DataSourceType.network] + !.moveToMailbox( session, accountId, moveRequest, ); + final updatedCurrentMailboxes = moveRequest.currentMailboxes.map( + (key, value) => MapEntry( + key, + value.where(result.emailIdsSuccess.contains).toList(), + ), + ); + + try { + await emailDataSource[DataSourceType.hiveCache] + !.moveToMailbox( + session, + accountId, + MoveToMailboxRequest( + updatedCurrentMailboxes, + moveRequest.destinationMailboxId, + moveRequest.moveAction, + moveRequest.emailActionType, + destinationPath: moveRequest.destinationPath, + ), + ); + } catch (e) { + logError('EmailRepositoryImpl::moveToMailbox:exception $e'); + } + + return result; } @override @@ -153,13 +193,24 @@ class EmailRepositoryImpl extends EmailRepository { AccountId accountId, List emailIds, MarkStarAction markStarAction - ) { - return emailDataSource[DataSourceType.network]!.markAsStar( + ) async { + final result = await emailDataSource[DataSourceType.network]!.markAsStar( session, accountId, emailIds, markStarAction, ); + try { + await emailDataSource[DataSourceType.hiveCache]!.markAsStar( + session, + accountId, + result.emailIdsSuccess, + markStarAction + ); + } catch (e) { + logError('EmailRepositoryImpl::markAsStar:exception $e'); + } + return result; } @override @@ -185,13 +236,24 @@ class EmailRepositoryImpl extends EmailRepository { AccountId accountId, Email email, {CancelToken? cancelToken} - ) { - return emailDataSource[DataSourceType.network]!.saveEmailAsDrafts( + ) async { + final result = await emailDataSource[DataSourceType.network]!.saveEmailAsDrafts( session, accountId, email, cancelToken: cancelToken ); + try { + await emailDataSource[DataSourceType.hiveCache]!.saveEmailAsDrafts( + session, + accountId, + result, + cancelToken: cancelToken + ); + } catch (e) { + logError('EmailRepositoryImpl::saveEmailAsDrafts:exception $e'); + } + return result; } @override @@ -200,13 +262,24 @@ class EmailRepositoryImpl extends EmailRepository { AccountId accountId, EmailId emailId, {CancelToken? cancelToken} - ) { - return emailDataSource[DataSourceType.network]!.removeEmailDrafts( + ) async { + final result = await emailDataSource[DataSourceType.network]!.removeEmailDrafts( session, accountId, emailId, cancelToken: cancelToken ); + try { + await emailDataSource[DataSourceType.hiveCache]!.removeEmailDrafts( + session, + accountId, + emailId, + cancelToken: cancelToken + ); + } catch (e) { + logError('EmailRepositoryImpl::removeEmailDrafts:exception $e'); + } + return result; } @override @@ -216,14 +289,25 @@ class EmailRepositoryImpl extends EmailRepository { Email newEmail, EmailId oldEmailId, {CancelToken? cancelToken} - ) { - return emailDataSource[DataSourceType.network]!.updateEmailDrafts( + ) async { + final result = await emailDataSource[DataSourceType.network]!.updateEmailDrafts( session, accountId, newEmail, oldEmailId, cancelToken: cancelToken ); + try { + await emailDataSource[DataSourceType.hiveCache]!.updateEmailDrafts( + session, + accountId, + result, + oldEmailId, + ); + } catch (e) { + logError('EmailRepositoryImpl::updateEmailDrafts:exception $e'); + } + return result; } @override @@ -254,12 +338,21 @@ class EmailRepositoryImpl extends EmailRepository { Session session, AccountId accountId, List emailIds, - ) { - return emailDataSource[DataSourceType.network]!.deleteMultipleEmailsPermanently( + ) async { + final result = await emailDataSource[DataSourceType.network] + !.deleteMultipleEmailsPermanently( session, accountId, emailIds, ); + try { + await emailDataSource[DataSourceType.hiveCache] + !.deleteMultipleEmailsPermanently(session, accountId, result.emailIdsSuccess); + } catch (e) { + logError('EmailRepositoryImpl::deleteMultipleEmailsPermanently:exception $e'); + } + + return result; } @override @@ -268,13 +361,24 @@ class EmailRepositoryImpl extends EmailRepository { AccountId accountId, EmailId emailId, {CancelToken? cancelToken} - ) { - return emailDataSource[DataSourceType.network]!.deleteEmailPermanently( + ) async { + final result = await emailDataSource[DataSourceType.network]!.deleteEmailPermanently( session, accountId, emailId, cancelToken: cancelToken ); + try { + await emailDataSource[DataSourceType.hiveCache]!.deleteEmailPermanently( + session, + accountId, + emailId, + ); + } catch (e) { + logError('EmailRepositoryImpl::deleteEmailPermanently:exception $e'); + } + + return result; } @override diff --git a/lib/features/email/domain/state/delete_email_permanently_state.dart b/lib/features/email/domain/state/delete_email_permanently_state.dart index 22f2a0a6c4..21f596ef30 100644 --- a/lib/features/email/domain/state/delete_email_permanently_state.dart +++ b/lib/features/email/domain/state/delete_email_permanently_state.dart @@ -1,9 +1,19 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; class StartDeleteEmailPermanently extends UIState {} -class DeleteEmailPermanentlySuccess extends UIState {} +class DeleteEmailPermanentlySuccess extends UIState { + final EmailId emailId; + final MailboxId? mailboxId; + + DeleteEmailPermanentlySuccess(this.emailId, this.mailboxId); + + @override + List get props => [emailId, mailboxId]; +} class DeleteEmailPermanentlyFailure extends FeatureFailure { diff --git a/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart b/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart index 07ac090d6c..bdcb3cd22d 100644 --- a/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart +++ b/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart @@ -1,27 +1,30 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; class LoadingDeleteMultipleEmailsPermanentlyAll extends UIState {} class DeleteMultipleEmailsPermanentlyAllSuccess extends UIState { - List emailIds; + final List emailIds; + final MailboxId? mailboxId; - DeleteMultipleEmailsPermanentlyAllSuccess(this.emailIds); + DeleteMultipleEmailsPermanentlyAllSuccess(this.emailIds, this.mailboxId); @override - List get props => [emailIds]; + List get props => [emailIds, mailboxId]; } class DeleteMultipleEmailsPermanentlyHasSomeEmailFailure extends UIState { - List emailIds; + final List emailIds; + final MailboxId? mailboxId; - DeleteMultipleEmailsPermanentlyHasSomeEmailFailure(this.emailIds); + DeleteMultipleEmailsPermanentlyHasSomeEmailFailure(this.emailIds, this.mailboxId); @override - List get props => [emailIds]; + List get props => [emailIds, mailboxId]; } class DeleteMultipleEmailsPermanentlyAllFailure extends FeatureFailure {} diff --git a/lib/features/email/domain/state/mark_as_email_read_state.dart b/lib/features/email/domain/state/mark_as_email_read_state.dart index 1c327f1da0..1bfb2e2052 100644 --- a/lib/features/email/domain/state/mark_as_email_read_state.dart +++ b/lib/features/email/domain/state/mark_as_email_read_state.dart @@ -1,6 +1,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/read_actions.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; @@ -8,15 +9,17 @@ class MarkAsEmailReadSuccess extends UIState { final EmailId emailId; final ReadActions readActions; final MarkReadAction markReadAction; + final MailboxId? mailboxId; MarkAsEmailReadSuccess( this.emailId, this.readActions, this.markReadAction, + this.mailboxId, ); @override - List get props => [emailId, readActions, markReadAction]; + List get props => [emailId, readActions, markReadAction, mailboxId]; } class MarkAsEmailReadFailure extends FeatureFailure { diff --git a/lib/features/email/domain/state/mark_as_email_star_state.dart b/lib/features/email/domain/state/mark_as_email_star_state.dart index 3313c95253..91e87c19f0 100644 --- a/lib/features/email/domain/state/mark_as_email_star_state.dart +++ b/lib/features/email/domain/state/mark_as_email_star_state.dart @@ -1,14 +1,16 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/email/mark_star_action.dart'; class MarkAsStarEmailSuccess extends UIState { final MarkStarAction markStarAction; + final EmailId emailId; - MarkAsStarEmailSuccess(this.markStarAction); + MarkAsStarEmailSuccess(this.markStarAction, this.emailId); @override - List get props => [markStarAction]; + List get props => [markStarAction, emailId]; } class MarkAsStarEmailFailure extends FeatureFailure { diff --git a/lib/features/email/domain/state/move_to_mailbox_state.dart b/lib/features/email/domain/state/move_to_mailbox_state.dart index 56809a70dd..f91ff1e39a 100644 --- a/lib/features/email/domain/state/move_to_mailbox_state.dart +++ b/lib/features/email/domain/state/move_to_mailbox_state.dart @@ -14,6 +14,8 @@ class MoveToMailboxSuccess extends UIState { final MoveAction moveAction; final EmailActionType emailActionType; final String? destinationPath; + final Map> originalMailboxIdsWithEmailIds; + final Map emailIdsWithReadStatus; MoveToMailboxSuccess( this.emailId, @@ -23,6 +25,8 @@ class MoveToMailboxSuccess extends UIState { this.emailActionType, { this.destinationPath, + required this.originalMailboxIdsWithEmailIds, + required this.emailIdsWithReadStatus, } ); @@ -34,6 +38,8 @@ class MoveToMailboxSuccess extends UIState { moveAction, emailActionType, destinationPath, + originalMailboxIdsWithEmailIds, + emailIdsWithReadStatus, ]; } diff --git a/lib/features/email/domain/usecases/delete_email_permanently_interactor.dart b/lib/features/email/domain/usecases/delete_email_permanently_interactor.dart index 20260256ee..8e874d33ab 100644 --- a/lib/features/email/domain/usecases/delete_email_permanently_interactor.dart +++ b/lib/features/email/domain/usecases/delete_email_permanently_interactor.dart @@ -4,6 +4,7 @@ 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:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; @@ -12,12 +13,17 @@ class DeleteEmailPermanentlyInteractor { DeleteEmailPermanentlyInteractor(this._emailRepository); - Stream> execute(Session session, AccountId accountId, EmailId emailId) async* { + Stream> execute( + Session session, + AccountId accountId, + EmailId emailId, + MailboxId? mailboxId, + ) async* { try { yield Right(StartDeleteEmailPermanently()); final result = await _emailRepository.deleteEmailPermanently(session, accountId, emailId); if (result) { - yield Right(DeleteEmailPermanentlySuccess()); + yield Right(DeleteEmailPermanentlySuccess(emailId, mailboxId)); } else { yield Left(DeleteEmailPermanentlyFailure(null)); } diff --git a/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart b/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart index b8195c8e60..3510c2bec2 100644 --- a/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart +++ b/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart @@ -4,6 +4,7 @@ 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:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; @@ -12,14 +13,19 @@ class DeleteMultipleEmailsPermanentlyInteractor { DeleteMultipleEmailsPermanentlyInteractor(this._emailRepository); - Stream> execute(Session session, AccountId accountId, List emailIds) async* { + Stream> execute( + Session session, + AccountId accountId, + List emailIds, + MailboxId? mailboxId, + ) async* { try { yield Right(LoadingDeleteMultipleEmailsPermanentlyAll()); final listResult = await _emailRepository.deleteMultipleEmailsPermanently(session, accountId, emailIds); if (listResult.emailIdsSuccess.length == emailIds.length) { - yield Right(DeleteMultipleEmailsPermanentlyAllSuccess(listResult.emailIdsSuccess)); + yield Right(DeleteMultipleEmailsPermanentlyAllSuccess(listResult.emailIdsSuccess, mailboxId)); } else if (listResult.emailIdsSuccess.isNotEmpty) { - yield Right(DeleteMultipleEmailsPermanentlyHasSomeEmailFailure(listResult.emailIdsSuccess)); + yield Right(DeleteMultipleEmailsPermanentlyHasSomeEmailFailure(listResult.emailIdsSuccess, mailboxId)); } else { yield Left(DeleteMultipleEmailsPermanentlyAllFailure()); } diff --git a/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart b/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart index 5f5659bc66..684289d7b7 100644 --- a/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart +++ b/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart @@ -4,6 +4,7 @@ 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:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/read_actions.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; @@ -20,6 +21,7 @@ class MarkAsEmailReadInteractor { EmailId emailId, ReadActions readAction, MarkReadAction markReadAction, + MailboxId? mailboxId, ) async* { try { final result = await _emailRepository.markAsRead( @@ -36,7 +38,8 @@ class MarkAsEmailReadInteractor { result.emailIdsSuccess.first, readAction, markReadAction, - )); + mailboxId, + )); } } catch (e) { yield Left(MarkAsEmailReadFailure(readAction, exception: e)); diff --git a/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart b/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart index aa8cc68c7e..db2b9704b8 100644 --- a/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart +++ b/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart @@ -26,7 +26,7 @@ class MarkAsStarEmailInteractor { [emailId], markStarAction, ); - yield Right(MarkAsStarEmailSuccess(markStarAction)); + yield Right(MarkAsStarEmailSuccess(markStarAction, emailId)); } catch (e) { yield Left(MarkAsStarEmailFailure(markStarAction, exception: e)); } diff --git a/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart b/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart index 0303c36483..0a92e06f92 100644 --- a/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart +++ b/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart @@ -3,6 +3,7 @@ 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:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; @@ -12,7 +13,12 @@ class MoveToMailboxInteractor { MoveToMailboxInteractor(this._emailRepository); - Stream> execute(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) async* { + Stream> execute( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, + ) async* { try { yield Right(LoadingMoveToMailbox()); final result = await _emailRepository.moveToMailbox(session, accountId, moveRequest); @@ -24,6 +30,8 @@ class MoveToMailboxInteractor { moveRequest.moveAction, moveRequest.emailActionType, destinationPath: moveRequest.destinationPath, + originalMailboxIdsWithEmailIds: moveRequest.currentMailboxes, + emailIdsWithReadStatus: emailIdsWithReadStatus, )); } else { yield Left(MoveToMailboxFailure(moveRequest.emailActionType)); diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 6918ee5256..558d776d7b 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -95,6 +95,7 @@ import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.d import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_action.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/download/download_task_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_email_rule_filter_request.dart'; @@ -660,11 +661,18 @@ class SingleEmailController extends BaseController with AppLoaderMixin { presentationEmail.id!, readActions, markReadAction, + presentationEmail.mailboxContain?.mailboxId, )); } } void _handleMarkAsEmailReadCompleted(ReadActions readActions) { + if (_currentEmailId != null) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + [_currentEmailId!], + readAction: readActions, + ); + } if (readActions == ReadActions.markAsUnread) { closeEmailView(context: currentContext); } @@ -931,7 +939,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { {currentMailbox.id: [emailSelected.id!]}, destinationMailbox.id, MoveAction.moving, - EmailActionType.moveToTrash)); + EmailActionType.moveToTrash), + {emailSelected.id!: emailSelected.hasRead}); } else if (destinationMailbox.isSpam) { _moveToSpamAction( context, @@ -941,7 +950,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { {currentMailbox.id: [emailSelected.id!]}, destinationMailbox.id, MoveAction.moving, - EmailActionType.moveToSpam)); + EmailActionType.moveToSpam), + {emailSelected.id!: emailSelected.hasRead}); } else { _moveToMailbox( context, @@ -952,13 +962,25 @@ class SingleEmailController extends BaseController with AppLoaderMixin { destinationMailbox.id, MoveAction.moving, EmailActionType.moveToMailbox, - destinationPath: destinationMailbox.mailboxPath)); + destinationPath: destinationMailbox.mailboxPath), + {emailSelected.id!: emailSelected.hasRead}); } } - void _moveToMailbox(BuildContext context, Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { + void _moveToMailbox( + BuildContext context, + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, + ) { closeEmailView(context: context); - consumeState(_moveToMailboxInteractor.execute(session, accountId, moveRequest)); + consumeState(_moveToMailboxInteractor.execute( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + )); } void _moveToMailboxSuccess(MoveToMailboxSuccess success) { @@ -970,10 +992,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { actionName: AppLocalizations.of(currentContext!).undo, onActionClick: () { _revertedToOriginalMailbox(MoveToMailboxRequest( - {success.destinationMailboxId: [success.emailId]}, - success.currentMailboxId, - MoveAction.undo, - success.emailActionType)); + {success.destinationMailboxId: [success.emailId]}, + success.currentMailboxId, + MoveAction.undo, + success.emailActionType), + success.emailIdsWithReadStatus, + ); }, leadingSVGIcon: imagePaths.icFolderMailbox, leadingSVGIconColor: Colors.white, @@ -984,9 +1008,18 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void _revertedToOriginalMailbox(MoveToMailboxRequest newMoveRequest) { + void _revertedToOriginalMailbox( + MoveToMailboxRequest newMoveRequest, + Map emailIdsWithReadStatus, + ) { if (accountId != null && session != null) { - _moveToMailbox(currentContext!, session!, accountId!, newMoveRequest); + _moveToMailbox( + currentContext!, + session!, + accountId!, + newMoveRequest, + emailIdsWithReadStatus, + ); } } @@ -1003,7 +1036,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { {currentMailbox.id: [email.id!]}, trashMailboxId, MoveAction.moving, - EmailActionType.moveToTrash) + EmailActionType.moveToTrash), + {email.id!: email.hasRead}, ); } } @@ -1012,10 +1046,16 @@ class SingleEmailController extends BaseController with AppLoaderMixin { BuildContext context, Session session, AccountId accountId, - MoveToMailboxRequest moveRequest + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, ) { closeEmailView(context: context); - mailboxDashBoardController.moveToMailbox(session, accountId, moveRequest); + mailboxDashBoardController.moveToMailbox( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + ); } void moveToSpam(BuildContext context, PresentationEmail email) { @@ -1031,7 +1071,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { {currentMailbox.id: [email.id!]}, spamMailboxId, MoveAction.moving, - EmailActionType.moveToSpam) + EmailActionType.moveToSpam), + {email.id!: email.hasRead}, ); } } @@ -1049,7 +1090,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { {spamMailboxId: [email.id!]}, inboxMailboxId, MoveAction.moving, - EmailActionType.unSpam) + EmailActionType.unSpam), + {email.id!: email.hasRead}, ); } } @@ -1058,10 +1100,16 @@ class SingleEmailController extends BaseController with AppLoaderMixin { BuildContext context, Session session, AccountId accountId, - MoveToMailboxRequest moveRequest + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, ) { closeEmailView(context: context); - mailboxDashBoardController.moveToMailbox(session, accountId, moveRequest); + mailboxDashBoardController.moveToMailbox( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + ); } void markAsStarEmail( @@ -1080,9 +1128,16 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _markAsEmailStarSuccess(MarkAsStarEmailSuccess success) { final newEmail = currentEmail?.updateKeywords({ - KeyWordIdentifier.emailFlagged: true, + KeyWordIdentifier.emailFlagged: success.markStarAction == MarkStarAction.markStar, }); mailboxDashBoardController.setSelectedEmail(newEmail); + + final emailId = newEmail?.id; + if (emailId == null) return; + mailboxDashBoardController.updateEmailFlagByEmailIds( + [emailId], + markStarAction: success.markStarAction, + ); } void handleEmailAction(BuildContext context, PresentationEmail presentationEmail, EmailActionType actionType) { 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 e0c604a39a..3c5496f133 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 @@ -13,6 +13,8 @@ import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/extensions/list_mailbox_extension.dart'; +import 'package:model/extensions/mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/local/mailbox_cache_manager.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/mailbox_change_response.dart'; @@ -68,8 +70,26 @@ class MailboxCacheDataSourceImpl extends MailboxDataSource { } @override - Future renameMailbox(Session session, AccountId accountId, RenameMailboxRequest request) { - throw UnimplementedError(); + Future renameMailbox( + Session session, + AccountId accountId, + RenameMailboxRequest request, + ) async { + final cachedMailboxes = await getAllMailboxCache(accountId, session.username); + final updatedMailbox = cachedMailboxes.findMailbox(request.mailboxId); + if (updatedMailbox == null) return false; + + final updatedMailboxIndex = cachedMailboxes.indexOf(updatedMailbox); + if (updatedMailboxIndex == -1) return false; + + cachedMailboxes[updatedMailboxIndex] = updatedMailbox.copyWith(name: request.newName); + await update( + accountId, + session.username, + updated: cachedMailboxes, + ); + + return true; } @override @@ -79,12 +99,29 @@ class MailboxCacheDataSourceImpl extends MailboxDataSource { @override Future> markAsMailboxRead( - Session session, - AccountId accountId, - MailboxId mailboxId, - int totalEmailUnread, - StreamController> onProgressController) { - throw UnimplementedError(); + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmailUnread, + StreamController> onProgressController, + ) async { + final mailboxes = await getAllMailboxCache(accountId, session.username); + final updatedMailbox = mailboxes.findMailbox(mailboxId); + if (updatedMailbox == null) return []; + + final updatedMailboxIndex = mailboxes.indexOf(updatedMailbox); + if (updatedMailboxIndex == -1) return []; + + mailboxes[updatedMailboxIndex] = updatedMailbox.copyWith( + unreadEmails: UnreadEmails(UnsignedInt(totalEmailUnread)), + ); + await _mailboxCacheManager.update( + accountId, + session.username, + updated: mailboxes, + ); + + return []; } @override diff --git a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart index bfc6fcc068..0aa44a6607 100644 --- a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart +++ b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart @@ -15,8 +15,10 @@ import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/email/read_actions.dart'; import 'package:model/extensions/list_mailbox_extension.dart'; import 'package:model/extensions/mailbox_extension.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/extensions/state_extension.dart'; @@ -36,10 +38,12 @@ class MailboxRepositoryImpl extends MailboxRepository { final Map mapDataSource; final StateDataSource stateDataSource; + final EmailDataSource? emailDataSource; MailboxRepositoryImpl( this.mapDataSource, this.stateDataSource, + [this.emailDataSource,] ); @override @@ -211,23 +215,54 @@ class MailboxRepositoryImpl extends MailboxRepository { } @override - Future renameMailbox(Session session, AccountId accountId, RenameMailboxRequest request) { - return mapDataSource[DataSourceType.network]!.renameMailbox(session, accountId, request); + Future renameMailbox(Session session, AccountId accountId, RenameMailboxRequest request) async { + final result = await mapDataSource[DataSourceType.network] + !.renameMailbox(session, accountId, request); + + try { + await mapDataSource[DataSourceType.local] + !.renameMailbox(session, accountId, request); + } catch (e) { + logError('MailboxRepositoryImpl::renameMailbox: Exception: $e'); + } + + return result; } @override Future> markAsMailboxRead( - Session session, - AccountId accountId, - MailboxId mailboxId, - int totalEmailUnread, - StreamController> onProgressController) async { - return mapDataSource[DataSourceType.network]!.markAsMailboxRead( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmailUnread, + StreamController> onProgressController, + ) async { + final result = await mapDataSource[DataSourceType.network]!.markAsMailboxRead( session, accountId, mailboxId, totalEmailUnread, onProgressController); + try { + await Future.wait([ + mapDataSource[DataSourceType.local]!.markAsMailboxRead( + session, + accountId, + mailboxId, + totalEmailUnread - result.length, + onProgressController, + ), + emailDataSource?.markAsRead( + session, + accountId, + result, + ReadActions.markAsRead, + ) ?? Future.value(), + ]); + } catch (e) { + logError('MailboxRepositoryImpl::markAsMailboxRead: Exception: $e'); + } + return result; } @override diff --git a/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart b/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart index 60a2df0c15..6be792a793 100644 --- a/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart +++ b/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart @@ -1,6 +1,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; @@ -24,8 +25,10 @@ class UpdatingMarkAsMailboxReadState extends UIState { class MarkAsMailboxReadAllSuccess extends UIActionState { final String mailboxDisplayName; + final MailboxId mailboxId; MarkAsMailboxReadAllSuccess(this.mailboxDisplayName, + this.mailboxId, { jmap.State? currentEmailState, jmap.State? currentMailboxState, @@ -35,6 +38,7 @@ class MarkAsMailboxReadAllSuccess extends UIActionState { @override List get props => [ mailboxDisplayName, + mailboxId, ...super.props ]; } @@ -43,16 +47,22 @@ class MarkAsMailboxReadHasSomeEmailFailure extends UIState { final String mailboxDisplayName; final int countEmailsRead; + final MailboxId mailboxId; + final List successEmailIds; MarkAsMailboxReadHasSomeEmailFailure( this.mailboxDisplayName, this.countEmailsRead, + this.mailboxId, + this.successEmailIds, ); @override List get props => [ mailboxDisplayName, countEmailsRead, + mailboxId, + successEmailIds, ]; } diff --git a/lib/features/mailbox/domain/state/rename_mailbox_state.dart b/lib/features/mailbox/domain/state/rename_mailbox_state.dart index 1861185bc9..f3300267a9 100644 --- a/lib/features/mailbox/domain/state/rename_mailbox_state.dart +++ b/lib/features/mailbox/domain/state/rename_mailbox_state.dart @@ -1,9 +1,17 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/rename_mailbox_request.dart'; class LoadingRenameMailbox extends UIState {} -class RenameMailboxSuccess extends UIState {} +class RenameMailboxSuccess extends UIState { + RenameMailboxSuccess({required this.request}); + + final RenameMailboxRequest request; + + @override + List get props => [request]; +} class RenameMailboxFailure extends FeatureFailure { diff --git a/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart b/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart index 074efdd514..547ec1e57e 100644 --- a/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart +++ b/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart @@ -34,11 +34,13 @@ class MarkAsMailboxReadInteractor { onProgressController); if (totalEmailUnread == listEmails.length) { - yield Right(MarkAsMailboxReadAllSuccess(mailboxDisplayName)); + yield Right(MarkAsMailboxReadAllSuccess(mailboxDisplayName, mailboxId)); } else if (listEmails.isNotEmpty) { yield Right(MarkAsMailboxReadHasSomeEmailFailure( mailboxDisplayName, listEmails.length, + mailboxId, + listEmails, )); } else { yield Left(MarkAsMailboxReadAllFailure(mailboxDisplayName: mailboxDisplayName)); diff --git a/lib/features/mailbox/domain/usecases/rename_mailbox_interactor.dart b/lib/features/mailbox/domain/usecases/rename_mailbox_interactor.dart index 8e2ef950da..267be45198 100644 --- a/lib/features/mailbox/domain/usecases/rename_mailbox_interactor.dart +++ b/lib/features/mailbox/domain/usecases/rename_mailbox_interactor.dart @@ -18,7 +18,7 @@ class RenameMailboxInteractor { yield Right(LoadingRenameMailbox()); final result = await _mailboxRepository.renameMailbox(session, accountId, request); if (result) { - yield Right(RenameMailboxSuccess()); + yield Right(RenameMailboxSuccess(request: request)); } else { yield Left(RenameMailboxFailure(null)); } diff --git a/lib/features/mailbox/presentation/mailbox_bindings.dart b/lib/features/mailbox/presentation/mailbox_bindings.dart index 2458018923..9fd2fd2a11 100644 --- a/lib/features/mailbox/presentation/mailbox_bindings.dart +++ b/lib/features/mailbox/presentation/mailbox_bindings.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart'; +import 'package:tmail_ui_user/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; @@ -122,6 +123,7 @@ class MailboxBindings extends BaseBindings { DataSourceType.local: Get.find() }, Get.find(), + Get.find(), )); } } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 904068006a..43f6d6d17a 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -19,7 +19,13 @@ import 'package:rxdart/transformers.dart'; import 'package:tmail_ui_user/features/base/base_mailbox_controller.dart'; import 'package:tmail_ui_user/features/base/mixin/contact_support_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/mailbox_action_handler_mixin.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'; +import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/get_restored_deleted_message_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; 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/home/domain/extensions/session_extensions.dart'; @@ -36,6 +42,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_request.da import 'package:tmail_ui_user/features/mailbox/domain/state/create_new_mailbox_state.dart'; 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_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'; @@ -63,6 +70,7 @@ import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_utils. 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'; import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/new_mailbox_arguments.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; @@ -70,6 +78,10 @@ import 'package:tmail_ui_user/features/push_notification/presentation/websocket/ import 'package:tmail_ui_user/features/push_notification/presentation/websocket/web_socket_queue_handler.dart'; import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/dialog_router.dart'; @@ -167,6 +179,8 @@ class MailboxController extends BaseMailboxController _deleteMultipleMailboxSuccess(success.listMailboxIdDeleted, success.currentMailboxState); } else if (success is DeleteMultipleMailboxHasSomeSuccess) { _deleteMultipleMailboxSuccess(success.listMailboxIdDeleted, success.currentMailboxState); + } else if (success is RenameMailboxSuccess) { + _renameMailboxSuccess(success); } else if (success is MoveMailboxSuccess) { _moveMailboxSuccess(success); } else if (success is SubscribeMailboxSuccess) { @@ -253,6 +267,202 @@ class MailboxController extends BaseMailboxController mailboxDashBoardController.clearMailboxUIAction(); } }); + + ever(mailboxDashBoardController.viewState, (viewState) { + final reactionState = viewState.getOrElse(() => UIState.idle); + if (reactionState is MarkAsEmailReadSuccess) { + _handleMarkEmailsAsReadOrUnread( + affectedMailboxId: reactionState.mailboxId, + readCount: reactionState.readActions == ReadActions.markAsRead + ? 1 + : null, + unreadCount: reactionState.readActions == ReadActions.markAsUnread + ? 1 + : null, + ); + } else if (reactionState is MarkAsMultipleEmailReadAllSuccess) { + for (var emailIdsByMailboxId in reactionState.markSuccessEmailIdsByMailboxId.entries) { + _handleMarkEmailsAsReadOrUnread( + affectedMailboxId: emailIdsByMailboxId.key, + readCount: reactionState.readActions == ReadActions.markAsRead + ? emailIdsByMailboxId.value.length + : null, + unreadCount: reactionState.readActions == ReadActions.markAsUnread + ? emailIdsByMailboxId.value.length + : null, + ); + } + } else if (reactionState is MarkAsMultipleEmailReadHasSomeEmailFailure) { + for (var emailIdsByMailboxId in reactionState.markSuccessEmailIdsByMailboxId.entries) { + _handleMarkEmailsAsReadOrUnread( + affectedMailboxId: emailIdsByMailboxId.key, + readCount: reactionState.readActions == ReadActions.markAsRead + ? emailIdsByMailboxId.value.length + : null, + unreadCount: reactionState.readActions == ReadActions.markAsUnread + ? emailIdsByMailboxId.value.length + : null, + ); + } + } else if (reactionState is MarkAsMailboxReadAllSuccess) { + _handleMarkMailboxAsRead( + affectedMailboxId: reactionState.mailboxId, + ); + } else if (reactionState is MarkAsMailboxReadHasSomeEmailFailure) { + _handleMarkEmailsAsReadOrUnread( + affectedMailboxId: reactionState.mailboxId, + readCount: reactionState.successEmailIds.length, + ); + } else if (reactionState is GetRestoredDeletedMessageCompleted) { + _handleMarkEmailsAsReadOrUnread( + affectedMailboxId: reactionState.recoveredMailbox?.id, + unreadCount: reactionState + .emailRecoveryAction + .successfulRestoreCount + ?.value + .toInt() ?? 0, + ); + } else if (reactionState is SaveEmailAsDraftsSuccess) { + _handleDraftSaved( + affectedMailboxId: reactionState.draftMailboxId, + totalEmailsChanged: 1, + ); + } else if (reactionState is RemoveEmailDraftsSuccess) { + _handleDraftSaved( + affectedMailboxId: reactionState.draftMailboxId, + totalEmailsChanged: -1, + ); + } else if (reactionState is DeleteEmailPermanentlySuccess) { + _handleDeleteEmailsFromMailbox( + affectedMailboxId: reactionState.mailboxId, + totalEmailsChanged: -1, + ); + } else if (reactionState is DeleteMultipleEmailsPermanentlyAllSuccess) { + _handleDeleteEmailsFromMailbox( + affectedMailboxId: reactionState.mailboxId, + totalEmailsChanged: -reactionState.emailIds.length, + ); + } else if (reactionState is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure) { + _handleDeleteEmailsFromMailbox( + affectedMailboxId: reactionState.mailboxId, + totalEmailsChanged: -reactionState.emailIds.length, + ); + } else if (reactionState is EmptyTrashFolderSuccess) { + _handleDeleteEmailsFromMailbox( + affectedMailboxId: reactionState.mailboxId, + totalEmailsChanged: -reactionState.emailIds.length, + ); + } else if (reactionState is EmptySpamFolderSuccess) { + _handleDeleteEmailsFromMailbox( + affectedMailboxId: reactionState.mailboxId, + totalEmailsChanged: -reactionState.emailIds.length, + ); + } else if (reactionState is MoveToMailboxSuccess) { + _handleMoveEmailsToMailbox( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + emailIdsWithReadStatus: reactionState.emailIdsWithReadStatus, + ); + } else if (reactionState is MoveMultipleEmailToMailboxAllSuccess) { + _handleMoveEmailsToMailbox( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + emailIdsWithReadStatus: reactionState.emailIdsWithReadStatus, + ); + } else if (reactionState is MoveMultipleEmailToMailboxHasSomeEmailFailure) { + _handleMoveEmailsToMailbox( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithMoveSucceededEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + emailIdsWithReadStatus: reactionState.moveSucceededEmailIdsWithReadStatus, + ); + } + }); + } + + void _handleMarkEmailsAsReadOrUnread({ + required MailboxId? affectedMailboxId, + int? readCount, + int? unreadCount, + }) { + if (affectedMailboxId == null) return; + + updateUnreadCountOfMailboxById( + affectedMailboxId, + unreadChanges: (unreadCount ?? 0) - (readCount ?? 0), + ); + } + + void _handleMarkMailboxAsRead({ + required MailboxId? affectedMailboxId, + }) { + if (affectedMailboxId == null) return; + + clearUnreadCount(affectedMailboxId); + } + + void _handleDraftSaved({ + required MailboxId? affectedMailboxId, + required int totalEmailsChanged, + }) { + if (affectedMailboxId == null) return; + + updateMailboxTotalEmailsCountById( + affectedMailboxId, + totalEmailsChanged, + ); + } + + void _handleDeleteEmailsFromMailbox({ + required MailboxId? affectedMailboxId, + required int totalEmailsChanged, + }) { + if (affectedMailboxId == null) return; + + updateMailboxTotalEmailsCountById( + affectedMailboxId, + totalEmailsChanged, + ); + } + + void _handleMoveEmailsToMailbox({ + required Map> originalMailboxIdsWithEmailIds, + required MailboxId destinationMailboxId, + required Map emailIdsWithReadStatus, + }) { + // Update changes in original mailboxes + for (var originalMailboxIdWithEmailIds in originalMailboxIdsWithEmailIds.entries) { + final originalMailboxId = originalMailboxIdWithEmailIds.key; + final emailsMovedCount = originalMailboxIdWithEmailIds.value.length; + final unreadEmailMovedCount = originalMailboxIdWithEmailIds.value + .where((emailId) => emailIdsWithReadStatus[emailId] == false) + .length; + updateMailboxTotalEmailsCountById( + originalMailboxId, + -emailsMovedCount, + ); + updateUnreadCountOfMailboxById( + originalMailboxId, + unreadChanges: -unreadEmailMovedCount, + ); + } + + // Update changes in destination mailbox + updateMailboxTotalEmailsCountById( + destinationMailboxId, + originalMailboxIdsWithEmailIds.entries.fold( + 0, + (sum, entry) => sum + entry.value.length, + ), + ); + updateUnreadCountOfMailboxById( + destinationMailboxId, + unreadChanges: originalMailboxIdsWithEmailIds + .values + .fold( + 0, + (sum, emails) => sum + emails.where((emailId) => emailIdsWithReadStatus[emailId] == false).length + ), + ); } void _initWebSocketQueueHandler() { @@ -636,6 +846,10 @@ class MailboxController extends BaseMailboxController } } + void _renameMailboxSuccess(RenameMailboxSuccess success) { + updateMailboxNameById(success.request.mailboxId, success.request.newName); + } + void _renameMailboxFailure(RenameMailboxFailure failure) { if (currentOverlayContext != null && currentContext != null) { final exception = failure.exception; diff --git a/lib/features/mailbox/presentation/model/mailbox_tree.dart b/lib/features/mailbox/presentation/model/mailbox_tree.dart index 125e6e3c63..2b1b54ecc7 100644 --- a/lib/features/mailbox/presentation/model/mailbox_tree.dart +++ b/lib/features/mailbox/presentation/model/mailbox_tree.dart @@ -3,6 +3,7 @@ import 'dart:collection'; import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/mailbox/expand_mode.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; @@ -83,6 +84,43 @@ class MailboxTree with EquatableMixin { } } + bool updateMailboxNameById(MailboxId mailboxId, MailboxName mailboxName) { + final matchedNode = findNode((node) => node.item.id == mailboxId); + if (matchedNode != null) { + matchedNode.item = matchedNode.item.copyWith(name: mailboxName); + return true; + } + return false; + } + + bool updateMailboxUnreadCountById(MailboxId mailboxId, int unreadCount) { + final matchedNode = findNode((node) => node.item.id == mailboxId); + if (matchedNode != null) { + final currentUnreadCount = matchedNode.item.unreadEmails?.value.value ?? 0; + final updatedUnreadCount = currentUnreadCount + unreadCount; + if (updatedUnreadCount < 0) return true; + matchedNode.item = matchedNode.item.copyWith( + unreadEmails: UnreadEmails(UnsignedInt(updatedUnreadCount)), + ); + return true; + } + return false; + } + + bool updateMailboxTotalEmailsCountById(MailboxId mailboxId, int totalEmailsCount) { + final matchedNode = findNode((node) => node.item.id == mailboxId); + if (matchedNode != null) { + final currentTotalEmailsCount = matchedNode.item.totalEmails?.value.value ?? 0; + final updatedTotalEmailsCount = currentTotalEmailsCount + totalEmailsCount; + if (updatedTotalEmailsCount < 0) return true; + matchedNode.item = matchedNode.item.copyWith( + totalEmails: TotalEmails(UnsignedInt(updatedTotalEmailsCount)), + ); + return true; + } + return false; + } + String? getNodePath(MailboxId mailboxId) { final matchedNode = findNode((node) => node.item.id == mailboxId); if (matchedNode == null) { diff --git a/lib/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart b/lib/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart index 5c79d586bd..f783378518 100644 --- a/lib/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart @@ -1,7 +1,15 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -class RemoveEmailDraftsSuccess extends UIState {} +class RemoveEmailDraftsSuccess extends UIState { + final MailboxId? draftMailboxId; + + RemoveEmailDraftsSuccess(this.draftMailboxId); + + @override + List get props => [draftMailboxId]; +} class RemoveEmailDraftsFailure extends FeatureFailure { diff --git a/lib/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart b/lib/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart index a832a29a1b..e0894477c7 100644 --- a/lib/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart +++ b/lib/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart @@ -4,6 +4,7 @@ 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:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart'; @@ -12,11 +13,16 @@ class RemoveEmailDraftsInteractor { RemoveEmailDraftsInteractor(this._emailRepository); - Stream> execute(Session session, AccountId accountId, EmailId emailId) async* { + Stream> execute( + Session session, + AccountId accountId, + EmailId emailId, + MailboxId? draftMailboxId, + ) async* { try { final result = await _emailRepository.removeEmailDrafts(session, accountId, emailId); if (result) { - yield Right(RemoveEmailDraftsSuccess()); + yield Right(RemoveEmailDraftsSuccess(draftMailboxId)); } else { yield Left(RemoveEmailDraftsFailure(result)); } 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 8fafc74f2d..2f5daca595 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -55,6 +55,7 @@ import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails import 'package:tmail_ui_user/features/email/domain/state/delete_sending_email_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_restored_deleted_message_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/restore_deleted_message_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/store_sending_email_state.dart'; @@ -97,7 +98,9 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/download/download_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/delete_emails_in_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/set_error_extension.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mixin/user_setting_popup_menu_mixin.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/composer_overlay_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; @@ -366,18 +369,20 @@ class MailboxDashBoardController extends ReloadableController } else if (success is UpdateVacationSuccess) { _handleUpdateVacationSuccess(success); } else if (success is MarkAsMultipleEmailReadAllSuccess) { - _markAsReadSelectedMultipleEmailSuccess(success.readActions); + _markAsReadSelectedMultipleEmailSuccess(success.readActions, success.emailIds); } else if (success is MarkAsMultipleEmailReadHasSomeEmailFailure) { - _markAsReadSelectedMultipleEmailSuccess(success.readActions); + _markAsReadSelectedMultipleEmailSuccess(success.readActions, success.successEmailIds); } else if (success is MarkAsStarMultipleEmailAllSuccess) { _markAsStarMultipleEmailSuccess( success.markStarAction, success.countMarkStarSuccess, + success.emailIds, ); } else if (success is MarkAsStarMultipleEmailHasSomeEmailFailure) { _markAsStarMultipleEmailSuccess( success.markStarAction, success.countMarkStarSuccess, + success.successEmailIds, ); } else if (success is MoveMultipleEmailToMailboxAllSuccess || success is MoveMultipleEmailToMailboxHasSomeEmailFailure) { @@ -415,6 +420,11 @@ class MailboxDashBoardController extends ReloadableController goToComposer(ComposerArguments.fromSessionStorageBrowser(success.composerCache)); } else if (success is GetIdentityCacheOnWebSuccess) { goToSettings(); + } else if (success is MarkAsStarEmailSuccess) { + updateEmailFlagByEmailIds( + [success.emailId], + markStarAction: success.markStarAction, + ); } } @@ -811,7 +821,7 @@ class MailboxDashBoardController extends ReloadableController currentOverlayContext!, AppLocalizations.of(currentContext!).drafts_saved, actionName: AppLocalizations.of(currentContext!).discard, - onActionClick: () => _discardEmail(success.emailId), + onActionClick: () => _discardEmail(success.emailId, success.draftMailboxId), leadingSVGIcon: imagePaths.icMailboxDrafts, leadingSVGIconColor: Colors.white, backgroundColor: AppColor.toastSuccessBackgroundColor, @@ -820,8 +830,28 @@ class MailboxDashBoardController extends ReloadableController } } - void moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { - consumeState(_moveToMailboxInteractor.execute(session, accountId, moveRequest)); + void moveToMailbox( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, + ) { + final currentMailboxes = moveRequest.currentMailboxes; + if (currentMailboxes.length == 1 && currentMailboxes.values.first.length == 1) { + consumeState(_moveToMailboxInteractor.execute( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + )); + } else { + consumeState(_moveMultipleEmailToMailboxInteractor.execute( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + )); + } } void _moveToMailboxSuccess(MoveToMailboxSuccess success) { @@ -836,7 +866,7 @@ class MailboxDashBoardController extends ReloadableController success.currentMailboxId, MoveAction.undo, success.emailActionType - )); + ), success.emailIdsWithReadStatus); }, leadingSVGIcon: imagePaths.icFolderMailbox, leadingSVGIconColor: Colors.white, @@ -846,19 +876,32 @@ class MailboxDashBoardController extends ReloadableController } } - void _revertedToOriginalMailbox(MoveToMailboxRequest newMoveRequest) { + void _revertedToOriginalMailbox( + MoveToMailboxRequest newMoveRequest, + Map emailIdsWithReadStatus, + ) { final currentAccountId = accountId.value; final session = sessionCurrent; if (currentAccountId != null && session != null) { - consumeState(_moveToMailboxInteractor.execute(session, currentAccountId, newMoveRequest)); + moveToMailbox( + session, + currentAccountId, + newMoveRequest, + emailIdsWithReadStatus, + ); } } - void _discardEmail(EmailId emailId) { + void _discardEmail(EmailId emailId, MailboxId? draftMailboxId) { final currentAccountId = accountId.value; final session = sessionCurrent; if (currentAccountId != null && session != null) { - consumeState(_removeEmailDraftsInteractor.execute(session, currentAccountId, emailId)); + consumeState(_removeEmailDraftsInteractor.execute( + session, + currentAccountId, + emailId, + draftMailboxId, + )); } } @@ -866,11 +909,20 @@ class MailboxDashBoardController extends ReloadableController final currentAccountId = accountId.value; final session = sessionCurrent; if (currentAccountId != null && session != null && email.id != null) { - consumeState(_deleteEmailPermanentlyInteractor.execute(session, currentAccountId, email.id!)); + consumeState(_deleteEmailPermanentlyInteractor.execute( + session, + currentAccountId, + email.id!, + email.mailboxContain?.mailboxId, + )); } } void _deleteEmailPermanentlySuccess(DeleteEmailPermanentlySuccess success) { + handleDeleteEmailsInMailbox( + emailIds: [success.emailId], + affectedMailboxId: success.mailboxId, + ); if (currentOverlayContext != null && currentContext != null) { appToast.showToastSuccessMessage( currentOverlayContext!, @@ -884,6 +936,7 @@ class MailboxDashBoardController extends ReloadableController EmailId emailId, ReadActions readActions, MarkReadAction markReadAction, + MailboxId? mailboxId, ) { if (accountId.value != null && sessionCurrent != null) { consumeState(_markAsEmailReadInteractor.execute( @@ -892,6 +945,7 @@ class MailboxDashBoardController extends ReloadableController emailId, readActions, markReadAction, + mailboxId, )); } } @@ -923,11 +977,13 @@ class MailboxDashBoardController extends ReloadableController accountId.value!, listEmailNeedMarkAsRead.listEmailIds, readActions, + listEmailNeedMarkAsRead.emailIdsByMailboxId, )); } } - void _markAsReadSelectedMultipleEmailSuccess(ReadActions readActions) { + void _markAsReadSelectedMultipleEmailSuccess(ReadActions readActions, List emailIds) { + updateEmailFlagByEmailIds(emailIds, readAction: readActions); if (currentContext != null && currentOverlayContext != null) { final message = readActions == ReadActions.markAsUnread ? AppLocalizations.of(currentContext!).marked_message_toast(AppLocalizations.of(currentContext!).unread) @@ -944,6 +1000,10 @@ class MailboxDashBoardController extends ReloadableController } void _markAsReadEmailSuccess(MarkAsEmailReadSuccess success) { + updateEmailFlagByEmailIds( + [success.emailId], + readAction: success.readActions, + ); if (currentContext != null && currentOverlayContext != null && success.markReadAction == MarkReadAction.swipeOnThread) { @@ -960,7 +1020,7 @@ class MailboxDashBoardController extends ReloadableController message, actionName: AppLocalizations.of(currentContext!).undo, onActionClick: () { - markAsEmailRead(success.emailId, undoAction, MarkReadAction.undo); + markAsEmailRead(success.emailId, undoAction, MarkReadAction.undo, success.mailboxId); }, leadingSVGIcon: imagePaths.icToastSuccessMessage, backgroundColor: AppColor.toastSuccessBackgroundColor, @@ -994,7 +1054,9 @@ class MailboxDashBoardController extends ReloadableController void _markAsStarMultipleEmailSuccess( MarkStarAction markStarAction, int countMarkStarSuccess, + List emailIds, ) { + updateEmailFlagByEmailIds(emailIds, markStarAction: markStarAction); if (currentOverlayContext != null && currentContext != null) { final message = markStarAction == MarkStarAction.unMarkStar ? AppLocalizations.of(currentContext!).marked_unstar_multiple_item(countMarkStarSuccess) @@ -1035,7 +1097,12 @@ class MailboxDashBoardController extends ReloadableController sessionCurrent!, listEmails.listEmailIds, currentMailbox, - destinationMailbox + destinationMailbox, + Map.fromEntries( + listEmails + .where((email) => email.id != null) + .map((email) => MapEntry(email.id!, email.hasRead)), + ), ); } } @@ -1046,7 +1113,8 @@ class MailboxDashBoardController extends ReloadableController Session session, List listEmailIds, PresentationMailbox currentMailbox, - PresentationMailbox destinationMailbox + PresentationMailbox destinationMailbox, + Map emailIdsWithReadStatus, ) { if (destinationMailbox.isTrash) { _moveSelectedEmailMultipleToMailboxAction( @@ -1056,7 +1124,8 @@ class MailboxDashBoardController extends ReloadableController {currentMailbox.id: listEmailIds}, destinationMailbox.id, MoveAction.moving, - EmailActionType.moveToTrash)); + EmailActionType.moveToTrash), + emailIdsWithReadStatus); } else if (destinationMailbox.isSpam) { _moveSelectedEmailMultipleToMailboxAction( session, @@ -1065,7 +1134,8 @@ class MailboxDashBoardController extends ReloadableController {currentMailbox.id: listEmailIds}, destinationMailbox.id, MoveAction.moving, - EmailActionType.moveToSpam)); + EmailActionType.moveToSpam), + emailIdsWithReadStatus); } else { _moveSelectedEmailMultipleToMailboxAction( session, @@ -1075,7 +1145,8 @@ class MailboxDashBoardController extends ReloadableController destinationMailbox.id, MoveAction.moving, EmailActionType.moveToMailbox, - destinationPath: destinationMailbox.mailboxPath)); + destinationPath: destinationMailbox.mailboxPath), + emailIdsWithReadStatus); } } @@ -1083,6 +1154,10 @@ class MailboxDashBoardController extends ReloadableController List listEmails, PresentationMailbox destinationMailbox, ) { + final emailIdsWithReadStatus = Map.fromEntries(listEmails + .where((email) => email.id != null) + .map((e) => MapEntry(e.id!, e.hasRead)) + ); if (searchController.isSearchEmailRunning){ final Map> mapListEmailSelectedByMailBoxId = {}; for (var element in listEmails) { @@ -1095,11 +1170,17 @@ class MailboxDashBoardController extends ReloadableController } } } - _handleDragSelectedMultipleEmailToMailboxAction(mapListEmailSelectedByMailBoxId, destinationMailbox); - } else { - if (selectedMailbox.value != null) { - _handleDragSelectedMultipleEmailToMailboxAction({selectedMailbox.value!.id: listEmails.listEmailIds}, destinationMailbox); - } + _handleDragSelectedMultipleEmailToMailboxAction( + mapListEmailSelectedByMailBoxId, + destinationMailbox, + emailIdsWithReadStatus, + ); + } else if (selectedMailbox.value != null) { + _handleDragSelectedMultipleEmailToMailboxAction( + {selectedMailbox.value!.id: listEmails.listEmailIds}, + destinationMailbox, + emailIdsWithReadStatus, + ); } } @@ -1107,6 +1188,7 @@ class MailboxDashBoardController extends ReloadableController void _handleDragSelectedMultipleEmailToMailboxAction( Map> mapListEmails, PresentationMailbox destinationMailbox, + Map emailIdsWithReadStatus, ) async { if (accountId.value != null && sessionCurrent != null) { if (destinationMailbox.isTrash) { @@ -1119,6 +1201,7 @@ class MailboxDashBoardController extends ReloadableController MoveAction.moving, EmailActionType.moveToTrash, ), + emailIdsWithReadStatus, ); } else if (destinationMailbox.isSpam) { moveToMailbox( @@ -1130,6 +1213,7 @@ class MailboxDashBoardController extends ReloadableController MoveAction.moving, EmailActionType.moveToSpam, ), + emailIdsWithReadStatus, ); } else { moveToMailbox( @@ -1142,6 +1226,7 @@ class MailboxDashBoardController extends ReloadableController EmailActionType.moveToMailbox, destinationPath: destinationMailbox.mailboxPath, ), + emailIdsWithReadStatus, ); } } @@ -1151,9 +1236,15 @@ class MailboxDashBoardController extends ReloadableController void _moveSelectedEmailMultipleToMailboxAction( Session session, AccountId accountId, - MoveToMailboxRequest moveRequest + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, ) { - consumeState(_moveMultipleEmailToMailboxInteractor.execute(session, accountId, moveRequest)); + consumeState(_moveMultipleEmailToMailboxInteractor.execute( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + )); } void _moveSelectedMultipleEmailToMailboxSuccess(Success success) { @@ -1163,6 +1254,7 @@ class MailboxDashBoardController extends ReloadableController MailboxId? destinationMailboxId; MoveAction? moveAction; EmailActionType? emailActionType; + Map? emailIdsWithReadStatus; if (success is MoveMultipleEmailToMailboxAllSuccess) { destinationPath = success.destinationPath; @@ -1171,6 +1263,7 @@ class MailboxDashBoardController extends ReloadableController destinationMailboxId = success.destinationMailboxId; moveAction = success.moveAction; emailActionType = success.emailActionType; + emailIdsWithReadStatus = success.emailIdsWithReadStatus; } else if (success is MoveMultipleEmailToMailboxHasSomeEmailFailure) { destinationPath = success.destinationPath; movedEmailIds = success.movedListEmailId; @@ -1178,6 +1271,7 @@ class MailboxDashBoardController extends ReloadableController destinationMailboxId = success.destinationMailboxId; moveAction = success.moveAction; emailActionType = success.emailActionType; + emailIdsWithReadStatus = success.moveSucceededEmailIdsWithReadStatus; } if (currentContext != null && @@ -1200,7 +1294,7 @@ class MailboxDashBoardController extends ReloadableController MoveAction.undo, emailActionType!, destinationPath: destinationPath - )); + ), emailIdsWithReadStatus ?? {}); } }, leadingSVGIconColor: Colors.white, @@ -1212,12 +1306,16 @@ class MailboxDashBoardController extends ReloadableController } } - void _revertedSelectionEmailToOriginalMailbox(MoveToMailboxRequest newMoveRequest) { + void _revertedSelectionEmailToOriginalMailbox( + MoveToMailboxRequest newMoveRequest, + Map emailIdsWithReadStatus, + ) { if (accountId.value != null && sessionCurrent != null) { consumeState(_moveMultipleEmailToMailboxInteractor.execute( sessionCurrent!, accountId.value!, - newMoveRequest)); + newMoveRequest, + emailIdsWithReadStatus)); } } @@ -1231,7 +1329,12 @@ class MailboxDashBoardController extends ReloadableController {mailboxCurrent.id: listEmails.listEmailIds}, trashMailboxId, MoveAction.moving, - EmailActionType.moveToTrash) + EmailActionType.moveToTrash), + Map.fromEntries( + listEmails + .where((email) => email.id != null) + .map((email) => MapEntry(email.id!, email.hasRead)), + ), ); } } @@ -1266,7 +1369,12 @@ class MailboxDashBoardController extends ReloadableController {mailboxCurrent.id: listEmail.listEmailIds}, spamMailboxId!, MoveAction.moving, - EmailActionType.moveToSpam) + EmailActionType.moveToSpam), + Map.fromEntries( + listEmail + .where((email) => email.id != null) + .map((email) => MapEntry(email.id!, email.hasRead)), + ), ); } @@ -1312,7 +1420,12 @@ class MailboxDashBoardController extends ReloadableController {spamMailboxId!: listEmail.listEmailIds}, inboxMailboxId, MoveAction.moving, - EmailActionType.unSpam) + EmailActionType.unSpam), + Map.fromEntries( + listEmail + .where((email) => email.id != null) + .map((email) => MapEntry(email.id!, email.hasRead)), + ), ); } @@ -1412,6 +1525,10 @@ class MailboxDashBoardController extends ReloadableController void _emptyTrashFolderSuccess(EmptyTrashFolderSuccess success) { viewStateMailboxActionProgress.value = Right(UIState.idle); + handleDeleteEmailsInMailbox( + emailIds: success.emailIds, + affectedMailboxId: success.mailboxId, + ); if (currentOverlayContext != null && currentContext != null) { appToast.showToastSuccessMessage( currentOverlayContext!, @@ -1426,15 +1543,24 @@ class MailboxDashBoardController extends ReloadableController consumeState(_deleteMultipleEmailsPermanentlyInteractor.execute( sessionCurrent!, accountId.value!, - listEmails.listEmailIds)); + listEmails.listEmailIds, + listEmails.firstOrNull?.mailboxContain?.mailboxId)); } } void _deleteMultipleEmailsPermanentlySuccess(Success success) { List listEmailIdResult = []; if (success is DeleteMultipleEmailsPermanentlyAllSuccess) { + handleDeleteEmailsInMailbox( + emailIds: success.emailIds, + affectedMailboxId: success.mailboxId, + ); listEmailIdResult = success.emailIds; } else if (success is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure) { + handleDeleteEmailsInMailbox( + emailIds: success.emailIds, + affectedMailboxId: success.mailboxId, + ); listEmailIdResult = success.emailIds; } @@ -2417,6 +2543,10 @@ class MailboxDashBoardController extends ReloadableController void _emptySpamFolderSuccess(EmptySpamFolderSuccess success) { viewStateMailboxActionProgress.value = Right(UIState.idle); + handleDeleteEmailsInMailbox( + emailIds: success.emailIds, + affectedMailboxId: success.mailboxId, + ); if (currentOverlayContext != null && currentContext != null) { appToast.showToastSuccessMessage( currentOverlayContext!, @@ -2779,7 +2909,8 @@ class MailboxDashBoardController extends ReloadableController moveToMailbox( sessionCurrent!, accountId.value!, - moveToArchiveMailboxRequest + moveToArchiveMailboxRequest, + {email.id!: email.hasRead} ); } } diff --git a/lib/features/mailbox_dashboard/presentation/extensions/delete_emails_in_mailbox_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/delete_emails_in_mailbox_extension.dart new file mode 100644 index 0000000000..6a542df58b --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/extensions/delete_emails_in_mailbox_extension.dart @@ -0,0 +1,16 @@ +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; + +extension DeleteEmailsInMailboxExtension on MailboxDashBoardController { + void handleDeleteEmailsInMailbox({ + required List emailIds, + required MailboxId? affectedMailboxId, + }) { + if (selectedMailbox.value?.id != affectedMailboxId) { + return; + } + + emailsInCurrentMailbox.removeWhere((email) => emailIds.contains(email.id)); + } +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart new file mode 100644 index 0000000000..468d3f5236 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart @@ -0,0 +1,31 @@ +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; + +extension MoveEmailsToMailboxExtension on MailboxDashBoardController { + void handleMoveEmailsToMailbox({ + required Map> originalMailboxIdsWithEmailIds, + required MailboxId destinationMailboxId, + required MoveAction moveAction, + }) { + final currentEmails = List.from( + emailsInCurrentMailbox, + ); + final movedEmailIds = originalMailboxIdsWithEmailIds.entries.fold( + {}, + (emailIds, entry) { + emailIds.addAll(entry.value); + return emailIds; + }, + ).toList(); + final currentEmailsToBeMoved = currentEmails + .where((email) => movedEmailIds.contains(email.id)) + .toList(); + if (currentEmailsToBeMoved.isNotEmpty && destinationMailboxId != selectedMailbox.value?.id) { + currentEmails.removeWhere(currentEmailsToBeMoved.contains); + updateEmailList(currentEmails); + } + } +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart new file mode 100644 index 0000000000..f0771be0f4 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart @@ -0,0 +1,61 @@ +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; +import 'package:model/email/mark_star_action.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/email/read_actions.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; + +extension UpdateCurrentEmailsFlagsExtension on MailboxDashBoardController { + void updateEmailFlagByEmailIds( + List emailIds, { + ReadActions? readAction, + MarkStarAction? markStarAction, + }) { + if (readAction == null && markStarAction == null) return; + + final currentEmails = dashboardRoute.value == DashboardRoutes.searchEmail + ? listResultSearch + : emailsInCurrentMailbox; + + for (var email in currentEmails) { + if (!emailIds.contains(email.id)) continue; + + switch (readAction) { + case ReadActions.markAsRead: + _updateKeyword(email, KeyWordIdentifier.emailSeen, true); + break; + case ReadActions.markAsUnread: + _updateKeyword(email, KeyWordIdentifier.emailSeen, false); + break; + default: + break; + } + + switch (markStarAction) { + case MarkStarAction.markStar: + _updateKeyword(email, KeyWordIdentifier.emailFlagged, true); + break; + case MarkStarAction.unMarkStar: + _updateKeyword(email, KeyWordIdentifier.emailFlagged, false); + break; + default: + break; + } + } + + currentEmails.refresh(); + } + + void _updateKeyword( + PresentationEmail presentationEmail, + KeyWordIdentifier keyword, + bool value, + ) { + if (value) { + presentationEmail.keywords?[keyword] = true; + } else { + presentationEmail.keywords?.remove(keyword); + } + } +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/extensions/update_emails_with_new_mailbox_id_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/update_emails_with_new_mailbox_id_extension.dart new file mode 100644 index 0000000000..729a8dab68 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/extensions/update_emails_with_new_mailbox_id_extension.dart @@ -0,0 +1,31 @@ +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; + +extension UpdateEmailsWithNewMailboxIdExtension on MailboxDashBoardController { + handleUpdateEmailsWithNewMailboxId({ + required Map> originalMailboxIdsWithEmailIds, + required MailboxId destinationMailboxId, + }) { + final currentEmails = List.from( + emailsInCurrentMailbox, + ); + final movedEmailIds = originalMailboxIdsWithEmailIds.entries.fold( + {}, + (emailIds, entry) { + emailIds.addAll(entry.value); + return emailIds; + }, + ).toList(); + for (int i = 0; i < currentEmails.length; i++) { + if (!movedEmailIds.contains(currentEmails[i].id)) continue; + + currentEmails[i] = currentEmails[i].copyWith( + mailboxIds: {destinationMailboxId: true}, + mailboxContain: mapMailboxById[destinationMailboxId], + ); + } + updateEmailList(currentEmails); + } +} \ No newline at end of file diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index ccd7b79f7b..e92905595f 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -33,6 +33,7 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_e import 'package:tmail_ui_user/features/contact/presentation/model/contact_arguments.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; +import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; @@ -44,6 +45,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_all import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/quick_search_email_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_recent_search_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_emails_with_new_mailbox_id_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart'; @@ -60,6 +62,7 @@ import 'package:tmail_ui_user/features/search/email/presentation/model/search_mo import 'package:tmail_ui_user/features/search/email/presentation/search_email_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_more_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/search_email_interactor.dart'; @@ -260,6 +263,27 @@ class SearchEmailController extends BaseController } }, ); + + ever(mailboxDashBoardController.viewState, (viewState) { + if (!mailboxDashBoardController.searchController.isSearchEmailRunning) return; + final reactionState = viewState.getOrElse(() => UIState.idle); + if (reactionState is MoveToMailboxSuccess) { + mailboxDashBoardController.handleUpdateEmailsWithNewMailboxId( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + ); + } else if (reactionState is MoveMultipleEmailToMailboxAllSuccess) { + mailboxDashBoardController.handleUpdateEmailsWithNewMailboxId( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + ); + } else if (reactionState is MoveMultipleEmailToMailboxHasSomeEmailFailure) { + mailboxDashBoardController.handleUpdateEmailsWithNewMailboxId( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithMoveSucceededEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + ); + } + }); } void _refreshEmailChanges({jmap.State? newState}) { diff --git a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart index 2ae4bc4455..6cc65fe53d 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart @@ -33,6 +33,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_request.da import 'package:tmail_ui_user/features/mailbox/domain/state/create_new_mailbox_state.dart'; 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_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'; @@ -147,6 +148,8 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa searchMailboxAction(); } else if (success is SearchMailboxSuccess) { _handleSearchMailboxSuccess(success); + } else if (success is RenameMailboxSuccess) { + updateMailboxNameById(success.request.mailboxId, success.request.newName); } else if (success is MoveMailboxSuccess) { _moveMailboxSuccess(success); } else if (success is DeleteMultipleMailboxAllSuccess) { @@ -183,6 +186,18 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa _refreshMailboxChanges(newState: action.newState); } }); + + ever(dashboardController.viewState, (viewState) { + final reactionState = viewState.getOrElse(() => UIState.idle); + if (reactionState is MarkAsMailboxReadAllSuccess) { + clearUnreadCount(reactionState.mailboxId); + } else if (reactionState is MarkAsMailboxReadHasSomeEmailFailure) { + updateUnreadCountOfMailboxById( + reactionState.mailboxId, + unreadChanges: -reactionState.countEmailsRead, + ); + } + }); } void _getAllMailboxAction() { diff --git a/lib/features/thread/data/local/email_cache_manager.dart b/lib/features/thread/data/local/email_cache_manager.dart index 9fa8d0383b..47bfe07eb6 100644 --- a/lib/features/thread/data/local/email_cache_manager.dart +++ b/lib/features/thread/data/local/email_cache_manager.dart @@ -95,6 +95,16 @@ class EmailCacheManager { return _emailCacheClient.insertItem(keyCache, emailCache); } + Future storeMultipleEmails(AccountId accountId, UserName userName, List emailsCache) async { + final emailsToCache = Map.fromEntries(emailsCache.map( + (emailCache) => MapEntry( + TupleKey(emailCache.id, accountId.asString, userName.value).encodeKey, + emailCache, + ), + )); + await _emailCacheClient.insertMultipleItem(emailsToCache); + } + Future getStoredEmail(AccountId accountId, UserName userName, EmailId emailId) async { final keyCache = TupleKey(emailId.asString, accountId.asString, userName.value).encodeKey; final emailCache = await _emailCacheClient.getItem(keyCache, needToReopen: true); @@ -104,4 +114,16 @@ class EmailCacheManager { throw NotFoundStoredEmailException(); } } + + Future> getMultipleStoredEmails( + AccountId accountId, + UserName userName, + List emailIds, + ) async { + final keys = emailIds + .map((emailId) => TupleKey(emailId.asString, accountId.asString, userName.value).encodeKey) + .toList(); + final emails = await _emailCacheClient.getValuesByListKey(keys); + return emails; + } } \ No newline at end of file diff --git a/lib/features/thread/domain/state/empty_spam_folder_state.dart b/lib/features/thread/domain/state/empty_spam_folder_state.dart index 6823c8f1b8..3a53ff3f1d 100644 --- a/lib/features/thread/domain/state/empty_spam_folder_state.dart +++ b/lib/features/thread/domain/state/empty_spam_folder_state.dart @@ -8,11 +8,12 @@ class EmptySpamFolderLoading extends LoadingState {} class EmptySpamFolderSuccess extends UIState { final List emailIds; + final MailboxId? mailboxId; - EmptySpamFolderSuccess(this.emailIds); + EmptySpamFolderSuccess(this.emailIds, this.mailboxId); @override - List get props => [emailIds]; + List get props => [emailIds, mailboxId]; } class EmptySpamFolderFailure extends FeatureFailure { diff --git a/lib/features/thread/domain/state/empty_trash_folder_state.dart b/lib/features/thread/domain/state/empty_trash_folder_state.dart index 577eb6d7cc..0d04ce4a27 100644 --- a/lib/features/thread/domain/state/empty_trash_folder_state.dart +++ b/lib/features/thread/domain/state/empty_trash_folder_state.dart @@ -1,17 +1,19 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; class EmptyTrashFolderLoading extends LoadingState {} class EmptyTrashFolderSuccess extends UIState { final List emailIds; + final MailboxId? mailboxId; - EmptyTrashFolderSuccess(this.emailIds); + EmptyTrashFolderSuccess(this.emailIds, this.mailboxId); @override - List get props => [emailIds]; + List get props => [emailIds, mailboxId]; } class EmptyTrashFolderFailure extends FeatureFailure { diff --git a/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart b/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart index 9134f49b2b..c3c322f5b5 100644 --- a/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart +++ b/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart @@ -1,20 +1,24 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/read_actions.dart'; class LoadingMarkAsMultipleEmailReadAll extends UIState {} class MarkAsMultipleEmailReadAllSuccess extends UIState { - final int countMarkAsReadSuccess; + final List emailIds; final ReadActions readActions; + final Map> markSuccessEmailIdsByMailboxId; MarkAsMultipleEmailReadAllSuccess( - this.countMarkAsReadSuccess, - this.readActions, + this.emailIds, + this.readActions, + this.markSuccessEmailIdsByMailboxId, ); @override - List get props => [countMarkAsReadSuccess, readActions]; + List get props => [emailIds, readActions, markSuccessEmailIdsByMailboxId]; } class MarkAsMultipleEmailReadAllFailure extends FeatureFailure { @@ -27,16 +31,18 @@ class MarkAsMultipleEmailReadAllFailure extends FeatureFailure { } class MarkAsMultipleEmailReadHasSomeEmailFailure extends UIState { - final int countMarkAsReadSuccess; + final List successEmailIds; final ReadActions readActions; + final Map> markSuccessEmailIdsByMailboxId; MarkAsMultipleEmailReadHasSomeEmailFailure( - this.countMarkAsReadSuccess, + this.successEmailIds, this.readActions, + this.markSuccessEmailIdsByMailboxId, ); @override - List get props => [countMarkAsReadSuccess, readActions]; + List get props => [successEmailIds, readActions, markSuccessEmailIdsByMailboxId]; } class MarkAsMultipleEmailReadFailure extends FeatureFailure { diff --git a/lib/features/thread/domain/state/mark_as_star_multiple_email_state.dart b/lib/features/thread/domain/state/mark_as_star_multiple_email_state.dart index b41a891546..c7c5cecabf 100644 --- a/lib/features/thread/domain/state/mark_as_star_multiple_email_state.dart +++ b/lib/features/thread/domain/state/mark_as_star_multiple_email_state.dart @@ -1,5 +1,6 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/email/mark_star_action.dart'; class LoadingMarkAsStarMultipleEmailAll extends UIState {} @@ -7,14 +8,16 @@ class LoadingMarkAsStarMultipleEmailAll extends UIState {} class MarkAsStarMultipleEmailAllSuccess extends UIState { final int countMarkStarSuccess; final MarkStarAction markStarAction; + final List emailIds; MarkAsStarMultipleEmailAllSuccess( this.countMarkStarSuccess, this.markStarAction, + this.emailIds, ); @override - List get props => [countMarkStarSuccess, markStarAction]; + List get props => [countMarkStarSuccess, markStarAction, emailIds]; } class MarkAsStarMultipleEmailAllFailure extends FeatureFailure { @@ -29,14 +32,16 @@ class MarkAsStarMultipleEmailAllFailure extends FeatureFailure { class MarkAsStarMultipleEmailHasSomeEmailFailure extends UIState { final int countMarkStarSuccess; final MarkStarAction markStarAction; + final List successEmailIds; MarkAsStarMultipleEmailHasSomeEmailFailure( this.countMarkStarSuccess, this.markStarAction, + this.successEmailIds, ); @override - List get props => [countMarkStarSuccess, markStarAction]; + List get props => [countMarkStarSuccess, markStarAction, successEmailIds]; } class MarkAsStarMultipleEmailFailure extends FeatureFailure { diff --git a/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart b/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart index b4f179a334..8d2ce732ad 100644 --- a/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart +++ b/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart @@ -14,6 +14,8 @@ class MoveMultipleEmailToMailboxAllSuccess extends UIState { final MoveAction moveAction; final EmailActionType emailActionType; final String? destinationPath; + final Map> originalMailboxIdsWithEmailIds; + final Map emailIdsWithReadStatus; MoveMultipleEmailToMailboxAllSuccess( this.movedListEmailId, @@ -23,6 +25,8 @@ class MoveMultipleEmailToMailboxAllSuccess extends UIState { this.emailActionType, { this.destinationPath, + required this.originalMailboxIdsWithEmailIds, + required this.emailIdsWithReadStatus, } ); @@ -34,6 +38,8 @@ class MoveMultipleEmailToMailboxAllSuccess extends UIState { moveAction, emailActionType, destinationPath, + originalMailboxIdsWithEmailIds, + emailIdsWithReadStatus, ]; } @@ -54,6 +60,8 @@ class MoveMultipleEmailToMailboxHasSomeEmailFailure extends UIState { final MoveAction moveAction; final EmailActionType emailActionType; final String? destinationPath; + final Map> originalMailboxIdsWithMoveSucceededEmailIds; + final Map moveSucceededEmailIdsWithReadStatus; MoveMultipleEmailToMailboxHasSomeEmailFailure( this.movedListEmailId, @@ -63,6 +71,8 @@ class MoveMultipleEmailToMailboxHasSomeEmailFailure extends UIState { this.emailActionType, { this.destinationPath, + required this.originalMailboxIdsWithMoveSucceededEmailIds, + required this.moveSucceededEmailIdsWithReadStatus, } ); @@ -74,6 +84,8 @@ class MoveMultipleEmailToMailboxHasSomeEmailFailure extends UIState { moveAction, emailActionType, destinationPath, + originalMailboxIdsWithMoveSucceededEmailIds, + moveSucceededEmailIdsWithReadStatus, ]; } diff --git a/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart b/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart index 0093f76447..0eb7d07d8e 100644 --- a/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart +++ b/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart @@ -32,7 +32,7 @@ class EmptySpamFolderInteractor { totalEmails, onProgressController ); - yield Right(EmptySpamFolderSuccess(emailIdDeleted)); + yield Right(EmptySpamFolderSuccess(emailIdDeleted, spamMailboxId)); } catch (e) { yield Left(EmptySpamFolderFailure(e)); } diff --git a/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart b/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart index d2474096f5..c3764860b3 100644 --- a/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart +++ b/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart @@ -32,7 +32,7 @@ class EmptyTrashFolderInteractor { totalEmails, onProgressController ); - yield Right(EmptyTrashFolderSuccess(emailIdDeleted,)); + yield Right(EmptyTrashFolderSuccess(emailIdDeleted, trashMailboxId)); } catch (e) { yield Left(EmptyTrashFolderFailure(e)); } diff --git a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart index 67299d960e..505a15642b 100644 --- a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart +++ b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart @@ -4,6 +4,7 @@ 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:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; @@ -17,7 +18,8 @@ class MarkAsMultipleEmailReadInteractor { Session session, AccountId accountId, List emailIds, - ReadActions readAction + ReadActions readAction, + Map> emailIdsByMailboxId, ) async* { try { yield Right(LoadingMarkAsMultipleEmailReadAll()); @@ -28,18 +30,26 @@ class MarkAsMultipleEmailReadInteractor { emailIds, readAction, ); + final markSuccessEmailIdsByMailboxId = emailIdsByMailboxId.map( + (key, value) => MapEntry( + key, + value.where(result.emailIdsSuccess.contains).toList(), + ), + ); if (emailIds.length == result.emailIdsSuccess.length) { yield Right(MarkAsMultipleEmailReadAllSuccess( - result.emailIdsSuccess.length, - readAction, + result.emailIdsSuccess, + readAction, + markSuccessEmailIdsByMailboxId, )); } else if (result.emailIdsSuccess.isEmpty) { yield Left(MarkAsMultipleEmailReadAllFailure(readAction)); } else { yield Right(MarkAsMultipleEmailReadHasSomeEmailFailure( - result.emailIdsSuccess.length, - readAction, + result.emailIdsSuccess, + readAction, + markSuccessEmailIdsByMailboxId, )); } } catch (e) { diff --git a/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart b/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart index 8ac2e00b4b..980a1ff967 100644 --- a/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart +++ b/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart @@ -26,15 +26,17 @@ class MarkAsStarMultipleEmailInteractor { if (emailIds.length == result.emailIdsSuccess.length) { yield Right(MarkAsStarMultipleEmailAllSuccess( - emailIds.length, - markStarAction, + emailIds.length, + markStarAction, + result.emailIdsSuccess, )); } else if (result.emailIdsSuccess.isEmpty) { yield Left(MarkAsStarMultipleEmailAllFailure(markStarAction)); } else { yield Right(MarkAsStarMultipleEmailHasSomeEmailFailure( - result.emailIdsSuccess.length, - markStarAction, + result.emailIdsSuccess.length, + markStarAction, + result.emailIdsSuccess, )); } } catch (e) { diff --git a/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart b/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart index f7481c5b6c..4ba72049f4 100644 --- a/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart +++ b/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart @@ -1,10 +1,13 @@ import 'dart:async'; +import 'package:core/presentation/extensions/map_extensions.dart'; 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:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; @@ -17,7 +20,8 @@ class MoveMultipleEmailToMailboxInteractor { Stream> execute( Session session, AccountId accountId, - MoveToMailboxRequest moveRequest + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, ) async* { try { yield Right(LoadingMoveMultipleEmailToMailboxAll()); @@ -30,10 +34,22 @@ class MoveMultipleEmailToMailboxInteractor { moveRequest.moveAction, moveRequest.emailActionType, destinationPath: moveRequest.destinationPath, + originalMailboxIdsWithEmailIds: moveRequest.currentMailboxes, + emailIdsWithReadStatus: emailIdsWithReadStatus, )); } else if (result.emailIdsSuccess.isEmpty) { yield Left(MoveMultipleEmailToMailboxAllFailure(moveRequest.moveAction, moveRequest.emailActionType)); } else { + final originalMailboxIdsWithEmailIds = Map>.from( + moveRequest.currentMailboxes, + ); + final originalMailboxIdsWithMoveSucceededEmailIds = originalMailboxIdsWithEmailIds + .map((key, value) => MapEntry( + key, + value.where(result.emailIdsSuccess.contains).toList() + )); + final moveSucceededEmailIdsWithReadStatus = emailIdsWithReadStatus + .where((emailId, _) => result.emailIdsSuccess.contains(emailId)); yield Right(MoveMultipleEmailToMailboxHasSomeEmailFailure( result.emailIdsSuccess, moveRequest.currentMailboxes.keys.first, @@ -41,6 +57,8 @@ class MoveMultipleEmailToMailboxInteractor { moveRequest.moveAction, moveRequest.emailActionType, destinationPath: moveRequest.destinationPath, + originalMailboxIdsWithMoveSucceededEmailIds: originalMailboxIdsWithMoveSucceededEmailIds, + moveSucceededEmailIdsWithReadStatus: moveSucceededEmailIdsWithReadStatus, )); } } catch (e) { diff --git a/lib/features/thread/presentation/mixin/email_action_controller.dart b/lib/features/thread/presentation/mixin/email_action_controller.dart index baefee994b..64e4b1e724 100644 --- a/lib/features/thread/presentation/mixin/email_action_controller.dart +++ b/lib/features/thread/presentation/mixin/email_action_controller.dart @@ -11,6 +11,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/email/email_action_type.dart'; import 'package:model/email/mark_star_action.dart'; import 'package:model/email/presentation_email.dart'; @@ -60,13 +61,24 @@ mixin EmailActionController { {mailboxContain.id: email.id != null ? [email.id!] : []}, trashMailboxId, MoveAction.moving, - EmailActionType.moveToTrash) + EmailActionType.moveToTrash), + email.id != null ? {email.id! : email.hasRead} : {}, ); } } - void _moveToTrashAction(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { - mailboxDashBoardController.moveToMailbox(session, accountId, moveRequest); + void _moveToTrashAction( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, + ) { + mailboxDashBoardController.moveToMailbox( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + ); } void moveToSpam(PresentationEmail email, {PresentationMailbox? mailboxContain}) async { @@ -82,7 +94,8 @@ mixin EmailActionController { {mailboxContain.id: email.id != null ? [email.id!] : []}, spamMailboxId, MoveAction.moving, - EmailActionType.moveToSpam) + EmailActionType.moveToSpam), + email.id != null ? {email.id! : email.hasRead} : {}, ); } } @@ -101,13 +114,24 @@ mixin EmailActionController { {spamMailboxId: email.id != null ? [email.id!] : []}, inboxMailboxId, MoveAction.moving, - EmailActionType.unSpam) + EmailActionType.unSpam), + email.id != null ? {email.id! : email.hasRead} : {}, ); } } - void moveToSpamAction(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { - mailboxDashBoardController.moveToMailbox(session, accountId, moveRequest); + void moveToSpamAction( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, + ) { + mailboxDashBoardController.moveToMailbox( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + ); } void moveToMailbox( @@ -157,7 +181,8 @@ mixin EmailActionController { {currentMailbox.id: emailSelected.id != null ? [emailSelected.id!] : []}, destinationMailbox.id, MoveAction.moving, - EmailActionType.moveToTrash)); + EmailActionType.moveToTrash), + emailSelected.id != null ? {emailSelected.id! : emailSelected.hasRead} : {}); } else if (destinationMailbox.isSpam) { moveToSpamAction( session, @@ -166,7 +191,8 @@ mixin EmailActionController { {currentMailbox.id: emailSelected.id != null ? [emailSelected.id!] : []}, destinationMailbox.id, MoveAction.moving, - EmailActionType.moveToSpam)); + EmailActionType.moveToSpam), + emailSelected.id != null ? {emailSelected.id! : emailSelected.hasRead} : {}); } else { _moveToMailboxAction( session, @@ -176,12 +202,23 @@ mixin EmailActionController { destinationMailbox.id, MoveAction.moving, EmailActionType.moveToMailbox, - destinationPath: destinationMailbox.mailboxPath)); + destinationPath: destinationMailbox.mailboxPath), + emailSelected.id != null ? {emailSelected.id! : emailSelected.hasRead} : {}); } } - void _moveToMailboxAction(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { - mailboxDashBoardController.moveToMailbox(session, accountId, moveRequest); + void _moveToMailboxAction( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, + ) { + mailboxDashBoardController.moveToMailbox( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + ); } void deleteEmailPermanently(BuildContext context, PresentationEmail email) { @@ -233,6 +270,7 @@ mixin EmailActionController { presentationEmail.id!, readActions, markReadAction, + presentationEmail.mailboxContain?.mailboxId, ); } diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 9b87815818..0fbb495016 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -20,11 +20,17 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; +import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart'; @@ -45,6 +51,7 @@ import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_email_by_id_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/load_more_emails_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/refresh_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/refresh_changes_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; @@ -331,6 +338,72 @@ class ThreadController extends BaseController with EmailActionController { mailboxDashBoardController.clearEmailUIAction(); } }); + + ever(mailboxDashBoardController.viewState, (viewState) { + if (mailboxDashBoardController.searchController.isSearchEmailRunning) return; + final reactionState = viewState.getOrElse(() => UIState.idle); + if (reactionState is MarkAsMailboxReadAllSuccess) { + _handleMarkEmailsAsReadByMailboxId(reactionState.mailboxId); + } else if (reactionState is MarkAsMailboxReadHasSomeEmailFailure) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + reactionState.successEmailIds, + readAction: ReadActions.markAsRead, + ); + } else if (reactionState is MoveToMailboxSuccess) { + mailboxDashBoardController.handleMoveEmailsToMailbox( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + moveAction: reactionState.moveAction, + ); + _checkIfCurrentMailboxCanLoadMore(); + } else if (reactionState is MoveMultipleEmailToMailboxAllSuccess) { + mailboxDashBoardController.handleMoveEmailsToMailbox( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + moveAction: reactionState.moveAction, + ); + _checkIfCurrentMailboxCanLoadMore(); + } else if (reactionState is MoveMultipleEmailToMailboxHasSomeEmailFailure) { + mailboxDashBoardController.handleMoveEmailsToMailbox( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithMoveSucceededEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + moveAction: reactionState.moveAction, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkIfCurrentMailboxCanLoadMore(); + }); + } else if (reactionState is DeleteEmailPermanentlySuccess + || reactionState is DeleteMultipleEmailsPermanentlyAllSuccess + || reactionState is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure + ) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkIfCurrentMailboxCanLoadMore(); + }); + } + }); + } + + void _handleMarkEmailsAsReadByMailboxId(MailboxId mailboxId) { + if (mailboxDashBoardController.selectedMailbox.value?.id != mailboxId) return; + + for (var presentationEmail in mailboxDashBoardController.emailsInCurrentMailbox) { + if (presentationEmail.mailboxContain?.id != mailboxId) continue; + + presentationEmail.keywords?[KeyWordIdentifier.emailSeen] = true; + } + mailboxDashBoardController.emailsInCurrentMailbox.refresh(); + } + + void _checkIfCurrentMailboxCanLoadMore() { + final currentMailbox = mailboxDashBoardController.selectedMailbox.value; + if (currentMailbox == null) return; + + final totalEmailsCount = currentMailbox.countTotalEmails; + if (totalEmailsCount == 0 + || mailboxDashBoardController.emailsInCurrentMailbox.isNotEmpty + ) return; + + dispatchState(Right(GetAllEmailLoading())); } void _registerBrowserResizeListener() { @@ -444,7 +517,9 @@ class ThreadController extends BaseController with EmailActionController { } } - void _getAllEmailAction({bool getLatestChanges = true}) { + void _getAllEmailAction({ + bool getLatestChanges = true, + }) { log('ThreadController::_getAllEmailAction:'); if (_session != null &&_accountId != null) { consumeState(_getEmailsInMailboxInteractor.execute( @@ -792,6 +867,9 @@ class ThreadController extends BaseController with EmailActionController { } void cancelSelectEmail() { + if (mailboxDashBoardController.currentSelectMode.value == SelectMode.INACTIVE) { + return; + } final newEmailList = mailboxDashBoardController.emailsInCurrentMailbox .map((email) => email.toSelectedEmail(selectMode: SelectMode.INACTIVE)) .toList(); diff --git a/model/lib/email/presentation_email.dart b/model/lib/email/presentation_email.dart index 1989247b83..d3ec44432e 100644 --- a/model/lib/email/presentation_email.dart +++ b/model/lib/email/presentation_email.dart @@ -176,4 +176,54 @@ class PresentationEmail with EquatableMixin, SearchSnippetMixin { searchSnippetSubject, searchSnippetPreview, ]; + + PresentationEmail copyWith({ + EmailId? id, + Id? blobId, + Map? keywords, + UnsignedInt? size, + UTCDate? receivedAt, + bool? hasAttachment, + String? preview, + String? subject, + UTCDate? sentAt, + Set? from, + Set? to, + Set? cc, + Set? bcc, + Set? replyTo, + Map? mailboxIds, + SelectMode? selectMode, + Uri? routeWeb, + PresentationMailbox? mailboxContain, + List? emailHeader, + Set? htmlBody, + Map? bodyValues, + Map? headerCalendarEvent, + }) { + return PresentationEmail( + id: id ?? this.id, + blobId: blobId ?? this.blobId, + keywords: keywords ?? this.keywords, + size: size ?? this.size, + receivedAt: receivedAt ?? this.receivedAt, + hasAttachment: hasAttachment ?? this.hasAttachment, + preview: preview ?? this.preview, + subject: subject ?? this.subject, + sentAt: sentAt ?? this.sentAt, + from: from ?? this.from, + to: to ?? this.to, + cc: cc ?? this.cc, + bcc: bcc ?? this.bcc, + replyTo: replyTo ?? this.replyTo, + mailboxIds: mailboxIds ?? this.mailboxIds, + selectMode: selectMode ?? this.selectMode, + routeWeb: routeWeb ?? this.routeWeb, + mailboxContain: mailboxContain ?? this.mailboxContain, + emailHeader: emailHeader ?? this.emailHeader, + htmlBody: htmlBody ?? this.htmlBody, + bodyValues: bodyValues ?? this.bodyValues, + headerCalendarEvent: headerCalendarEvent ?? this.headerCalendarEvent, + ); + } } \ No newline at end of file diff --git a/model/lib/extensions/list_presentation_email_extension.dart b/model/lib/extensions/list_presentation_email_extension.dart index 2535b39a84..7ab345961b 100644 --- a/model/lib/extensions/list_presentation_email_extension.dart +++ b/model/lib/extensions/list_presentation_email_extension.dart @@ -22,6 +22,18 @@ extension ListPresentationEmailExtension on List { List get listEmailIds => map((email) => email.id).whereNotNull().toList(); + Map> get emailIdsByMailboxId { + final Map> result = {}; + for (final email in this) { + final mailboxId = email.mailboxContain?.mailboxId; + final emailId = email.id; + if (mailboxId != null && emailId != null) { + (result[mailboxId] ??= []).add(emailId); + } + } + return result; + } + bool isAllCanDeletePermanently(Map mapMailbox) { final listMailboxContain = map((email) => email.findMailboxContain(mapMailbox)) .whereType() diff --git a/model/lib/extensions/mailbox_extension.dart b/model/lib/extensions/mailbox_extension.dart index 44dcefa265..6219188be3 100644 --- a/model/lib/extensions/mailbox_extension.dart +++ b/model/lib/extensions/mailbox_extension.dart @@ -1,5 +1,7 @@ import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_rights.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/namespace.dart'; import 'package:model/model.dart'; extension MailboxExtension on Mailbox { @@ -68,4 +70,36 @@ extension MailboxExtension on Mailbox { namespace: namespace, ); } + + Mailbox copyWith({ + MailboxId? id, + MailboxName? name, + MailboxId? parentId, + Role? role, + SortOrder? sortOrder, + TotalEmails? totalEmails, + UnreadEmails? unreadEmails, + TotalThreads? totalThreads, + UnreadThreads? unreadThreads, + MailboxRights? myRights, + IsSubscribed? isSubscribed, + Namespace? namespace, + Map?>? rights, + }) { + return Mailbox( + id: id ?? this.id, + name: name ?? this.name, + parentId: parentId ?? this.parentId, + role: role ?? this.role, + sortOrder: sortOrder ?? this.sortOrder, + totalEmails: totalEmails ?? this.totalEmails, + unreadEmails: unreadEmails ?? this.unreadEmails, + totalThreads: totalThreads ?? this.totalThreads, + unreadThreads: unreadThreads ?? this.unreadThreads, + myRights: myRights ?? this.myRights, + isSubscribed: isSubscribed ?? this.isSubscribed, + namespace: namespace ?? this.namespace, + rights: rights ?? this.rights, + ); + } } \ No newline at end of file diff --git a/model/lib/mailbox/presentation_mailbox.dart b/model/lib/mailbox/presentation_mailbox.dart index a810e1037c..5765975131 100644 --- a/model/lib/mailbox/presentation_mailbox.dart +++ b/model/lib/mailbox/presentation_mailbox.dart @@ -89,4 +89,42 @@ class PresentationMailbox with EquatableMixin { namespace, displayName, ]; + + PresentationMailbox copyWith({ + MailboxId? id, + MailboxName? name, + MailboxId? parentId, + Role? role, + SortOrder? sortOrder, + TotalEmails? totalEmails, + UnreadEmails? unreadEmails, + TotalThreads? totalThreads, + UnreadThreads? unreadThreads, + MailboxRights? myRights, + IsSubscribed? isSubscribed, + SelectMode? selectMode, + String? mailboxPath, + MailboxState? state, + Namespace? namespace, + String? displayName, + }) { + return PresentationMailbox( + id ?? this.id, + name: name ?? this.name, + parentId: parentId ?? this.parentId, + role: role ?? this.role, + sortOrder: sortOrder ?? this.sortOrder, + totalEmails: totalEmails ?? this.totalEmails, + unreadEmails: unreadEmails ?? this.unreadEmails, + totalThreads: totalThreads ?? this.totalThreads, + unreadThreads: unreadThreads ?? this.unreadThreads, + myRights: myRights ?? this.myRights, + isSubscribed: isSubscribed ?? this.isSubscribed, + selectMode: selectMode ?? this.selectMode, + mailboxPath: mailboxPath ?? this.mailboxPath, + state: state ?? this.state, + namespace: namespace ?? this.namespace, + displayName: displayName ?? this.displayName, + ); + } } \ No newline at end of file diff --git a/test/features/composer/presentation/composer_controller_test.dart b/test/features/composer/presentation/composer_controller_test.dart index fa4e11ed4d..6315f219e6 100644 --- a/test/features/composer/presentation/composer_controller_test.dart +++ b/test/features/composer/presentation/composer_controller_test.dart @@ -591,7 +591,7 @@ void main() { createEmailRequest: anyNamed('createEmailRequest'), cancelToken: anyNamed('cancelToken'))) .thenAnswer((_) => Stream.value( - Right(SaveEmailAsDraftsSuccess(EmailId(Id('123')))))); + Right(SaveEmailAsDraftsSuccess(EmailId(Id('123')), null)))); final savedEmailDraft = SavedEmailDraft( content: emailContent, @@ -1039,7 +1039,7 @@ void main() { createEmailRequest: anyNamed('createEmailRequest'), cancelToken: anyNamed('cancelToken'))) .thenAnswer((_) => Stream.value( - Right(SaveEmailAsDraftsSuccess(EmailId(Id('123')))))); + Right(SaveEmailAsDraftsSuccess(EmailId(Id('123')), null)))); final savedEmailDraft = SavedEmailDraft( content: emailContent, diff --git a/test/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension_test.dart b/test/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension_test.dart new file mode 100644 index 0000000000..76bf1fac1f --- /dev/null +++ b/test/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension_test.dart @@ -0,0 +1,152 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:model/email/mark_star_action.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/email/read_actions.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; + +import 'update_current_emails_flags_extension_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + const numberOfEmails = 3; + late List emailIds; + final mailboxDashBoardController = MockMailboxDashBoardController(); + + setUp(() { + emailIds = List.generate( + numberOfEmails, + (index) => EmailId(Id('email-id-$index')), + ); + when(mailboxDashBoardController.dashboardRoute).thenReturn(DashboardRoutes.thread.obs); + }); + + group('updateEmailFlagByEmailIds test:', () { + test( + 'should mark emails as read', + () { + // arrange + final readEmailIds = emailIds.sublist(1); + when(mailboxDashBoardController.emailsInCurrentMailbox).thenReturn( + emailIds.map((emailId) => PresentationEmail( + id: emailId, + keywords: {}, + )).toList().obs, + ); + expect( + mailboxDashBoardController.emailsInCurrentMailbox.every( + (presentationEmail) => !presentationEmail.hasRead, + ), + true, + ); + + // act + mailboxDashBoardController.updateEmailFlagByEmailIds( + readEmailIds, + readAction: ReadActions.markAsRead, + ); + + // assert + expect(mailboxDashBoardController.emailsInCurrentMailbox[0].hasRead, false); + expect(mailboxDashBoardController.emailsInCurrentMailbox[1].hasRead, true); + expect(mailboxDashBoardController.emailsInCurrentMailbox[2].hasRead, true); + }); + + test( + 'should mark emails as unread', + () { + // arrange + final unreadEmailIds = emailIds.sublist(1); + when(mailboxDashBoardController.emailsInCurrentMailbox).thenReturn( + emailIds.map((emailId) => PresentationEmail( + id: emailId, + keywords: {KeyWordIdentifier.emailSeen: true}, + )).toList().obs, + ); + expect( + mailboxDashBoardController.emailsInCurrentMailbox.every( + (presentationEmail) => presentationEmail.hasRead, + ), + true, + ); + + // act + mailboxDashBoardController.updateEmailFlagByEmailIds( + unreadEmailIds, + readAction: ReadActions.markAsUnread, + ); + + // assert + expect(mailboxDashBoardController.emailsInCurrentMailbox[0].hasRead, true); + expect(mailboxDashBoardController.emailsInCurrentMailbox[1].hasRead, false); + expect(mailboxDashBoardController.emailsInCurrentMailbox[2].hasRead, false); + }); + + test( + 'should mark emails as starred', + () { + // arrange + final starredEmailIds = emailIds.sublist(1); + when(mailboxDashBoardController.emailsInCurrentMailbox).thenReturn( + emailIds.map((emailId) => PresentationEmail( + id: emailId, + keywords: {}, + )).toList().obs, + ); + expect( + mailboxDashBoardController.emailsInCurrentMailbox.every( + (presentationEmail) => !presentationEmail.hasStarred, + ), + true, + ); + + // act + mailboxDashBoardController.updateEmailFlagByEmailIds( + starredEmailIds, + markStarAction: MarkStarAction.markStar, + ); + + // assert + expect(mailboxDashBoardController.emailsInCurrentMailbox[0].hasStarred, false); + expect(mailboxDashBoardController.emailsInCurrentMailbox[1].hasStarred, true); + expect(mailboxDashBoardController.emailsInCurrentMailbox[2].hasStarred, true); + }); + + test( + 'should mark emails as unstarred', + () { + // arrange + final unstarredEmailIds = emailIds.sublist(1); + when(mailboxDashBoardController.emailsInCurrentMailbox).thenReturn( + emailIds.map((emailId) => PresentationEmail( + id: emailId, + keywords: {KeyWordIdentifier.emailFlagged: true}, + )).toList().obs, + ); + expect( + mailboxDashBoardController.emailsInCurrentMailbox.every( + (presentationEmail) => presentationEmail.hasStarred, + ), + true, + ); + + // act + mailboxDashBoardController.updateEmailFlagByEmailIds( + unstarredEmailIds, + markStarAction: MarkStarAction.unMarkStar, + ); + + // assert + expect(mailboxDashBoardController.emailsInCurrentMailbox[0].hasStarred, true); + expect(mailboxDashBoardController.emailsInCurrentMailbox[1].hasStarred, false); + expect(mailboxDashBoardController.emailsInCurrentMailbox[2].hasStarred, false); + }); + }); +} \ No newline at end of file