diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index 272a1e5b5..7f5ef0877 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -245,7 +245,6 @@ extension AppColor on Color { static const blue400 = Color(0xFF80BDFF); static const blue900 = Color(0xFF0F76E7); static const m3Tertiary = Color(0xFF8C9CAF); - static const lightIconTertiary = Color(0xFFB8C1CC); static const m3Neutral70 = Color(0xFFAEAAAE); static const m3Neutral90 = Color(0xFFE6E1E5); static const grayBackgroundColor = Color(0xFFF3F6F9); diff --git a/core/lib/presentation/utils/html_transformer/transform_configuration.dart b/core/lib/presentation/utils/html_transformer/transform_configuration.dart index bcba4149c..b551bb5e8 100644 --- a/core/lib/presentation/utils/html_transformer/transform_configuration.dart +++ b/core/lib/presentation/utils/html_transformer/transform_configuration.dart @@ -48,6 +48,11 @@ class TransformConfiguration { const RemoveCollapsedSignatureButtonTransformer(), ]); + factory TransformConfiguration.forReplyForwardEmptyEmail() => TransformConfiguration.fromDomTransformers([ + ...TransformConfiguration.forReplyForwardEmail().domTransformers, + const ImageTransformer(), + ]); + factory TransformConfiguration.forDraftsEmail() => TransformConfiguration.create( customDomTransformers: [const ImageTransformer()] ); diff --git a/lib/features/composer/presentation/extensions/email_action_type_extension.dart b/lib/features/composer/presentation/extensions/email_action_type_extension.dart index 55c79e95f..96860ec6b 100644 --- a/lib/features/composer/presentation/extensions/email_action_type_extension.dart +++ b/lib/features/composer/presentation/extensions/email_action_type_extension.dart @@ -196,7 +196,7 @@ extension EmailActionTypeExtension on EmailActionType { case EmailActionType.openInNewTab: return appLocalizations.openInNewTab; case EmailActionType.printAll: - return appLocalizations.printAll; + return appLocalizations.print; case EmailActionType.forward: return appLocalizations.forward; case EmailActionType.replyAll: diff --git a/lib/features/composer/presentation/extensions/setup_email_content_extension.dart b/lib/features/composer/presentation/extensions/setup_email_content_extension.dart index b72ee2563..e689fabfb 100644 --- a/lib/features/composer/presentation/extensions/setup_email_content_extension.dart +++ b/lib/features/composer/presentation/extensions/setup_email_content_extension.dart @@ -27,78 +27,13 @@ extension SetupEmailContentExtension on ComposerController { switch(currentEmailActionType) { case EmailActionType.editAsNewEmail: case EmailActionType.editDraft: - final session = mailboxDashBoardController.sessionCurrent; - final accountId = mailboxDashBoardController.accountId.value; - final emailId = arguments.presentationEmail?.id; - - if (session == null) { - emailContentsViewState.value = Left(GetEmailContentFailure(NotFoundSessionException())); - return; - } - - if (accountId == null) { - emailContentsViewState.value = Left(GetEmailContentFailure(NotFoundAccountIdException())); - return; - } - - if (emailId == null) { - emailContentsViewState.value = Left(GetEmailContentFailure(NotFoundEmailException())); - return; - } - - final resultState = await getEmailContentInteractor.execute( - session, - accountId, - emailId, - mailboxDashBoardController.baseDownloadUrl, - TransformConfiguration.forEditDraftsEmail(), + await _getEmailContent( + arguments, + transformConfiguration: TransformConfiguration.forEditDraftsEmail(), additionalProperties: Properties({ IndividualHeaderIdentifier.identityHeader.value, }), - ).last; - - final uiState = resultState.fold((failure) => failure, (success) => success); - - if (uiState is GetEmailContentSuccess) { - initAttachmentsAndInlineImages( - attachments: uiState.attachments, - inlineImages: uiState.inlineImages, - ); - - if (currentEmailActionType == EmailActionType.editDraft || - currentEmailActionType == EmailActionType.editAsNewEmail) { - setupEmailRequestReadReceiptFlag( - uiState.emailCurrent!.hasRequestReadReceipt, - ); - setupSelectedIdentityForEditDraft( - uiState.emailCurrent!.identityIdFromHeader, - ); - } - - emailContentsViewState.value = Right(uiState); - } else if (uiState is GetEmailContentFromCacheSuccess) { - initAttachmentsAndInlineImages( - attachments: uiState.attachments, - inlineImages: uiState.inlineImages, - ); - - if (currentEmailActionType == EmailActionType.editDraft || - currentEmailActionType == EmailActionType.editAsNewEmail) { - setupEmailRequestReadReceiptFlag( - uiState.emailCurrent.hasRequestReadReceipt, - ); - setupSelectedIdentityForEditDraft( - uiState.emailCurrent.identityIdFromHeader, - ); - } - - emailContentsViewState.value = Right(uiState); - } else if (uiState is GetEmailContentFailure) { - emailContentsViewState.value = Left(uiState); - consumeState(Stream.value(Left(uiState))); - } else { - emailContentsViewState.value = Right(LoadEmailContentCompleted()); - } + ); break; case EmailActionType.editSendingEmail: final sendingEmail = arguments.sendingEmail; @@ -130,7 +65,10 @@ extension SetupEmailContentExtension on ComposerController { case EmailActionType.replyAll: case EmailActionType.forward: if (arguments.emailContents?.trim().isNotEmpty != true) { - emailContentsViewState.value = Left(GetEmailContentFailure(EmptyEmailContentException())); + await _getEmailContent( + arguments, + transformConfiguration: TransformConfiguration.forReplyForwardEmptyEmail(), + ); } else { final resultState = await transformHtmlEmailContentInteractor.execute( arguments.emailContents ?? '', @@ -224,4 +162,81 @@ extension SetupEmailContentExtension on ComposerController { break; } } + + Future _getEmailContent( + ComposerArguments arguments, { + required TransformConfiguration transformConfiguration, + Properties? additionalProperties, + }) async { + final session = mailboxDashBoardController.sessionCurrent; + final accountId = mailboxDashBoardController.accountId.value; + final emailId = arguments.presentationEmail?.id; + + if (session == null) { + emailContentsViewState.value = Left(GetEmailContentFailure(NotFoundSessionException())); + return; + } + + if (accountId == null) { + emailContentsViewState.value = Left(GetEmailContentFailure(NotFoundAccountIdException())); + return; + } + + if (emailId == null) { + emailContentsViewState.value = Left(GetEmailContentFailure(NotFoundEmailException())); + return; + } + + final resultState = await getEmailContentInteractor.execute( + session, + accountId, + emailId, + mailboxDashBoardController.baseDownloadUrl, + transformConfiguration, + additionalProperties: additionalProperties, + ).last; + + final uiState = resultState.fold((failure) => failure, (success) => success); + + if (uiState is GetEmailContentSuccess) { + initAttachmentsAndInlineImages( + attachments: uiState.attachments, + inlineImages: uiState.inlineImages, + ); + + if (currentEmailActionType == EmailActionType.editDraft || + currentEmailActionType == EmailActionType.editAsNewEmail) { + setupEmailRequestReadReceiptFlag( + uiState.emailCurrent!.hasRequestReadReceipt, + ); + setupSelectedIdentityForEditDraft( + uiState.emailCurrent!.identityIdFromHeader, + ); + } + + emailContentsViewState.value = Right(uiState); + } else if (uiState is GetEmailContentFromCacheSuccess) { + initAttachmentsAndInlineImages( + attachments: uiState.attachments, + inlineImages: uiState.inlineImages, + ); + + if (currentEmailActionType == EmailActionType.editDraft + || currentEmailActionType == EmailActionType.editAsNewEmail) { + setupEmailRequestReadReceiptFlag( + uiState.emailCurrent.hasRequestReadReceipt, + ); + setupSelectedIdentityForEditDraft( + uiState.emailCurrent.identityIdFromHeader, + ); + } + + emailContentsViewState.value = Right(uiState); + } else if (uiState is GetEmailContentFailure) { + emailContentsViewState.value = Left(uiState); + consumeState(Stream.value(Left(uiState))); + } else { + emailContentsViewState.value = Right(LoadEmailContentCompleted()); + } + } } \ No newline at end of file diff --git a/lib/features/email/domain/state/unsubscribe_email_state.dart b/lib/features/email/domain/state/unsubscribe_email_state.dart index 362570f7a..ad38ad05a 100644 --- a/lib/features/email/domain/state/unsubscribe_email_state.dart +++ b/lib/features/email/domain/state/unsubscribe_email_state.dart @@ -1,9 +1,17 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; class UnsubscribeEmailLoading extends LoadingState {} -class UnsubscribeEmailSuccess extends UIState {} +class UnsubscribeEmailSuccess extends UIState { + UnsubscribeEmailSuccess(this.emailId); + + final EmailId emailId; + + @override + List get props => [emailId]; +} class UnsubscribeEmailFailure extends FeatureFailure { diff --git a/lib/features/email/domain/usecases/unsubscribe_email_interactor.dart b/lib/features/email/domain/usecases/unsubscribe_email_interactor.dart index 874022853..f8b8957f8 100644 --- a/lib/features/email/domain/usecases/unsubscribe_email_interactor.dart +++ b/lib/features/email/domain/usecases/unsubscribe_email_interactor.dart @@ -16,7 +16,7 @@ class UnsubscribeEmailInteractor { try { yield Right(UnsubscribeEmailLoading()); await emailRepository.unsubscribeMail(session, accountId, emailId); - yield Right(UnsubscribeEmailSuccess()); + yield Right(UnsubscribeEmailSuccess(emailId)); } catch (e) { yield Left(UnsubscribeEmailFailure(exception: e)); } diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index a70aa5614..d6865232c 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -43,11 +43,9 @@ import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; import 'package:tmail_ui_user/features/base/state/button_state.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; -import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/extensions/list_attachments_extension.dart'; import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart'; -import 'package:tmail_ui_user/features/email/domain/model/email_print.dart'; import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; @@ -110,39 +108,35 @@ import 'package:tmail_ui_user/features/email/presentation/model/composer_argumen import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart'; import 'package:tmail_ui_user/features/email/presentation/model/eml_previewer.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_dialog_builder.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_bottom_sheet_builder.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_dialog_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/html_attachment_previewer.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/pdf_viewer/pdf_viewer.dart'; import 'package:tmail_ui_user/features/email_previewer/email_previewer_dialog_view.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'; 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/get_mailbox_contain_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/open_and_close_composer_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/download/download_task_state.dart'; -import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_email_rule_filter_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/create_new_rule_filter_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/create_new_email_rule_filter_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/datetime_extension.dart'; -import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/rules_filter_creator_arguments.dart'; import 'package:tmail_ui_user/features/search/email/presentation/search_email_controller.dart'; -import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/focus_thread_detail_expanded_email.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/localization_service.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; -import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/navigation_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_utils.dart'; @@ -203,6 +197,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { Identity? _identitySelected; ButtonState? _printEmailButtonState; final obxListeners = []; + late final EmailActionReactor emailActionReactor; final StreamController> _downloadProgressStateController = StreamController>.broadcast(); @@ -254,6 +249,14 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _injectCalendarEventBindings(session, accountId); _registerObxStreamListener(); _listenDownloadAttachmentProgressState(); + emailActionReactor = EmailActionReactor( + _markAsEmailReadInteractor, + _markAsStarEmailInteractor, + _createNewEmailRuleFilterInteractor, + _printEmailInteractor, + _getEmailContentInteractor, + _downloadAttachmentForWebInteractor, + ); super.onInit(); } @@ -408,6 +411,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { obxListeners.add(ever(mailboxDashBoardController.viewState, (viewState) { viewState.map((success) { if (success is UnsubscribeEmailSuccess) { + if (success.emailId != _currentEmailId) return; emailUnsubscribe.value = null; } }); @@ -766,13 +770,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } PresentationMailbox? getMailboxContain(PresentationEmail email) { - if (mailboxDashBoardController.selectedMailbox.value == null) { - return email.findMailboxContain(mailboxDashBoardController.mapMailboxById); - } else { - return mailboxDashBoardController.searchController.isSearchEmailRunning - ? email.findMailboxContain(mailboxDashBoardController.mapMailboxById) - : mailboxDashBoardController.selectedMailbox.value; - } + return mailboxDashBoardController.getMailboxContain(email); } void markAsEmailRead( @@ -1200,81 +1198,28 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } void moveToMailbox(BuildContext context, PresentationEmail email) async { - final currentMailbox = getMailboxContain(email); - - if (currentMailbox != null && accountId != null) { - final arguments = DestinationPickerArguments( + if (session != null && accountId != null) { + final moveActionRequest = await emailActionReactor.moveToMailbox( + session!, accountId!, - MailboxActions.moveEmail, - session, - mailboxIdSelected: currentMailbox.mailboxId + email, + mapMailbox: mailboxDashBoardController.mapMailboxById, + selectedMailbox: mailboxDashBoardController.selectedMailbox.value, + isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning, ); - - final destinationMailbox = PlatformInfo.isWeb - ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.destinationPicker, arguments: arguments) - : await push(AppRoutes.destinationPicker, arguments: arguments); - - if (destinationMailbox != null && - destinationMailbox is PresentationMailbox && - session != null && - context.mounted - ) { - _dispatchMoveToAction( - context, - accountId!, - session!, - email, - currentMailbox, - destinationMailbox); + if (!context.mounted || moveActionRequest == null) return; + mailboxDashBoardController.moveToMailbox( + session!, + accountId!, + moveActionRequest.moveRequest, + moveActionRequest.emailIdsWithReadStatus, + ); + if (_threadDetailController?.emailIdsPresentation.length == 1) { + _threadDetailController?.closeThreadDetailAction(context); } } } - void _dispatchMoveToAction( - BuildContext context, - AccountId accountId, - Session session, - PresentationEmail emailSelected, - PresentationMailbox currentMailbox, - PresentationMailbox destinationMailbox - ) { - if (destinationMailbox.isTrash) { - _moveToTrashAction( - context, - session, - accountId, - MoveToMailboxRequest( - {currentMailbox.id: [emailSelected.id!]}, - destinationMailbox.id, - MoveAction.moving, - EmailActionType.moveToTrash), - {emailSelected.id!: emailSelected.hasRead}); - } else if (destinationMailbox.isSpam) { - _moveToSpamAction( - context, - session, - accountId, - MoveToMailboxRequest( - {currentMailbox.id: [emailSelected.id!]}, - destinationMailbox.id, - MoveAction.moving, - EmailActionType.moveToSpam), - {emailSelected.id!: emailSelected.hasRead}); - } else { - _moveToMailbox( - context, - session, - accountId, - MoveToMailboxRequest( - {currentMailbox.id: [emailSelected.id!]}, - destinationMailbox.id, - MoveAction.moving, - EmailActionType.moveToMailbox, - destinationPath: destinationMailbox.mailboxPath), - {emailSelected.id!: emailSelected.hasRead}); - } - } - void _moveToMailbox( BuildContext context, Session session, @@ -1282,7 +1227,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { MoveToMailboxRequest moveRequest, Map emailIdsWithReadStatus, ) { - closeEmailView(context: context); consumeState(_moveToMailboxInteractor.execute( session, accountId, @@ -1332,94 +1276,71 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } void moveToTrash(BuildContext context, PresentationEmail email) { - final trashMailboxId = mailboxDashBoardController.getMailboxIdByRole(PresentationMailbox.roleTrash); - final currentMailbox = getMailboxContain(email); - - if (session != null && accountId != null && currentMailbox != null && trashMailboxId != null) { - _moveToTrashAction( - context, + if (session != null && accountId != null) { + final moveActionRequest = emailActionReactor.moveToTrash( + email, + mapMailbox: mailboxDashBoardController.mapMailboxById, + selectedMailbox: mailboxDashBoardController.selectedMailbox.value, + isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning, + mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole, + ); + if (!context.mounted || moveActionRequest == null) return; + mailboxDashBoardController.moveToMailbox( session!, accountId!, - MoveToMailboxRequest( - {currentMailbox.id: [email.id!]}, - trashMailboxId, - MoveAction.moving, - EmailActionType.moveToTrash), - {email.id!: email.hasRead}, + moveActionRequest.moveRequest, + moveActionRequest.emailIdsWithReadStatus, ); + if (_threadDetailController?.emailIdsPresentation.length == 1) { + _threadDetailController?.closeThreadDetailAction(context); + } } } - void _moveToTrashAction( - BuildContext context, - Session session, - AccountId accountId, - MoveToMailboxRequest moveRequest, - Map emailIdsWithReadStatus, - ) { - closeEmailView(context: context); - mailboxDashBoardController.moveToMailbox( - session, - accountId, - moveRequest, - emailIdsWithReadStatus, - ); - } - void moveToSpam(BuildContext context, PresentationEmail email) { - final spamMailboxId = mailboxDashBoardController.spamMailboxId; - final currentMailbox = getMailboxContain(email); - - if (session != null && accountId != null && currentMailbox != null && spamMailboxId != null) { - _moveToSpamAction( - context, + if (session != null && accountId != null) { + final moveActionRequest = emailActionReactor.moveToSpam( + email, + mapMailbox: mailboxDashBoardController.mapMailboxById, + selectedMailbox: mailboxDashBoardController.selectedMailbox.value, + isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning, + mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole, + ); + if (!context.mounted || moveActionRequest == null) return; + mailboxDashBoardController.moveToMailbox( session!, accountId!, - MoveToMailboxRequest( - {currentMailbox.id: [email.id!]}, - spamMailboxId, - MoveAction.moving, - EmailActionType.moveToSpam), - {email.id!: email.hasRead}, + moveActionRequest.moveRequest, + moveActionRequest.emailIdsWithReadStatus, ); + if (_threadDetailController?.emailIdsPresentation.length == 1) { + _threadDetailController?.closeThreadDetailAction(context); + } } } void unSpam(BuildContext context, PresentationEmail email) { - final spamMailboxId = mailboxDashBoardController.spamMailboxId; - final inboxMailboxId = mailboxDashBoardController.getMailboxIdByRole(PresentationMailbox.roleInbox); - - if (session != null && accountId != null && spamMailboxId != null && inboxMailboxId != null) { - _moveToSpamAction( - context, + if (session != null && accountId != null) { + final moveActionRequest = emailActionReactor.unSpam( + email, + mapMailbox: mailboxDashBoardController.mapMailboxById, + selectedMailbox: mailboxDashBoardController.selectedMailbox.value, + isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning, + mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole, + ); + if (!context.mounted || moveActionRequest == null) return; + mailboxDashBoardController.moveToMailbox( session!, accountId!, - MoveToMailboxRequest( - {spamMailboxId: [email.id!]}, - inboxMailboxId, - MoveAction.moving, - EmailActionType.unSpam), - {email.id!: email.hasRead}, + moveActionRequest.moveRequest, + moveActionRequest.emailIdsWithReadStatus, ); + if (_threadDetailController?.emailIdsPresentation.length == 1) { + _threadDetailController?.closeThreadDetailAction(context); + } } } - void _moveToSpamAction( - BuildContext context, - Session session, - AccountId accountId, - MoveToMailboxRequest moveRequest, - Map emailIdsWithReadStatus, - ) { - closeEmailView(context: context); - mailboxDashBoardController.moveToMailbox( - session, - accountId, - moveRequest, - emailIdsWithReadStatus, - ); - } - void markAsStarEmail( PresentationEmail presentationEmail, MarkStarAction markStarAction, @@ -1451,13 +1372,34 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void handleEmailAction(BuildContext context, PresentationEmail presentationEmail, EmailActionType actionType) { switch(actionType) { case EmailActionType.markAsUnread: - markAsEmailRead(presentationEmail, ReadActions.markAsUnread, MarkReadAction.tap); + if (session != null && accountId != null) { + consumeState(emailActionReactor.markAsEmailRead( + session!, + accountId!, + presentationEmail, + readAction: ReadActions.markAsUnread, + )); + } break; case EmailActionType.markAsStarred: - markAsStarEmail(presentationEmail, MarkStarAction.markStar); + if (session != null && accountId != null) { + consumeState(emailActionReactor.markAsStarEmail( + session!, + accountId!, + presentationEmail, + markStarAction: MarkStarAction.markStar, + )); + } break; case EmailActionType.unMarkAsStarred: - markAsStarEmail(presentationEmail, MarkStarAction.unMarkStar); + if (session != null && accountId != null) { + consumeState(emailActionReactor.markAsStarEmail( + session!, + accountId!, + presentationEmail, + markStarAction: MarkStarAction.unMarkStar, + )); + } break; case EmailActionType.moveToMailbox: moveToMailbox(context, presentationEmail); @@ -1495,69 +1437,38 @@ class SingleEmailController extends BaseController with AppLoaderMixin { case EmailActionType.reply: _replyEmail(presentationEmail); break; + case EmailActionType.replyAll: + case EmailActionType.replyToList: + case EmailActionType.forward: + pressEmailAction(actionType, presentationEmail); + break; default: break; } } - void openEmailAddressDialog(BuildContext context, EmailAddress emailAddress) { - if (responsiveUtils.isScreenWithShortestSide(context)) { - Get.bottomSheet( - PointerInterceptor( - child: EmailAddressBottomSheetBuilder( - imagePaths: imagePaths, - emailAddress: emailAddress, - onCloseDialogAction: popBack, - onCopyEmailAddressAction: (emailAddress) => - copyEmailAddress(context, emailAddress), - onComposeEmailAction: composeEmailFromEmailAddress, - onQuickCreatingRuleEmailDialogAction: (emailAddress) => - quickCreatingRule(context, emailAddress), - ), - ), - useRootNavigator: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadiusDirectional.only( - topStart: Radius.circular(16.0), - topEnd: Radius.circular(16.0), - ), - ), - isScrollControlled: true, - enableDrag: false, - backgroundColor: Colors.transparent, - ); - } else { - Get.dialog( - PointerInterceptor( - child: EmailAddressDialogBuilder( - imagePaths: imagePaths, - emailAddress: emailAddress, - onCloseDialogAction: popBack, - onCopyEmailAddressAction: (emailAddress) => - copyEmailAddress(context, emailAddress), - onComposeEmailAction: composeEmailFromEmailAddress, - onQuickCreatingRuleEmailDialogAction: (emailAddress) => - quickCreatingRule(context, emailAddress), - ), - ), - barrierColor: AppColor.colorDefaultCupertinoActionSheet, - ); - } - } + void openEmailAddressDialog(EmailAddress emailAddress) { + if (session == null || accountId == null) return; - void copyEmailAddress(BuildContext context, EmailAddress emailAddress) { - Clipboard.setData(ClipboardData(text: emailAddress.emailAddress)); - appToast.showToastSuccessMessage( - context, - AppLocalizations.of(context).email_address_copied_to_clipboard, + emailActionReactor.openEmailAddressDialog( + session!, + accountId!, + emailAddress: emailAddress, + responsiveUtils: responsiveUtils, + imagePaths: imagePaths, + appToast: appToast, + onComposeEmailFromEmailAddressRequest: (emailAddress) { + popBack(); + mailboxDashBoardController.openComposer( + ComposerArguments.fromEmailAddress(emailAddress), + ); + }, + onQuickCreateRuleRequest: (quickCreateRuleStream) { + consumeState(quickCreateRuleStream); + }, ); } - void composeEmailFromEmailAddress(EmailAddress emailAddress) { - popBack(); - mailboxDashBoardController.openComposer(ComposerArguments.fromEmailAddress(emailAddress)); - } - Future openMailToLink(Uri? uri) async { if (uri == null) return; @@ -1577,34 +1488,18 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } void deleteEmailPermanently(BuildContext context, PresentationEmail email) { - if (responsiveUtils.isMobile(context)) { - (ConfirmationDialogActionSheetBuilder(context) - ..messageText(DeleteActionType.single.getContentDialog(context)) - ..onCancelAction(AppLocalizations.of(context).cancel, () => popBack()) - ..onConfirmAction(DeleteActionType.single.getConfirmActionName(context), () => _deleteEmailPermanentlyAction(context, email))) - .show(); - } else { - Get.dialog( - PointerInterceptor(child: ConfirmationDialogBuilder( - key: const Key('confirm_dialog_delete_email_permanently'), - imagePath: imagePaths, - title: DeleteActionType.single.getTitleDialog(context), - textContent: DeleteActionType.single.getContentDialog(context), - confirmText: DeleteActionType.single.getConfirmActionName(context), - cancelText: AppLocalizations.of(context).cancel, - onConfirmButtonAction: () => _deleteEmailPermanentlyAction(context, email), - onCancelButtonAction: popBack, - onCloseButtonAction: popBack, - )), - barrierColor: AppColor.colorDefaultCupertinoActionSheet, - ); - } - } - - void _deleteEmailPermanentlyAction(BuildContext context, PresentationEmail email) { - popBack(); - closeEmailView(context: context); - mailboxDashBoardController.deleteEmailPermanently(email); + emailActionReactor.deleteEmailPermanently( + email, + onDeleteEmailRequest: (email) { + popBack(); + mailboxDashBoardController.deleteEmailPermanently(email); + if (_threadDetailController?.emailIdsPresentation.length == 1) { + _threadDetailController?.closeThreadDetailAction(context); + } + }, + responsiveUtils: responsiveUtils, + imagePaths: imagePaths, + ); } void _handleSendReceiptToSenderAction(BuildContext context) { @@ -1793,32 +1688,11 @@ class SingleEmailController extends BaseController with AppLoaderMixin { popBack(); if (accountId != null && session != null) { - final arguments = RulesFilterCreatorArguments( - accountId!, + consumeState(emailActionReactor.quickCreateRule( session!, - emailAddress: emailAddress); - - final newRuleFilterRequest = PlatformInfo.isWeb - ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.rulesFilterCreator, arguments: arguments) - : await push(AppRoutes.rulesFilterCreator, arguments: arguments); - - if (newRuleFilterRequest is CreateNewEmailRuleFilterRequest) { - _createNewRuleFilterAction(accountId!, newRuleFilterRequest); - } - } - } - - void _createNewRuleFilterAction( - AccountId accountId, - CreateNewEmailRuleFilterRequest ruleFilterRequest - ) async { - try { - _createNewEmailRuleFilterInteractor = Get.find(); - } catch (e) { - logError('SingleEmailController::onInit(): ${e.toString()}'); - } - if (_createNewEmailRuleFilterInteractor != null) { - consumeState(_createNewEmailRuleFilterInteractor!.execute(accountId, ruleFilterRequest)); + accountId!, + emailAddress: emailAddress, + )); } } @@ -1929,70 +1803,30 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } void _unsubscribeEmail(BuildContext context, PresentationEmail presentationEmail) { - showConfirmDialogAction( - context, - '', - AppLocalizations.of(context).unsubscribe, - onConfirmAction: () { - if (emailUnsubscribe.value?.httpLinks.isNotEmpty == true) { - _handleUnsubscribeMailByHttpsLink( - context: context, - emailId: presentationEmail.id!, - httpLinks: emailUnsubscribe.value!.httpLinks - ); - } else if (emailUnsubscribe.value?.mailtoLinks.isNotEmpty == true) { - _handleUnsubscribeMailByMailtoLink( - context: context, - emailId: presentationEmail.id!, - mailtoLinks: emailUnsubscribe.value!.mailtoLinks - ); - } + emailActionReactor.unsubscribeEmail( + presentationEmail, + emailUnsubscribe: emailUnsubscribe.value, + onUnsubscribeByHttpsLink: mailboxDashBoardController.unsubscribeMail, + onUnsubscribeByMailtoLink: (emailId, navigationRouter) { + mailboxDashBoardController.openComposer( + ComposerArguments.fromUnsubscribeMailtoLink( + listEmailAddress: navigationRouter.listEmailAddress, + subject: navigationRouter.subject, + body: navigationRouter.body, + previousEmailId: emailId, + ) + ); }, - showAsBottomSheet: true, - dialogMargin: MediaQuery.paddingOf(context).add(const EdgeInsets.only(bottom: 12)), - title: AppLocalizations.of(context).unsubscribeMail, - listTextSpan: [ - TextSpan(text: AppLocalizations.of(context).unsubscribeMailDialogMessage), - TextSpan( - text: ' ${presentationEmail.getSenderName()}', - style: ThemeUtils.textStyleM3BodyMedium1.copyWith( - fontWeight: FontWeight.w700, - ), - ), - const TextSpan(text: ' ?'), - ] - ); - } - - void _handleUnsubscribeMailByHttpsLink({ - required BuildContext context, - required EmailId emailId, - required List httpLinks - }) { - log('SingleEmailController::_handleUnsubscribeMailByHttpsLink:httpLinks: $httpLinks'); - mailboxDashBoardController.unsubscribeMail(emailId); - AppUtils.launchLink(httpLinks.first); - } - - void _handleUnsubscribeMailByMailtoLink({ - required BuildContext context, - required EmailId emailId, - required List mailtoLinks - }) { - log('SingleEmailController::_handleUnsubscribeMailByMailtoLink:mailtoLinks: $mailtoLinks'); - final navigationRouter = RouteUtils.generateNavigationRouterFromMailtoLink(mailtoLinks.first); - mailboxDashBoardController.openComposer( - ComposerArguments.fromUnsubscribeMailtoLink( - listEmailAddress: navigationRouter.listEmailAddress, - subject: navigationRouter.subject, - body: navigationRouter.body, - previousEmailId: emailId, - ) ); } void archiveMessage(BuildContext context, PresentationEmail email) { - mailboxDashBoardController.archiveMessage(context, email); + emailActionReactor.archiveMessage( + email, + onArchiveEmailRequest: (presentationEmail) { + mailboxDashBoardController.archiveMessage(context, presentationEmail); + }, + ); } void _printEmail(BuildContext context, PresentationEmail email) { @@ -2003,40 +1837,11 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _printEmailButtonState = ButtonState.disabled; - final locale = Localizations.localeOf(context); - final appLocalizations = AppLocalizations.of(context); - - consumeState( - _printEmailInteractor.execute( - EmailPrint( - appName: appLocalizations.app_name, - userName: mailboxDashBoardController.ownEmailAddress.value, - attachments: currentEmailLoaded.value!.attachments, - emailContent: currentEmailLoaded.value!.htmlContent, - fromPrefix: appLocalizations.from_email_address_prefix, - toPrefix: appLocalizations.to_email_address_prefix, - ccPrefix: appLocalizations.cc_email_address_prefix, - bccPrefix: appLocalizations.bcc_email_address_prefix, - replyToPrefix: appLocalizations.replyToEmailAddressPrefix, - titleAttachment: appLocalizations.attachments.toLowerCase(), - toAddress: email.to?.listEmailAddressToString(isFullEmailAddress: true), - ccAddress: email.cc?.listEmailAddressToString(isFullEmailAddress: true), - bccAddress: email.bcc?.listEmailAddressToString(isFullEmailAddress: true), - replyToAddress: email.replyTo?.listEmailAddressToString(isFullEmailAddress: true), - sender: email.from?.isNotEmpty == true - ? email.from!.first - : null, - receiveTime: email.getReceivedAt( - locale.toLanguageTag(), - pattern: email.receivedAt - ?.value - .toLocal() - .toPatternForPrinting(locale.toLanguageTag()), - ), - subject: email.subject, - ) - ) - ); + consumeState(emailActionReactor.printEmail( + email, + ownEmailAddress: mailboxDashBoardController.ownEmailAddress.value, + emailLoaded: currentEmailLoaded.value!, + )); } void _showMessageWhenStartingEmailPrinting() { @@ -2181,34 +1986,46 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } void _downloadMessageAsEML(PresentationEmail presentationEmail) { - final emlAttachment = presentationEmail.createEMLAttachment(); - if (emlAttachment.blobId == null) { - consumeState(Stream.value(Left(DownloadAttachmentForWebFailure(exception: NotFoundEmailBlobIdException())))); - return; - } + if (accountId == null || session == null) return; - downloadAttachmentForWeb(emlAttachment); + consumeState(emailActionReactor.downloadMessageAsEML( + session!, + accountId!, + presentationEmail, + downloadProgressStateController: _downloadProgressStateController, + )); } void _editAsNewEmail(PresentationEmail presentationEmail) { if (accountId == null || session == null) return; - mailboxDashBoardController.openComposer( - ComposerArguments.editAsNewEmail(presentationEmail), + emailActionReactor.editAsNewEmail( + presentationEmail, + onEditAsEmailRequest: (presentationEmail) { + mailboxDashBoardController.openComposer( + ComposerArguments.editAsNewEmail(presentationEmail), + ); + }, ); } void _replyEmail(PresentationEmail presentationEmail) { - mailboxDashBoardController.openComposer( - ComposerArguments.replyEmail( - presentationEmail: presentationEmail, - content: currentEmailLoaded.value?.htmlContent ?? '', - inlineImages: currentEmailLoaded.value?.inlineImages ?? [], - mailboxRole: presentationEmail.mailboxContain?.role, - messageId: currentEmailLoaded.value?.emailCurrent?.messageId, - references: currentEmailLoaded.value?.emailCurrent?.references, - listPost: currentEmailLoaded.value?.emailCurrent?.listPost, - ) + emailActionReactor.replyEmail( + presentationEmail, + emailLoaded: currentEmailLoaded.value, + onReplyEmailRequest: (presentationEmail, emailLoaded) { + mailboxDashBoardController.openComposer( + ComposerArguments.replyEmail( + presentationEmail: presentationEmail, + content: emailLoaded?.htmlContent, + inlineImages: emailLoaded?.inlineImages, + mailboxRole: presentationEmail.mailboxContain?.role, + messageId: emailLoaded?.emailCurrent?.messageId, + references: emailLoaded?.emailCurrent?.references, + listPost: emailLoaded?.emailCurrent?.listPost, + ) + ); + }, ); } diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index e45f393a1..c5e7afcb5 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -11,22 +11,15 @@ import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/email/email_action_type.dart'; import 'package:model/email/presentation_email.dart'; -import 'package:model/extensions/email_extension.dart'; import 'package:model/extensions/list_email_address_extension.dart'; import 'package:model/extensions/presentation_email_extension.dart'; -import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/features/base/widget/optional_expanded.dart'; import 'package:tmail_ui_user/features/base/widget/optional_scroll.dart'; -import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_event_extension.dart'; -import 'package:tmail_ui_user/features/email/presentation/model/context_item_email_action.dart'; -import 'package:tmail_ui_user/features/email/presentation/model/popup_menu_item_email_action.dart'; -import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart'; import 'package:tmail_ui_user/features/email/presentation/styles/email_view_styles.dart'; -import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_event_action_banner_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart'; @@ -39,12 +32,10 @@ import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_loa import 'package:tmail_ui_user/features/email/presentation/widgets/information_sender_and_receiver_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/mail_unsubscribed_banner.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/view_entire_message_with_message_clipped_widget.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_open_context_menu_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/verify_display_overlay_view_on_iframe_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; class EmailView extends GetWidget { @@ -97,11 +88,15 @@ class EmailView extends GetWidget { isSearchActivated: controller.mailboxDashBoardController.searchController.isSearchEmailRunning, onBackAction: () => controller.closeEmailView(context: context), onEmailActionClick: (email, action) => controller.handleEmailAction(context, email, action), - onMoreActionClick: (presentationEmail, position) => _handleMoreEmailAction( - context: context, + onMoreActionClick: (presentationEmail, position) => controller.emailActionReactor.handleMoreEmailAction( + mailboxContain: controller.getMailboxContain(presentationEmail), presentationEmail: presentationEmail, position: position, - emailLoaded: controller.currentEmailLoaded.value, + responsiveUtils: controller.responsiveUtils, + imagePaths: controller.imagePaths, + username: controller.session?.username, + handleEmailAction: (email, action) => controller.handleEmailAction(context, email, action), + additionalActions: [], ), supportBackAction: !isInsideThreadDetailView, appBarDecoration: isInsideThreadDetailView @@ -278,18 +273,38 @@ class EmailView extends GetWidget { sMimeStatus: controller.currentEmailLoaded.value?.sMimeStatus, emailUnsubscribe: controller.emailUnsubscribe.value, maxBodyHeight: maxBodyHeight, - openEmailAddressDetailAction: controller.openEmailAddressDialog, + openEmailAddressDetailAction: (_, emailAddress) => controller.openEmailAddressDialog(emailAddress), onEmailActionClick: (presentationEmail, actionType) => controller.handleEmailAction(context, presentationEmail, actionType), isInsideThreadDetailView: isInsideThreadDetailView, emailLoaded: controller.currentEmailLoaded.value, - onMoreActionClick: (email, position) => _handleMoreEmailAction( - context: context, - presentationEmail: email, + onMoreActionClick: (presentationEmail, position) => controller.emailActionReactor.handleMoreEmailAction( + mailboxContain: controller.getMailboxContain(presentationEmail), + presentationEmail: presentationEmail, position: position, - emailLoaded: controller.currentEmailLoaded.value, + responsiveUtils: controller.responsiveUtils, + imagePaths: controller.imagePaths, + username: controller.session?.username, + handleEmailAction: (email, action) => controller.handleEmailAction(context, email, action), + additionalActions: [ + EmailActionType.forward, + EmailActionType.replyAll, + EmailActionType.replyToList, + EmailActionType.printAll, + if (controller.responsiveUtils.isMobile(context)) + EmailActionType.moveToMailbox, + if (!controller.responsiveUtils.isDesktop(context)) ...[ + EmailActionType.markAsStarred, + EmailActionType.unMarkAsStarred, + EmailActionType.moveToTrash, + EmailActionType.deletePermanently, + ], + ], ), onToggleThreadDetailCollapseExpand: onToggleThreadDetailCollapseExpand, onTapAvatarActionClick: onToggleThreadDetailCollapseExpand, + mailboxContain: presentationEmail.findMailboxContain( + controller.mailboxDashBoardController.mapMailboxById, + ), )), Obx(() => MailUnsubscribedBanner( presentationEmail: controller.currentEmail, @@ -340,7 +355,7 @@ class EmailView extends GetWidget { .ownEmailAddress .value, onMailtoAttendeesAction: controller.handleMailToAttendees, - openEmailAddressDetailAction: controller.openEmailAddressDialog, + openEmailAddressDetailAction: (_, emailAddress) => controller.openEmailAddressDialog(emailAddress), isFree: controller.isCalendarEventFree, listEmailAddressSender: emailAddressSender ?? [], isPortraitMobile: controller @@ -496,113 +511,4 @@ class EmailView extends GetWidget { return usernameEvent.isNotEmpty && titleEvent.isNotEmpty; } - - bool canDeletePermanently(PresentationEmail email) { - return email.mailboxContain?.isTrash - ?? email.mailboxContain?.isSpam - ?? false; - } - - void _handleMoreEmailAction({ - required BuildContext context, - required PresentationEmail presentationEmail, - RelativeRect? position, - EmailLoaded? emailLoaded, - }) { - final mailboxContain = controller.getMailboxContain(presentationEmail); - final isChildOfTeamMailboxes = mailboxContain?.isChildOfTeamMailboxes ?? false; - final isSpam = mailboxContain?.isSpam ?? false; - final isArchive = mailboxContain?.isArchive ?? false; - final isTemplates = mailboxContain?.isTemplates ?? false; - final isUnsubscribeEnabled = !presentationEmail.isSubscribed && - controller.emailUnsubscribe.value != null; - final isCreateRuleEnabled = presentationEmail.from?.isNotEmpty == true; - final isDownloadMessageAsEMLEnabled = - PlatformInfo.isWeb && PlatformInfo.isCanvasKit; - - final emailActions = [ - if (isInsideThreadDetailView) ...[ - EmailActionType.forward, - if (presentationEmail.getCountMailAddressWithoutMe(controller.session?.username.value ?? '') > 1) - EmailActionType.replyAll, - if (EmailUtils.isReplyToListEnabled(emailLoaded?.emailCurrent?.listPost ?? '')) - EmailActionType.replyToList, - if (PlatformInfo.isWeb && PlatformInfo.isCanvasKit) - EmailActionType.printAll, - if (controller.responsiveUtils.isMobile(context)) - EmailActionType.moveToMailbox, - if (!controller.responsiveUtils.isDesktop(context)) ...[ - presentationEmail.hasStarred - ? EmailActionType.unMarkAsStarred - : EmailActionType.markAsStarred, - canDeletePermanently(presentationEmail) - ? EmailActionType.deletePermanently - : EmailActionType.moveToTrash, - ], - ], - EmailActionType.markAsUnread, - if (!isChildOfTeamMailboxes) - isSpam ? EmailActionType.unSpam : EmailActionType.moveToSpam, - if (isCreateRuleEnabled) - EmailActionType.createRule, - if (isUnsubscribeEnabled) - EmailActionType.unsubscribe, - if (!isArchive) - EmailActionType.archiveMessage, - if (isDownloadMessageAsEMLEnabled) - EmailActionType.downloadMessageAsEML, - if (!isTemplates) - EmailActionType.editAsNewEmail, - ]; - - if (position == null) { - final contextMenuActions = emailActions - .map((action) => ContextItemEmailAction( - action, - AppLocalizations.of(context), - controller.imagePaths, - )) - .toList(); - - controller.mailboxDashBoardController.openBottomSheetContextMenu( - context: context, - itemActions: contextMenuActions, - onContextMenuActionClick: (menuAction) { - popBack(); - controller.handleEmailAction( - context, - presentationEmail, - menuAction.action, - ); - }, - ); - } else { - final popupMenuItems = emailActions.map((actionType) { - return PopupMenuItem( - padding: EdgeInsets.zero, - child: PopupMenuItemActionWidget( - menuAction: PopupMenuItemEmailAction( - actionType, - AppLocalizations.of(context), - controller.imagePaths, - ), - menuActionClick: (menuAction) { - popBack(); - controller.handleEmailAction( - context, - presentationEmail, - menuAction.action, - ); - }, - ), - ); - }).toList(); - - controller.mailboxDashBoardController.openPopupMenu( - context, - position, - popupMenuItems, - ); - } - } } \ No newline at end of file diff --git a/lib/features/email/presentation/model/composer_arguments.dart b/lib/features/email/presentation/model/composer_arguments.dart index 805d1df3d..c6fd05eeb 100644 --- a/lib/features/email/presentation/model/composer_arguments.dart +++ b/lib/features/email/presentation/model/composer_arguments.dart @@ -144,8 +144,8 @@ class ComposerArguments extends RouterArguments { factory ComposerArguments.replyEmail({ required PresentationEmail presentationEmail, - required String content, - required List inlineImages, + required String? content, + required List? inlineImages, Role? mailboxRole, MessageIdsHeaderValue? messageId, MessageIdsHeaderValue? references, @@ -163,8 +163,8 @@ class ComposerArguments extends RouterArguments { factory ComposerArguments.replyToListEmail({ required PresentationEmail presentationEmail, - required String content, - required List inlineImages, + required String? content, + required List? inlineImages, Role? mailboxRole, MessageIdsHeaderValue? messageId, MessageIdsHeaderValue? references, @@ -182,8 +182,8 @@ class ComposerArguments extends RouterArguments { factory ComposerArguments.replyAllEmail({ required PresentationEmail presentationEmail, - required String content, - required List inlineImages, + required String? content, + required List? inlineImages, Role? mailboxRole, MessageIdsHeaderValue? messageId, MessageIdsHeaderValue? references, @@ -201,9 +201,9 @@ class ComposerArguments extends RouterArguments { factory ComposerArguments.forwardEmail({ required PresentationEmail presentationEmail, - required String content, - required List attachments, - required List inlineImages, + required String? content, + required List? attachments, + required List? inlineImages, MessageIdsHeaderValue? messageId, MessageIdsHeaderValue? references, }) => ComposerArguments( diff --git a/lib/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart b/lib/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart new file mode 100644 index 000000000..3b8e9cc31 --- /dev/null +++ b/lib/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart @@ -0,0 +1,886 @@ +import 'dart:async'; + +import 'package:core/data/network/config/dynamic_url_interceptors.dart'; +import 'package:core/domain/extensions/datetime_extension.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/app_toast.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/utils/theme_utils.dart'; +import 'package:core/presentation/views/bottom_popup/confirmation_dialog_action_sheet_builder.dart'; +import 'package:core/presentation/views/dialog/confirmation_dialog_builder.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +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/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/download/download_task_id.dart'; +import 'package:model/email/email_action_type.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:model/extensions/email_address_extension.dart'; +import 'package:model/extensions/list_email_address_extension.dart'; +import 'package:model/extensions/presentation_email_extension.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; +import 'package:model/extensions/session_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.dart'; +import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixin.dart'; +import 'package:tmail_ui_user/features/base/widget/popup_item_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; +import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; +import 'package:tmail_ui_user/features/email/domain/model/email_print.dart'; +import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; +import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; +import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; +import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_bottom_sheet_builder.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_dialog_builder.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_email_rule_filter_request.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/create_new_email_rule_filter_interactor.dart'; +import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/rules_filter_creator_arguments.dart'; +import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/dialog_router.dart'; +import 'package:tmail_ui_user/main/routes/navigation_router.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; +import 'package:tmail_ui_user/main/routes/route_utils.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; +import 'package:uuid/uuid.dart'; + +class EmailActionReactor with MessageDialogActionMixin, PopupContextMenuActionMixin { + const EmailActionReactor( + this._markAsEmailReadInteractor, + this._markAsStarEmailInteractor, + this._createNewEmailRuleFilterInteractor, + this._printEmailInteractor, + this._getEmailContentInteractor, + this._downloadAttachmentForWebInteractor, + ); + + final MarkAsEmailReadInteractor _markAsEmailReadInteractor; + final MarkAsStarEmailInteractor _markAsStarEmailInteractor; + final CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor; + final PrintEmailInteractor _printEmailInteractor; + final GetEmailContentInteractor _getEmailContentInteractor; + final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor; + + Stream> markAsEmailRead( + Session session, + AccountId accountId, + PresentationEmail presentationEmail, { + required ReadActions readAction, + }) { + return _markAsEmailReadInteractor.execute( + session, + accountId, + presentationEmail.id!, + readAction, + MarkReadAction.tap, + presentationEmail.mailboxContain?.mailboxId, + ); + } + + Stream> markAsStarEmail( + Session session, + AccountId accountId, + PresentationEmail presentationEmail, { + required MarkStarAction markStarAction, + }) { + return _markAsStarEmailInteractor.execute( + session, + accountId, + presentationEmail.id!, + markStarAction, + ); + } + + Future<({ + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus + })?> moveToMailbox( + Session session, + AccountId accountId, + PresentationEmail presentationEmail, { + required Map mapMailbox, + required PresentationMailbox? selectedMailbox, + required bool isSearchEmailRunning, + }) async { + final moveRequest = await _moveToMailbox( + session, + accountId, + presentationEmail, + mapMailbox: mapMailbox, + isSearchEmailRunning: isSearchEmailRunning, + selectedMailbox: selectedMailbox, + ); + if (moveRequest == null) return null; + + return ( + moveRequest: moveRequest.moveRequest, + emailIdsWithReadStatus: moveRequest.emailIdsWithReadStatus, + ); + } + + ({ + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus + })? moveToTrash( + PresentationEmail presentationEmail, { + required Map mapMailbox, + required PresentationMailbox? selectedMailbox, + required bool isSearchEmailRunning, + required Map mapDefaultMailboxIdByRole, + }) { + final trashMailboxId = mapDefaultMailboxIdByRole[ + PresentationMailbox.roleTrash + ]; + final currentMailbox = _getMailboxContain( + presentationEmail, + mapMailbox: mapMailbox, + isSearchEmailRunning: isSearchEmailRunning, + selectedMailbox: selectedMailbox, + ); + if (trashMailboxId == null || currentMailbox == null) return null; + + return ( + moveRequest: MoveToMailboxRequest( + {currentMailbox.id: [presentationEmail.id!]}, + trashMailboxId, + MoveAction.moving, + EmailActionType.moveToTrash), + emailIdsWithReadStatus: {presentationEmail.id!: presentationEmail.hasRead}, + ); + } + + void deleteEmailPermanently( + PresentationEmail presentationEmail, { + required void Function(PresentationEmail email) onDeleteEmailRequest, + required ResponsiveUtils responsiveUtils, + required ImagePaths imagePaths, + }) async { + if (currentContext == null || !currentContext!.mounted) return; + if (responsiveUtils.isMobile(currentContext!)) { + await (ConfirmationDialogActionSheetBuilder(currentContext!) + ..messageText(DeleteActionType.single.getContentDialog(currentContext!)) + ..onCancelAction(AppLocalizations.of(currentContext!).cancel, () => popBack()) + ..onConfirmAction( + DeleteActionType.single.getConfirmActionName(currentContext!), + () => onDeleteEmailRequest.call(presentationEmail) + ) + ).show(); + } else { + await Get.dialog( + PointerInterceptor(child: ConfirmationDialogBuilder( + key: const Key('confirm_dialog_delete_email_permanently'), + imagePath: imagePaths, + title: DeleteActionType.single.getTitleDialog(currentContext!), + textContent: DeleteActionType.single.getContentDialog(currentContext!), + cancelText: DeleteActionType.single.getConfirmActionName(currentContext!), + confirmText: AppLocalizations.of(currentContext!).cancel, + onCancelButtonAction: () => onDeleteEmailRequest.call(presentationEmail), + onConfirmButtonAction: popBack, + onCloseButtonAction: popBack, + )), + barrierColor: AppColor.colorDefaultCupertinoActionSheet, + ); + } + } + + ({ + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus + })? moveToSpam( + PresentationEmail presentationEmail, { + required Map mapMailbox, + required PresentationMailbox? selectedMailbox, + required bool isSearchEmailRunning, + required Map mapDefaultMailboxIdByRole, + }) { + final spamMailboxId = _spamMailboxId(mapDefaultMailboxIdByRole); + final currentMailbox = _getMailboxContain( + presentationEmail, + mapMailbox: mapMailbox, + isSearchEmailRunning: isSearchEmailRunning, + selectedMailbox: selectedMailbox, + ); + if (spamMailboxId == null || currentMailbox == null) return null; + + return ( + moveRequest: MoveToMailboxRequest( + {currentMailbox.id: [presentationEmail.id!]}, + spamMailboxId, + MoveAction.moving, + EmailActionType.moveToSpam), + emailIdsWithReadStatus: {presentationEmail.id!: presentationEmail.hasRead}, + ); + } + + ({ + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus + })? unSpam( + PresentationEmail presentationEmail, { + required Map mapMailbox, + required PresentationMailbox? selectedMailbox, + required bool isSearchEmailRunning, + required Map mapDefaultMailboxIdByRole, + }) { + final spamMailboxId = _spamMailboxId(mapDefaultMailboxIdByRole); + final inboxMailboxId = mapDefaultMailboxIdByRole[ + PresentationMailbox.roleInbox + ]; + if (spamMailboxId == null || inboxMailboxId == null) return null; + + return ( + moveRequest: MoveToMailboxRequest( + {spamMailboxId: [presentationEmail.id!]}, + inboxMailboxId, + MoveAction.moving, + EmailActionType.unSpam + ), + emailIdsWithReadStatus: {presentationEmail.id!: presentationEmail.hasRead}, + ); + } + + MailboxId? _spamMailboxId( + Map mapDefaultMailboxIdByRole + ) { + return mapDefaultMailboxIdByRole[ + PresentationMailbox.roleSpam + ] ?? mapDefaultMailboxIdByRole[ + PresentationMailbox.roleJunk + ]; + } + + Stream> quickCreateRule( + Session session, + AccountId accountId, { + required EmailAddress? emailAddress, + }) async* { + Get.back(); + final arguments = RulesFilterCreatorArguments( + accountId, + session, + emailAddress: emailAddress, + ); + + final newRuleFilterRequest = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog( + routeName: AppRoutes.rulesFilterCreator, + arguments: arguments, + ) + : await push(AppRoutes.rulesFilterCreator, arguments: arguments); + + if (newRuleFilterRequest is! CreateNewEmailRuleFilterRequest) return; + + yield* _createNewEmailRuleFilterInteractor?.execute( + accountId, + newRuleFilterRequest + ) ?? const Stream.empty(); + } + + Future unsubscribeEmail( + PresentationEmail presentationEmail, { + required EmailUnsubscribe? emailUnsubscribe, + required void Function(EmailId emailId) onUnsubscribeByHttpsLink, + required void Function( + EmailId emailId, + NavigationRouter navigationRouter, + ) onUnsubscribeByMailtoLink, + }) async { + if (currentContext == null || !currentContext!.mounted) return; + + await showConfirmDialogAction( + currentContext!, + '', + AppLocalizations.of(currentContext!).unsubscribe, + onConfirmAction: () { + if (emailUnsubscribe?.httpLinks.isNotEmpty == true) { + onUnsubscribeByHttpsLink(presentationEmail.id!); + AppUtils.launchLink(emailUnsubscribe!.httpLinks.first); + } else if (emailUnsubscribe?.mailtoLinks.isNotEmpty == true) { + final navigationRouter = RouteUtils.generateNavigationRouterFromMailtoLink( + emailUnsubscribe!.mailtoLinks.first, + ); + onUnsubscribeByMailtoLink(presentationEmail.id!, navigationRouter); + } + }, + showAsBottomSheet: true, + title: AppLocalizations.of(currentContext!).unsubscribeMail, + listTextSpan: [ + TextSpan(text: AppLocalizations.of(currentContext!).unsubscribeMailDialogMessage), + TextSpan( + text: ' ${presentationEmail.getSenderName()}', + style: ThemeUtils.textStyleBodyBody2( + color: AppColor.steelGray400, + fontWeight: FontWeight.w700, + ), + ), + const TextSpan(text: ' ?'), + ] + ); + } + + void archiveMessage( + PresentationEmail presentationEmail, { + required void Function( + PresentationEmail presentationEmail, + ) onArchiveEmailRequest, + }) => onArchiveEmailRequest(presentationEmail); + + Stream> printEmail( + PresentationEmail presentationEmail, { + required String ownEmailAddress, + required EmailLoaded? emailLoaded, + VoidCallback? onGetEmailContentFailure, + Session? session, + AccountId? accountId, + String? baseDownloadUrl, + TransformConfiguration? transformConfiguration, + }) async* { + emailLoaded ??= await _getEmailLoaded( + session, + accountId, + presentationEmail.id, + baseDownloadUrl, + transformConfiguration, + ); + + if (emailLoaded == null) { + onGetEmailContentFailure?.call(); + return; + } + + if (currentContext == null || !currentContext!.mounted) return; + final locale = Localizations.localeOf(currentContext!); + final appLocalizations = AppLocalizations.of(currentContext!); + + yield* _printEmailInteractor.execute( + EmailPrint( + appName: appLocalizations.app_name, + userName: ownEmailAddress, + attachments: emailLoaded.attachments, + emailContent: emailLoaded.htmlContent, + fromPrefix: appLocalizations.from_email_address_prefix, + toPrefix: appLocalizations.to_email_address_prefix, + ccPrefix: appLocalizations.cc_email_address_prefix, + bccPrefix: appLocalizations.bcc_email_address_prefix, + replyToPrefix: appLocalizations.replyToEmailAddressPrefix, + titleAttachment: appLocalizations.attachments.toLowerCase(), + toAddress: presentationEmail.to?.listEmailAddressToString(isFullEmailAddress: true), + ccAddress: presentationEmail.cc?.listEmailAddressToString(isFullEmailAddress: true), + bccAddress: presentationEmail.bcc?.listEmailAddressToString(isFullEmailAddress: true), + replyToAddress: presentationEmail.replyTo?.listEmailAddressToString(isFullEmailAddress: true), + sender: presentationEmail.from?.isNotEmpty == true + ? presentationEmail.from!.first + : null, + receiveTime: presentationEmail.getReceivedAt( + locale.toLanguageTag(), + pattern: presentationEmail.receivedAt + ?.value + .toLocal() + .toPatternForPrinting(locale.toLanguageTag()), + ), + subject: presentationEmail.subject, + ) + ); + } + + Stream> downloadMessageAsEML( + Session session, + AccountId accountId, + PresentationEmail presentationEmail, { + required StreamController> downloadProgressStateController, + }) async* { + final emlAttachment = presentationEmail.createEMLAttachment(); + if (emlAttachment.blobId == null) { + yield* Stream.value(Left(DownloadAttachmentForWebFailure( + exception: NotFoundEmailBlobIdException(), + ))); + return; + } + + final generateTaskId = DownloadTaskId(const Uuid().v4()); + try { + final baseDownloadUrl = session.getDownloadUrl( + jmapUrl: getBinding()?.jmapUrl, + ); + final cancelToken = CancelToken(); + yield* _downloadAttachmentForWebInteractor.execute( + generateTaskId, + emlAttachment, + accountId, + baseDownloadUrl, + downloadProgressStateController, + cancelToken: cancelToken, + previewerSupported: false, + ); + } catch (e) { + logError('SingleEmailController::downloadAttachmentForWeb(): $e'); + yield* Stream.value(Left(DownloadAttachmentForWebFailure( + attachment: emlAttachment, + taskId: generateTaskId, + exception: e, + ))); + } + } + + void editAsNewEmail( + PresentationEmail presentationEmail, { + required void Function( + PresentationEmail presentationEmail, + ) onEditAsEmailRequest, + }) => onEditAsEmailRequest(presentationEmail); + + void replyEmail( + PresentationEmail presentationEmail, { + required void Function( + PresentationEmail presentationEmail, + EmailLoaded? emailLoaded, + ) onReplyEmailRequest, + required EmailLoaded? emailLoaded, + }) { + onReplyEmailRequest(presentationEmail, emailLoaded); + } + + void replyAll( + PresentationEmail presentationEmail, { + required void Function( + PresentationEmail presentationEmail, + EmailLoaded? emailLoaded, + ) onReplyAllRequest, + required EmailLoaded? emailLoaded, + }) { + onReplyAllRequest(presentationEmail, emailLoaded); + } + + void replyToList( + PresentationEmail presentationEmail, { + required void Function( + PresentationEmail presentationEmail, + EmailLoaded? emailLoaded, + ) onReplyToListRequest, + required EmailLoaded? emailLoaded, + }) { + onReplyToListRequest(presentationEmail, emailLoaded); + } + + void forward( + PresentationEmail presentationEmail, { + required void Function( + PresentationEmail presentationEmail, + EmailLoaded? emailLoaded, + ) onForwardRequest, + required EmailLoaded? emailLoaded, + }) { + onForwardRequest(presentationEmail, emailLoaded); + } + + void handleMoreEmailAction({ + required PresentationEmail presentationEmail, + required PresentationMailbox? mailboxContain, + required RelativeRect? position, + required ResponsiveUtils responsiveUtils, + required ImagePaths imagePaths, + required UserName? username, + required void Function( + PresentationEmail presentationEmail, + EmailActionType action, + ) handleEmailAction, + required List additionalActions, + }) { + if (currentContext == null) return; + + final moreActions = [ + if (additionalActions.contains(EmailActionType.forward)) + EmailActionType.forward, + if (presentationEmail.getCountMailAddressWithoutMe(username?.value ?? '') > 1 && + additionalActions.contains(EmailActionType.replyAll)) + EmailActionType.replyAll, + if (EmailUtils.isReplyToListEnabled(presentationEmail.listPost ?? '') && + additionalActions.contains(EmailActionType.replyToList)) + EmailActionType.replyToList, + if (PlatformInfo.isWeb && + PlatformInfo.isCanvasKit && + additionalActions.contains(EmailActionType.printAll)) + EmailActionType.printAll, + if (additionalActions.contains(EmailActionType.moveToMailbox)) + EmailActionType.moveToMailbox, + if (additionalActions.contains(EmailActionType.markAsStarred) && + additionalActions.contains(EmailActionType.unMarkAsStarred)) + presentationEmail.hasStarred + ? EmailActionType.unMarkAsStarred + : EmailActionType.markAsStarred, + if (additionalActions.contains(EmailActionType.moveToTrash) && + additionalActions.contains(EmailActionType.deletePermanently)) + _canDeletePermanently(presentationEmail) + ? EmailActionType.deletePermanently + : EmailActionType.moveToTrash, + EmailActionType.markAsUnread, + if (mailboxContain?.isChildOfTeamMailboxes == false) + if (mailboxContain?.isSpam == true) + EmailActionType.unSpam + else + EmailActionType.moveToSpam, + if (presentationEmail.from?.isNotEmpty == true) + EmailActionType.createRule, + if (!presentationEmail.isSubscribed && presentationEmail.listUnsubscribe != null) + EmailActionType.unsubscribe, + if (mailboxContain?.isArchive == false) + EmailActionType.archiveMessage, + if (PlatformInfo.isWeb && PlatformInfo.isCanvasKit) + EmailActionType.downloadMessageAsEML, + EmailActionType.editAsNewEmail, + ]; + + if (position == null) { + openContextMenuAction( + currentContext!, + _emailActionMoreActionTile( + currentContext!, + presentationEmail, + moreActions, + responsiveUtils, + imagePaths, + handleEmailAction: handleEmailAction, + ) + ); + } else { + openPopupMenuAction( + currentContext!, + position, + _popupMenuEmailActionTile( + currentContext!, + presentationEmail, + moreActions, + imagePaths, + handleEmailAction: handleEmailAction, + ) + ); + } + } + + bool _canDeletePermanently(PresentationEmail email) { + return email.mailboxContain?.isTrash + ?? email.mailboxContain?.isSpam + ?? false; + } + + List _emailActionMoreActionTile( + BuildContext context, + PresentationEmail presentationEmail, + List actionTypes, + ResponsiveUtils responsiveUtils, + ImagePaths imagePaths, { + required void Function( + PresentationEmail presentationEmail, + EmailActionType action, + ) handleEmailAction + }) { + return actionTypes.map((action) { + return (EmailActionCupertinoActionSheetActionBuilder( + Key('${action.name}_action'), + SvgPicture.asset( + action.getIcon(imagePaths), + width: 24, + height: 24, + fit: BoxFit.fill, + colorFilter: AppColor.colorTextButton.asFilter() + ), + action.getTitle(AppLocalizations.of(context)), + presentationEmail, + iconLeftPadding: responsiveUtils.isScreenWithShortestSide(context) + ? const EdgeInsetsDirectional.only(start: 12, end: 16) + : const EdgeInsetsDirectional.only(end: 12), + iconRightPadding: responsiveUtils.isScreenWithShortestSide(context) + ? const EdgeInsetsDirectional.only(end: 12) + : EdgeInsets.zero + ) + ..onActionClick((presentationEmail) { + popBack(); + handleEmailAction(presentationEmail, action); + })).build(); + }).toList(); + } + + List _popupMenuEmailActionTile( + BuildContext context, + PresentationEmail presentationEmail, + List actionTypes, + ImagePaths imagePaths, { + required void Function( + PresentationEmail presentationEmail, + EmailActionType action, + ) handleEmailAction, + }) { + return actionTypes.map((action) { + return PopupMenuItem( + key: Key('${action.name}_action'), + padding: EdgeInsets.zero, + child: PopupItemWidget( + iconAction: action.getIcon(imagePaths), + nameAction: action.getTitle(AppLocalizations.of(context)), + colorIcon: AppColor.colorTextButton, + padding: const EdgeInsetsDirectional.only(start: 12), + styleName: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + color: Colors.black + ), + onCallbackAction: () { + popBack(); + handleEmailAction(presentationEmail, action); + } + ) + ); + }).toList(); + } + + void openEmailAddressDialog( + Session session, + AccountId accountId, { + required EmailAddress emailAddress, + required ResponsiveUtils responsiveUtils, + required ImagePaths imagePaths, + required AppToast appToast, + required void Function(EmailAddress emailAddress) onComposeEmailFromEmailAddressRequest, + required void Function(Stream> quickCreateRuleStream) onQuickCreateRuleRequest, + }) { + if (currentContext?.mounted != true) return; + + if (responsiveUtils.isScreenWithShortestSide(currentContext!)) { + Get.bottomSheet( + PointerInterceptor( + child: EmailAddressBottomSheetBuilder( + imagePaths: imagePaths, + emailAddress: emailAddress, + onCloseDialogAction: popBack, + onCopyEmailAddressAction: (emailAddress) => + _copyEmailAddress(currentContext!, emailAddress, appToast), + onComposeEmailAction: (emailAddress) => + onComposeEmailFromEmailAddressRequest(emailAddress), + onQuickCreatingRuleEmailDialogAction: (emailAddress) => + onQuickCreateRuleRequest( + quickCreateRule(session, accountId, emailAddress: emailAddress), + ), + ), + ), + useRootNavigator: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadiusDirectional.only( + topStart: Radius.circular(16.0), + topEnd: Radius.circular(16.0), + ), + ), + isScrollControlled: true, + enableDrag: false, + backgroundColor: Colors.transparent, + ); + } else { + Get.dialog( + PointerInterceptor( + child: EmailAddressDialogBuilder( + imagePaths: imagePaths, + emailAddress: emailAddress, + onCloseDialogAction: popBack, + onCopyEmailAddressAction: (emailAddress) => + _copyEmailAddress(currentContext!, emailAddress, appToast), + onComposeEmailAction: (emailAddress) => + onComposeEmailFromEmailAddressRequest(emailAddress), + onQuickCreatingRuleEmailDialogAction: (emailAddress) => + onQuickCreateRuleRequest( + quickCreateRule(session, accountId, emailAddress: emailAddress), + ), + ), + ), + barrierColor: AppColor.colorDefaultCupertinoActionSheet, + ); + } + } + + void _copyEmailAddress( + BuildContext context, + EmailAddress emailAddress, + AppToast appToast, + ) { + Clipboard.setData(ClipboardData(text: emailAddress.emailAddress)); + appToast.showToastSuccessMessage( + context, + AppLocalizations.of(context).email_address_copied_to_clipboard, + ); + } + + Future<({ + Map emailIdsWithReadStatus, + MoveToMailboxRequest moveRequest, + })?> _moveToMailbox( + Session session, + AccountId accountId, + PresentationEmail presentationEmail, { + required Map mapMailbox, + PresentationMailbox? selectedMailbox, + bool isSearchEmailRunning = false, + }) async { + final currentMailbox = _getMailboxContain( + presentationEmail, + mapMailbox: mapMailbox, + selectedMailbox: selectedMailbox, + isSearchEmailRunning: isSearchEmailRunning, + ); + + if (currentMailbox == null) return null; + + final arguments = DestinationPickerArguments( + accountId, + MailboxActions.moveEmail, + session, + mailboxIdSelected: currentMailbox.mailboxId + ); + + final destinationMailbox = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.destinationPicker, arguments: arguments) + : await push(AppRoutes.destinationPicker, arguments: arguments); + + if (destinationMailbox is PresentationMailbox) { + return _generateMoveMailboxRequest( + accountId, + session, + presentationEmail, + currentMailbox, + destinationMailbox, + ); + } + + return null; + } + + PresentationMailbox? _getMailboxContain( + PresentationEmail presentationEmail, { + required Map mapMailbox, + PresentationMailbox? selectedMailbox, + bool isSearchEmailRunning = false, + }) { + if (selectedMailbox == null || isSearchEmailRunning) { + return presentationEmail.findMailboxContain(mapMailbox); + } + + return selectedMailbox; + } + + ({ + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, + }) _generateMoveMailboxRequest( + AccountId accountId, + Session session, + PresentationEmail emailSelected, + PresentationMailbox currentMailbox, + PresentationMailbox destinationMailbox + ) { + if (destinationMailbox.isTrash) { + return ( + moveRequest: MoveToMailboxRequest( + {currentMailbox.id: [emailSelected.id!]}, + destinationMailbox.id, + MoveAction.moving, + EmailActionType.moveToTrash, + ), + emailIdsWithReadStatus: {emailSelected.id!: emailSelected.hasRead}, + ); + } else if (destinationMailbox.isSpam) { + return ( + moveRequest: MoveToMailboxRequest( + {currentMailbox.id: [emailSelected.id!]}, + destinationMailbox.id, + MoveAction.moving, + EmailActionType.moveToSpam, + ), + emailIdsWithReadStatus: {emailSelected.id!: emailSelected.hasRead}, + ); + } else { + return ( + moveRequest: MoveToMailboxRequest( + {currentMailbox.id: [emailSelected.id!]}, + destinationMailbox.id, + MoveAction.moving, + EmailActionType.moveToMailbox, + destinationPath: destinationMailbox.mailboxPath), + emailIdsWithReadStatus: {emailSelected.id!: emailSelected.hasRead}, + ); + } + } + + Future _getEmailLoaded( + Session? session, + AccountId? accountId, + EmailId? emailId, + String? baseDownloadUrl, + TransformConfiguration? transformConfiguration, + ) { + if (session == null || + accountId == null || + emailId == null || + baseDownloadUrl == null || + transformConfiguration == null + ) { + return Future.value(null); + } + + return _getEmailContentInteractor + .execute( + session, + accountId, + emailId, + baseDownloadUrl, + transformConfiguration, + ) + .last + .then( + (value) { + return value.fold( + (failure) => null, + (success) => success is GetEmailContentSuccess + ? EmailLoaded( + htmlContent: success.htmlEmailContent, + attachments: List.of(success.attachments ?? []), + inlineImages: List.of(success.inlineImages ?? []), + emailCurrent: success.emailCurrent, + ) + : null, + ); + }, + onError: (error) { + logError('EmailActionReactor::_getEmailLoaded(): error: $error'); + return null; + }, + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/email_view_app_bar_widget.dart b/lib/features/email/presentation/widgets/email_view_app_bar_widget.dart index 5da432ef8..34b6ab842 100644 --- a/lib/features/email/presentation/widgets/email_view_app_bar_widget.dart +++ b/lib/features/email/presentation/widgets/email_view_app_bar_widget.dart @@ -31,7 +31,6 @@ class EmailViewAppBarWidget extends StatelessWidget { final BoxDecoration? appBarDecoration; final EmailLoaded? emailLoaded; final bool isInsideThreadDetailView; - final double? height; final EdgeInsetsGeometry? iconPadding; final EdgeInsetsGeometry? iconMargin; final EdgeInsetsGeometry? padding; @@ -49,7 +48,6 @@ class EmailViewAppBarWidget extends StatelessWidget { this.supportBackAction = true, this.appBarDecoration, this.isInsideThreadDetailView = false, - this.height, this.iconPadding, this.iconMargin, this.padding, @@ -59,9 +57,9 @@ class EmailViewAppBarWidget extends StatelessWidget { Widget build(BuildContext context) { final child = LayoutBuilder(builder: (context, constraints) { return Container( - height: height ?? (PlatformInfo.isIOS + height: PlatformInfo.isIOS ? EmailViewAppBarWidgetStyles.heightIOS(context, _responsiveUtils) - : EmailViewAppBarWidgetStyles.height), + : EmailViewAppBarWidgetStyles.height, padding: padding ?? (PlatformInfo.isIOS ? EmailViewAppBarWidgetStyles.paddingIOS(context, _responsiveUtils) : EmailViewAppBarWidgetStyles.padding), @@ -248,7 +246,8 @@ class EmailViewAppBarWidget extends StatelessWidget { }) { if (!isInsideThreadDetailView) { return [ - _getPrintButton(appLocalizations), + if (PlatformInfo.isWeb && PlatformInfo.isCanvasKit) + _getPrintButton(appLocalizations), _getMoveEmailButton(appLocalizations), _getMarkStarButton(appLocalizations), _getDeleteButton(appLocalizations), diff --git a/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart b/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart index b7e8086b5..f8d069b5f 100644 --- a/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart +++ b/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart @@ -5,15 +5,19 @@ import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/utils/theme_utils.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/presentation/views/image/avatar_builder.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:model/email/email_action_type.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/presentation_email_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/base/widget/email_avatar_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart'; import 'package:tmail_ui_user/features/email/presentation/model/smime_signature_status.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/email_view_app_bar_widget_styles.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_receiver_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_sender_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_app_bar_widget.dart'; @@ -36,6 +40,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { final bool showRecipients; final VoidCallback? onToggleThreadDetailCollapseExpand; final OnTapAvatarActionClick? onTapAvatarActionClick; + final PresentationMailbox? mailboxContain; const InformationSenderAndReceiverBuilder({ Key? key, @@ -53,6 +58,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { this.showRecipients = true, this.onToggleThreadDetailCollapseExpand, this.onTapAvatarActionClick, + this.mailboxContain, }) : super(key: key); @override @@ -127,21 +133,30 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { padding: const EdgeInsetsDirectional.only(start: 16, top: 2), ), if (isInsideThreadDetailView) - EmailViewAppBarWidget( - key: const Key('email_view_app_bar_widget'), - presentationEmail: emailSelected, - isSearchActivated: false, - onBackAction: () {}, - onEmailActionClick: onEmailActionClick, - onMoreActionClick: onMoreActionClick, - supportBackAction: false, - appBarDecoration: const BoxDecoration(), - emailLoaded: emailLoaded, - isInsideThreadDetailView: isInsideThreadDetailView, + SizedBox( height: IconUtils.defaultIconSize, - iconPadding: EdgeInsets.zero, - iconMargin: const EdgeInsetsDirectional.only(start: 16), - padding: const EdgeInsets.symmetric(horizontal: 8), + child: OverflowBox( + maxHeight: PlatformInfo.isIOS + ? EmailViewAppBarWidgetStyles.heightIOS(context, responsiveUtils) + : EmailViewAppBarWidgetStyles.height, + fit: OverflowBoxFit.deferToChild, + child: EmailViewAppBarWidget( + key: const Key('email_view_app_bar_widget'), + presentationEmail: emailSelected, + mailboxContain: mailboxContain, + isSearchActivated: false, + onBackAction: () {}, + onEmailActionClick: onEmailActionClick, + onMoreActionClick: onMoreActionClick, + supportBackAction: false, + appBarDecoration: const BoxDecoration(), + emailLoaded: emailLoaded, + isInsideThreadDetailView: isInsideThreadDetailView, + iconPadding: const EdgeInsets.all(8), + iconMargin: EdgeInsets.zero, + padding: EdgeInsets.zero, + ), + ), ), ], ), diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index a41898779..f59ced8b2 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -151,10 +151,10 @@ class MailboxDashBoardBindings extends BaseBindings { SendingQueueBindings().dependencies(); MailboxBindings().dependencies(); ThreadBindings().dependencies(); - SearchEmailBindings().dependencies(); - ThreadDetailBindings().dependencies(); EmailBindings().dependencies(); + SearchEmailBindings().dependencies(); QuotasBindings().dependencies(); + ThreadDetailBindings().dependencies(); } @override 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 f7612c844..544872f32 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -174,6 +174,7 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_multiple_e import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/action/thread_detail_ui_action.dart'; import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart'; import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart'; import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_data.dart'; @@ -252,6 +253,7 @@ class MailboxDashBoardController extends ReloadableController final dashBoardAction = Rxn(); final mailboxUIAction = Rxn(); final emailUIAction = Rxn(); + final threadDetailUIAction = Rxn(); final dashboardRoute = DashboardRoutes.waiting.obs; final currentSelectMode = SelectMode.INACTIVE.obs; final filterMessageOption = FilterMessageOption.all.obs; @@ -447,7 +449,7 @@ class MailboxDashBoardController extends ReloadableController } else if (success is DeleteSendingEmailSuccess) { getAllSendingEmails(); } else if (success is UnsubscribeEmailSuccess) { - _handleUnsubscribeMailSuccess(); + _handleUnsubscribeMailSuccess(success.emailId); } else if (success is RestoreDeletedMessageSuccess) { _handleRestoreDeletedMessageSuccess(success.emailRecoveryAction.id!); } else if (success is GetRestoredDeletedMessageSuccess) { @@ -1766,6 +1768,11 @@ class MailboxDashBoardController extends ReloadableController emailUIAction.value = newAction; } + void dispatchThreadDetailUIAction(ThreadDetailUIAction newAction) { + log('MailboxDashBoardController::dispatchThreadDetailUIAction():newAction: ${newAction.runtimeType}'); + threadDetailUIAction.value = newAction; + } + void dispatchRoute(DashboardRoutes route) { log('MailboxDashBoardController::dispatchRoute(): $route'); dashboardRoute.value = route; @@ -2848,16 +2855,29 @@ class MailboxDashBoardController extends ReloadableController } } - void _handleUnsubscribeMailSuccess() { + void _handleUnsubscribeMailSuccess(EmailId emailId) { if (currentContext != null && currentOverlayContext != null) { appToast.showToastSuccessMessage( currentOverlayContext!, AppLocalizations.of(currentContext!).unsubscribedFromThisMailingList); } - final newEmail = selectedEmail.value?.updateKeywords({ + dispatchThreadDetailUIAction(UpdatedEmailKeywordsAction( + emailId, + KeyWordIdentifierExtension.unsubscribeMail, + true, + )); + // Reset threadDetailUIAction + dispatchThreadDetailUIAction(ThreadDetailUIAction()); + + final listEmail = searchController.isSearchEmailRunning + ? listResultSearch + : emailsInCurrentMailbox; + var newEmailIndex = listEmail.indexWhere((email) => email.id == emailId); + if (newEmailIndex == -1) return; + + listEmail[newEmailIndex] = listEmail[newEmailIndex].updateKeywords({ KeyWordIdentifierExtension.unsubscribeMail: true, }); - setSelectedEmail(newEmail); } void _replaceBrowserHistory({Uri? uri}) { diff --git a/lib/features/mailbox_dashboard/presentation/extensions/get_mailbox_contain_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/get_mailbox_contain_extension.dart new file mode 100644 index 000000000..3da1a8278 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/extensions/get_mailbox_contain_extension.dart @@ -0,0 +1,16 @@ +import 'package:model/email/presentation_email.dart'; +import 'package:model/extensions/presentation_email_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; + +extension GetMailboxContainExtension on MailboxDashBoardController { + PresentationMailbox? getMailboxContain(PresentationEmail email) { + if (selectedMailbox.value == null) { + return email.findMailboxContain(mapMailboxById); + } else { + return searchController.isSearchEmailRunning + ? email.findMailboxContain(mapMailboxById) + : selectedMailbox.value; + } + } +} \ 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 index b52ef900b..9080a568a 100644 --- 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 @@ -3,9 +3,9 @@ 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:model/extensions/presentation_email_extension.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'; +import 'package:tmail_ui_user/features/thread_detail/presentation/action/thread_detail_ui_action.dart'; extension UpdateCurrentEmailsFlagsExtension on MailboxDashBoardController { void updateEmailFlagByEmailIds( @@ -76,23 +76,25 @@ extension UpdateCurrentEmailsFlagsExtension on MailboxDashBoardController { } void updateEmailAnswered(EmailId emailId) { - if (selectedEmail.value != null) { - final newEmail = selectedEmail.value!.updateKeywords({ - KeyWordIdentifier.emailAnswered: true, - }); - setSelectedEmail(newEmail); - } + dispatchThreadDetailUIAction(UpdatedEmailKeywordsAction( + emailId, + KeyWordIdentifier.emailAnswered, + true, + )); + // Reset threadDetailUIAction + dispatchThreadDetailUIAction(ThreadDetailUIAction()); updateEmailFlagByEmailIds([emailId], markAsAnswered: true); } void updateEmailForwarded(EmailId emailId) { - if (selectedEmail.value != null) { - final newEmail = selectedEmail.value!.updateKeywords({ - KeyWordIdentifier.emailForwarded: true, - }); - setSelectedEmail(newEmail); - } + dispatchThreadDetailUIAction(UpdatedEmailKeywordsAction( + emailId, + KeyWordIdentifier.emailForwarded, + true, + )); + // Reset threadDetailUIAction + dispatchThreadDetailUIAction(ThreadDetailUIAction()); updateEmailFlagByEmailIds([emailId], markAsForwarded: true); } diff --git a/lib/features/thread_detail/presentation/action/thread_detail_ui_action.dart b/lib/features/thread_detail/presentation/action/thread_detail_ui_action.dart new file mode 100644 index 000000000..6cfe639d7 --- /dev/null +++ b/lib/features/thread_detail/presentation/action/thread_detail_ui_action.dart @@ -0,0 +1,23 @@ +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; +import 'package:tmail_ui_user/features/base/action/ui_action.dart'; + +class ThreadDetailUIAction extends UIAction { + @override + List get props => []; +} + +class UpdatedEmailKeywordsAction extends ThreadDetailUIAction { + UpdatedEmailKeywordsAction( + this.emailId, + this.updatedKeyword, + this.value, + ); + + final EmailId emailId; + final KeyWordIdentifier updatedKeyword; + final bool value; + + @override + List get props => [emailId, updatedKeyword, value]; +} \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/close_thread_detail_action.dart b/lib/features/thread_detail/presentation/extension/close_thread_detail_action.dart index 139d80cfd..49949ee33 100644 --- a/lib/features/thread_detail/presentation/extension/close_thread_detail_action.dart +++ b/lib/features/thread_detail/presentation/extension/close_thread_detail_action.dart @@ -10,10 +10,11 @@ extension CloseThreadDetailAction on ThreadDetailController { } else { mailboxDashBoardController.dispatchRoute(DashboardRoutes.thread); } - for (var emailId in emailIdsPresentation.keys) { - mailboxDashBoardController.dispatchEmailUIAction( - CloseEmailInThreadDetailAction(emailId), - ); + final cleanUpEmailIds = List.from(emailIdsPresentation.keys); + for (var emailId in cleanUpEmailIds) { + mailboxDashBoardController + ..dispatchEmailUIAction(CloseEmailInThreadDetailAction(emailId)) + ..dispatchEmailUIAction(EmailUIAction()); } reset(); diff --git a/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart b/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart index e6f6c9205..b8e0fcdda 100644 --- a/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart +++ b/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart @@ -1,12 +1,18 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:model/extensions/presentation_email_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/email_view.dart'; import 'package:model/email/email_in_thread_status.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/get_mailbox_contain_extension.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/load_more_thread_detail_emails.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_open_email_address_detail_action.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/toggle_thread_detail_collape_expand.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_load_more_circle.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; extension GetThreadDetailEmailViews on ThreadDetailController { List getThreadDetailEmailViews() { @@ -58,10 +64,37 @@ extension GetThreadDetailEmailViews on ThreadDetailController { showSubject: isFirstEmailInThreadDetail, imagePaths: imagePaths, responsiveUtils: responsiveUtils, - mailboxContain: presentationEmail.mailboxContain, + mailboxContain: presentationEmail.findMailboxContain( + mailboxDashBoardController.mapMailboxById, + ), emailLoaded: null, - openEmailAddressDetailAction: (context, emailAddress) { - // TODO: Next PR + onEmailActionClick: threadDetailOnEmailActionClick, + onMoreActionClick: (presentationEmail, position) => emailActionReactor.handleMoreEmailAction( + mailboxContain: mailboxDashBoardController.getMailboxContain(presentationEmail), + presentationEmail: presentationEmail, + position: position, + responsiveUtils: responsiveUtils, + imagePaths: imagePaths, + username: session?.username, + handleEmailAction: threadDetailOnEmailActionClick, + additionalActions: [ + EmailActionType.forward, + EmailActionType.replyAll, + EmailActionType.replyToList, + EmailActionType.printAll, + if (currentContext != null && + responsiveUtils.isMobile(currentContext!)) + EmailActionType.moveToMailbox, + if (!responsiveUtils.isDesktop(currentContext!)) ...[ + EmailActionType.markAsStarred, + EmailActionType.unMarkAsStarred, + EmailActionType.moveToTrash, + EmailActionType.deletePermanently, + ], + ], + ), + openEmailAddressDetailAction: (_, emailAddress) { + openEmailAddressDetailAction(emailAddress); }, onToggleThreadDetailCollapseExpand: () { toggleThreadDetailCollapeExpand(presentationEmail); diff --git a/lib/features/thread_detail/presentation/extension/handle_collapsed_email_download_states.dart b/lib/features/thread_detail/presentation/extension/handle_collapsed_email_download_states.dart new file mode 100644 index 000000000..19b0f31d8 --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/handle_collapsed_email_download_states.dart @@ -0,0 +1,74 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/download/download_task_state.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +extension HandleCollapsedEmailDownloadStates on ThreadDetailController { + void handleDownloadProgressState(Either state) { + state.fold( + (failure) => null, + (success) { + if (success is StartDownloadAttachmentForWeb && + !success.previewerSupported + ) { + mailboxDashBoardController.addDownloadTask( + DownloadTaskState( + taskId: success.taskId, + attachment: success.attachment, + onCancel: () => success.cancelToken?.cancel(), + ), + ); + + if (currentOverlayContext != null && + currentContext != null && + !success.previewerSupported + ) { + appToast.showToastMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).your_download_has_started, + leadingSVGIconColor: AppColor.primaryColor, + leadingSVGIcon: imagePaths.icDownload, + ); + } + } else if (success is DownloadingAttachmentForWeb) { + mailboxDashBoardController.updateDownloadTask( + success.taskId, + (currentTask) { + final newTask = currentTask.copyWith( + progress: success.progress, + downloaded: success.downloaded, + total: success.total, + ); + + return newTask; + }, + ); + } + } + ); + } + + void handleDownloadSuccess(DownloadAttachmentForWebSuccess success) { + mailboxDashBoardController.deleteDownloadTask(success.taskId); + + downloadManager.createAnchorElementDownloadFileWeb( + success.bytes, + success.attachment.generateFileName()); + } + + void handleDownloadFailure(DownloadAttachmentForWebFailure failure) { + if (failure.taskId != null) { + mailboxDashBoardController.deleteDownloadTask(failure.taskId!); + } + + if (currentOverlayContext == null || currentContext == null) return; + + final message = AppLocalizations.of(currentContext!).downloadMessageAsEMLFailed; + appToast.showToastErrorMessage(currentOverlayContext!, message); + } +} \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/initialize_thread_detail_emails.dart b/lib/features/thread_detail/presentation/extension/initialize_thread_detail_emails.dart index 6e3a56a36..a1fbbee10 100644 --- a/lib/features/thread_detail/presentation/extension/initialize_thread_detail_emails.dart +++ b/lib/features/thread_detail/presentation/extension/initialize_thread_detail_emails.dart @@ -25,7 +25,7 @@ extension InitializeThreadDetailEmails on ThreadDetailController { properties: EmailUtils.getPropertiesForEmailGetMethod( session!, accountId!, - ), + ).union(additionalProperties), )); } } \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/load_more_thread_detail_emails.dart b/lib/features/thread_detail/presentation/extension/load_more_thread_detail_emails.dart index e79489d74..c3961c6bb 100644 --- a/lib/features/thread_detail/presentation/extension/load_more_thread_detail_emails.dart +++ b/lib/features/thread_detail/presentation/extension/load_more_thread_detail_emails.dart @@ -26,7 +26,7 @@ extension LoadMoreThreadDetailEmails on ThreadDetailController { properties: EmailUtils.getPropertiesForEmailGetMethod( session!, accountId!, - ), + ).union(additionalProperties), )); } } \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/mark_collapsed_email_star_success.dart b/lib/features/thread_detail/presentation/extension/mark_collapsed_email_star_success.dart new file mode 100644 index 000000000..60ef72a49 --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/mark_collapsed_email_star_success.dart @@ -0,0 +1,24 @@ +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; +import 'package:model/email/mark_star_action.dart'; +import 'package:model/extensions/presentation_email_extension.dart'; +import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; + +extension MarkCollapsedEmailStarSuccess on ThreadDetailController { + void markCollapsedEmailStarSuccess(MarkAsStarEmailSuccess success) { + final updatedMarkedEmail = emailIdsPresentation[success.emailId] + ?.updateKeywords({ + KeyWordIdentifier.emailFlagged: success.markStarAction == MarkStarAction.markStar, + }); + if (updatedMarkedEmail == null) return; + + if (mailboxDashBoardController.selectedEmail.value?.id == success.emailId) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + [success.emailId], + markStarAction: success.markStarAction, + ); + } + emailIdsPresentation[success.emailId] = updatedMarkedEmail; + } +} \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/mark_collapsed_email_unread_success.dart b/lib/features/thread_detail/presentation/extension/mark_collapsed_email_unread_success.dart new file mode 100644 index 000000000..e9023de12 --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/mark_collapsed_email_unread_success.dart @@ -0,0 +1,18 @@ +import 'package:model/email/read_actions.dart'; +import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +extension MarkCollapsedEmailReadSuccess on ThreadDetailController { + void markCollapsedEmailReadSuccess(MarkAsEmailReadSuccess success) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + [success.emailId], + readAction: success.readActions, + ); + if (success.readActions == ReadActions.markAsUnread) { + closeThreadDetailAction(currentContext); + } + } +} \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/quick_create_rule_from_collapsed_email_success.dart b/lib/features/thread_detail/presentation/extension/quick_create_rule_from_collapsed_email_success.dart new file mode 100644 index 000000000..7e031d289 --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/quick_create_rule_from_collapsed_email_success.dart @@ -0,0 +1,19 @@ +import 'package:tmail_ui_user/features/manage_account/domain/state/create_new_rule_filter_state.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +extension QuickCreateRuleFromCollapsedEmailSuccess on ThreadDetailController { + void quickCreateRuleFromCollapsedEmailSuccess( + CreateNewRuleFilterSuccess success, + ) { + if (success.newListRules.isEmpty || + currentContext == null || + currentOverlayContext == null) return; + + appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).newFilterWasCreated, + ); + } +} \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart b/lib/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart new file mode 100644 index 000000000..86f0fa1c2 --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart @@ -0,0 +1,376 @@ +import 'dart:async'; + +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:model/email/email_action_type.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:model/extensions/email_extension.dart'; +import 'package:model/extensions/session_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/open_and_close_composer_extension.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +extension ThreadDetailOnEmailActionClick on ThreadDetailController { + void threadDetailOnEmailActionClick( + PresentationEmail presentationEmail, + EmailActionType emailActionType, + ) { + switch(emailActionType) { + case EmailActionType.markAsRead: + _markRead(presentationEmail); + break; + case EmailActionType.markAsUnread: + _unRead(presentationEmail); + break; + case EmailActionType.markAsStarred: + _markStar(presentationEmail, MarkStarAction.markStar); + break; + case EmailActionType.unMarkAsStarred: + _markStar(presentationEmail, MarkStarAction.unMarkStar); + break; + case EmailActionType.moveToMailbox: + _moveEmail(presentationEmail); + break; + case EmailActionType.moveToTrash: + _trashEmail(presentationEmail); + break; + case EmailActionType.deletePermanently: + _deleteEmailPermanently(presentationEmail); + break; + case EmailActionType.moveToSpam: + _markEmailSpam(presentationEmail); + break; + case EmailActionType.unSpam: + _unSpamEmail(presentationEmail); + break; + case EmailActionType.createRule: + _quickCreateRule(presentationEmail); + break; + case EmailActionType.unsubscribe: + _unsubscribeEmail(presentationEmail); + break; + case EmailActionType.archiveMessage: + _archiveMessage(presentationEmail); + break; + case EmailActionType.printAll: + _printEmail(presentationEmail); + break; + case EmailActionType.downloadMessageAsEML: + _downloadMessageAsEML(presentationEmail); + break; + case EmailActionType.editAsNewEmail: + _editAsNewEmail(presentationEmail); + break; + case EmailActionType.reply: + _replyEmail(presentationEmail); + break; + case EmailActionType.replyAll: + _replyAll(presentationEmail); + break; + case EmailActionType.replyToList: + _replyToList(presentationEmail); + break; + case EmailActionType.forward: + _forward(presentationEmail); + break; + default: + break; + } + } + + void _markRead(PresentationEmail presentationEmail) { + if (session == null || accountId == null) return; + + consumeState(emailActionReactor.markAsEmailRead( + session!, + accountId!, + presentationEmail, + readAction: ReadActions.markAsRead, + )); + } + + void _unRead(PresentationEmail presentationEmail) { + if (session == null || accountId == null) return; + + consumeState(emailActionReactor.markAsEmailRead( + session!, + accountId!, + presentationEmail, + readAction: ReadActions.markAsUnread, + )); + } + + void _markStar( + PresentationEmail presentationEmail, + MarkStarAction markStarAction, + ) { + if (session == null || accountId == null) return; + + consumeState(emailActionReactor.markAsStarEmail( + session!, + accountId!, + presentationEmail, + markStarAction: markStarAction, + )); + } + + Future _moveEmail(PresentationEmail presentationEmail) async { + if (session == null || accountId == null) return; + + final moveActionRequest = await emailActionReactor.moveToMailbox( + session!, + accountId!, + presentationEmail, + mapMailbox: mailboxDashBoardController.mapMailboxById, + selectedMailbox: mailboxDashBoardController.selectedMailbox.value, + isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning, + ); + + if (moveActionRequest == null) return; + + mailboxDashBoardController.moveToMailbox( + session!, + accountId!, + moveActionRequest.moveRequest, + moveActionRequest.emailIdsWithReadStatus, + ); + } + + void _trashEmail(PresentationEmail presentationEmail) { + if (session == null || accountId == null) return; + + final moveActionRequest = emailActionReactor.moveToTrash( + presentationEmail, + mapMailbox: mailboxDashBoardController.mapMailboxById, + selectedMailbox: mailboxDashBoardController.selectedMailbox.value, + isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning, + mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole, + ); + if (moveActionRequest == null) return; + mailboxDashBoardController.moveToMailbox( + session!, + accountId!, + moveActionRequest.moveRequest, + moveActionRequest.emailIdsWithReadStatus, + ); + } + + void _markEmailSpam(PresentationEmail presentationEmail) { + if (session == null || accountId == null) return; + + final moveActionRequest = emailActionReactor.moveToSpam( + presentationEmail, + mapMailbox: mailboxDashBoardController.mapMailboxById, + selectedMailbox: mailboxDashBoardController.selectedMailbox.value, + isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning, + mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole, + ); + if (moveActionRequest == null) return; + mailboxDashBoardController.moveToMailbox( + session!, + accountId!, + moveActionRequest.moveRequest, + moveActionRequest.emailIdsWithReadStatus, + ); + } + + void _unSpamEmail(PresentationEmail presentationEmail) { + if (session == null || accountId == null) return; + + final moveActionRequest = emailActionReactor.unSpam( + presentationEmail, + mapMailbox: mailboxDashBoardController.mapMailboxById, + selectedMailbox: mailboxDashBoardController.selectedMailbox.value, + isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning, + mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole, + ); + if (moveActionRequest == null) return; + mailboxDashBoardController.moveToMailbox( + session!, + accountId!, + moveActionRequest.moveRequest, + moveActionRequest.emailIdsWithReadStatus, + ); + } + + void _deleteEmailPermanently(PresentationEmail email) { + emailActionReactor.deleteEmailPermanently( + email, + onDeleteEmailRequest: (email) { + popBack(); + mailboxDashBoardController.deleteEmailPermanently(email); + }, + responsiveUtils: responsiveUtils, + imagePaths: imagePaths, + ); + } + + void _quickCreateRule(PresentationEmail presentationEmail) { + final emailAddress = presentationEmail.from?.first; + if (session == null || accountId == null || emailAddress == null) return; + + consumeState(emailActionReactor.quickCreateRule( + session!, + accountId!, + emailAddress: emailAddress, + )); + } + + void _unsubscribeEmail(PresentationEmail presentationEmail) { + emailActionReactor.unsubscribeEmail( + presentationEmail, + emailUnsubscribe: EmailUtils.parsingUnsubscribe( + presentationEmail.listUnsubscribe ?? '', + ), + onUnsubscribeByHttpsLink: mailboxDashBoardController.unsubscribeMail, + onUnsubscribeByMailtoLink: (emailId, navigationRouter) { + mailboxDashBoardController.openComposer( + ComposerArguments.fromUnsubscribeMailtoLink( + listEmailAddress: navigationRouter.listEmailAddress, + subject: navigationRouter.subject, + body: navigationRouter.body, + previousEmailId: emailId, + ) + ); + }, + ); + } + + void _archiveMessage(PresentationEmail presentationEmail) { + if (currentContext == null) return; + + emailActionReactor.archiveMessage( + presentationEmail, + onArchiveEmailRequest: (presentationEmail) { + mailboxDashBoardController.archiveMessage(currentContext!, presentationEmail); + }, + ); + } + + void _printEmail(PresentationEmail presentationEmail) { + consumeState(emailActionReactor.printEmail( + presentationEmail, + ownEmailAddress: mailboxDashBoardController.ownEmailAddress.value, + emailLoaded: null, + session: session, + accountId: accountId, + baseDownloadUrl: session?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl), + transformConfiguration: TransformConfiguration.forPreviewEmailOnWeb(), + onGetEmailContentFailure: _showUnknownErrorToast, + )); + } + + void _downloadMessageAsEML(PresentationEmail presentationEmail) { + if (accountId == null || session == null) return; + + consumeState(emailActionReactor.downloadMessageAsEML( + session!, + accountId!, + presentationEmail, + downloadProgressStateController: downloadProgressState, + )); + } + + void _editAsNewEmail(PresentationEmail presentationEmail) { + if (accountId == null || session == null) return; + + emailActionReactor.editAsNewEmail( + presentationEmail, + onEditAsEmailRequest: (presentationEmail) { + mailboxDashBoardController.openComposer( + ComposerArguments.editAsNewEmail(presentationEmail), + ); + }, + ); + } + + void _replyEmail(PresentationEmail presentationEmail) { + emailActionReactor.replyEmail( + presentationEmail, + onReplyEmailRequest: (presentationEmail, emailLoaded) { + mailboxDashBoardController.openComposer( + ComposerArguments.replyEmail( + presentationEmail: presentationEmail, + content: emailLoaded?.htmlContent, + inlineImages: emailLoaded?.inlineImages, + mailboxRole: presentationEmail.mailboxContain?.role, + messageId: emailLoaded?.emailCurrent?.messageId, + references: emailLoaded?.emailCurrent?.references, + listPost: emailLoaded?.emailCurrent?.listPost, + ) + ); + }, + emailLoaded: null, + ); + } + + void _replyAll(PresentationEmail presentationEmail) { + emailActionReactor.replyAll( + presentationEmail, + onReplyAllRequest: (presentationEmail, emailLoaded) { + mailboxDashBoardController.openComposer( + ComposerArguments.replyAllEmail( + presentationEmail: presentationEmail, + content: emailLoaded?.htmlContent, + inlineImages: emailLoaded?.inlineImages, + mailboxRole: presentationEmail.mailboxContain?.role, + messageId: emailLoaded?.emailCurrent?.messageId, + references: emailLoaded?.emailCurrent?.references, + listPost: emailLoaded?.emailCurrent?.listPost, + ) + ); + }, + emailLoaded: null, + ); + } + void _replyToList(PresentationEmail presentationEmail) { + emailActionReactor.replyToList( + presentationEmail, + onReplyToListRequest: (presentationEmail, emailLoaded) { + mailboxDashBoardController.openComposer( + ComposerArguments.replyToListEmail( + presentationEmail: presentationEmail, + content: emailLoaded?.htmlContent, + inlineImages: emailLoaded?.inlineImages, + mailboxRole: presentationEmail.mailboxContain?.role, + messageId: emailLoaded?.emailCurrent?.messageId, + references: emailLoaded?.emailCurrent?.references, + listPost: emailLoaded?.emailCurrent?.listPost, + ) + ); + }, + emailLoaded: null, + ); + } + void _forward(PresentationEmail presentationEmail) { + emailActionReactor.forward( + presentationEmail, + onForwardRequest: (presentationEmail, emailLoaded) { + mailboxDashBoardController.openComposer( + ComposerArguments.forwardEmail( + presentationEmail: presentationEmail, + content: emailLoaded?.htmlContent, + attachments: emailLoaded?.attachments, + inlineImages: emailLoaded?.inlineImages, + messageId: emailLoaded?.emailCurrent?.messageId, + references: emailLoaded?.emailCurrent?.references, + ) + ); + }, + emailLoaded: null, + ); + } + + _showUnknownErrorToast() { + if (currentOverlayContext != null && currentContext != null) { + appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).unknownError, + ); + } + } +} \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/thread_detail_open_email_address_detail_action.dart b/lib/features/thread_detail/presentation/extension/thread_detail_open_email_address_detail_action.dart new file mode 100644 index 000000000..3beccc84c --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/thread_detail_open_email_address_detail_action.dart @@ -0,0 +1,29 @@ +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/open_and_close_composer_extension.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +extension ThreadDetailOpenEmailAddressDetailAction on ThreadDetailController { + void openEmailAddressDetailAction(EmailAddress emailAddress) { + if (session == null || accountId == null) return; + + emailActionReactor.openEmailAddressDialog( + session!, + accountId!, + emailAddress: emailAddress, + responsiveUtils: responsiveUtils, + imagePaths: imagePaths, + appToast: appToast, + onComposeEmailFromEmailAddressRequest: (emailAddress) { + popBack(); + mailboxDashBoardController.openComposer( + ComposerArguments.fromEmailAddress(emailAddress), + ); + }, + onQuickCreateRuleRequest: (quickCreateRuleStream) { + consumeState(quickCreateRuleStream); + }, + ); + } +} \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/thread_detail_bindings.dart b/lib/features/thread_detail/presentation/thread_detail_bindings.dart index 51d79483a..c1a8a8488 100644 --- a/lib/features/thread_detail/presentation/thread_detail_bindings.dart +++ b/lib/features/thread_detail/presentation/thread_detail_bindings.dart @@ -2,6 +2,11 @@ import 'package:core/data/model/source_type/data_source_type.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/http/http_client.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart'; import 'package:tmail_ui_user/features/thread_detail/data/data_source/thread_detail_data_source.dart'; import 'package:tmail_ui_user/features/thread_detail/data/data_source/thread_detail_remote_data_source_impl.dart'; import 'package:tmail_ui_user/features/thread_detail/data/network/thread_detail_api.dart'; @@ -25,6 +30,11 @@ class ThreadDetailBindings extends BaseBindings { Get.put(ThreadDetailController( Get.find(), Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), )); } diff --git a/lib/features/thread_detail/presentation/thread_detail_controller.dart b/lib/features/thread_detail/presentation/thread_detail_controller.dart index ae79ded2e..00ae3496d 100644 --- a/lib/features/thread_detail/presentation/thread_detail_controller.dart +++ b/lib/features/thread_detail/presentation/thread_detail_controller.dart @@ -1,43 +1,88 @@ import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:core/data/network/download/download_manager.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +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/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/email/email_property.dart'; import 'package:model/email/presentation_email.dart'; +import 'package:model/extensions/keyword_identifier_extension.dart'; import 'package:model/extensions/session_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart'; +import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_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/usecases/download_attachment_for_web_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/create_new_rule_filter_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/create_new_email_rule_filter_interactor.dart'; import 'package:tmail_ui_user/features/search/email/presentation/search_email_controller.dart'; import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_by_id_state.dart'; import 'package:tmail_ui_user/features/thread_detail/domain/state/get_emails_by_ids_state.dart'; import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart'; import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_emails_by_ids_interactor.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/action/thread_detail_ui_action.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_get_emails_by_ids_success.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/initialize_thread_detail_emails.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_collapsed_email_download_states.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/mark_collapsed_email_star_success.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/mark_collapsed_email_unread_success.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/quick_create_rule_from_collapsed_email_success.dart'; class ThreadDetailController extends BaseController { final GetThreadByIdInteractor _getEmailIdsByThreadIdInteractor; final GetEmailsByIdsInteractor getEmailsByIdsInteractor; + final MarkAsEmailReadInteractor _markAsEmailReadInteractor; + final MarkAsStarEmailInteractor _markAsStarEmailInteractor; + final PrintEmailInteractor _printEmailInteractor; + final GetEmailContentInteractor _getEmailContentInteractor; + final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor; ThreadDetailController( this._getEmailIdsByThreadIdInteractor, this.getEmailsByIdsInteractor, + this._markAsEmailReadInteractor, + this._markAsStarEmailInteractor, + this._printEmailInteractor, + this._getEmailContentInteractor, + this._downloadAttachmentForWebInteractor, ); final emailIdsPresentation = {}.obs; final currentExpandedEmailId = Rxn(); final currentEmailLoaded = Rxn(); + late final EmailActionReactor emailActionReactor; + final additionalProperties = Properties({ + IndividualHeaderIdentifier.listPostHeader.value, + IndividualHeaderIdentifier.listUnsubscribeHeader.value, + }); + final mailboxDashBoardController = Get.find(); final searchEmailController = Get.find(); + final downloadManager = Get.find(); + final downloadProgressState = StreamController>(); ScrollController? scrollController; + CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor; AccountId? get accountId => mailboxDashBoardController.accountId.value; Session? get session => mailboxDashBoardController.sessionCurrent; @@ -67,6 +112,17 @@ class ThreadDetailController extends BaseController { @override void onInit() { super.onInit(); + injectRuleFilterBindings(session, accountId); + _createNewEmailRuleFilterInteractor = getBinding(); + emailActionReactor = EmailActionReactor( + _markAsEmailReadInteractor, + _markAsStarEmailInteractor, + _createNewEmailRuleFilterInteractor, + _printEmailInteractor, + _getEmailContentInteractor, + _downloadAttachmentForWebInteractor, + ); + downloadProgressState.stream.listen(handleDownloadProgressState); ever(mailboxDashBoardController.selectedEmail, (presentationEmail) { if (presentationEmail?.threadId == null) { closeThreadDetailAction(currentContext); @@ -86,6 +142,22 @@ class ThreadDetailController extends BaseController { )); } }); + ever(mailboxDashBoardController.threadDetailUIAction, (action) { + if (action is UpdatedEmailKeywordsAction) { + emailIdsPresentation + [action.emailId] + ?.keywords + ?[action.updatedKeyword] = action.value; + if (action.updatedKeyword == KeyWordIdentifierExtension.unsubscribeMail) { + emailIdsPresentation + [action.emailId] + ?..emailHeader?.removeWhere((element) { + return element.name == EmailProperty.headerUnsubscribeKey; + }) + ..listUnsubscribeHeader?.clear(); + } + } + }); } void reset() { @@ -103,6 +175,14 @@ class ThreadDetailController extends BaseController { initializeThreadDetailEmails(); } else if (success is GetEmailsByIdsSuccess) { handleGetEmailsByIdsSuccess(success); + } else if (success is MarkAsEmailReadSuccess) { + markCollapsedEmailReadSuccess(success); + } else if (success is MarkAsStarEmailSuccess) { + markCollapsedEmailStarSuccess(success); + } else if (success is CreateNewRuleFilterSuccess) { + quickCreateRuleFromCollapsedEmailSuccess(success); + } else if (success is DownloadAttachmentForWebSuccess) { + handleDownloadSuccess(success); } else { super.handleSuccessViewState(success); } @@ -118,6 +198,10 @@ class ThreadDetailController extends BaseController { showRetryToast(failure); return; } + if (failure is DownloadAttachmentForWebFailure) { + handleDownloadFailure(failure); + return; + } super.handleFailureViewState(failure); } } \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/thread_detail_view.dart b/lib/features/thread_detail/presentation/thread_detail_view.dart index 81428b566..5286ea230 100644 --- a/lib/features/thread_detail/presentation/thread_detail_view.dart +++ b/lib/features/thread_detail/presentation/thread_detail_view.dart @@ -4,18 +4,19 @@ import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:model/extensions/session_extension.dart'; +import 'package:model/email/email_action_type.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; -import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/email_view.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_bottom_bar_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/get_mailbox_contain_extension.dart'; import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_by_id_state.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_detail_loading_view.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_details_email_views.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart'; -import 'package:tmail_ui_user/main/routes/route_navigation.dart'; class ThreadDetailView extends GetWidget { const ThreadDetailView({super.key}); @@ -39,15 +40,29 @@ class ThreadDetailView extends GetWidget { lastEmailOfThread: controller.emailIdsPresentation.values.lastOrNull, ownUserName: controller.session?.getOwnEmailAddress() ?? '', mailboxContain: _getMailboxContain(), - emailLoaded: getBinding( - tag: controller.emailIdsPresentation.keys.lastOrNull?.id.value - )?.currentEmailLoaded.value, - onEmailActionClick: (email, action) { - // TODO: Next PR - }, - onMoreActionClick: (p0, p1) { - // TODO: Next PR - }, + onEmailActionClick: controller.threadDetailOnEmailActionClick, + onMoreActionClick: (presentationEmail, position) => controller.emailActionReactor.handleMoreEmailAction( + mailboxContain: controller.mailboxDashBoardController.getMailboxContain(presentationEmail), + presentationEmail: presentationEmail, + position: position, + responsiveUtils: controller.responsiveUtils, + imagePaths: controller.imagePaths, + username: controller.session?.username, + handleEmailAction: controller.threadDetailOnEmailActionClick, + additionalActions: [ + if (controller.responsiveUtils.isMobile(context)) ...[ + EmailActionType.forward, + EmailActionType.replyAll, + EmailActionType.replyToList, + ], + EmailActionType.markAsStarred, + EmailActionType.unMarkAsStarred, + EmailActionType.moveToTrash, + EmailActionType.deletePermanently, + EmailActionType.printAll, + EmailActionType.moveToMailbox, + ], + ), ); }), Obx(() => controller.getThreadDetailLoadingView( diff --git a/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart b/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart index 7b7e35581..90996957e 100644 --- a/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart +++ b/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart @@ -5,10 +5,8 @@ import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:model/email/email_action_type.dart'; import 'package:model/email/presentation_email.dart'; -import 'package:model/extensions/email_extension.dart'; import 'package:model/extensions/presentation_email_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; -import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart'; import 'package:tmail_ui_user/features/email/presentation/styles/email_view_app_bar_widget_styles.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_app_bar_widget.dart'; @@ -28,7 +26,6 @@ class ThreadDetailAppBar extends StatelessWidget { this.optionWidgets = const [], this.onEmailActionClick, this.onMoreActionClick, - this.emailLoaded, }); final ResponsiveUtils responsiveUtils; @@ -41,16 +38,31 @@ class ThreadDetailAppBar extends StatelessWidget { final List optionWidgets; final OnEmailActionClick? onEmailActionClick; final OnMoreActionClick? onMoreActionClick; - final EmailLoaded? emailLoaded; @override Widget build(BuildContext context) { final isReplyToListEnabled = EmailUtils.isReplyToListEnabled( - emailLoaded?.emailCurrent?.listPost ?? '', + lastEmailOfThread?.listPost ?? '', ); final child = LayoutBuilder( builder: (context, constraints) { + Widget backButton = EmailViewBackButton( + imagePaths: imagePaths, + onBackAction: () => closeThreadDetailAction(context), + mailboxContain: mailboxContain, + isSearchActivated: isSearchRunning, + maxWidth: constraints.maxWidth, + ); + if (responsiveUtils.isMobile(context)) { + backButton = Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: backButton, + ), + ); + } + return Container( height: PlatformInfo.isIOS ? EmailViewAppBarWidgetStyles.heightIOS(context, responsiveUtils) @@ -76,20 +88,7 @@ class ThreadDetailAppBar extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - if (_supportDisplayMailboxNameTitle(context)) - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: EmailViewBackButton( - imagePaths: imagePaths, - onBackAction: () => closeThreadDetailAction(context), - mailboxContain: mailboxContain, - isSearchActivated: isSearchRunning, - maxWidth: constraints.maxWidth, - ), - ), - ), - ...optionWidgets, + if (_supportDisplayMailboxNameTitle(context)) backButton, if (lastEmailOfThread != null) ...[ TMailButtonWidget.fromIcon( icon: imagePaths.icReply, @@ -102,60 +101,45 @@ class ThreadDetailAppBar extends StatelessWidget { EmailActionType.reply, ), ), - if (lastEmailOfThread!.getCountMailAddressWithoutMe(ownUserName) > 1) + if (!responsiveUtils.isMobile(context)) ...[ + if (lastEmailOfThread!.getCountMailAddressWithoutMe(ownUserName) > 1) + TMailButtonWidget.fromIcon( + icon: imagePaths.icReplyAll, + iconSize: EmailViewAppBarWidgetStyles.buttonIconSize, + iconColor: EmailViewAppBarWidgetStyles.iconColor, + tooltipMessage: AppLocalizations.of(context).reply_all, + backgroundColor: Colors.transparent, + onTapActionCallback: () => onEmailActionClick?.call( + lastEmailOfThread!, + EmailActionType.replyAll, + ), + ), + if (isReplyToListEnabled) + TMailButtonWidget.fromIcon( + icon: imagePaths.icReply, + iconSize: EmailViewAppBarWidgetStyles.buttonIconSize, + iconColor: EmailViewAppBarWidgetStyles.iconColor, + tooltipMessage: AppLocalizations.of(context).replyToList, + backgroundColor: Colors.transparent, + onTapActionCallback: () => onEmailActionClick?.call( + lastEmailOfThread!, + EmailActionType.replyToList, + ), + ), TMailButtonWidget.fromIcon( - icon: imagePaths.icReplyAll, + icon: imagePaths.icForward, iconSize: EmailViewAppBarWidgetStyles.buttonIconSize, iconColor: EmailViewAppBarWidgetStyles.iconColor, - tooltipMessage: AppLocalizations.of(context).reply_all, + tooltipMessage: AppLocalizations.of(context).forward, backgroundColor: Colors.transparent, onTapActionCallback: () => onEmailActionClick?.call( lastEmailOfThread!, - EmailActionType.replyAll, + EmailActionType.forward, ), ), - if (isReplyToListEnabled) - TMailButtonWidget.fromIcon( - icon: imagePaths.icReply, - iconSize: EmailViewAppBarWidgetStyles.buttonIconSize, - iconColor: EmailViewAppBarWidgetStyles.iconColor, - tooltipMessage: AppLocalizations.of(context).replyToList, - backgroundColor: Colors.transparent, - onTapActionCallback: () => onEmailActionClick?.call( - lastEmailOfThread!, - EmailActionType.replyToList, - ), - ), - TMailButtonWidget.fromIcon( - icon: imagePaths.icForward, - iconSize: EmailViewAppBarWidgetStyles.buttonIconSize, - iconColor: EmailViewAppBarWidgetStyles.iconColor, - tooltipMessage: AppLocalizations.of(context).forward, - backgroundColor: Colors.transparent, - onTapActionCallback: () => onEmailActionClick?.call( - lastEmailOfThread!, - EmailActionType.forward, - ), - ), - TMailButtonWidget.fromIcon( - icon: lastEmailOfThread!.hasStarred - ? imagePaths.icStar - : imagePaths.icUnStar, - iconSize: EmailViewAppBarWidgetStyles.buttonIconSize, - iconColor: lastEmailOfThread!.hasStarred - ? null - : EmailViewAppBarWidgetStyles.iconColor, - backgroundColor: Colors.transparent, - tooltipMessage: lastEmailOfThread!.hasStarred - ? AppLocalizations.of(context).not_starred - : AppLocalizations.of(context).mark_as_starred, - onTapActionCallback: () => onEmailActionClick?.call( - lastEmailOfThread!, - lastEmailOfThread!.hasStarred - ? EmailActionType.unMarkAsStarred - : EmailActionType.markAsStarred - ), - ), + ], + if (!responsiveUtils.isMobile(context)) const Spacer(), + ...optionWidgets, TMailButtonWidget.fromIcon( icon: imagePaths.icMoreVertical, iconSize: EmailViewAppBarWidgetStyles.buttonIconSize, @@ -169,7 +153,7 @@ class ThreadDetailAppBar extends StatelessWidget { ? (position) => onMoreActionClick?.call(lastEmailOfThread!, position) : null, ), - ], + ] else const Spacer(), ], ), ); diff --git a/lib/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart b/lib/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart index 62c12f4cc..61fd02db0 100644 --- a/lib/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart +++ b/lib/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart @@ -72,6 +72,7 @@ class ThreadDetailCollapsedEmail extends StatelessWidget { openEmailAddressDetailAction: openEmailAddressDetailAction, showRecipients: false, onTapAvatarActionClick: onToggleThreadDetailCollapseExpand, + mailboxContain: mailboxContain, ), Padding( padding: const EdgeInsets.all(16), diff --git a/model/lib/email/presentation_email.dart b/model/lib/email/presentation_email.dart index e65fa52b1..312cf504e 100644 --- a/model/lib/email/presentation_email.dart +++ b/model/lib/email/presentation_email.dart @@ -17,6 +17,7 @@ import 'package:model/email/email_in_thread_status.dart'; import 'package:model/email/mail_priority_header.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:model/extensions/keyword_identifier_extension.dart'; +import 'package:model/extensions/list_email_header_extension.dart'; import 'package:model/extensions/media_type_nullable_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:model/mailbox/select_mode.dart'; @@ -50,6 +51,8 @@ class PresentationEmail with EquatableMixin, SearchSnippetMixin { final Map? xPriorityHeader; final Map? importanceHeader; final Map? priorityHeader; + final Map? listPostHeader; + final Map? listUnsubscribeHeader; final EmailInThreadStatus? emailInThreadStatus; PresentationEmail({ @@ -79,6 +82,8 @@ class PresentationEmail with EquatableMixin, SearchSnippetMixin { this.xPriorityHeader, this.importanceHeader, this.priorityHeader, + this.listPostHeader, + this.listUnsubscribeHeader, this.emailInThreadStatus, }); @@ -141,6 +146,12 @@ class PresentationEmail with EquatableMixin, SearchSnippetMixin { bool get hasCalendarEvent => headerCalendarEvent?[IndividualHeaderIdentifier.headerCalendarEvent]?.isNotEmpty == true; + String? get listPost => emailHeader?.toSet().listPost?.trim() + ?? listPostHeader?[IndividualHeaderIdentifier.listPostHeader]?.trim(); + + String? get listUnsubscribe => emailHeader?.toSet().listUnsubscribe?.trim() + ?? listUnsubscribeHeader?[IndividualHeaderIdentifier.listUnsubscribeHeader]?.trim(); + bool get isMarkAsImportant { final xPriority = xPriorityHeader?[IndividualHeaderIdentifier.xPriorityHeader] ?.trim() @@ -201,6 +212,8 @@ class PresentationEmail with EquatableMixin, SearchSnippetMixin { xPriorityHeader, importanceHeader, priorityHeader, + listPostHeader, + listUnsubscribeHeader, emailInThreadStatus, ]; @@ -231,6 +244,8 @@ class PresentationEmail with EquatableMixin, SearchSnippetMixin { Map? xPriorityHeader, Map? importanceHeader, Map? priorityHeader, + Map? listPostHeader, + Map? listUnsubscribeHeader, EmailInThreadStatus? emailInThreadStatus, }) { return PresentationEmail( @@ -260,6 +275,8 @@ class PresentationEmail with EquatableMixin, SearchSnippetMixin { xPriorityHeader: xPriorityHeader ?? this.xPriorityHeader, importanceHeader: importanceHeader ?? this.importanceHeader, priorityHeader: priorityHeader ?? this.priorityHeader, + listPostHeader: listPostHeader ?? this.listPostHeader, + listUnsubscribeHeader: listUnsubscribeHeader ?? this.listUnsubscribeHeader, emailInThreadStatus: emailInThreadStatus ?? this.emailInThreadStatus, ); } diff --git a/model/lib/extensions/email_extension.dart b/model/lib/extensions/email_extension.dart index 1d1db3226..a993d43ab 100644 --- a/model/lib/extensions/email_extension.dart +++ b/model/lib/extensions/email_extension.dart @@ -33,7 +33,9 @@ extension EmailExtension on Email { bool get withAttachments => hasAttachment == true; - String get listUnsubscribe => headers.listUnsubscribe; + String get listUnsubscribe => headers.listUnsubscribe + ?? listUnsubscribeHeader?[IndividualHeaderIdentifier.listUnsubscribeHeader] + ?? ''; bool get hasRequestReadReceipt => headers.readReceiptHasBeenRequested || headerMdn?.containsKey(IndividualHeaderIdentifier.headerMdn) == true; @@ -42,7 +44,9 @@ extension EmailExtension on Email { String get sMimeStatusHeaderParsed => sMimeStatusHeader?[IndividualHeaderIdentifier.sMimeStatusHeader]?.trim() ?? ''; - String get listPost => headers.listPost.trim(); + String get listPost => headers.listPost?.trim() + ?? listPostHeader?[IndividualHeaderIdentifier.listPostHeader]?.trim() + ?? ''; IdentityId? get identityIdFromHeader { final rawIdentityId = identityHeader?[IndividualHeaderIdentifier.identityHeader]; @@ -125,6 +129,8 @@ extension EmailExtension on Email { xPriorityHeader: xPriorityHeader, importanceHeader: importanceHeader, priorityHeader: priorityHeader, + listPostHeader: listPostHeader, + listUnsubscribeHeader: listUnsubscribeHeader, ) ..searchSnippetSubject = searchSnippetSubject ..searchSnippetPreview = searchSnippetPreview; @@ -158,6 +164,12 @@ extension EmailExtension on Email { priorityHeader: updatedProperties.contain(IndividualHeaderIdentifier.priorityHeader.value) ? newEmail.priorityHeader : priorityHeader, + listPostHeader: updatedProperties.contain(IndividualHeaderIdentifier.listPostHeader.value) + ? newEmail.listPostHeader + : listPostHeader, + listUnsubscribeHeader: updatedProperties.contain(IndividualHeaderIdentifier.listUnsubscribeHeader.value) + ? newEmail.listUnsubscribeHeader + : listUnsubscribeHeader, ); } @@ -246,6 +258,8 @@ extension EmailExtension on Email { Map? xPriorityHeader, Map? importanceHeader, Map? priorityHeader, + Map? listPostHeader, + Map? listUnsubscribeHeader, }) { return Email( id: id ?? this.id, @@ -283,6 +297,8 @@ extension EmailExtension on Email { xPriorityHeader: xPriorityHeader ?? this.xPriorityHeader, importanceHeader: importanceHeader ?? this.importanceHeader, priorityHeader: priorityHeader ?? this.priorityHeader, + listPostHeader: listPostHeader ?? this.listPostHeader, + listUnsubscribeHeader: listUnsubscribeHeader ?? this.listUnsubscribeHeader, ); } } \ No newline at end of file diff --git a/model/lib/extensions/list_email_header_extension.dart b/model/lib/extensions/list_email_header_extension.dart index c3be89673..7b2e575d5 100644 --- a/model/lib/extensions/list_email_header_extension.dart +++ b/model/lib/extensions/list_email_header_extension.dart @@ -14,9 +14,9 @@ extension ListEmailHeaderExtension on Set? { } } - String get listUnsubscribe { + String? get listUnsubscribe { final listUnsubscribe = this?.firstWhereOrNull((header) => header.name == EmailProperty.headerUnsubscribeKey); - return listUnsubscribe?.value ?? ''; + return listUnsubscribe?.value; } String get sMimeStatus { @@ -25,8 +25,8 @@ extension ListEmailHeaderExtension on Set? { return sMimeStatus?.value.trim() ?? ''; } - String get listPost { + String? get listPost { final listPost = this?.firstWhereOrNull((header) => header.name == EmailProperty.headerListPostKey); - return listPost?.value ?? ''; + return listPost?.value; } } \ No newline at end of file diff --git a/model/lib/extensions/presentation_email_extension.dart b/model/lib/extensions/presentation_email_extension.dart index 503e40304..f68a2818c 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -111,6 +111,9 @@ extension PresentationEmailExtension on PresentationEmail { xPriorityHeader: xPriorityHeader, importanceHeader: importanceHeader, priorityHeader: priorityHeader, + listPostHeader: listPostHeader, + listUnsubscribeHeader: listUnsubscribeHeader, + threadId: threadId, ); } diff --git a/test/features/thread_detail/presentation/extension/initialize_thread_detail_emails_test.dart b/test/features/thread_detail/presentation/extension/initialize_thread_detail_emails_test.dart index 4f455f979..5f889ef08 100644 --- a/test/features/thread_detail/presentation/extension/initialize_thread_detail_emails_test.dart +++ b/test/features/thread_detail/presentation/extension/initialize_thread_detail_emails_test.dart @@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -40,6 +41,7 @@ void main() { Map.fromEntries(emailIds.map((emailId) => MapEntry(emailId, null))).obs, ); when(threadDetailController.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor); + when(threadDetailController.additionalProperties).thenReturn(Properties.empty()); when(getEmailsByIdsInteractor.execute( any, any, diff --git a/test/features/thread_detail/presentation/extension/load_more_thread_detail_emails_test.dart b/test/features/thread_detail/presentation/extension/load_more_thread_detail_emails_test.dart index fe745c8fd..7177ecd07 100644 --- a/test/features/thread_detail/presentation/extension/load_more_thread_detail_emails_test.dart +++ b/test/features/thread_detail/presentation/extension/load_more_thread_detail_emails_test.dart @@ -1,6 +1,7 @@ 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/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -37,6 +38,7 @@ void main() { when(controller.session).thenReturn(SessionFixtures.aliceSession); when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId); when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor); + when(controller.additionalProperties).thenReturn(Properties.empty()); // Act controller.loadMoreThreadDetailEmails(); @@ -86,6 +88,7 @@ void main() { when(controller.session).thenReturn(SessionFixtures.aliceSession); when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId); when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor); + when(controller.additionalProperties).thenReturn(Properties.empty()); // Act controller.loadMoreThreadDetailEmails();