From e232514f6b7f954512e26905e96b2a9484c2e974 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 5 Mar 2024 22:57:51 +0700 Subject: [PATCH] TF-2667 Show sending message dialog when click send button --- core/lib/data/constants/constant.dart | 1 + core/lib/utils/application_manager.dart | 1 + .../widget/application_version_widget.dart | 24 +- .../repository/composer_repository_impl.dart | 80 ++++++- .../repository/composer_repository.dart | 4 + .../domain/state/generate_email_state.dart | 9 + .../domain/state/send_email_state.dart | 14 +- .../create_new_and_send_email_interactor.dart | 128 +++++++++++ .../usecases/send_email_interactor.dart | 32 +-- .../presentation/composer_bindings.dart | 19 +- .../presentation/composer_controller.dart | 109 +++++---- .../create_email_request_extension.dart | 176 +++++++++++++++ .../extensions/identity_extension.dart | 6 + .../model/create_email_request.dart | 88 ++++++++ .../widgets/sending_message_dialog_view.dart | 206 ++++++++++++++++++ .../data/datasource/email_datasource.dart | 2 +- .../data/datasource/html_datasource.dart | 13 +- .../email_datasource_impl.dart | 2 +- .../datasource_impl/html_datasource_impl.dart | 25 +++ .../email/data/local/html_analyzer.dart | 58 +++++ .../repository/email_repository_impl.dart | 2 +- .../domain/exceptions/email_exceptions.dart | 4 +- .../list_attachments_extension.dart | 4 + .../domain/repository/email_repository.dart | 2 +- .../presentation/bindings/email_bindings.dart | 2 +- .../bindings/mailbox_dashboard_bindings.dart | 2 +- .../mailbox_dashboard_controller.dart | 10 +- .../manage_account_dashboard_view.dart | 5 +- .../web_network_connection_controller.dart | 2 + .../sending_email_interactor_bindings.dart | 2 +- .../bindings/fcm_interactor_bindings.dart | 2 +- .../controller/upload_controller.dart | 3 +- .../send_email_exception_thrower.dart | 3 +- .../localizations/localization_service.dart | 27 ++- model/lib/email/attachment.dart | 13 -- .../lib/extensions/attachment_extension.dart | 7 +- model/lib/extensions/username_extension.dart | 7 +- 37 files changed, 953 insertions(+), 141 deletions(-) create mode 100644 lib/features/composer/domain/state/generate_email_state.dart create mode 100644 lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart create mode 100644 lib/features/composer/presentation/extensions/create_email_request_extension.dart create mode 100644 lib/features/composer/presentation/extensions/identity_extension.dart create mode 100644 lib/features/composer/presentation/model/create_email_request.dart create mode 100644 lib/features/composer/presentation/widgets/sending_message_dialog_view.dart diff --git a/core/lib/data/constants/constant.dart b/core/lib/data/constants/constant.dart index adf3581cb..5540b0ac1 100644 --- a/core/lib/data/constants/constant.dart +++ b/core/lib/data/constants/constant.dart @@ -2,6 +2,7 @@ class Constant { static const acceptHeaderDefault = 'application/json'; static const contentTypeHeaderDefault = 'application/json'; static const pdfMimeType = 'application/pdf'; + static const base64Charset = 'base64'; static const textHtmlMimeType = 'text/html'; static const octetStreamMimeType = 'application/octet-stream'; static const pdfExtension = '.pdf'; diff --git a/core/lib/utils/application_manager.dart b/core/lib/utils/application_manager.dart index c7b291c47..b452f6670 100644 --- a/core/lib/utils/application_manager.dart +++ b/core/lib/utils/application_manager.dart @@ -34,6 +34,7 @@ class ApplicationManager { userAgent = FkUserAgent.userAgent ?? ''; FkUserAgent.release(); } + log('ApplicationManager::getUserAgent: $userAgent'); return userAgent; } catch(e) { logError('ApplicationManager::getUserAgent: Exception: $e'); diff --git a/lib/features/base/widget/application_version_widget.dart b/lib/features/base/widget/application_version_widget.dart index 38eab2716..8a05bd040 100644 --- a/lib/features/base/widget/application_version_widget.dart +++ b/lib/features/base/widget/application_version_widget.dart @@ -2,7 +2,7 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/utils/application_manager.dart'; import 'package:flutter/material.dart'; -class ApplicationVersionWidget extends StatelessWidget { +class ApplicationVersionWidget extends StatefulWidget { final ApplicationManager applicationManager; final EdgeInsetsGeometry? padding; @@ -17,18 +17,32 @@ class ApplicationVersionWidget extends StatelessWidget { this.padding, }); + @override + State createState() => _ApplicationVersionWidgetState(); +} + +class _ApplicationVersionWidgetState extends State { + + Future? _versionStream; + + @override + void initState() { + super.initState(); + _versionStream = widget.applicationManager.getVersion(); + } + @override Widget build(BuildContext context) { return FutureBuilder( - future: applicationManager.getVersion(), + future: _versionStream, builder: (context, snapshot) { if (snapshot.hasData) { return Padding( - padding: padding ?? const EdgeInsets.only(top: 6), + padding: widget.padding ?? const EdgeInsets.only(top: 6), child: Text( - '${title ?? 'v.'}${snapshot.data}', + '${widget.title ?? 'v.'}${snapshot.data}', textAlign: TextAlign.center, - style: textStyle ?? Theme.of(context).textTheme.labelMedium?.copyWith( + style: widget.textStyle ?? Theme.of(context).textTheme.labelMedium?.copyWith( fontSize: 13, color: AppColor.colorContentEmail, fontWeight: FontWeight.w500 diff --git a/lib/features/composer/data/repository/composer_repository_impl.dart b/lib/features/composer/data/repository/composer_repository_impl.dart index 196e4de47..1ee0c4b91 100644 --- a/lib/features/composer/data/repository/composer_repository_impl.dart +++ b/lib/features/composer/data/repository/composer_repository_impl.dart @@ -1,19 +1,37 @@ - +import 'package:core/data/constants/constant.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/application_manager.dart'; +import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:model/email/attachment.dart'; import 'package:model/upload/file_info.dart'; import 'package:tmail_ui_user/features/composer/data/datasource/composer_datasource.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/create_email_request_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/list_attachments_extension.dart'; import 'package:tmail_ui_user/features/upload/data/datasource/attachment_upload_datasource.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_attachment.dart'; +import 'package:uuid/uuid.dart'; class ComposerRepositoryImpl extends ComposerRepository { final AttachmentUploadDataSource _attachmentUploadDataSource; final ComposerDataSource _composerDataSource; + final HtmlDataSource _htmlDataSource; + final ApplicationManager _applicationManager; + final Uuid _uuid; ComposerRepositoryImpl( this._attachmentUploadDataSource, - this._composerDataSource); + this._composerDataSource, + this._htmlDataSource, + this._applicationManager, + this._uuid, + ); @override Future uploadAttachment(FileInfo fileInfo, Uri uploadUri, {CancelToken? cancelToken}) { @@ -24,4 +42,62 @@ class ComposerRepositoryImpl extends ComposerRepository { Future downloadImageAsBase64(String url, String cid, FileInfo fileInfo, {double? maxWidth, bool? compress}) { return _composerDataSource.downloadImageAsBase64(url, cid, fileInfo, maxWidth: maxWidth, compress: compress); } + + @override + Future generateEmail(CreateEmailRequest createEmailRequest) async { + String emailContent = createEmailRequest.emailContent; + Set emailAttachments = Set.from(createEmailRequest.createAttachments()); + + if (createEmailRequest.inlineAttachments?.isNotEmpty == true) { + final tupleContentInlineAttachments = await _replaceImageBase64ToImageCID( + emailContent: emailContent, + inlineAttachments: createEmailRequest.inlineAttachments! + ); + + emailContent = tupleContentInlineAttachments.value1; + emailAttachments.addAll(tupleContentInlineAttachments.value2); + } + + emailContent = await _removeCollapsedExpandedSignatureEffect(emailContent: emailContent); + + final userAgent = await _applicationManager.generateApplicationUserAgent(); + final emailBodyPartId = PartId(_uuid.v1()); + + final emailObject = createEmailRequest.generateEmail( + newEmailContent: emailContent, + newEmailAttachments: emailAttachments, + userAgent: userAgent, + partId: emailBodyPartId + ); + + return emailObject; + } + + Future>> _replaceImageBase64ToImageCID({ + required String emailContent, + required Map inlineAttachments + }) { + try { + return _htmlDataSource.replaceImageBase64ToImageCID( + emailContent: emailContent, + inlineAttachments: inlineAttachments); + } catch (e) { + logError('ComposerRepositoryImpl::_replaceImageBase64ToImageCID: Exception: $e'); + return Future.value( + Tuple2( + emailContent, + inlineAttachments.values.toList().toEmailBodyPart(charset: Constant.base64Charset) + ) + ); + } + } + + Future _removeCollapsedExpandedSignatureEffect({required String emailContent}) { + try { + return _htmlDataSource.removeCollapsedExpandedSignatureEffect(emailContent: emailContent); + } catch (e) { + logError('ComposerRepositoryImpl::_removeCollapsedExpandedSignatureEffect: Exception: $e'); + return Future.value(emailContent); + } + } } \ No newline at end of file diff --git a/lib/features/composer/domain/repository/composer_repository.dart b/lib/features/composer/domain/repository/composer_repository.dart index 33c9cf4fd..8ba787980 100644 --- a/lib/features/composer/domain/repository/composer_repository.dart +++ b/lib/features/composer/domain/repository/composer_repository.dart @@ -1,8 +1,12 @@ import 'package:dio/dio.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/upload/file_info.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_attachment.dart'; abstract class ComposerRepository { + Future generateEmail(CreateEmailRequest createEmailRequest); + Future uploadAttachment(FileInfo fileInfo, Uri uploadUri, {CancelToken? cancelToken}); Future downloadImageAsBase64(String url, String cid, FileInfo fileInfo, {double? maxWidth, bool? compress}); diff --git a/lib/features/composer/domain/state/generate_email_state.dart b/lib/features/composer/domain/state/generate_email_state.dart new file mode 100644 index 000000000..f41c7e300 --- /dev/null +++ b/lib/features/composer/domain/state/generate_email_state.dart @@ -0,0 +1,9 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class GenerateEmailLoading extends LoadingState {} + +class GenerateEmailFailure extends FeatureFailure { + + GenerateEmailFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/composer/domain/state/send_email_state.dart b/lib/features/composer/domain/state/send_email_state.dart index 3aa23b7f0..01312e80f 100644 --- a/lib/features/composer/domain/state/send_email_state.dart +++ b/lib/features/composer/domain/state/send_email_state.dart @@ -8,7 +8,7 @@ import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart' import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_action_type.dart'; -class SendEmailLoading extends UIState {} +class SendEmailLoading extends LoadingState {} class SendEmailSuccess extends UIActionState { @@ -29,17 +29,17 @@ class SendEmailSuccess extends UIActionState { class SendEmailFailure extends FeatureFailure { - final Session session; - final AccountId accountId; - final EmailRequest emailRequest; + final Session? session; + final AccountId? accountId; + final EmailRequest? emailRequest; final CreateNewMailboxRequest? mailboxRequest; final SendingEmailActionType? sendingEmailActionType; SendEmailFailure({ dynamic exception, - required this.session, - required this.accountId, - required this.emailRequest, + this.session, + this.accountId, + this.emailRequest, this.mailboxRequest, this.sendingEmailActionType }) : super(exception: exception); diff --git a/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart b/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart new file mode 100644 index 000000000..2c21547d9 --- /dev/null +++ b/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart @@ -0,0 +1,128 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart' as dartz; +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/state.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/create_email_request_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; + +class CreateNewAndSendEmailInteractor { + final EmailRepository _emailRepository; + final MailboxRepository _mailboxRepository; + final ComposerRepository _composerRepository; + + CreateNewAndSendEmailInteractor( + this._emailRepository, + this._mailboxRepository, + this._composerRepository, + ); + + Stream> execute(CreateEmailRequest createEmailRequest) async* { + SendingEmailArguments? sendingEmailArguments; + try { + yield dartz.Right(GenerateEmailLoading()); + + final listCurrentState = await _getStoredCurrentState( + session: createEmailRequest.session, + accountId: createEmailRequest.accountId + ); + + sendingEmailArguments = await _createEmailObject(createEmailRequest); + + if (sendingEmailArguments != null) { + yield dartz.Right(SendEmailLoading()); + + await _emailRepository.sendEmail( + sendingEmailArguments.session, + sendingEmailArguments.accountId, + sendingEmailArguments.emailRequest, + mailboxRequest: sendingEmailArguments.mailboxRequest + ); + + if (sendingEmailArguments.emailRequest.emailIdDestroyed != null) { + await _deleteOldDraftsEmail( + session: sendingEmailArguments.session, + accountId: sendingEmailArguments.accountId, + draftEmailId: sendingEmailArguments.emailRequest.emailIdDestroyed! + ); + } + + yield dartz.Right( + SendEmailSuccess( + currentMailboxState: listCurrentState?.value1, + currentEmailState: listCurrentState?.value2, + emailRequest: sendingEmailArguments.emailRequest + ) + ); + } else { + yield dartz.Left(GenerateEmailFailure(CannotCreateEmailObjectException())); + } + } catch (e) { + logError('CreateNewAndSendEmailInteractor::execute: Exception: $e'); + yield dartz.Left(SendEmailFailure( + exception: e, + session: sendingEmailArguments?.session, + accountId: sendingEmailArguments?.accountId, + emailRequest: sendingEmailArguments?.emailRequest, + mailboxRequest: sendingEmailArguments?.mailboxRequest, + )); + } + } + + Future _createEmailObject(CreateEmailRequest createEmailRequest) async { + try { + final emailCreated = await _composerRepository.generateEmail(createEmailRequest); + final sendingEmailArgument = createEmailRequest.toSendingEmailArguments(emailObject: emailCreated); + return sendingEmailArgument; + } catch (e) { + logError('CreateNewAndSendEmailInteractor::_createEmailObject: Exception: $e'); + return null; + } + } + + Future?> _getStoredCurrentState({ + required Session session, + required AccountId accountId + }) async { + try { + final listState = await Future.wait([ + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), + ]); + + final mailboxState = listState.first; + final emailState = listState.last; + + return dartz.Tuple2(mailboxState, emailState); + } catch (e) { + logError('CreateNewAndSendEmailInteractor::_getStoredCurrentState: Exception: $e'); + return null; + } + } + + Future _deleteOldDraftsEmail({ + required Session session, + required AccountId accountId, + required EmailId draftEmailId + }) async { + try { + await _emailRepository.deleteEmailPermanently( + session, + accountId, + draftEmailId + ); + } catch (e) { + logError('CreateNewAndSendEmailInteractor::_deleteOldDraftsEmail: Exception: $e'); + } + } +} \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/send_email_interactor.dart b/lib/features/composer/domain/usecases/send_email_interactor.dart index 218d9b71a..83ade78ac 100644 --- a/lib/features/composer/domain/usecases/send_email_interactor.dart +++ b/lib/features/composer/domain/usecases/send_email_interactor.dart @@ -38,34 +38,24 @@ class SendEmailInteractor { final currentMailboxState = listState.first; final currentEmailState = listState.last; - final result = await _emailRepository.sendEmail( + await _emailRepository.sendEmail( session, accountId, emailRequest, mailboxRequest: mailboxRequest ); - if (result) { - if (emailRequest.emailIdDestroyed != null) { - await _emailRepository.deleteEmailPermanently(session, accountId, emailRequest.emailIdDestroyed!); - } - - yield Right( - SendEmailSuccess( - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState, - emailRequest: emailRequest - ) - ); - } else { - yield Left(SendEmailFailure( - session: session, - accountId: accountId, - emailRequest: emailRequest, - mailboxRequest: mailboxRequest, - sendingEmailActionType: sendingEmailActionType, - )); + if (emailRequest.emailIdDestroyed != null) { + await _emailRepository.deleteEmailPermanently(session, accountId, emailRequest.emailIdDestroyed!); } + + yield Right( + SendEmailSuccess( + currentEmailState: currentEmailState, + currentMailboxState: currentMailboxState, + emailRequest: emailRequest + ) + ); } catch (e) { yield Left(SendEmailFailure( exception: e, diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index 341bcd4be..7f282169a 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -1,5 +1,6 @@ import 'package:core/core.dart'; -import 'package:device_info_plus/device_info_plus.dart'; +import 'package:core/utils/application_manager.dart'; +import 'package:core/utils/file_utils.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; import 'package:tmail_ui_user/features/composer/data/datasource/composer_datasource.dart'; @@ -10,6 +11,7 @@ import 'package:tmail_ui_user/features/composer/data/repository/composer_reposit import 'package:tmail_ui_user/features/composer/data/repository/contact_repository_impl.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/contact_repository.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/download_image_as_base64_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_as_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/update_email_drafts_interactor.dart'; @@ -104,7 +106,7 @@ class ComposerBindings extends BaseBindings { Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find())); + Get.find())); Get.lazyPut(() => StateDataSourceImpl( Get.find(), Get.find(), @@ -147,8 +149,12 @@ class ComposerBindings extends BaseBindings { @override void bindingsRepositoryImpl() { Get.lazyPut(() => ComposerRepositoryImpl( - Get.find(), - Get.find())); + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + )); Get.lazyPut(() => ContactRepositoryImpl(Get.find())); Get.lazyPut(() => MailboxRepositoryImpl( { @@ -197,6 +203,11 @@ class ComposerBindings extends BaseBindings { Get.lazyPut(() => DownloadImageAsBase64Interactor(Get.find())); Get.lazyPut(() => TransformHtmlEmailContentInteractor(Get.find())); Get.lazyPut(() => GetAlwaysReadReceiptSettingInteractor(Get.find())); + Get.lazyPut(() => CreateNewAndSendEmailInteractor( + Get.find(), + Get.find(), + Get.find(), + )); IdentityInteractorsBindings().dependencies(); } diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index f0717c2d2..dfc8b0b0e 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -17,7 +17,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:html_editor_enhanced/html_editor.dart' as web_html_editor; import 'package:http_parser/http_parser.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; @@ -29,6 +28,7 @@ import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:super_tag_editor/tag_editor.dart'; @@ -36,12 +36,12 @@ import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/state/base_ui_state.dart'; import 'package:tmail_ui_user/features/base/state/button_state.dart'; import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.dart'; -import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; import 'package:tmail_ui_user/features/composer/domain/state/download_image_as_base64_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_device_contact_suggestions_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/download_image_as_base64_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_all_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; @@ -53,6 +53,7 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/file_upl import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/list_shared_media_file_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/mixin/drag_drog_file_mixin.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart'; @@ -60,13 +61,13 @@ import 'package:tmail_ui_user/features/composer/presentation/model/save_to_draft import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; import 'package:tmail_ui_user/features/composer/presentation/styles/composer_style.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from_composer_bottom_sheet_builder.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/sending_message_dialog_view.dart'; import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; @@ -76,7 +77,6 @@ import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_id import 'package:tmail_ui_user/features/manage_account/presentation/extensions/identity_extension.dart'; import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart' if (dart.library.html) 'package:tmail_ui_user/features/network_connection/presentation/web_network_connection_controller.dart'; -import 'package:tmail_ui_user/features/sending_queue/domain/extensions/sending_email_extension.dart'; import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; import 'package:tmail_ui_user/features/server_settings/domain/state/get_always_read_receipt_setting_state.dart'; @@ -131,6 +131,7 @@ class ComposerController extends BaseController with DragDropFileMixin { final DownloadImageAsBase64Interactor _downloadImageAsBase64Interactor; final TransformHtmlEmailContentInteractor _transformHtmlEmailContentInteractor; final GetAlwaysReadReceiptSettingInteractor _getAlwaysReadReceiptSettingInteractor; + final CreateNewAndSendEmailInteractor _createNewAndSendEmailInteractor; GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; @@ -198,6 +199,7 @@ class ComposerController extends BaseController with DragDropFileMixin { this._downloadImageAsBase64Interactor, this._transformHtmlEmailContentInteractor, this._getAlwaysReadReceiptSettingInteractor, + this._createNewAndSendEmailInteractor, ); @override @@ -760,7 +762,6 @@ class ComposerController extends BaseController with DragDropFileMixin { bodyValues: { generatePartId: EmailBodyValue(emailBodyText, false, false) }, - headerUserAgent: {IndividualHeaderIdentifier.headerUserAgent : userAgent}, attachments: attachments.isNotEmpty ? attachments : null, headerMdn: hasRequestReadReceipt.value ? { IndividualHeaderIdentifier.headerMdn: getEmailAddressSender() } : {}, ); @@ -899,67 +900,64 @@ class ComposerController extends BaseController with DragDropFileMixin { _handleSendMessages(context); } - bool get _isParamUserNull { + Future _getContentInEditor() async { + final htmlTextEditor = PlatformInfo.isWeb + ? _textEditorWeb + : await htmlEditorApi?.getText(); + if (htmlTextEditor?.isNotEmpty == true) { + return htmlTextEditor!.removeEditorStartTag(); + } else { + return ''; + } + } + + void _handleSendMessages(BuildContext context) async { if (composerArguments.value == null || mailboxDashBoardController.sessionCurrent == null || mailboxDashBoardController.accountId.value == null ) { - logError('ComposerController::isParamUserNotNull: Param is NULL'); - return true; - } - return false; - } - - void _handleSendMessages(BuildContext context) async { - if (_isParamUserNull) { - logError('ComposerController::_handleSendMessages: Param is NULL'); + log('ComposerController::_handleSendMessages: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); _closeComposerAction(); return; } - final sendingArgs = await createSendingEmailArguments(context); - _closeComposerAction(result: sendingArgs); - } + final emailContent = await _getContentInEditor(); - Future createSendingEmailArguments(BuildContext context) async { - final session = mailboxDashBoardController.sessionCurrent!; - final arguments = composerArguments.value!; - final accountId = mailboxDashBoardController.accountId.value!; - - final createdEmail = await _generateEmail( - context, - session.username, - outboxMailboxId: mailboxDashBoardController.outboxMailbox?.id, - arguments: arguments + final resultState = await Get.dialog( + PointerInterceptor( + child: SendingMessageDialogView( + createEmailRequest: CreateEmailRequest( + session: mailboxDashBoardController.sessionCurrent!, + accountId: mailboxDashBoardController.accountId.value!, + emailActionType: composerArguments.value!.emailActionType, + subject: subjectEmail.value ?? '', + emailContent: emailContent, + fromSender: composerArguments.value!.presentationEmail?.from ?? {}, + toRecipients: listToEmailAddress.toSet(), + ccRecipients: listCcEmailAddress.toSet(), + bccRecipients: listBccEmailAddress.toSet(), + isRequestReadReceipt: hasRequestReadReceipt.value, + identity: identitySelected.value, + attachments: uploadController.attachmentsUploaded, + inlineAttachments: uploadController.mapInlineAttachments, + outboxMailboxId: mailboxDashBoardController.outboxMailbox?.mailboxId, + sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], + draftsEmailId: composerArguments.value!.emailActionType == EmailActionType.editDraft + ? composerArguments.value!.presentationEmail?.id + : null, + answerForwardEmailId: composerArguments.value!.presentationEmail?.id, + unsubscribeEmailId: composerArguments.value!.previousEmailId, + messageId: composerArguments.value!.messageId, + references: composerArguments.value!.references, + emailSendingQueue: composerArguments.value!.sendingEmail + ), + createNewAndSendEmailInteractor: _createNewAndSendEmailInteractor + ), + ), + barrierColor: AppColor.colorDefaultCupertinoActionSheet, ); - final emailRequest = arguments.emailActionType == EmailActionType.editSendingEmail - ? arguments.sendingEmail!.toEmailRequest(newEmail: createdEmail) - : EmailRequest( - email: createdEmail, - sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], - identityId: identitySelected.value?.id, - emailIdDestroyed: arguments.emailActionType == EmailActionType.editDraft - ? arguments.presentationEmail?.id - : null, - emailIdAnsweredOrForwarded: arguments.presentationEmail?.id, - emailActionType: arguments.emailActionType, - previousEmailId: arguments.previousEmailId, - ); - - final mailboxRequest = mailboxDashBoardController.outboxMailbox?.id == null - ? CreateNewMailboxRequest( - Id(uuid.v1()), - MailboxName(PresentationMailbox.outboxRole.inCaps) - ) - : null; - - return SendingEmailArguments( - session, - accountId, - emailRequest, - mailboxRequest, - ); + _closeComposerAction(result: resultState); } void _checkContactPermission() async { @@ -1370,7 +1368,6 @@ class ComposerController extends BaseController with DragDropFileMixin { } void _closeComposerAction({dynamic result}) { - log('ComposerController::_closeComposerAction:'); isSendEmailLoading.value = false; if (PlatformInfo.isWeb) { diff --git a/lib/features/composer/presentation/extensions/create_email_request_extension.dart b/lib/features/composer/presentation/extensions/create_email_request_extension.dart new file mode 100644 index 000000000..cc3b18b34 --- /dev/null +++ b/lib/features/composer/presentation/extensions/create_email_request_extension.dart @@ -0,0 +1,176 @@ +import 'package:core/core.dart'; +import 'package:http_parser/http_parser.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/email/email_body_part.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_value.dart'; +import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:model/extensions/email_address_extension.dart'; +import 'package:model/extensions/username_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/identity_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/list_attachments_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/extensions/sending_email_extension.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; + +extension CreateEmailRequestExtension on CreateEmailRequest { + + Set createSenders() { + if (identity?.email?.isNotEmpty == true) { + return { identity!.toEmailAddress() }; + } else { + return { session.username.toEmailAddress() }; + } + } + + String createMdnEmailAddress() { + if (emailActionType == EmailActionType.editDraft && fromSender.isNotEmpty) { + return fromSender.first.emailAddress; + } else { + return session.username.value; + } + } + + Set createReplyToRecipients() { + if (identity?.replyTo?.isNotEmpty == true) { + return identity!.replyTo!.toSet(); + } else { + return { session.username.toEmailAddress() }; + } + } + + Set createAttachments() => attachments?.toEmailBodyPart() ?? {}; + + Map? createKeywords() { + if (draftsMailboxId != null) { + return { + KeyWordIdentifier.emailDraft: true, + KeyWordIdentifier.emailSeen: true, + }; + } else { + return null; + } + } + + Map? createMailboxIds() { + if (draftsMailboxId != null || outboxMailboxId != null) { + return { + if (draftsMailboxId != null) + draftsMailboxId!: true, + if (outboxMailboxId != null) + outboxMailboxId!: true, + }; + } else { + return null; + } + } + + MessageIdsHeaderValue? createInReplyTo() { + if (emailActionType == EmailActionType.reply || + emailActionType == EmailActionType.replyAll + ) { + return messageId; + } + return null; + } + + MessageIdsHeaderValue? createReferences() { + if (emailActionType == EmailActionType.reply || + emailActionType == EmailActionType.replyAll || + emailActionType == EmailActionType.forward + ) { + Set ids = {}; + if (messageId?.ids.isNotEmpty == true) { + ids.addAll(messageId!.ids); + } + if (references?.ids.isNotEmpty == true) { + ids.addAll(references!.ids); + } + if (ids.isNotEmpty) { + return MessageIdsHeaderValue(ids); + } + } + return null; + } + + Email generateEmail({ + required String newEmailContent, + required Set newEmailAttachments, + required String userAgent, + required PartId partId, + }) { + return Email( + mailboxIds: createMailboxIds(), + from: createSenders(), + to: toRecipients, + cc: ccRecipients, + bcc: bccRecipients, + replyTo: createReplyToRecipients(), + inReplyTo: createInReplyTo(), + references: createReferences(), + keywords: createKeywords(), + subject: subject, + htmlBody: { + EmailBodyPart( + partId: partId, + type: MediaType.parse(Constant.textHtmlMimeType) + ) + }, + bodyValues: { + partId: EmailBodyValue( + newEmailContent, + false, + false + ) + }, + headerUserAgent: { + IndividualHeaderIdentifier.headerUserAgent : userAgent + }, + attachments: newEmailAttachments.isNotEmpty + ? newEmailAttachments + : null, + headerMdn: isRequestReadReceipt + ? { IndividualHeaderIdentifier.headerMdn: createMdnEmailAddress() } + : null, + ); + } + + EmailRequest createEmailRequest({required Email emailObject}) { + if (emailActionType == EmailActionType.editSendingEmail) { + return emailSendingQueue!.toEmailRequest(newEmail: emailObject); + } else { + return EmailRequest( + email: emailObject, + sentMailboxId: sentMailboxId, + identityId: identity?.id, + emailIdDestroyed: draftsEmailId, + emailIdAnsweredOrForwarded: answerForwardEmailId, + emailActionType: emailActionType, + previousEmailId: unsubscribeEmailId, + ); + } + } + + CreateNewMailboxRequest? createMailboxRequest() { + if (outboxMailboxId == null) { + return CreateNewMailboxRequest(MailboxName(PresentationMailbox.outboxRole.inCaps)); + } else { + return null; + } + } + + SendingEmailArguments toSendingEmailArguments({required Email emailObject}) { + return SendingEmailArguments( + session, + accountId, + createEmailRequest(emailObject: emailObject), + createMailboxRequest() + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/identity_extension.dart b/lib/features/composer/presentation/extensions/identity_extension.dart new file mode 100644 index 000000000..b5f15ba16 --- /dev/null +++ b/lib/features/composer/presentation/extensions/identity_extension.dart @@ -0,0 +1,6 @@ +import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; + +extension IdentityExtension on Identity { + EmailAddress toEmailAddress() => EmailAddress(name, email); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/model/create_email_request.dart b/lib/features/composer/presentation/model/create_email_request.dart new file mode 100644 index 000000000..1cc956a16 --- /dev/null +++ b/lib/features/composer/presentation/model/create_email_request.dart @@ -0,0 +1,88 @@ + +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.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/email/attachment.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; + +class CreateEmailRequest with EquatableMixin { + + final Session session; + final AccountId accountId; + final EmailActionType emailActionType; + final String subject; + final String emailContent; + final bool isRequestReadReceipt; + final Set fromSender; + final Set toRecipients; + final Set ccRecipients; + final Set bccRecipients; + final Identity? identity; + final List? attachments; + final Map? inlineAttachments; + final MailboxId? outboxMailboxId; + final MailboxId? sentMailboxId; + final MailboxId? draftsMailboxId; + final EmailId? draftsEmailId; + final EmailId? answerForwardEmailId; + final EmailId? unsubscribeEmailId; + final MessageIdsHeaderValue? messageId; + final MessageIdsHeaderValue? references; + final SendingEmail? emailSendingQueue; + + CreateEmailRequest({ + required this.session, + required this.accountId, + required this.emailActionType, + required this.subject, + required this.emailContent, + required this.fromSender, + required this.toRecipients, + required this.ccRecipients, + required this.bccRecipients, + this.isRequestReadReceipt = true, + this.identity, + this.attachments, + this.inlineAttachments, + this.outboxMailboxId, + this.sentMailboxId, + this.draftsMailboxId, + this.draftsEmailId, + this.answerForwardEmailId, + this.unsubscribeEmailId, + this.messageId, + this.references, + this.emailSendingQueue + }); + + @override + List get props => [ + session, + accountId, + emailActionType, + subject, + emailContent, + fromSender, + toRecipients, + ccRecipients, + bccRecipients, + identity, + isRequestReadReceipt, + attachments, + inlineAttachments, + outboxMailboxId, + sentMailboxId, + draftsMailboxId, + draftsEmailId, + answerForwardEmailId, + unsubscribeEmailId, + references, + references, + emailSendingQueue + ]; +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart b/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart new file mode 100644 index 000000000..a1aa4d173 --- /dev/null +++ b/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart @@ -0,0 +1,206 @@ +import 'dart:async'; + +import 'package:core/presentation/extensions/capitalize_extension.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart' as dartz; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +class SendingMessageDialogView extends StatefulWidget { + + final CreateEmailRequest createEmailRequest; + final CreateNewAndSendEmailInteractor createNewAndSendEmailInteractor; + + const SendingMessageDialogView({ + super.key, + required this.createEmailRequest, + required this.createNewAndSendEmailInteractor, + }); + + @override + State createState() => _SendingMessageDialogViewState(); +} + +class _SendingMessageDialogViewState extends State { + + StreamSubscription? _sendingStreamSubscription; + final ValueNotifier?> _sendingNotifier = ValueNotifier(null); + + @override + void initState() { + super.initState(); + _sendingStreamSubscription = widget.createNewAndSendEmailInteractor + .execute(widget.createEmailRequest) + .listen( + _handleDataStream, + onError: _handleErrorStream + ); + } + + void _handleDataStream(dartz.Either newState) { + _sendingNotifier.value = newState; + + newState.fold( + (failure) { + if (failure is SendEmailFailure || failure is GenerateEmailFailure) { + popBack(result: failure); + } + }, + (success) { + if (success is SendEmailSuccess) { + popBack(result: success); + } + } + ); + } + + void _handleErrorStream(Object error, StackTrace stackTrace) { + logError('_SendingMessageDialogViewState::_handleErrorStream: Exception = $error'); + popBack(result: SendEmailFailure(exception: error)); + } + + @override + Widget build(BuildContext context) { + return Dialog( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12))), + insetPadding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 16.0 + ), + alignment: Alignment.center, + child: Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: Colors.white, + ), + width: 400, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + padding: const EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 12), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: AppColor.colorItemSelected, + ), + alignment: Alignment.center, + child: Text( + AppLocalizations.of(context).sendingMessage.capitalizeFirstEach, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 17 + ), + ), + ), + const Divider(), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 12, bottom: 4), + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).status}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + ), + const SizedBox(width: 8), + Expanded( + child: ValueListenableBuilder( + valueListenable: _sendingNotifier, + builder: (context, value, child) { + if (value == null) { + return child!; + } + + return value.fold( + (failure) => child!, + (success) { + if (success is GenerateEmailLoading) { + return Text( + 'Creating email...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ); + } else if (success is SendEmailLoading) { + return Text( + '${AppLocalizations.of(context).sendingMessage}...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ); + } else { + return child!; + } + } + ); + }, + child: Text( + '${AppLocalizations.of(context).sendingMessage}...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ), + ), + ) + ], + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 4, bottom: 24), + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).progress}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + ), + const SizedBox(width: 8), + Expanded( + child: LinearProgressIndicator( + color: Colors.white.withOpacity(0.6), + backgroundColor: AppColor.primaryColor, + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + ) + ], + ), + ) + ], + ) + ], + ), + ), + ); + } + + @override + void dispose() { + _sendingStreamSubscription?.cancel(); + _sendingNotifier.dispose(); + super.dispose(); + } +} diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index 05c236aff..9255b5fe2 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -28,7 +28,7 @@ import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email. abstract class EmailDataSource { Future getEmailContent(Session session, AccountId accountId, EmailId emailId); - Future sendEmail( + Future sendEmail( Session session, AccountId accountId, EmailRequest emailRequest, diff --git a/lib/features/email/data/datasource/html_datasource.dart b/lib/features/email/data/datasource/html_datasource.dart index 36f7d8975..354577462 100644 --- a/lib/features/email/data/datasource/html_datasource.dart +++ b/lib/features/email/data/datasource/html_datasource.dart @@ -1,6 +1,8 @@ - import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; -import 'package:model/model.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:model/email/attachment.dart'; +import 'package:model/email/email_content.dart'; abstract class HtmlDataSource { Future transformEmailContent( @@ -13,4 +15,11 @@ abstract class HtmlDataSource { String htmlContent, TransformConfiguration configuration ); + + Future>> replaceImageBase64ToImageCID({ + required String emailContent, + required Map inlineAttachments + }); + + Future removeCollapsedExpandedSignatureEffect({required String emailContent}); } \ No newline at end of file diff --git a/lib/features/email/data/datasource_impl/email_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_datasource_impl.dart index 3983a16df..36fa298aa 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -42,7 +42,7 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future sendEmail( + Future sendEmail( Session session, AccountId accountId, EmailRequest emailRequest, diff --git a/lib/features/email/data/datasource_impl/html_datasource_impl.dart b/lib/features/email/data/datasource_impl/html_datasource_impl.dart index 7ce83177e..5d695d739 100644 --- a/lib/features/email/data/datasource_impl/html_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/html_datasource_impl.dart @@ -1,4 +1,7 @@ import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:model/email/attachment.dart'; import 'package:model/email/email_content.dart'; import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; @@ -35,4 +38,26 @@ class HtmlDataSourceImpl extends HtmlDataSource { ); }).catchError(_exceptionThrower.throwException); } + + @override + Future>> replaceImageBase64ToImageCID({ + required String emailContent, + required Map inlineAttachments + }) { + return Future.sync(() async { + return await _htmlAnalyzer.replaceImageBase64ToImageCID( + emailContent: emailContent, + inlineAttachments: inlineAttachments + ); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future removeCollapsedExpandedSignatureEffect({required String emailContent}) { + return Future.sync(() async { + return await _htmlAnalyzer.removeCollapsedExpandedSignatureEffect( + emailContent: emailContent + ); + }).catchError(_exceptionThrower.throwException); + } } \ No newline at end of file diff --git a/lib/features/email/data/local/html_analyzer.dart b/lib/features/email/data/local/html_analyzer.dart index ca7550dc0..904c320a0 100644 --- a/lib/features/email/data/local/html_analyzer.dart +++ b/lib/features/email/data/local/html_analyzer.dart @@ -1,10 +1,15 @@ import 'package:collection/collection.dart'; +import 'package:core/data/constants/constant.dart'; import 'package:core/presentation/utils/html_transformer/html_transform.dart'; import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; import 'package:html/parser.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:model/email/attachment.dart'; import 'package:model/email/email_content.dart'; import 'package:model/email/email_content_type.dart'; +import 'package:model/extensions/attachment_extension.dart'; import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; class HtmlAnalyzer { @@ -99,4 +104,57 @@ class HtmlAnalyzer { ); return htmlContentTransformed; } + + Future>> replaceImageBase64ToImageCID({ + required String emailContent, + required Map inlineAttachments + }) async { + final document = parse(emailContent); + final listImgTag = document.querySelectorAll('img[src^="data:image/"][id^="cid:"]'); + + final listInlineAttachment = await Future.wait(listImgTag.map((imgTag) async { + final idImg = imgTag.attributes['id']; + final cid = idImg!.replaceFirst('cid:', '').trim(); + imgTag.attributes['src'] = 'cid:$cid'; + imgTag.attributes.remove('id'); + return cid; + })).then((listCid) { + final listInlineAttachment = listCid + .map((cid) { + if (inlineAttachments.containsKey(cid)) { + return inlineAttachments[cid]!.toEmailBodyPart(charset: Constant.base64Charset); + } else { + return null; + } + }) + .whereNotNull() + .toSet(); + + return listInlineAttachment; + }); + + final newContent = document.body?.innerHtml ?? emailContent; + + return Tuple2(newContent, listInlineAttachment); + } + + Future removeCollapsedExpandedSignatureEffect({required String emailContent}) async { + log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect: BEFORE = $emailContent'); + final document = parse(emailContent); + final signatureElements = document.querySelectorAll('div.tmail-signature'); + await Future.wait(signatureElements.map((signatureTag) async { + final signatureChildren = signatureTag.children; + for (var child in signatureChildren) { + log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect: CHILD = ${child.outerHtml}'); + if (child.attributes['class']?.contains('tmail-signature-button') == true) { + child.remove(); + } else if (child.attributes['class']?.contains('tmail-signature-content') == true) { + signatureTag.innerHtml = child.innerHtml; + } + } + })); + final newContent = document.body?.innerHtml ?? emailContent; + log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect: AFTER = $newContent'); + return newContent; + } } \ No newline at end of file diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index 91f9f1e75..422682ca2 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -54,7 +54,7 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future sendEmail( + Future sendEmail( Session session, AccountId accountId, EmailRequest emailRequest, diff --git a/lib/features/email/domain/exceptions/email_exceptions.dart b/lib/features/email/domain/exceptions/email_exceptions.dart index 805744163..8199c94c2 100644 --- a/lib/features/email/domain/exceptions/email_exceptions.dart +++ b/lib/features/email/domain/exceptions/email_exceptions.dart @@ -6,4 +6,6 @@ class EmptyEmailContentException implements Exception {} class NotFoundEmailRecoveryActionException implements Exception {} -class NotFoundEmailBlobIdException implements Exception {} \ No newline at end of file +class NotFoundEmailBlobIdException implements Exception {} + +class CannotCreateEmailObjectException implements Exception {} \ No newline at end of file diff --git a/lib/features/email/domain/extensions/list_attachments_extension.dart b/lib/features/email/domain/extensions/list_attachments_extension.dart index 4bfa32c97..dfa9fb104 100644 --- a/lib/features/email/domain/extensions/list_attachments_extension.dart +++ b/lib/features/email/domain/extensions/list_attachments_extension.dart @@ -1,7 +1,9 @@ import 'package:collection/collection.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; import 'package:model/email/attachment.dart'; +import 'package:model/extensions/attachment_extension.dart'; import 'package:tmail_ui_user/features/email/domain/extensions/attachment_extension.dart'; import 'package:tmail_ui_user/features/offline_mode/model/attachment_hive_cache.dart'; @@ -19,4 +21,6 @@ extension ListAttachmentsExtension on List { .map((attachment) => attachment.blobId) .whereNotNull() .toSet(); + + Set toEmailBodyPart({String? charset}) => map((attachment) => attachment.toEmailBodyPart(charset: charset)).toSet(); } \ No newline at end of file diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart index acedd1006..a2906e0af 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -30,7 +30,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_r abstract class EmailRepository { Future getEmailContent(Session session, AccountId accountId, EmailId emailId); - Future sendEmail( + Future sendEmail( Session session, AccountId accountId, EmailRequest emailRequest, diff --git a/lib/features/email/presentation/bindings/email_bindings.dart b/lib/features/email/presentation/bindings/email_bindings.dart index 849339bd0..a589ded96 100644 --- a/lib/features/email/presentation/bindings/email_bindings.dart +++ b/lib/features/email/presentation/bindings/email_bindings.dart @@ -96,7 +96,7 @@ class EmailBindings extends BaseBindings { Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find())); + Get.find())); Get.lazyPut(() => StateDataSourceImpl( Get.find(), Get.find(), 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 c2004adb9..0c33fba6e 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -204,7 +204,7 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find())); + Get.find())); Get.lazyPut(() => SearchDataSourceImpl( Get.find(), Get.find())); 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 a3773f68d..784e1d67b 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -30,6 +30,7 @@ import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dar import 'package:tmail_ui_user/features/composer/domain/exceptions/set_method_exception.dart'; import 'package:tmail_ui_user/features/composer/domain/extensions/email_request_extension.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; @@ -1256,11 +1257,14 @@ class MailboxDashBoardController extends ReloadableController { composerArguments = null; ComposerBindings().dispose(); composerOverlayState.value = ComposerOverlayState.inActive; - if (result is SendingEmailArguments) { handleSendEmailAction(result); } else if (result is SaveToDraftArguments) { saveEmailToDraft(arguments: result); + } else if (result is SendEmailSuccess) { + consumeState(Stream.value(Right(result))); + } else if (result is SendEmailFailure || result is GenerateEmailFailure) { + consumeState(Stream.value(Left(result))); } } @@ -1394,6 +1398,10 @@ class MailboxDashBoardController extends ReloadableController { handleSendEmailAction(result); } else if (result is SaveToDraftArguments) { saveEmailToDraft(arguments: result); + } else if (result is SendEmailSuccess) { + consumeState(Stream.value(Right(result))); + } else if (result is SendEmailFailure || result is GenerateEmailFailure) { + consumeState(Stream.value(Left(result))); } } } diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart index efb63fabf..8d3a0096d 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart @@ -132,7 +132,10 @@ class ManageAccountDashBoardView extends GetWidget (AvatarBuilder() - ..text(controller.sessionCurrent?.username.firstCharacter ?? '') + ..text(controller.accountId.value != null + ? controller.sessionCurrent?.username.firstCharacter ?? '' + : '' + ) ..backgroundColor(Colors.white) ..textColor(Colors.black) ..context(context) diff --git a/lib/features/network_connection/presentation/web_network_connection_controller.dart b/lib/features/network_connection/presentation/web_network_connection_controller.dart index 4afdb5be7..c4bbde67e 100644 --- a/lib/features/network_connection/presentation/web_network_connection_controller.dart +++ b/lib/features/network_connection/presentation/web_network_connection_controller.dart @@ -94,4 +94,6 @@ class NetworkConnectionController extends GetxController { ); } } + + Future hasInternetConnection() async => isNetworkConnectionAvailable(); } \ No newline at end of file diff --git a/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart b/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart index 807536be9..1829ad024 100644 --- a/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart +++ b/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart @@ -68,7 +68,7 @@ class SendEmailInteractorBindings extends InteractorsBindings { Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find())); + Get.find())); Get.lazyPut(() => StateDataSourceImpl( Get.find(), Get.find(), diff --git a/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart b/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart index 45f6c3490..9b31a9906 100644 --- a/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart +++ b/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart @@ -105,7 +105,7 @@ class FcmInteractorBindings extends InteractorsBindings { Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find())); + Get.find())); Get.lazyPut(() => StateDataSourceImpl( Get.find(), Get.find(), diff --git a/lib/features/upload/presentation/controller/upload_controller.dart b/lib/features/upload/presentation/controller/upload_controller.dart index f59ef2188..626d7ced0 100644 --- a/lib/features/upload/presentation/controller/upload_controller.dart +++ b/lib/features/upload/presentation/controller/upload_controller.dart @@ -255,8 +255,7 @@ class UploadController extends BaseController { return null; } return attachmentsUploaded - .map((attachment) => attachment.toEmailBodyPart( - disposition: ContentDisposition.attachment.value)) + .map((attachment) => attachment.toEmailBodyPart()) .toSet(); } diff --git a/lib/main/exceptions/send_email_exception_thrower.dart b/lib/main/exceptions/send_email_exception_thrower.dart index d5d908cb4..2e1bf96e3 100644 --- a/lib/main/exceptions/send_email_exception_thrower.dart +++ b/lib/main/exceptions/send_email_exception_thrower.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'package:core/utils/app_logger.dart'; -import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart'; +import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart' + if (dart.library.html) 'package:tmail_ui_user/features/network_connection/presentation/web_network_connection_controller.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; diff --git a/lib/main/localizations/localization_service.dart b/lib/main/localizations/localization_service.dart index eb787db76..f58a3773b 100644 --- a/lib/main/localizations/localization_service.dart +++ b/lib/main/localizations/localization_service.dart @@ -38,17 +38,22 @@ class LocalizationService extends Translations { } static Locale getLocaleFromLanguage({String? langCode}) { - final languageCacheManager = getBinding(); - log('LocalizationService::_getLocaleFromLanguage:languageCacheManager: $languageCacheManager'); - final localeStored = languageCacheManager?.getStoredLanguage(); - log('LocalizationService::_getLocaleFromLanguage():localeStored: $localeStored'); - if (localeStored != null) { - return localeStored; - } else { - final languageCodeCurrent = langCode ?? Get.deviceLocale?.languageCode; - log('LocalizationService::_getLocaleFromLanguage():languageCodeCurrent: $languageCodeCurrent'); - final localeSelected = supportedLocales.firstWhereOrNull((locale) => locale.languageCode == languageCodeCurrent); - return localeSelected ?? Get.deviceLocale ?? defaultLocale; + try { + final languageCacheManager = getBinding(); + log('LocalizationService::_getLocaleFromLanguage:languageCacheManager: $languageCacheManager'); + final localeStored = languageCacheManager?.getStoredLanguage(); + log('LocalizationService::_getLocaleFromLanguage():localeStored: $localeStored'); + if (localeStored != null) { + return localeStored; + } else { + final languageCodeCurrent = langCode ?? Get.deviceLocale?.languageCode; + log('LocalizationService::_getLocaleFromLanguage():languageCodeCurrent: $languageCodeCurrent'); + final localeSelected = supportedLocales.firstWhereOrNull((locale) => locale.languageCode == languageCodeCurrent); + return localeSelected ?? Get.deviceLocale ?? defaultLocale; + } + } catch (e) { + logError('LocalizationService::getLocaleFromLanguage: Exception: $e'); + return Get.deviceLocale ?? defaultLocale; } } diff --git a/model/lib/email/attachment.dart b/model/lib/email/attachment.dart index 9819bdf81..4347b829f 100644 --- a/model/lib/email/attachment.dart +++ b/model/lib/email/attachment.dart @@ -68,19 +68,6 @@ enum ContentDisposition { other } -extension ContentDispositionExtension on ContentDisposition { - String get value { - switch(this) { - case ContentDisposition.inline: - return 'inline'; - case ContentDisposition.attachment: - return 'attachment'; - case ContentDisposition.other: - return toString(); - } - } -} - extension DispositionStringExtension on String? { ContentDisposition? toContentDisposition() { if (this != null) { diff --git a/model/lib/extensions/attachment_extension.dart b/model/lib/extensions/attachment_extension.dart index dcc25b7b4..8b5d63162 100644 --- a/model/lib/extensions/attachment_extension.dart +++ b/model/lib/extensions/attachment_extension.dart @@ -1,9 +1,10 @@ import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; -import 'package:model/model.dart'; +import 'package:model/email/attachment.dart'; +import 'package:model/extensions/list_attachment_extension.dart'; extension AttachmentExtension on Attachment { - EmailBodyPart toEmailBodyPart({String? disposition, String? charset}) => EmailBodyPart( + EmailBodyPart toEmailBodyPart({String? charset}) => EmailBodyPart( partId: partId, blobId: blobId, size: size, @@ -11,7 +12,7 @@ extension AttachmentExtension on Attachment { type: type, cid: cid, charset: charset, - disposition: disposition ?? this.disposition?.value); + disposition: disposition?.name ?? ContentDisposition.attachment.name); Attachment toAttachmentWithDisposition({ ContentDisposition? disposition, diff --git a/model/lib/extensions/username_extension.dart b/model/lib/extensions/username_extension.dart index 66005a6b4..ffcd436ba 100644 --- a/model/lib/extensions/username_extension.dart +++ b/model/lib/extensions/username_extension.dart @@ -1,7 +1,8 @@ - -import 'package:core/presentation/extensions/string_extension.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; extension UsernameExtension on UserName { - String get firstCharacter => value.firstLetterToUpperCase.toUpperCase(); + String get firstCharacter => value.isNotEmpty ? value[0].toUpperCase() : ''; + + EmailAddress toEmailAddress() => EmailAddress(null, value); } \ No newline at end of file