From 8eed49fd731355dbc8d4c9c6ee3e03cbe1244fcd Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 18 Aug 2023 19:42:04 +0700 Subject: [PATCH] TF-1961 Transform html email content in composer (cherry picked from commit f5470c130744242195b265a0ea4aad044ad8e740) --- .../base/text_transformer.dart | 4 +- .../dom/image_transformers.dart | 21 +- .../dom/remove_tooltip_link_transformers.dart | 35 ++ .../replace_lazy_load_image_transformer.dart | 32 ++ .../html_transformer/html_transform.dart | 17 +- .../message_content_transformer.dart | 27 +- .../sanitize_autolink_html_transformers.dart | 6 +- .../transform_configuration.dart | 53 +- .../reloadable/reloadable_controller.dart | 16 +- .../base/widget/cupertino_loading_widget.dart | 34 +- .../presentation/base_composer_view.dart | 28 +- .../presentation/composer_bindings.dart | 3 + .../presentation/composer_controller.dart | 540 ++++++++++-------- .../composer/presentation/composer_view.dart | 96 +++- .../presentation/composer_view_web.dart | 94 ++- .../email_action_type_extension.dart | 85 ++- .../presentation/styles/composer_style.dart | 2 +- .../data/datasource/html_datasource.dart | 10 +- .../datasource_impl/html_datasource_impl.dart | 16 +- .../email/data/local/html_analyzer.dart | 51 +- .../repository/email_repository_impl.dart | 31 +- .../domain/exceptions/email_exceptions.dart | 4 +- .../domain/repository/email_repository.dart | 14 +- .../domain/state/get_email_content_state.dart | 47 +- .../transform_html_email_content_state.dart | 18 + .../get_email_content_interactor.dart | 65 +-- ...t_list_detailed_email_by_id_interator.dart | 24 +- ...ansform_html_email_content_interactor.dart | 26 + .../controller/single_email_controller.dart | 115 ++-- .../model/composer_arguments.dart | 79 +++ .../presentation/model/email_loaded.dart | 9 +- .../bottom_bar_mail_widget_builder.dart | 13 +- ...ssion_storage_composer_datasoure_impl.dart | 16 +- .../mailbox_dashboard_controller.dart | 74 +-- .../identity_data_source_impl.dart | 2 +- .../presentation/search_email_controller.dart | 2 +- .../sending_queue_controller.dart | 13 +- .../mixin/email_action_controller.dart | 9 +- .../presentation/thread_controller.dart | 2 +- .../bindings/network/network_bindings.dart | 4 +- model/lib/email/email_action_type.dart | 6 +- model/lib/extensions/email_extension.dart | 9 + .../extensions/list_attachment_extension.dart | 15 +- .../list_email_address_extension.dart | 4 +- .../presentation_email_extension.dart | 12 +- 45 files changed, 1139 insertions(+), 644 deletions(-) create mode 100644 core/lib/presentation/utils/html_transformer/dom/remove_tooltip_link_transformers.dart create mode 100644 core/lib/presentation/utils/html_transformer/dom/replace_lazy_load_image_transformer.dart create mode 100644 lib/features/email/domain/state/transform_html_email_content_state.dart create mode 100644 lib/features/email/domain/usecases/transform_html_email_content_interactor.dart diff --git a/core/lib/presentation/utils/html_transformer/base/text_transformer.dart b/core/lib/presentation/utils/html_transformer/base/text_transformer.dart index af064550d..e7100b8b1 100644 --- a/core/lib/presentation/utils/html_transformer/base/text_transformer.dart +++ b/core/lib/presentation/utils/html_transformer/base/text_transformer.dart @@ -1,7 +1,9 @@ +import 'dart:convert'; + /// Transforms plain text messages. abstract class TextTransformer { const TextTransformer(); - String process(String text); + String process(String text, HtmlEscape htmlEscape); } \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/dom/image_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/image_transformers.dart index e5e1874be..6d2a7aeef 100644 --- a/core/lib/presentation/utils/html_transformer/dom/image_transformers.dart +++ b/core/lib/presentation/utils/html_transformer/dom/image_transformers.dart @@ -12,7 +12,9 @@ import 'package:html/dom.dart'; class ImageTransformer extends DomTransformer { - const ImageTransformer(); + final bool useLoadingAttribute; + + const ImageTransformer({this.useLoadingAttribute = false}); @override Future process({ @@ -40,14 +42,19 @@ class ImageTransformer extends DomTransformer { ); imageElement.attributes['src'] = imageBase64 ?? src; } else if (src.startsWith('https://') || src.startsWith('http://')) { - final classAttribute = imageElement.attributes['class']; - if (classAttribute != null) { - imageElement.attributes['class'] = '$classAttribute lazy-loading'; + if (useLoadingAttribute) { + imageElement.attributes['loading'] = 'lazy'; } else { - imageElement.attributes['class'] = 'lazy-loading'; + final classAttribute = imageElement.attributes['class']; + if (classAttribute != null) { + imageElement.attributes['class'] = '$classAttribute lazy-loading'; + } else { + imageElement.attributes['class'] = 'lazy-loading'; + } + imageElement.attributes['data-src'] = src; + imageElement.attributes.remove('src'); + imageElement.attributes.remove('loading'); } - imageElement.attributes['data-src'] = src; - imageElement.attributes.remove('src'); } })); } diff --git a/core/lib/presentation/utils/html_transformer/dom/remove_tooltip_link_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/remove_tooltip_link_transformers.dart new file mode 100644 index 000000000..54a7b0708 --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/dom/remove_tooltip_link_transformers.dart @@ -0,0 +1,35 @@ + +import 'package:core/data/network/dio_client.dart'; +import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart'; +import 'package:core/presentation/utils/html_transformer/html_template.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:html/dom.dart'; + +class RemoveTooltipLinkTransformer extends DomTransformer { + + const RemoveTooltipLinkTransformer(); + + @override + Future process({ + required Document document, + Map? mapUrlDownloadCID, + DioClient? dioClient + }) async { + final linkElements = document.querySelectorAll('a.$nameClassToolTip'); + await Future.wait(linkElements.map((linkElement) async { + final classAttribute = linkElement.attributes['class']; + if (classAttribute != null) { + final newClassAttribute = classAttribute.replaceFirst(nameClassToolTip, ''); + linkElement.attributes['class'] = newClassAttribute; + } + final listSpanTag = linkElement.querySelectorAll('span.tooltiptext'); + log('RemoveTooltipLinkTransformer::process:listSpanTag: ${listSpanTag.length}'); + if (listSpanTag.isNotEmpty) { + for (var element in listSpanTag) { + element.remove(); + } + } + })); + } + +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/dom/replace_lazy_load_image_transformer.dart b/core/lib/presentation/utils/html_transformer/dom/replace_lazy_load_image_transformer.dart new file mode 100644 index 000000000..32f8d4c74 --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/dom/replace_lazy_load_image_transformer.dart @@ -0,0 +1,32 @@ + + +import 'package:core/data/network/dio_client.dart'; +import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart'; +import 'package:html/dom.dart'; + +class ReplaceLazyLoadImageTransformer extends DomTransformer { + + const ReplaceLazyLoadImageTransformer(); + + @override + Future process({ + required Document document, + Map? mapUrlDownloadCID, + DioClient? dioClient + }) async { + final imageElements = document.querySelectorAll('img.lazy-loading'); + await Future.wait(imageElements.map((imageElement) async { + final classAttribute = imageElement.attributes['class']; + if (classAttribute != null) { + final newClassAttribute = classAttribute.replaceFirst('lazy-loading', ''); + imageElement.attributes['class'] = newClassAttribute; + } + final dataSrc = imageElement.attributes['data-src']; + if (dataSrc != null) { + imageElement.attributes['src'] = dataSrc; + imageElement.attributes.remove('data-src'); + } + imageElement.attributes['loading'] = 'lazy'; + })); + } +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/html_transform.dart b/core/lib/presentation/utils/html_transformer/html_transform.dart index b0ed967b5..1d523af59 100644 --- a/core/lib/presentation/utils/html_transformer/html_transform.dart +++ b/core/lib/presentation/utils/html_transformer/html_transform.dart @@ -1,23 +1,26 @@ +import 'dart:convert'; + import 'package:core/core.dart'; import 'package:core/presentation/utils/html_transformer/message_content_transformer.dart'; class HtmlTransform { final DioClient _dioClient; + final HtmlEscape _htmlEscape; - HtmlTransform(this._dioClient); + HtmlTransform(this._dioClient, this._htmlEscape); /// Transforms this message to HTML code. Future transformToHtml({ - required String contentHtml, - Map? mapUrlDownloadCID, + required String htmlContent, + Map? mapCidImageDownloadUrl, TransformConfiguration? transformConfiguration, }) async { transformConfiguration ??= TransformConfiguration.create(); - final transformer = MessageContentTransformer(transformConfiguration, _dioClient); + final transformer = MessageContentTransformer(transformConfiguration, _dioClient, _htmlEscape); final document = await transformer.toDocument( - message: contentHtml, - mapUrlDownloadCID: mapUrlDownloadCID + message: htmlContent, + mapUrlDownloadCID: mapCidImageDownloadUrl ); return document.outerHtml; } @@ -28,7 +31,7 @@ class HtmlTransform { TransformConfiguration? transformConfiguration }) { transformConfiguration ??= TransformConfiguration.create(); - final transformer = MessageContentTransformer(transformConfiguration, _dioClient); + final transformer = MessageContentTransformer(transformConfiguration, _dioClient, _htmlEscape); final message = transformer.toMessage(content); return message; } diff --git a/core/lib/presentation/utils/html_transformer/message_content_transformer.dart b/core/lib/presentation/utils/html_transformer/message_content_transformer.dart index ac31e27f8..f4e5a1624 100644 --- a/core/lib/presentation/utils/html_transformer/message_content_transformer.dart +++ b/core/lib/presentation/utils/html_transformer/message_content_transformer.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:core/data/network/dio_client.dart'; import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:html/dom.dart'; @@ -5,23 +7,28 @@ import 'package:html/parser.dart' show parse; /// Transforms messages class MessageContentTransformer { - /// The configuration used for the transformation - final TransformConfiguration configuration; - final DioClient dioClient; + /// The _configuration used for the transformation + final TransformConfiguration _configuration; + final DioClient _dioClient; + final HtmlEscape _htmlEscape; - MessageContentTransformer(this.configuration, this.dioClient); + MessageContentTransformer( + this._configuration, + this._dioClient, + this._htmlEscape + ); Future _transformDocument({ required Document document, Map? mapUrlDownloadCID }) async { await Future.wait([ - if (configuration.domTransformers.isNotEmpty) - ...configuration.domTransformers.map((domTransformer) async => + if (_configuration.domTransformers.isNotEmpty) + ..._configuration.domTransformers.map((domTransformer) async => domTransformer.process( document: document, mapUrlDownloadCID: mapUrlDownloadCID, - dioClient: dioClient + dioClient: _dioClient ) ) ]); @@ -40,9 +47,9 @@ class MessageContentTransformer { } String _transformMessage(String message) { - if (configuration.textTransformers.isNotEmpty) { - for (var transformer in configuration.textTransformers) { - message = transformer.process(message); + if (_configuration.textTransformers.isNotEmpty) { + for (var transformer in _configuration.textTransformers) { + message = transformer.process(message, _htmlEscape); } } return message; diff --git a/core/lib/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart b/core/lib/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart index e1bcbb30c..0bab1f355 100644 --- a/core/lib/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart +++ b/core/lib/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart @@ -6,10 +6,8 @@ import 'package:core/presentation/utils/html_transformer/base/text_transformer.d class SanitizeAutolinkHtmlTransformers extends TextTransformer { - final HtmlEscape htmlEscape; - - SanitizeAutolinkHtmlTransformers(this.htmlEscape); + const SanitizeAutolinkHtmlTransformers(); @override - String process(String text) => SanitizeAutolinkFilter(htmlEscape).process(text); + String process(String text, HtmlEscape htmlEscape) => SanitizeAutolinkFilter(htmlEscape).process(text); } \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/transform_configuration.dart b/core/lib/presentation/utils/html_transformer/transform_configuration.dart index e9c15b7ef..af4af62ad 100644 --- a/core/lib/presentation/utils/html_transformer/transform_configuration.dart +++ b/core/lib/presentation/utils/html_transformer/transform_configuration.dart @@ -1,12 +1,16 @@ +import 'package:core/core.dart'; import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart'; import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart'; import 'package:core/presentation/utils/html_transformer/dom/add_target_blank_in_tag_a_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/blockcode_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/blockquoted_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/image_transformers.dart'; +import 'package:core/presentation/utils/html_transformer/dom/remove_tooltip_link_transformers.dart'; +import 'package:core/presentation/utils/html_transformer/dom/replace_lazy_load_image_transformer.dart'; import 'package:core/presentation/utils/html_transformer/dom/script_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/sigature_transformers.dart'; +import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart'; /// Contains the configuration for all transformations. class TransformConfiguration { @@ -25,6 +29,45 @@ class TransformConfiguration { this.textTransformers ); + factory TransformConfiguration.forReplyForwardEmail() => TransformConfiguration.create( + customDomTransformers: [ + const ReplaceLazyLoadImageTransformer(), + if (PlatformInfo.isWeb) + const RemoveTooltipLinkTransformer(), + ] + ); + + factory TransformConfiguration.forDraftsEmail() => TransformConfiguration.create( + customDomTransformers: [ + const RemoveScriptTransformer(), + const BlockQuotedTransformer(), + const BlockCodeTransformer(), + const AddTargetBlankInTagATransformer(), + const ImageTransformer(useLoadingAttribute: true), + ] + ); + + factory TransformConfiguration.forComposeEmailPlatformWeb() => TransformConfiguration.create( + customDomTransformers: [ + const RemoveScriptTransformer(), + const BlockQuotedTransformer(), + const BlockCodeTransformer(), + const AddTargetBlankInTagATransformer(), + const ImageTransformer(useLoadingAttribute: true) + ] + ); + + factory TransformConfiguration.forPreviewEmailPlatformWeb() => TransformConfiguration.create( + customDomTransformers: [ + const RemoveScriptTransformer(), + const BlockQuotedTransformer(), + const BlockCodeTransformer(), + const AddTargetBlankInTagATransformer(), + const ImageTransformer(), + const AddTooltipLinkTransformer(), + ] + ); + /// Provides easy access to a standard configuration that does not block external images. static const TransformConfiguration standardConfiguration = TransformConfiguration( standardDomTransformers, @@ -61,13 +104,7 @@ class TransformConfiguration { ImageTransformer(), ]; - static const List domTransformersForDraftEmail = [ - RemoveScriptTransformer(), - BlockQuotedTransformer(), - BlockCodeTransformer(), - AddTargetBlankInTagATransformer(), - ImageTransformer(), + static const List standardTextTransformers = [ + SanitizeAutolinkHtmlTransformers() ]; - - static const List standardTextTransformers = []; } \ No newline at end of file diff --git a/lib/features/base/reloadable/reloadable_controller.dart b/lib/features/base/reloadable/reloadable_controller.dart index e72d65fd7..c5d962a69 100644 --- a/lib/features/base/reloadable/reloadable_controller.dart +++ b/lib/features/base/reloadable/reloadable_controller.dart @@ -24,7 +24,7 @@ import 'package:tmail_ui_user/features/session/domain/usecases/get_session_inter import 'package:tmail_ui_user/main/error/capability_validator.dart'; abstract class ReloadableController extends BaseController { - final DynamicUrlInterceptors _dynamicUrlInterceptors = Get.find(); + final DynamicUrlInterceptors dynamicUrlInterceptors = Get.find(); final GetSessionInteractor _getSessionInteractor = Get.find(); final GetAuthenticatedAccountInteractor _getAuthenticatedAccountInteractor; final UpdateAuthenticationAccountInteractor _updateAuthenticationAccountInteractor; @@ -72,8 +72,8 @@ abstract class ReloadableController extends BaseController { } void _setUpInterceptors(GetCredentialViewState credentialViewState) { - _dynamicUrlInterceptors.setJmapUrl(credentialViewState.baseUrl.origin); - _dynamicUrlInterceptors.changeBaseUrl(credentialViewState.baseUrl.origin); + dynamicUrlInterceptors.setJmapUrl(credentialViewState.baseUrl.origin); + dynamicUrlInterceptors.changeBaseUrl(credentialViewState.baseUrl.origin); authorizationInterceptors.setBasicAuthorization( credentialViewState.userName.value, credentialViewState.password.value, @@ -100,10 +100,10 @@ abstract class ReloadableController extends BaseController { void _handleGetSessionSuccess(GetSessionSuccess success) { final session = success.session; final personalAccount = session.personalAccount; - final apiUrl = session.getQualifiedApiUrl(baseUrl: _dynamicUrlInterceptors.jmapUrl); + final apiUrl = session.getQualifiedApiUrl(baseUrl: dynamicUrlInterceptors.jmapUrl); log('ReloadableController::_handleGetSessionSuccess():apiUrl: $apiUrl'); if (apiUrl.isNotEmpty) { - _dynamicUrlInterceptors.changeBaseUrl(apiUrl); + dynamicUrlInterceptors.changeBaseUrl(apiUrl); updateAuthenticationAccount(session, personalAccount.accountId, session.username); handleReloaded(session); } else { @@ -120,8 +120,8 @@ abstract class ReloadableController extends BaseController { } void _setUpInterceptorsOidc(GetStoredTokenOidcSuccess tokenOidcSuccess) { - _dynamicUrlInterceptors.setJmapUrl(tokenOidcSuccess.baseUrl.toString()); - _dynamicUrlInterceptors.changeBaseUrl(tokenOidcSuccess.baseUrl.toString()); + dynamicUrlInterceptors.setJmapUrl(tokenOidcSuccess.baseUrl.toString()); + dynamicUrlInterceptors.changeBaseUrl(tokenOidcSuccess.baseUrl.toString()); authorizationInterceptors.setTokenAndAuthorityOidc( newToken: tokenOidcSuccess.tokenOidc.toToken(), newConfig: tokenOidcSuccess.oidcConfiguration); @@ -140,7 +140,7 @@ abstract class ReloadableController extends BaseController { } void updateAuthenticationAccount(Session session, AccountId accountId, UserName userName) { - final apiUrl = session.getQualifiedApiUrl(baseUrl: _dynamicUrlInterceptors.jmapUrl); + final apiUrl = session.getQualifiedApiUrl(baseUrl: dynamicUrlInterceptors.jmapUrl); log('ReloadableController::updateAuthenticationAccount():apiUrl: $apiUrl'); if (apiUrl.isNotEmpty) { consumeState(_updateAuthenticationAccountInteractor.execute(accountId, apiUrl, userName)); diff --git a/lib/features/base/widget/cupertino_loading_widget.dart b/lib/features/base/widget/cupertino_loading_widget.dart index 69db2e76d..52a4c4a2b 100644 --- a/lib/features/base/widget/cupertino_loading_widget.dart +++ b/lib/features/base/widget/cupertino_loading_widget.dart @@ -4,17 +4,35 @@ import 'package:tmail_ui_user/features/base/styles/cupertino_loading_widget_styl class CupertinoLoadingWidget extends StatelessWidget { final double? size; + final EdgeInsetsGeometry? padding; - const CupertinoLoadingWidget({super.key, this.size}); + const CupertinoLoadingWidget({super.key, this.size, this.padding}); @override Widget build(BuildContext context) { - return Center( - child: SizedBox( - width: size ?? CupertinoLoadingWidgetStyles.size, - height: size ?? CupertinoLoadingWidgetStyles.size, - child: const CupertinoActivityIndicator(color: CupertinoLoadingWidgetStyles.progressColor) - ) - ); + if (padding != null) { + return Padding( + padding: padding!, + child: Center( + child: SizedBox( + width: size ?? CupertinoLoadingWidgetStyles.size, + height: size ?? CupertinoLoadingWidgetStyles.size, + child: const CupertinoActivityIndicator( + color: CupertinoLoadingWidgetStyles.progressColor + ) + ) + ), + ); + } else { + return Center( + child: SizedBox( + width: size ?? CupertinoLoadingWidgetStyles.size, + height: size ?? CupertinoLoadingWidgetStyles.size, + child: const CupertinoActivityIndicator( + color: CupertinoLoadingWidgetStyles.progressColor + ) + ) + ); + } } } \ No newline at end of file diff --git a/lib/features/composer/presentation/base_composer_view.dart b/lib/features/composer/presentation/base_composer_view.dart index 3648241fe..25d25148a 100644 --- a/lib/features/composer/presentation/base_composer_view.dart +++ b/lib/features/composer/presentation/base_composer_view.dart @@ -5,6 +5,7 @@ import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/presentation/views/list/sliver_grid_delegate_fixed_height.dart'; import 'package:core/presentation/views/text/text_field_builder.dart'; import 'package:core/presentation/views/text/text_overflow_builder.dart'; @@ -321,22 +322,17 @@ abstract class BaseComposerView extends GetWidget ) ), const Spacer(), - Material( - type: MaterialType.circle, - color: Colors.transparent, - child: TextButton( - onPressed: controller.toggleDisplayAttachments, - child: Text( - expandModeAttachment == ExpandMode.EXPAND - ? AppLocalizations.of(context).hide - : '${AppLocalizations.of(context).showAll} (${uploadFilesState.length})', - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 12, - color: AppColor.colorTextButton - ) - ) - ) + TMailButtonWidget.fromText( + text: expandModeAttachment == ExpandMode.EXPAND + ? AppLocalizations.of(context).hide + : '${AppLocalizations.of(context).showAll} (${uploadFilesState.length})', + onTapActionCallback: controller.toggleDisplayAttachments, + textStyle: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 12, + color: AppColor.colorTextButton + ), + backgroundColor: Colors.transparent, ) ], ); diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index 0210639ac..254b3b229 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -29,6 +29,7 @@ import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/email/data/repository/email_repository_impl.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.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/mailbox/data/datasource/mailbox_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart'; @@ -163,6 +164,7 @@ class ComposerBindings extends BaseBindings { Get.lazyPut(() => RemoveComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => SaveComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => DownloadImageAsBase64Interactor(Get.find())); + Get.lazyPut(() => TransformHtmlEmailContentInteractor(Get.find())); IdentityInteractorsBindings().dependencies(); } @@ -184,6 +186,7 @@ class ComposerBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); } diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index d0345c0f1..2a4816f33 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -48,8 +48,11 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/email_ac import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.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'; @@ -60,6 +63,7 @@ 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'; 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_action_type.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; @@ -95,7 +99,7 @@ class ComposerController extends BaseController { final bccAddressExpandMode = ExpandMode.EXPAND.obs; final identitySelected = Rxn(); final listIdentities = [].obs; - final emailContentsViewState = Rx>(Right(UIState.idle)); + final emailContentsViewState = Rxn>(); final hasRequestReadReceipt = false.obs; final LocalFilePickerInteractor _localFilePickerInteractor; @@ -109,6 +113,7 @@ class ComposerController extends BaseController { final SaveComposerCacheOnWebInteractor _saveComposerCacheOnWebInteractor; final RichTextWebController richTextWebController; final DownloadImageAsBase64Interactor _downloadImageAsBase64Interactor; + final TransformHtmlEmailContentInteractor _transformHtmlEmailContentInteractor; GetAutoCompleteWithDeviceContactInteractor? _getAutoCompleteWithDeviceContactInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; @@ -145,7 +150,6 @@ class ComposerController extends BaseController { List initialAttachments = []; String? _textEditorWeb; String? _initTextEditor; - String? _emailContents; double? maxWithEditor; late Worker uploadInlineImageWorker; @@ -201,6 +205,7 @@ class ComposerController extends BaseController { this._saveComposerCacheOnWebInteractor, this.richTextWebController, this._downloadImageAsBase64Interactor, + this._transformHtmlEmailContentInteractor, ); @override @@ -264,7 +269,9 @@ class ComposerController extends BaseController { @override void handleSuccessViewState(Success success) { super.handleSuccessViewState(success); - if (success is GetEmailContentLoading) { + if (success is GetEmailContentLoading || + success is TransformHtmlEmailContentLoading || + success is TransformHtmlEmailContentSuccess) { emailContentsViewState.value = Right(success); } else if (success is LocalFilePickerSuccess) { _pickFileSuccess(success); @@ -299,7 +306,8 @@ class ComposerController extends BaseController { super.handleFailureViewState(failure); if (failure is LocalFilePickerFailure || failure is LocalFilePickerCancel) { _pickFileFailure(failure); - } else if (failure is GetEmailContentFailure) { + } else if (failure is GetEmailContentFailure || + failure is TransformHtmlEmailContentFailure) { emailContentsViewState.value = Left(failure); } } @@ -387,39 +395,105 @@ class ComposerController extends BaseController { : Get.arguments; if (arguments is ComposerArguments) { composerArguments.value = arguments; + injectAutoCompleteBindings( - mailboxDashBoardController.sessionCurrent, - mailboxDashBoardController.accountId.value); + mailboxDashBoardController.sessionCurrent, + mailboxDashBoardController.accountId.value + ); - if (arguments.emailActionType == EmailActionType.edit) { - _getEmailContentAction(arguments); + switch(arguments.emailActionType) { + case EmailActionType.editDraft: + _initEmailAddress( + presentationEmail: arguments.presentationEmail!, + actionType: EmailActionType.editDraft + ); + _initSubjectEmail( + presentationEmail: arguments.presentationEmail!, + actionType: EmailActionType.editDraft + ); + _getEmailContentFromEmailId( + emailId: arguments.presentationEmail!.id!, + isDraftEmail: arguments.presentationEmail!.isDraft + ); + break; + case EmailActionType.editSendingEmail: + _initEmailAddress( + presentationEmail: arguments.sendingEmail!.presentationEmail, + actionType: EmailActionType.editSendingEmail + ); + _initSubjectEmail( + presentationEmail: arguments.sendingEmail!.presentationEmail, + actionType: EmailActionType.editSendingEmail + ); + _getEmailContentFromSendingEmail(arguments.sendingEmail!); + break; + case EmailActionType.composeFromContentShared: + _getEmailContentFromContentShared(arguments.emailContents!); + break; + case EmailActionType.composeFromFileShared: + _addAttachmentFromFileShare(arguments.listSharedMediaFile!); + break; + case EmailActionType.composeFromEmailAddress: + listToEmailAddress.add(arguments.emailAddress!); + isInitialRecipient.value = true; + toAddressExpandMode.value = ExpandMode.COLLAPSE; + _updateStatusEmailSendButton(); + break; + case EmailActionType.reply: + case EmailActionType.replyAll: + _initEmailAddress( + presentationEmail: arguments.presentationEmail!, + actionType: arguments.emailActionType, + mailboxRole: arguments.presentationEmail!.mailboxContain?.role ?? mailboxDashBoardController.selectedMailbox.value?.role + ); + _initSubjectEmail( + presentationEmail: arguments.presentationEmail!, + actionType: arguments.emailActionType + ); + _transformHtmlEmailContent(arguments.emailContents); + break; + case EmailActionType.forward: + _initSubjectEmail( + presentationEmail: arguments.presentationEmail!, + actionType: arguments.emailActionType + ); + _initAttachments(arguments.attachments ?? []); + _transformHtmlEmailContent(arguments.emailContents); + break; + case EmailActionType.reopenComposerBrowser: + _initEmailAddress( + presentationEmail: arguments.presentationEmail!, + actionType: EmailActionType.reopenComposerBrowser + ); + _initSubjectEmail( + presentationEmail: arguments.presentationEmail!, + actionType: EmailActionType.reopenComposerBrowser + ); + _initAttachments(arguments.attachments ?? []); + _getEmailContentFromSessionStorageBrowser(arguments.emailContents!); + break; + default: + break; } - - _initEmailAddress(arguments); - _initSubjectEmail(arguments); - _initAttachments(arguments); } _autoFocusFieldWhenLauncher(); } - void _initSubjectEmail(ComposerArguments arguments) { - if (currentContext != null) { - final subjectEmail = arguments.presentationEmail?.getEmailTitle().trim() ?? ''; - final newSubject = arguments.emailActionType.getSubjectComposer(currentContext!, subjectEmail); - setSubjectEmail(newSubject); - subjectEmailInputController.text = newSubject; - } + void _initSubjectEmail({ + required PresentationEmail presentationEmail, + required EmailActionType actionType + }) { + final subjectEmail = presentationEmail.getEmailTitle().trim(); + final newSubject = actionType.getSubjectComposer(currentContext, subjectEmail); + setSubjectEmail(newSubject); + subjectEmailInputController.text = newSubject; } - void _initAttachments(ComposerArguments arguments) { - if (arguments.attachments?.isNotEmpty == true) { - initialAttachments = arguments.attachments!; - uploadController.initializeUploadAttachments( - arguments.attachments!.listAttachmentsDisplayedOutSide); - } - if (PlatformInfo.isWeb) { - expandModeAttachments.value = ExpandMode.EXPAND; + void _initAttachments(List attachments) { + if (attachments.isNotEmpty) { + initialAttachments = attachments; + uploadController.initializeUploadAttachments(attachments.listAttachmentsDisplayedOutSide); } } @@ -438,7 +512,6 @@ class ComposerController extends BaseController { .toList(); if (listIdentities.isNotEmpty) { - _initTextEditor = null; await selectIdentity(listIdentities.first); } } @@ -446,113 +519,49 @@ class ComposerController extends BaseController { _autoFocusFieldWhenLauncher(); } - String? _getHeaderEmailQuoted(BuildContext context, ComposerArguments arguments) { - final presentationEmail = arguments.presentationEmail; - if (presentationEmail != null) { - final locale = Localizations.localeOf(context).toLanguageTag(); - log('ComposerController::_getHeaderEmailQuoted(): emailActionType: ${arguments.emailActionType}'); - switch(arguments.emailActionType) { - case EmailActionType.reply: - case EmailActionType.replyAll: - final receivedAt = presentationEmail.receivedAt; - final emailAddress = presentationEmail.from.listEmailAddressToString(isFullEmailAddress: true); - return AppLocalizations.of(context).header_email_quoted( - receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: locale), - emailAddress); - case EmailActionType.forward: - var headerQuoted = '------- ${AppLocalizations.of(context).forwarded_message} -------'.addNewLineTag(); - - final subject = presentationEmail.subject ?? ''; - final receivedAt = presentationEmail.receivedAt; - final fromEmailAddress = presentationEmail.from.listEmailAddressToString(isFullEmailAddress: true); - final toEmailAddress = presentationEmail.to.listEmailAddressToString(isFullEmailAddress: true); - final ccEmailAddress = presentationEmail.cc.listEmailAddressToString(isFullEmailAddress: true); - final bccEmailAddress = presentationEmail.bcc.listEmailAddressToString(isFullEmailAddress: true); - - if (subject.isNotEmpty) { - headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).subject_email}: ') - .append(subject) - .addNewLineTag(); - } - if (receivedAt != null) { - headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).date}: ') - .append(receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: locale)) - .addNewLineTag(); - } - if (fromEmailAddress.isNotEmpty) { - headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).from_email_address_prefix}: ') - .append(fromEmailAddress) - .addNewLineTag(); - } - if (toEmailAddress.isNotEmpty) { - headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).to_email_address_prefix}: ') - .append(toEmailAddress) - .addNewLineTag(); - } - if (ccEmailAddress.isNotEmpty) { - headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).cc_email_address_prefix}: ') - .append(ccEmailAddress) - .addNewLineTag(); - } - if (bccEmailAddress.isNotEmpty) { - headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).bcc_email_address_prefix}: ') - .append(bccEmailAddress) - .addNewLineTag(); - } - - return headerQuoted; - default: - return null; - } - } - return null; - } - - void _initEmailAddress(ComposerArguments arguments) { + void _initEmailAddress({ + required PresentationEmail presentationEmail, + required EmailActionType actionType, + Role? mailboxRole, + }) { + final recipients = presentationEmail.generateRecipientsEmailAddressForComposer( + emailActionType: actionType, + mailboxRole: mailboxRole + ); + log('ComposerController::_initEmailAddress:recipients: $recipients'); final userProfile = mailboxDashBoardController.userProfile.value; - if (arguments.presentationEmail != null && userProfile != null) { - final userEmailAddress = EmailAddress(null, userProfile.email); - final recipients = arguments.presentationEmail!.generateRecipientsEmailAddressForComposer( - arguments.emailActionType, - arguments.mailboxRole); - - final isSender = arguments.presentationEmail!.from.asList().every((element) => element.email == userEmailAddress.email); - + if (userProfile != null) { + final isSender = presentationEmail.from.asList().every((element) => element.email == userProfile.email); if (isSender) { - listToEmailAddress = List.from(recipients.value1); - listCcEmailAddress = List.from(recipients.value2); - listBccEmailAddress = List.from(recipients.value3); + listToEmailAddress = List.from(recipients.value1.toSet()); + listCcEmailAddress = List.from(recipients.value2.toSet()); + listBccEmailAddress = List.from(recipients.value3.toSet()); } else { - listToEmailAddress = List.from(recipients.value1.toSet().filterEmailAddress(userEmailAddress)); - listCcEmailAddress = List.from(recipients.value2.toSet().filterEmailAddress(userEmailAddress)); - listBccEmailAddress = List.from(recipients.value3.toSet().filterEmailAddress(userEmailAddress)); + listToEmailAddress = List.from(recipients.value1.toSet().filterEmailAddress(userProfile.email)); + listCcEmailAddress = List.from(recipients.value2.toSet().filterEmailAddress(userProfile.email)); + listBccEmailAddress = List.from(recipients.value3.toSet().filterEmailAddress(userProfile.email)); } + } else { + listToEmailAddress = List.from(recipients.value1.toSet()); + listCcEmailAddress = List.from(recipients.value2.toSet()); + listBccEmailAddress = List.from(recipients.value3.toSet()); + } - if (listToEmailAddress.isNotEmpty || listCcEmailAddress.isNotEmpty || listBccEmailAddress.isNotEmpty) { - isInitialRecipient.value = true; - toAddressExpandMode.value = ExpandMode.COLLAPSE; - } - - if (listCcEmailAddress.isNotEmpty) { - listEmailAddressType.add(PrefixEmailAddress.cc); - ccAddressExpandMode.value = ExpandMode.COLLAPSE; - } - - if (listBccEmailAddress.isNotEmpty) { - listEmailAddressType.add(PrefixEmailAddress.bcc); - bccAddressExpandMode.value = ExpandMode.COLLAPSE; - } - } else if (arguments.emailAddress != null) { - listToEmailAddress.add(arguments.emailAddress!); + if (listToEmailAddress.isNotEmpty || listCcEmailAddress.isNotEmpty || listBccEmailAddress.isNotEmpty) { isInitialRecipient.value = true; toAddressExpandMode.value = ExpandMode.COLLAPSE; } + + if (listCcEmailAddress.isNotEmpty) { + listEmailAddressType.add(PrefixEmailAddress.cc); + ccAddressExpandMode.value = ExpandMode.COLLAPSE; + } + + if (listBccEmailAddress.isNotEmpty) { + listEmailAddressType.add(PrefixEmailAddress.bcc); + bccAddressExpandMode.value = ExpandMode.COLLAPSE; + } + _updateStatusEmailSendButton(); } @@ -586,13 +595,21 @@ class ComposerController extends BaseController { } } - String getEmailContentQuotedAsHtml(BuildContext context, ComposerArguments arguments) { - final headerEmailQuoted = _getHeaderEmailQuoted(context, arguments); + String getEmailContentQuotedAsHtml({ + required BuildContext context, + required String emailContent, + required EmailActionType emailActionType, + required PresentationEmail presentationEmail, + }) { + final headerEmailQuoted = emailActionType.getHeaderEmailQuoted( + context: context, + presentationEmail: presentationEmail + ); log('ComposerController::getEmailContentQuotedAsHtml(): headerEmailQuoted: $headerEmailQuoted'); - final headerEmailQuotedAsHtml = headerEmailQuoted != null ? headerEmailQuoted.addBlockTag('cite') : ''; - - final trustAsHtml = arguments.emailContents ?? ''; - final emailQuotedHtml = '${HtmlExtension.editorStartTags}$headerEmailQuotedAsHtml${trustAsHtml.addBlockQuoteTag()}'; + final headerEmailQuotedAsHtml = headerEmailQuoted != null + ? headerEmailQuoted.addCiteTag() + : ''; + final emailQuotedHtml = '${HtmlExtension.editorStartTags}$headerEmailQuotedAsHtml${emailContent.addBlockQuoteTag()}'; return emailQuotedHtml; } @@ -789,13 +806,13 @@ class ComposerController extends BaseController { if (arguments != null && accountId != null && userProfile != null && session != null) { final createdEmail = await _generateEmail(context, userProfile, outboxMailboxId: outboxMailboxId); - final emailRequest = arguments.sendingEmail != null + final emailRequest = arguments.emailActionType == EmailActionType.editSendingEmail ? arguments.sendingEmail!.toEmailRequest(newEmail: createdEmail) : EmailRequest( email: createdEmail, sentMailboxId: sentMailboxId, identityId: identitySelected.value?.id, - emailIdDestroyed: arguments.emailActionType == EmailActionType.edit + emailIdDestroyed: arguments.emailActionType == EmailActionType.editDraft ? arguments.presentationEmail?.id : null, emailIdAnsweredOrForwarded: arguments.presentationEmail?.id, @@ -952,36 +969,40 @@ class ComposerController extends BaseController { uploadController.deleteFileUploaded(uploadId); } - Future _isEmailChanged( - BuildContext context, - ComposerArguments arguments, - ) async { + Future _isEmailChanged({ + required BuildContext context, + required EmailActionType emailActionType, + PresentationEmail? presentationEmail, + Role? mailboxRole, + }) async { final newEmailBody = await _getEmailBodyText(context, changedEmail: true); log('ComposerController::_isEmailChanged(): newEmailBody: $newEmailBody'); - var oldEmailBody = _initTextEditor ?? ''; + final oldEmailBody = _initTextEditor ?? ''; log('ComposerController::_isEmailChanged(): oldEmailBody: $oldEmailBody'); final isEmailBodyChanged = !oldEmailBody.trim().isSame(newEmailBody.trim()); log('ComposerController::_isEmailChanged(): isEmailBodyChanged: $isEmailBodyChanged'); - final newEmailSubject = subjectEmail.value ?? ''; - final titleEmail = arguments.presentationEmail?.getEmailTitle().trim() ?? ''; - final oldEmailSubject = arguments.emailActionType == EmailActionType.edit ? titleEmail : ''; + final oldEmailSubject = emailActionType == EmailActionType.editDraft + ? presentationEmail?.getEmailTitle().trim() ?? '' + : ''; final isEmailSubjectChanged = !oldEmailSubject.trim().isSame(newEmailSubject.trim()); - final recipients = arguments.presentationEmail - ?.generateRecipientsEmailAddressForComposer(arguments.emailActionType, arguments.mailboxRole) - ?? const Tuple3([], [], []); + final recipients = presentationEmail + ?.generateRecipientsEmailAddressForComposer( + emailActionType: emailActionType, + mailboxRole: mailboxRole + ) ?? const Tuple3([], [], []); final newToEmailAddress = listToEmailAddress; - final oldToEmailAddress = arguments.emailActionType == EmailActionType.edit ? recipients.value1 : []; + final oldToEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value1 : []; final isToEmailAddressChanged = !oldToEmailAddress.isSame(newToEmailAddress); final newCcEmailAddress = listCcEmailAddress; - final oldCcEmailAddress = arguments.emailActionType == EmailActionType.edit ? recipients.value2 : []; + final oldCcEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value2 : []; final isCcEmailAddressChanged = !oldCcEmailAddress.isSame(newCcEmailAddress); final newBccEmailAddress = listBccEmailAddress; - final oldBccEmailAddress = arguments.emailActionType == EmailActionType.edit ? recipients.value3 : []; + final oldBccEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value3 : []; final isBccEmailAddressChanged = !oldBccEmailAddress.isSame(newBccEmailAddress); final isAttachmentsChanged = !initialAttachments.isSame(uploadController.attachmentsUploaded.toList()); @@ -999,36 +1020,60 @@ class ComposerController extends BaseController { clearFocusEditor(context); final arguments = composerArguments.value; - final draftMailboxId = mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts]; final userProfile = mailboxDashBoardController.userProfile.value; final accountId = mailboxDashBoardController.accountId.value; final session = mailboxDashBoardController.sessionCurrent; - if (arguments != null && userProfile != null && accountId != null && session != null) { - final isChanged = await _isEmailChanged(context, arguments); - if (isChanged && context.mounted) { - final newEmail = await _generateEmail( - context, - userProfile, - asDrafts: true, - draftMailboxId: draftMailboxId); - final oldEmail = arguments.presentationEmail; - - if (arguments.emailActionType == EmailActionType.edit && oldEmail != null && oldEmail.id != null) { - mailboxDashBoardController.consumeState( - _updateEmailDraftsInteractor.execute( - session, - accountId, - newEmail, - oldEmail.id! - ) - ); - } else { - mailboxDashBoardController.consumeState(_saveEmailAsDraftsInteractor.execute(session, accountId, newEmail)); - } - - uploadController.clearInlineFileUploaded(); + if (arguments == null || + userProfile == null || + session == null || + accountId == null + ) { + if (PlatformInfo.isWeb) { + mailboxDashBoardController.closeComposerOverlay(); + } else { + if (canPop) popBack(); } + return; + } + + final isChanged = await _isEmailChanged( + context: context, + emailActionType: arguments.emailActionType, + presentationEmail: arguments.presentationEmail, + mailboxRole: arguments.mailboxRole + ); + + if (isChanged && context.mounted) { + final draftMailboxId = mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts]; + + final newEmail = await _generateEmail( + context, + userProfile, + asDrafts: true, + draftMailboxId: draftMailboxId + ); + + if (arguments.emailActionType == EmailActionType.editDraft) { + mailboxDashBoardController.consumeState( + _updateEmailDraftsInteractor.execute( + session, + accountId, + newEmail, + arguments.presentationEmail!.id! + ) + ); + } else { + mailboxDashBoardController.consumeState( + _saveEmailAsDraftsInteractor.execute( + session, + accountId, + newEmail + ) + ); + } + + uploadController.clearInlineFileUploaded(); } if (PlatformInfo.isWeb) { @@ -1087,90 +1132,109 @@ class ComposerController extends BaseController { return listFileInfo; } - void _getEmailContentAction(ComposerArguments arguments) async { - - final listSharedMediaFile = arguments.listSharedMediaFile; - if (listSharedMediaFile != null && listSharedMediaFile.isNotEmpty) { - final listImageSharedMediaFile = listSharedMediaFile.where((element) => element.type == SharedMediaType.IMAGE); - final listFileAttachmentSharedMediaFile = listSharedMediaFile.where((element) => element.type != SharedMediaType.IMAGE); - if (listImageSharedMediaFile.isNotEmpty) { - final listInlineImage = covertListSharedMediaFileToInlineImage(arguments.listSharedMediaFile!); - for (var e in listInlineImage) { - _uploadInlineAttachmentsAction(e.fileInfo!); - } + void _addAttachmentFromFileShare(List listSharedMediaFile) { + final listImageSharedMediaFile = listSharedMediaFile.where((element) => element.type == SharedMediaType.IMAGE); + final listFileAttachmentSharedMediaFile = listSharedMediaFile.where((element) => element.type != SharedMediaType.IMAGE); + if (listImageSharedMediaFile.isNotEmpty) { + final listInlineImage = covertListSharedMediaFileToInlineImage(listSharedMediaFile); + for (var e in listInlineImage) { + _uploadInlineAttachmentsAction(e.fileInfo!); } - if (listFileAttachmentSharedMediaFile.isNotEmpty) { - final listFile = covertListSharedMediaFileToFileInfo(arguments.listSharedMediaFile!); - if (uploadController.hasEnoughMaxAttachmentSize(listFiles: listFile)) { - _uploadAttachmentsAction(listFile); - } else { - if (currentContext != null) { - showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( - filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, - onConfirmAction: () => {}, - title: AppLocalizations.of(currentContext!).maximum_files_size, - hasCancelButton: false, - ); - } + } + if (listFileAttachmentSharedMediaFile.isNotEmpty) { + final listFile = covertListSharedMediaFileToFileInfo(listSharedMediaFile); + if (uploadController.hasEnoughMaxAttachmentSize(listFiles: listFile)) { + _uploadAttachmentsAction(listFile); + } else { + if (currentContext != null) { + showConfirmDialogAction( + currentContext!, + AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( + filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), + AppLocalizations.of(currentContext!).got_it, + title: AppLocalizations.of(currentContext!).maximum_files_size, + hasCancelButton: false, + ); } } } + } - if (arguments.emailContents != null && arguments.emailContents!.isNotEmpty && arguments.sendingEmail != null) { - _emailContents = arguments.emailContents; - emailContentsViewState.value = Right( - GetEmailContentSuccess( - emailContent: _emailContents!, - emailContentDisplayed: '', - attachments: [], - emailCurrent: arguments.presentationEmail?.toEmail() - ) - ); - } else { - final session = mailboxDashBoardController.sessionCurrent; - final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors.jmapUrl); - final accountId = mailboxDashBoardController.sessionCurrent?.accounts.keys.first; - final emailId = arguments.presentationEmail?.id; - if (session != null && emailId != null && baseDownloadUrl != null && accountId != null) { - consumeState(_getEmailContentInteractor.execute( - session, - accountId, - emailId, - baseDownloadUrl, - composeEmail: true, - draftsEmail: arguments.presentationEmail?.isDraft ?? false - )); + void _getEmailContentFromSendingEmail(SendingEmail sendingEmail) { + consumeState(Stream.value( + Right(GetEmailContentSuccess( + htmlEmailContent: sendingEmail.presentationEmail.emailContentList.asHtmlString, + attachments: sendingEmail.email.allAttachments, + emailCurrent: sendingEmail.email + )) + )); + } + + void _getEmailContentFromSessionStorageBrowser(String content) { + consumeState(Stream.value( + Right(GetEmailContentSuccess( + htmlEmailContent: content, + attachments: [], + )) + )); + } + + void _getEmailContentFromContentShared(String content) { + consumeState(Stream.value( + Right(GetEmailContentSuccess( + htmlEmailContent: content, + attachments: [], + )) + )); + } + + void _getEmailContentFromEmailId({required EmailId emailId, bool isDraftEmail = false}) { + final session = mailboxDashBoardController.sessionCurrent; + final accountId = mailboxDashBoardController.accountId.value; + if (session != null && accountId != null) { + TransformConfiguration transformConfiguration = TransformConfiguration.standardConfiguration; + if (isDraftEmail) { + transformConfiguration = TransformConfiguration.forDraftsEmail(); + } else if (PlatformInfo.isWeb) { + transformConfiguration = TransformConfiguration.forComposeEmailPlatformWeb(); } + + consumeState(_getEmailContentInteractor.execute( + session, + accountId, + emailId, + mailboxDashBoardController.baseDownloadUrl, + transformConfiguration + )); } } void _getEmailContentOffLineSuccess(GetEmailContentFromCacheSuccess success) { - if (success.attachments.isNotEmpty) { - initialAttachments = success.attachments; - uploadController.initializeUploadAttachments( - success.attachments.listAttachmentsDisplayedOutSide); - } + _initAttachments(success.attachments); emailContentsViewState.value = Right(success); - _emailContents = success.emailContent; } void _getEmailContentSuccess(GetEmailContentSuccess success) { - if (success.attachments.isNotEmpty) { - initialAttachments = success.attachments; - uploadController.initializeUploadAttachments( - success.attachments.listAttachmentsDisplayedOutSide); - } + _initAttachments(success.attachments); emailContentsViewState.value = Right(success); - _emailContents = success.emailContent; + } + + void _transformHtmlEmailContent(String? emailContent) { + emailContentsViewState(Right(TransformHtmlEmailContentLoading())); + if (emailContent?.isEmpty == true) { + consumeState(Stream.value(Left(TransformHtmlEmailContentFailure(EmptyEmailContentException())))); + } else { + consumeState(_transformHtmlEmailContentInteractor.execute( + emailContent!, + TransformConfiguration.forReplyForwardEmail() + )); + } } String getEmailAddressSender() { final arguments = composerArguments.value; if (arguments != null) { - if (arguments.emailActionType == EmailActionType.edit) { + if (arguments.emailActionType == EmailActionType.editDraft) { return arguments.presentationEmail?.from?.first.emailAddress ?? ''; } else { return mailboxDashBoardController.userProfile.value?.email ?? ''; diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index ec2c3c8f1..3413fde7c 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -15,9 +15,11 @@ import 'package:get/get.dart'; import 'package:model/email/email_action_type.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:rich_text_composer/views/widgets/rich_text_keyboard_toolbar.dart'; +import 'package:tmail_ui_user/features/base/widget/cupertino_loading_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/base_composer_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/styles/composer_style.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/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; @@ -276,38 +278,96 @@ class ComposerView extends BaseComposerView { switch(argsComposer.emailActionType) { case EmailActionType.compose: case EmailActionType.composeFromEmailAddress: - return _buildHtmlEditor(context, initialContent: HtmlExtension.editorStartTags); - case EmailActionType.edit: - return controller.emailContentsViewState.value.fold( - (failure) => _buildHtmlEditor(context, initialContent: HtmlExtension.editorStartTags), + case EmailActionType.composeFromFileShared: + return _buildHtmlEditor( + context, + initialContent: HtmlExtension.editorStartTags + ); + case EmailActionType.editDraft: + case EmailActionType.editSendingEmail: + case EmailActionType.composeFromContentShared: + case EmailActionType.reopenComposerBrowser: + final emailContentsViewState = controller.emailContentsViewState.value; + if (emailContentsViewState == null) { + return const SizedBox.shrink(); + } + return emailContentsViewState.fold( + (failure) => _buildHtmlEditor( + context, + initialContent: HtmlExtension.editorStartTags + ), (success) { if (success is GetEmailContentLoading) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: loadingWidget, - ); + return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); } else if (success is GetEmailContentSuccess) { - var contentHtml = success.emailContent; + var contentHtml = success.htmlEmailContent; if (contentHtml.isEmpty == true) { contentHtml = HtmlExtension.editorStartTags; } - return _buildHtmlEditor(context, initialContent: contentHtml); + return _buildHtmlEditor( + context, + initialContent: contentHtml + ); } else { - return _buildHtmlEditor(context, initialContent: HtmlExtension.editorStartTags); + return _buildHtmlEditor( + context, + initialContent: HtmlExtension.editorStartTags + ); } }); case EmailActionType.reply: case EmailActionType.replyAll: case EmailActionType.forward: - var contentHtml = controller.getEmailContentQuotedAsHtml( - context, - argsComposer); - if (contentHtml.isEmpty == true) { - contentHtml = HtmlExtension.editorStartTags; + final emailContentsViewState = controller.emailContentsViewState.value; + if (emailContentsViewState == null) { + return const SizedBox.shrink(); } - return _buildHtmlEditor(context, initialContent: contentHtml); + return emailContentsViewState.fold( + (failure) { + final emailContentQuoted = controller.getEmailContentQuotedAsHtml( + context: context, + emailContent: '', + emailActionType: argsComposer.emailActionType, + presentationEmail: argsComposer.presentationEmail! + ); + return _buildHtmlEditor( + context, + initialContent: emailContentQuoted + ); + }, + (success) { + if (success is TransformHtmlEmailContentLoading) { + return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); + } else if (success is TransformHtmlEmailContentSuccess) { + final emailContentQuoted = controller.getEmailContentQuotedAsHtml( + context: context, + emailContent: success.htmlContent, + emailActionType: argsComposer.emailActionType, + presentationEmail: argsComposer.presentationEmail! + ); + return _buildHtmlEditor( + context, + initialContent: emailContentQuoted + ); + } else { + final emailContentQuoted = controller.getEmailContentQuotedAsHtml( + context: context, + emailContent: '', + emailActionType: argsComposer.emailActionType, + presentationEmail: argsComposer.presentationEmail! + ); + return _buildHtmlEditor( + context, + initialContent: emailContentQuoted + ); + } + } + ); default: - return _buildHtmlEditor(context, initialContent: HtmlExtension.editorStartTags); + return _buildHtmlEditor( + context, + initialContent: HtmlExtension.editorStartTags + ); } }); } diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index b77e8d002..a252fb9c8 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -10,12 +10,14 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:model/email/email_action_type.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/base/widget/cupertino_loading_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/base_composer_view.dart'; 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/email_editor_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/toolbar_rich_text_builder.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/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; @@ -385,46 +387,96 @@ class ComposerView extends BaseComposerView { switch(argsComposer.emailActionType) { case EmailActionType.compose: case EmailActionType.composeFromEmailAddress: + case EmailActionType.composeFromFileShared: return _buildHtmlEditor( - context, - currentTextEditor ?? HtmlExtension.editorStartTags); - case EmailActionType.edit: - return controller.emailContentsViewState.value.fold( + context, + currentTextEditor ?? HtmlExtension.editorStartTags + ); + case EmailActionType.editDraft: + case EmailActionType.editSendingEmail: + case EmailActionType.composeFromContentShared: + case EmailActionType.reopenComposerBrowser: + final emailContentsViewState = controller.emailContentsViewState.value; + if (emailContentsViewState == null) { + return const SizedBox.shrink(); + } + return emailContentsViewState.fold( (failure) => _buildHtmlEditor( - context, - currentTextEditor ?? HtmlExtension.editorStartTags), + context, + currentTextEditor ?? HtmlExtension.editorStartTags + ), (success) { if (success is GetEmailContentLoading) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: loadingWidget, - ); + return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); } else if (success is GetEmailContentSuccess) { - var contentHtml = success.emailContent; + var contentHtml = success.htmlEmailContent; if (contentHtml.isEmpty == true) { contentHtml = HtmlExtension.editorStartTags; } - return _buildHtmlEditor(context, currentTextEditor ?? contentHtml); + return _buildHtmlEditor( + context, + currentTextEditor ?? contentHtml + ); } else { return _buildHtmlEditor( context, - currentTextEditor ?? HtmlExtension.editorStartTags); + currentTextEditor ?? HtmlExtension.editorStartTags + ); } }); case EmailActionType.reply: case EmailActionType.replyAll: case EmailActionType.forward: - var contentHtml = controller.getEmailContentQuotedAsHtml( - context, - argsComposer); - if (contentHtml.isEmpty == true) { - contentHtml = HtmlExtension.editorStartTags; + final emailContentsViewState = controller.emailContentsViewState.value; + if (emailContentsViewState == null) { + return const SizedBox.shrink(); } - return _buildHtmlEditor(context, currentTextEditor ?? contentHtml); + return emailContentsViewState.fold( + (failure) { + final emailContentQuoted = controller.getEmailContentQuotedAsHtml( + context: context, + emailContent: '', + emailActionType: argsComposer.emailActionType, + presentationEmail: argsComposer.presentationEmail! + ); + return _buildHtmlEditor( + context, + currentTextEditor ?? emailContentQuoted + ); + }, + (success) { + if (success is TransformHtmlEmailContentLoading) { + return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); + } else if (success is TransformHtmlEmailContentSuccess) { + final emailContentQuoted = controller.getEmailContentQuotedAsHtml( + context: context, + emailContent: success.htmlContent, + emailActionType: argsComposer.emailActionType, + presentationEmail: argsComposer.presentationEmail! + ); + return _buildHtmlEditor( + context, + currentTextEditor ?? emailContentQuoted + ); + } else { + final emailContentQuoted = controller.getEmailContentQuotedAsHtml( + context: context, + emailContent: '', + emailActionType: argsComposer.emailActionType, + presentationEmail: argsComposer.presentationEmail! + ); + return _buildHtmlEditor( + context, + currentTextEditor ?? emailContentQuoted + ); + } + } + ); default: return _buildHtmlEditor( - context, - currentTextEditor ?? HtmlExtension.editorStartTags); + context, + currentTextEditor ?? HtmlExtension.editorStartTags + ); } }); } 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 362507bb3..8a7f35dc8 100644 --- a/lib/features/composer/presentation/extensions/email_action_type_extension.dart +++ b/lib/features/composer/presentation/extensions/email_action_type_extension.dart @@ -1,25 +1,35 @@ +import 'package:core/presentation/extensions/html_extension.dart'; import 'package:flutter/cupertino.dart'; import 'package:model/email/email_action_type.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/extensions/list_email_address_extension.dart'; +import 'package:model/extensions/utc_date_extension.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; extension EmailActionTypeExtension on EmailActionType { - String getSubjectComposer(BuildContext context, String subject) { + String getSubjectComposer(BuildContext? context, String subject) { switch(this) { case EmailActionType.reply: case EmailActionType.replyAll: if (subject.toLowerCase().startsWith('re:')) { return subject; } else { - return '${AppLocalizations.of(context).prefix_reply_email} $subject'; + return context != null + ? '${AppLocalizations.of(context).prefix_reply_email} $subject' + : subject; } case EmailActionType.forward: if (subject.toLowerCase().startsWith('fwd:')) { return subject; } else { - return '${AppLocalizations.of(context).prefix_forward_email} $subject'; + return context != null + ? '${AppLocalizations.of(context).prefix_forward_email} $subject' + : subject; } - case EmailActionType.edit: + case EmailActionType.editDraft: + case EmailActionType.editSendingEmail: + case EmailActionType.reopenComposerBrowser: return subject; default: return ''; @@ -40,4 +50,71 @@ extension EmailActionTypeExtension on EmailActionType { return ''; } } + + String? getHeaderEmailQuoted({ + required BuildContext context, + required PresentationEmail presentationEmail + }) { + final locale = Localizations.localeOf(context).toLanguageTag(); + switch(this) { + case EmailActionType.reply: + case EmailActionType.replyAll: + final receivedAt = presentationEmail.receivedAt; + final emailAddress = presentationEmail.from.listEmailAddressToString(isFullEmailAddress: true); + return AppLocalizations.of(context).header_email_quoted( + receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: locale), + emailAddress + ); + case EmailActionType.forward: + var headerQuoted = '------- ${AppLocalizations.of(context).forwarded_message} -------'.addNewLineTag(); + + final subject = presentationEmail.subject ?? ''; + final receivedAt = presentationEmail.receivedAt; + final fromEmailAddress = presentationEmail.from.listEmailAddressToString(isFullEmailAddress: true); + final toEmailAddress = presentationEmail.to.listEmailAddressToString(isFullEmailAddress: true); + final ccEmailAddress = presentationEmail.cc.listEmailAddressToString(isFullEmailAddress: true); + final bccEmailAddress = presentationEmail.bcc.listEmailAddressToString(isFullEmailAddress: true); + + if (subject.isNotEmpty) { + headerQuoted = headerQuoted + .append('${AppLocalizations.of(context).subject_email}: ') + .append(subject) + .addNewLineTag(); + } + if (receivedAt != null) { + headerQuoted = headerQuoted + .append('${AppLocalizations.of(context).date}: ') + .append(receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: locale)) + .addNewLineTag(); + } + if (fromEmailAddress.isNotEmpty) { + headerQuoted = headerQuoted + .append('${AppLocalizations.of(context).from_email_address_prefix}: ') + .append(fromEmailAddress) + .addNewLineTag(); + } + if (toEmailAddress.isNotEmpty) { + headerQuoted = headerQuoted + .append('${AppLocalizations.of(context).to_email_address_prefix}: ') + .append(toEmailAddress) + .addNewLineTag(); + } + if (ccEmailAddress.isNotEmpty) { + headerQuoted = headerQuoted + .append('${AppLocalizations.of(context).cc_email_address_prefix}: ') + .append(ccEmailAddress) + .addNewLineTag(); + } + if (bccEmailAddress.isNotEmpty) { + headerQuoted = headerQuoted + .append('${AppLocalizations.of(context).bcc_email_address_prefix}: ') + .append(bccEmailAddress) + .addNewLineTag(); + } + + return headerQuoted; + default: + return null; + } + } } \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/composer_style.dart b/lib/features/composer/presentation/styles/composer_style.dart index 32a034428..61948eda5 100644 --- a/lib/features/composer/presentation/styles/composer_style.dart +++ b/lib/features/composer/presentation/styles/composer_style.dart @@ -99,7 +99,7 @@ class ComposerStyle { if (responsiveUtils.isPortraitMobile(context) || responsiveUtils.isLandscapeMobile(context)) { return const EdgeInsetsDirectional.symmetric(horizontal: 16); } else { - return const EdgeInsetsDirectional.only(start: 88, end: 48); + return const EdgeInsetsDirectional.only(start: 88, end: 48, top: 8); } } diff --git a/lib/features/email/data/datasource/html_datasource.dart b/lib/features/email/data/datasource/html_datasource.dart index 8cce703fb..36f7d8975 100644 --- a/lib/features/email/data/datasource/html_datasource.dart +++ b/lib/features/email/data/datasource/html_datasource.dart @@ -1,12 +1,16 @@ +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:model/model.dart'; abstract class HtmlDataSource { Future transformEmailContent( EmailContent emailContent, - Map mapUrlDownloadCID, - {bool draftsEmail = false} + Map mapCidImageDownloadUrl, + TransformConfiguration transformConfiguration ); - Future addTooltipWhenHoverOnLink(EmailContent emailContent); + Future transformHtmlEmailContent( + String htmlContent, + TransformConfiguration configuration + ); } \ No newline at end of file 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 c6f65a32d..7ce83177e 100644 --- a/lib/features/email/data/datasource_impl/html_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/html_datasource_impl.dart @@ -1,3 +1,4 @@ +import 'package:core/presentation/utils/html_transformer/transform_configuration.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'; @@ -13,22 +14,25 @@ class HtmlDataSourceImpl extends HtmlDataSource { @override Future transformEmailContent( EmailContent emailContent, - Map? mapUrlDownloadCID, - {bool draftsEmail = false} + Map mapCidImageDownloadUrl, + TransformConfiguration transformConfiguration ) { return Future.sync(() async { return await _htmlAnalyzer.transformEmailContent( emailContent, - mapUrlDownloadCID, - draftsEmail: draftsEmail + mapCidImageDownloadUrl, + transformConfiguration ); }).catchError(_exceptionThrower.throwException); } @override - Future addTooltipWhenHoverOnLink(EmailContent emailContent) { + Future transformHtmlEmailContent(String htmlContent, TransformConfiguration configuration) { return Future.sync(() async { - return await _htmlAnalyzer.addTooltipWhenHoverOnLink(emailContent); + return await _htmlAnalyzer.transformHtmlEmailContent( + htmlContent, + configuration + ); }).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 ebdee846f..d6c5a2610 100644 --- a/lib/features/email/data/local/html_analyzer.dart +++ b/lib/features/email/data/local/html_analyzer.dart @@ -1,11 +1,5 @@ - -import 'dart:convert'; - import 'package:collection/collection.dart'; -import 'package:core/data/network/dio_client.dart'; -import 'package:core/presentation/utils/html_transformer/dom/add_tooltip_link_transformers.dart'; import 'package:core/presentation/utils/html_transformer/html_transform.dart'; -import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart'; import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:html/parser.dart'; import 'package:model/email/email_content.dart'; @@ -14,32 +8,27 @@ import 'package:model/email/email_content_type.dart'; class HtmlAnalyzer { final HtmlTransform _htmlTransform; - final HtmlEscape _htmlEscape; - HtmlAnalyzer(this._htmlTransform, this._htmlEscape); + HtmlAnalyzer(this._htmlTransform); Future transformEmailContent( EmailContent emailContent, - Map? mapUrlDownloadCID, - {bool draftsEmail = false} + Map mapCidImageDownloadUrl, + TransformConfiguration transformConfiguration ) async { switch(emailContent.type) { case EmailContentType.textHtml: final htmlContent = await _htmlTransform.transformToHtml( - contentHtml: emailContent.content, - mapUrlDownloadCID: mapUrlDownloadCID, - transformConfiguration: draftsEmail - ? TransformConfiguration.create(customDomTransformers: TransformConfiguration.domTransformersForDraftEmail) - : null + htmlContent: emailContent.content, + mapCidImageDownloadUrl: mapCidImageDownloadUrl, + transformConfiguration: transformConfiguration ); return EmailContent(emailContent.type, htmlContent); case EmailContentType.textPlain: final message = _htmlTransform.transformToTextPlain( content: emailContent.content, - transformConfiguration: TransformConfiguration.create( - customTextTransformers: [SanitizeAutolinkHtmlTransformers(_htmlEscape)] - ) + transformConfiguration: transformConfiguration ); return EmailContent(emailContent.type, message); default: @@ -47,21 +36,6 @@ class HtmlAnalyzer { } } - Future addTooltipWhenHoverOnLink(EmailContent emailContent) async { - switch(emailContent.type) { - case EmailContentType.textHtml: - final htmlContent = await _htmlTransform.transformToHtml( - contentHtml: emailContent.content, - transformConfiguration: TransformConfiguration.create( - customDomTransformers: [const AddTooltipLinkTransformer()] - ) - ); - return EmailContent(emailContent.type, htmlContent); - default: - return emailContent; - } - } - Future> getListLinkCalendarEvent(String emailContents) async { final document = parse(emailContents); final linkElements = document.querySelectorAll('a.part-button'); @@ -71,4 +45,15 @@ class HtmlAnalyzer { .toList(); return listLink; } + + Future transformHtmlEmailContent( + String htmlContent, + TransformConfiguration configuration + ) async { + final htmlContentTransformed = await _htmlTransform.transformToHtml( + htmlContent: htmlContent, + transformConfiguration: configuration + ); + return htmlContentTransformed; + } } \ 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 36d2217a0..554b7e486 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -6,6 +6,7 @@ import 'package:core/data/model/source_type/data_source_type.dart'; import 'package:core/data/network/download/downloaded_response.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; @@ -109,22 +110,16 @@ class EmailRepositoryImpl extends EmailRepository { @override Future> transformEmailContent( - List emailContents, - List attachmentInlines, - String? baseUrlDownload, - AccountId accountId, - {bool draftsEmail = false} - ) async { - final mapUrlDownloadCID = { - for (var attachment in attachmentInlines) - attachment.cid! : attachment.getDownloadUrl(baseUrlDownload!, accountId) - }; + List emailContents, + Map mapCidImageDownloadUrl, + TransformConfiguration transformConfiguration + ) async { return await Future.wait(emailContents .map((emailContent) async { return await _htmlDataSource.transformEmailContent( emailContent, - mapUrlDownloadCID, - draftsEmail: draftsEmail + mapCidImageDownloadUrl, + transformConfiguration, ); }) .toList()); @@ -173,13 +168,6 @@ class EmailRepositoryImpl extends EmailRepository { return emailDataSource[DataSourceType.network]!.deleteEmailPermanently(session, accountId, emailId); } - @override - Future> addTooltipWhenHoverOnLink(List emailContents) { - return Future.wait(emailContents - .map((emailContent) => _htmlDataSource.addTooltipWhenHoverOnLink(emailContent)) - .toList()); - } - @override Future getEmailState(Session session, AccountId accountId) { return _stateDataSource.getState(accountId, session.username, StateType.email); @@ -219,4 +207,9 @@ class EmailRepositoryImpl extends EmailRepository { Future getStoredNewEmail(Session session, AccountId accountId, EmailId emailId) { return emailDataSource[DataSourceType.hiveCache]!.getStoredNewEmail(session, accountId, emailId); } + + @override + Future transformHtmlEmailContent(String htmlContent, TransformConfiguration configuration) { + return _htmlDataSource.transformHtmlEmailContent(htmlContent, configuration); + } } \ No newline at end of file diff --git a/lib/features/email/domain/exceptions/email_exceptions.dart b/lib/features/email/domain/exceptions/email_exceptions.dart index 054268e88..a59cbe218 100644 --- a/lib/features/email/domain/exceptions/email_exceptions.dart +++ b/lib/features/email/domain/exceptions/email_exceptions.dart @@ -3,4 +3,6 @@ class CannotDeleteOldEmailException implements Exception {} class NotFoundEmailException implements Exception {} -class NotFoundEmailContentException implements Exception {} \ No newline at end of file +class NotFoundEmailContentException implements Exception {} + +class EmptyEmailContentException implements Exception {} \ 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 4db11ed56..88f22aea4 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:core/data/network/download/downloaded_response.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; @@ -69,14 +70,10 @@ abstract class EmailRepository { Future> transformEmailContent( List emailContents, - List attachmentInlines, - String? baseUrlDownload, - AccountId accountId, - {bool draftsEmail = false} + Map mapCidImageDownloadUrl, + TransformConfiguration transformConfiguration ); - Future> addTooltipWhenHoverOnLink(List emailContents); - Future saveEmailAsDrafts(Session session, AccountId accountId, Email email); Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId); @@ -102,4 +99,9 @@ abstract class EmailRepository { Future getStoredOpenedEmail(Session session, AccountId accountId, EmailId emailId); Future getStoredNewEmail(Session session, AccountId accountId, EmailId emailId); + + Future transformHtmlEmailContent( + String htmlContent, + TransformConfiguration configuration + ); } \ No newline at end of file diff --git a/lib/features/email/domain/state/get_email_content_state.dart b/lib/features/email/domain/state/get_email_content_state.dart index 8581209dc..9742216d9 100644 --- a/lib/features/email/domain/state/get_email_content_state.dart +++ b/lib/features/email/domain/state/get_email_content_state.dart @@ -6,41 +6,38 @@ import 'package:model/email/attachment.dart'; class GetEmailContentLoading extends LoadingState {} class GetEmailContentSuccess extends UIState { - final String emailContent; - final String emailContentDisplayed; + final String htmlEmailContent; final List attachments; final Email? emailCurrent; GetEmailContentSuccess({ - required this.emailContent, - required this.emailContentDisplayed, - required this.attachments, - required this.emailCurrent - }); - - @override - List get props => [ - emailContent, - emailContentDisplayed, - attachments, - emailCurrent - ]; -} - -class GetEmailContentFromCacheSuccess extends UIState { - final String emailContent; - final List attachments; - final Email? emailCurrent; - - GetEmailContentFromCacheSuccess({ - required this.emailContent, + required this.htmlEmailContent, required this.attachments, this.emailCurrent }); @override List get props => [ - emailContent, + htmlEmailContent, + attachments, + emailCurrent + ]; +} + +class GetEmailContentFromCacheSuccess extends UIState { + final String htmlEmailContent; + final List attachments; + final Email? emailCurrent; + + GetEmailContentFromCacheSuccess({ + required this.htmlEmailContent, + required this.attachments, + this.emailCurrent + }); + + @override + List get props => [ + htmlEmailContent, attachments, emailCurrent, ]; diff --git a/lib/features/email/domain/state/transform_html_email_content_state.dart b/lib/features/email/domain/state/transform_html_email_content_state.dart new file mode 100644 index 000000000..f3b70df11 --- /dev/null +++ b/lib/features/email/domain/state/transform_html_email_content_state.dart @@ -0,0 +1,18 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class TransformHtmlEmailContentLoading extends LoadingState {} + +class TransformHtmlEmailContentSuccess extends UIState { + final String htmlContent; + + TransformHtmlEmailContentSuccess(this.htmlContent); + + @override + List get props => [htmlContent]; +} + +class TransformHtmlEmailContentFailure extends FeatureFailure { + + TransformHtmlEmailContentFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/email/domain/usecases/get_email_content_interactor.dart b/lib/features/email/domain/usecases/get_email_content_interactor.dart index b37561cfc..44a8e19c0 100644 --- a/lib/features/email/domain/usecases/get_email_content_interactor.dart +++ b/lib/features/email/domain/usecases/get_email_content_interactor.dart @@ -1,6 +1,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; @@ -20,19 +21,16 @@ class GetEmailContentInteractor { Session session, AccountId accountId, EmailId emailId, - String? baseDownloadUrl, - { - bool composeEmail = false, - bool draftsEmail = false - } + String baseDownloadUrl, + TransformConfiguration transformConfiguration, ) async* { try { yield Right(GetEmailContentLoading()); if (PlatformInfo.isMobile) { - yield* _getStoredOpenedEmail(session, accountId, emailId, baseDownloadUrl, composeEmail: composeEmail, draftsEmail: draftsEmail); + yield* _getStoredOpenedEmail(session, accountId, emailId, baseDownloadUrl, transformConfiguration); } else { - yield* _getContentEmailFromServer(session, accountId, emailId, baseDownloadUrl, composeEmail: composeEmail, draftsEmail: draftsEmail); + yield* _getContentEmailFromServer(session, accountId, emailId, baseDownloadUrl, transformConfiguration); } } catch (e) { log('GetEmailContentInteractor::execute(): exception = $e'); @@ -44,38 +42,31 @@ class GetEmailContentInteractor { Session session, AccountId accountId, EmailId emailId, - String? baseDownloadUrl, - { - bool composeEmail = false, - bool draftsEmail = false - } + String baseDownloadUrl, + TransformConfiguration transformConfiguration, ) async* { try { final email = await emailRepository.getEmailContent(session, accountId, emailId); if (email.emailContentList.isNotEmpty) { + final mapCidImageDownloadUrl = email.attachmentsWithCid.toMapCidImageDownloadUrl( + accountId: accountId, + downloadUrl: baseDownloadUrl + ); final newEmailContents = await emailRepository.transformEmailContent( email.emailContentList, - email.allAttachments.listAttachmentsDisplayedInContent, - baseDownloadUrl, - accountId, - draftsEmail: draftsEmail + mapCidImageDownloadUrl, + transformConfiguration ); - final newEmailContentsDisplayed = PlatformInfo.isWeb && !composeEmail - ? await emailRepository.addTooltipWhenHoverOnLink(newEmailContents) - : newEmailContents; - yield Right(GetEmailContentSuccess( - emailContent: newEmailContents.asHtmlString, - emailContentDisplayed: newEmailContentsDisplayed.asHtmlString, + htmlEmailContent: newEmailContents.asHtmlString, attachments: email.allAttachments, emailCurrent: email )); } else { yield Right(GetEmailContentSuccess( - emailContent: '', - emailContentDisplayed: '', + htmlEmailContent: '', attachments: email.allAttachments, emailCurrent: email )); @@ -90,17 +81,14 @@ class GetEmailContentInteractor { Session session, AccountId accountId, EmailId emailId, - String? baseDownloadUrl, - { - bool composeEmail = false, - bool draftsEmail = false - } + String baseDownloadUrl, + TransformConfiguration transformConfiguration, ) async* { try { log('GetEmailContentInteractor::_getStoredOpenedEmail(): CALLED'); final detailedEmail = await emailRepository.getStoredOpenedEmail(session, accountId, emailId); yield Right(GetEmailContentFromCacheSuccess( - emailContent: detailedEmail.htmlEmailContent ?? "", + htmlEmailContent: detailedEmail.htmlEmailContent ?? '', attachments: detailedEmail.attachments ?? [], emailCurrent: Email( id: emailId, @@ -115,8 +103,8 @@ class GetEmailContentInteractor { accountId, emailId, baseDownloadUrl, - composeEmail: composeEmail, - draftsEmail: draftsEmail); + transformConfiguration + ); } } @@ -124,17 +112,14 @@ class GetEmailContentInteractor { Session session, AccountId accountId, EmailId emailId, - String? baseDownloadUrl, - { - bool composeEmail = false, - bool draftsEmail = false - } + String baseDownloadUrl, + TransformConfiguration transformConfiguration, ) async* { try { log('GetEmailContentInteractor::_getStoredNewEmail():CALLED'); final detailedEmail = await emailRepository.getStoredNewEmail(session, accountId, emailId); yield Right(GetEmailContentFromCacheSuccess( - emailContent: detailedEmail.htmlEmailContent ?? "", + htmlEmailContent: detailedEmail.htmlEmailContent ?? '', attachments: detailedEmail.attachments ?? [], emailCurrent: Email( id: emailId, @@ -149,8 +134,8 @@ class GetEmailContentInteractor { accountId, emailId, baseDownloadUrl, - composeEmail: composeEmail, - draftsEmail: draftsEmail); + transformConfiguration + ); } } } \ No newline at end of file diff --git a/lib/features/email/domain/usecases/get_list_detailed_email_by_id_interator.dart b/lib/features/email/domain/usecases/get_list_detailed_email_by_id_interator.dart index a77c57560..255acecf5 100644 --- a/lib/features/email/domain/usecases/get_list_detailed_email_by_id_interator.dart +++ b/lib/features/email/domain/usecases/get_list_detailed_email_by_id_interator.dart @@ -1,5 +1,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/extensions/utc_date_extension.dart'; @@ -23,7 +25,7 @@ class GetListDetailedEmailByIdInteractor { Session session, AccountId accountId, Set emailIds, - String? baseDownloadUrl, + String baseDownloadUrl, {Set? sort} ) async* { try { @@ -57,17 +59,27 @@ class GetListDetailedEmailByIdInteractor { Future> _parsingEmailToDetailedEmail( AccountId accountId, Email email, - String? baseDownloadUrl + String baseDownloadUrl ) async { String? htmlEmailContent; final listEmailContent = email.emailContentList; if (listEmailContent.isNotEmpty) { + final mapCidImageDownloadUrl = email.attachmentsWithCid.toMapCidImageDownloadUrl( + accountId: accountId, + downloadUrl: baseDownloadUrl + ); + TransformConfiguration transformConfiguration = TransformConfiguration.standardConfiguration; + if (email.isDraft) { + transformConfiguration = TransformConfiguration.forDraftsEmail(); + } else if (PlatformInfo.isWeb) { + transformConfiguration = TransformConfiguration.forPreviewEmailPlatformWeb(); + } final newEmailContents = await _emailRepository.transformEmailContent( - listEmailContent, - email.allAttachments.listAttachmentsDisplayedInContent, - baseDownloadUrl, - accountId); + email.emailContentList, + mapCidImageDownloadUrl, + transformConfiguration + ); htmlEmailContent = newEmailContents.asHtmlString; } diff --git a/lib/features/email/domain/usecases/transform_html_email_content_interactor.dart b/lib/features/email/domain/usecases/transform_html_email_content_interactor.dart new file mode 100644 index 000000000..00b18c893 --- /dev/null +++ b/lib/features/email/domain/usecases/transform_html_email_content_interactor.dart @@ -0,0 +1,26 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart'; + +class TransformHtmlEmailContentInteractor { + final EmailRepository emailRepository; + + TransformHtmlEmailContentInteractor(this.emailRepository); + + Stream> execute( + String htmlContent, + TransformConfiguration configuration + ) async* { + try { + yield Right(TransformHtmlEmailContentLoading()); + final htmlContentTransformed = await emailRepository.transformHtmlEmailContent(htmlContent, configuration); + yield Right(TransformHtmlEmailContentSuccess(htmlContentTransformed)); + } catch (e) { + yield Left(TransformHtmlEmailContentFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 982b83f43..348caf295 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -360,22 +360,33 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } void _getEmailContentAction(EmailId emailId) async { - final session = mailboxDashBoardController.sessionCurrent; - final accountId = mailboxDashBoardController.accountId.value; - final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors.jmapUrl); final emailLoaded = emailSupervisorController.getEmailInQueueByEmailId(emailId); if (emailLoaded != null) { consumeState(Stream.value(Right( GetEmailContentSuccess( - emailContent: emailLoaded.originalContent, - emailContentDisplayed: emailLoaded.displayedContent, + htmlEmailContent: emailLoaded.htmlContent, attachments: emailLoaded.attachments, emailCurrent: emailLoaded.emailCurrent ) ))); - } else if (session != null && accountId != null && baseDownloadUrl != null) { - consumeState(_getEmailContentInteractor.execute(session, accountId, emailId, baseDownloadUrl)); + } else { + final session = mailboxDashBoardController.sessionCurrent; + final accountId = mailboxDashBoardController.accountId.value; + if (session != null && accountId != null) { + final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors.jmapUrl) ?? ''; + TransformConfiguration transformConfiguration = PlatformInfo.isWeb + ? TransformConfiguration.forPreviewEmailPlatformWeb() + : TransformConfiguration.standardConfiguration; + + consumeState(_getEmailContentInteractor.execute( + session, + accountId, + emailId, + baseDownloadUrl, + transformConfiguration + )); + } } } @@ -387,8 +398,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { emailSupervisorController.popEmailQueue(success.emailCurrent?.id); _currentEmailLoaded = EmailLoaded( - originalContent: success.emailContent, - displayedContent: success.emailContent, + htmlContent: success.htmlEmailContent, attachments: List.of(success.attachments), emailCurrent: success.emailCurrent, ); @@ -401,10 +411,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _parseCalendarEventAction( accountId: mailboxDashBoardController.accountId.value!, blobIds: success.attachments.calendarEventBlobIds, - emailContents: success.emailContent + emailContents: success.htmlEmailContent ); } else { - emailContents.value = success.emailContent; + emailContents.value = success.htmlEmailContent; } final isShowMessageReadReceipt = success.emailCurrent?.hasReadReceipt(mailboxDashBoardController.mapMailboxById) == true; @@ -422,8 +432,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { emailSupervisorController.popEmailQueue(success.emailCurrent?.id); _currentEmailLoaded = EmailLoaded( - originalContent: success.emailContent, - displayedContent: success.emailContentDisplayed, + htmlContent: success.htmlEmailContent, attachments: List.of(success.attachments), emailCurrent: success.emailCurrent, ); @@ -436,10 +445,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _parseCalendarEventAction( accountId: mailboxDashBoardController.accountId.value!, blobIds: success.attachments.calendarEventBlobIds, - emailContents: success.emailContent + emailContents: success.htmlEmailContent ); } else { - emailContents.value = success.emailContentDisplayed; + emailContents.value = success.htmlEmailContent; } if (PlatformInfo.isMobile) { @@ -449,7 +458,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { attachments: success.attachments, headers: currentEmail?.emailHeader?.toSet(), keywords: currentEmail?.keywords, - htmlEmailContent: success.emailContentDisplayed + htmlEmailContent: success.htmlEmailContent ); _storeOpenedEmailAction( @@ -978,13 +987,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void composeEmailFromEmailAddress(EmailAddress emailAddress) { popBack(); - - final arguments = ComposerArguments( - emailActionType: EmailActionType.composeFromEmailAddress, - emailAddress: emailAddress, - mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role); - - mailboxDashBoardController.goToComposer(arguments); + mailboxDashBoardController.goToComposer(ComposerArguments.fromEmailAddress(emailAddress)); } void openMailToLink(Uri? uri) { @@ -993,12 +996,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { log('SingleEmailController::openMailToLink(): address: $address'); if (address.isNotEmpty) { final emailAddress = EmailAddress(null, address); - final arguments = ComposerArguments( - emailActionType: EmailActionType.composeFromEmailAddress, - emailAddress: emailAddress, - mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role); - - mailboxDashBoardController.goToComposer(arguments); + mailboxDashBoardController.goToComposer(ComposerArguments.fromEmailAddress(emailAddress)); } } @@ -1158,19 +1156,43 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void pressEmailAction(EmailActionType emailActionType) { - if (emailActionType == EmailActionType.compose) { - mailboxDashBoardController.goToComposer(ComposerArguments()); - } else { - final arguments = ComposerArguments( - emailActionType: emailActionType, - presentationEmail: mailboxDashBoardController.selectedEmail.value!, - emailContents: _currentEmailLoaded?.originalContent, - attachments: emailActionType == EmailActionType.forward ? attachments : null, - mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role - ); - - mailboxDashBoardController.goToComposer(arguments); + void pressEmailAction( + EmailActionType emailActionType, + PresentationEmail presentationEmail + ) { + switch(emailActionType) { + case EmailActionType.compose: + mailboxDashBoardController.goToComposer(ComposerArguments()); + break; + case EmailActionType.reply: + mailboxDashBoardController.goToComposer( + ComposerArguments.replyEmail( + presentationEmail: presentationEmail, + content: _currentEmailLoaded?.htmlContent ?? '', + mailboxRole: presentationEmail.mailboxContain?.role + ) + ); + break; + case EmailActionType.replyAll: + mailboxDashBoardController.goToComposer( + ComposerArguments.replyAllEmail( + presentationEmail: presentationEmail, + content: _currentEmailLoaded?.htmlContent ?? '', + mailboxRole: presentationEmail.mailboxContain?.role + ) + ); + break; + case EmailActionType.forward: + mailboxDashBoardController.goToComposer( + ComposerArguments.forwardEmail( + presentationEmail: presentationEmail, + content: _currentEmailLoaded?.htmlContent ?? '', + attachments: attachments + ) + ); + break; + default: + break; } } @@ -1276,7 +1298,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _handleParseCalendarEventFailure(ParseCalendarEventFailure failure) { emailLoadedViewState.value = Left(failure); - emailContents.value = _currentEmailLoaded?.displayedContent; + emailContents.value = _currentEmailLoaded?.htmlContent; } void _enableScrollPageView() { @@ -1289,11 +1311,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void openNewComposerAction(String mailTo) { final emailAddress = EmailAddress(mailTo, mailTo); - final arguments = ComposerArguments( - emailActionType: EmailActionType.composeFromEmailAddress, - emailAddress: emailAddress, - mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role - ); - mailboxDashBoardController.goToComposer(arguments); + mailboxDashBoardController.goToComposer(ComposerArguments.fromEmailAddress(emailAddress)); } } \ 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 ec1b97a86..ffe02607c 100644 --- a/lib/features/email/presentation/model/composer_arguments.dart +++ b/lib/features/email/presentation/model/composer_arguments.dart @@ -2,6 +2,7 @@ import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/composer_cache.dart'; import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; import 'package:tmail_ui_user/main/routes/router_arguments.dart'; @@ -26,6 +27,84 @@ class ComposerArguments extends RouterArguments { this.sendingEmail }); + factory ComposerArguments.fromSendingEmail(SendingEmail sendingEmail) => + ComposerArguments( + emailActionType: EmailActionType.editSendingEmail, + sendingEmail: sendingEmail + ); + + factory ComposerArguments.fromContentShared(String content) => + ComposerArguments( + emailActionType: EmailActionType.composeFromContentShared, + emailContents: content + ); + + factory ComposerArguments.fromFileShared(List filesShared) => + ComposerArguments( + emailActionType: EmailActionType.composeFromFileShared, + listSharedMediaFile: filesShared + ); + + factory ComposerArguments.fromEmailAddress(EmailAddress emailAddress) => + ComposerArguments( + emailActionType: EmailActionType.composeFromEmailAddress, + emailAddress: emailAddress + ); + + factory ComposerArguments.editDraftEmail(PresentationEmail presentationEmail) => + ComposerArguments( + emailActionType: EmailActionType.editDraft, + presentationEmail: presentationEmail + ); + + factory ComposerArguments.fromSessionStorageBrowser(ComposerCache composerCache) => + ComposerArguments( + emailActionType: EmailActionType.reopenComposerBrowser, + presentationEmail: PresentationEmail( + id: composerCache.id, + subject: composerCache.subject, + from: composerCache.from, + to: composerCache.to, + cc: composerCache.cc, + bcc: composerCache.bcc, + ), + emailContents: composerCache.emailContentList.asHtmlString, + ); + + factory ComposerArguments.replyEmail({ + required PresentationEmail presentationEmail, + required String content, + Role? mailboxRole, + }) => ComposerArguments( + emailActionType: EmailActionType.reply, + presentationEmail: presentationEmail, + emailContents: content, + mailboxRole: mailboxRole + ); + + factory ComposerArguments.replyAllEmail({ + required PresentationEmail presentationEmail, + required String content, + Role? mailboxRole, + }) => ComposerArguments( + emailActionType: EmailActionType.replyAll, + presentationEmail: presentationEmail, + emailContents: content, + mailboxRole: mailboxRole + ); + + factory ComposerArguments.forwardEmail({ + required PresentationEmail presentationEmail, + required String content, + required List attachments, + }) => ComposerArguments( + emailActionType: EmailActionType.forward, + presentationEmail: presentationEmail, + emailContents: content, + attachments: attachments, + mailboxRole: presentationEmail.mailboxContain?.role + ); + @override List get props => [ emailActionType, diff --git a/lib/features/email/presentation/model/email_loaded.dart b/lib/features/email/presentation/model/email_loaded.dart index f7924ac64..948a68421 100644 --- a/lib/features/email/presentation/model/email_loaded.dart +++ b/lib/features/email/presentation/model/email_loaded.dart @@ -3,22 +3,19 @@ import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/email/attachment.dart'; class EmailLoaded with EquatableMixin { - final String originalContent; - final String displayedContent; + final String htmlContent; final List attachments; final Email? emailCurrent; EmailLoaded({ - required this.originalContent, - required this.displayedContent, + required this.htmlContent, required this.attachments, this.emailCurrent, }); @override List get props => [ - originalContent, - displayedContent, + htmlContent, attachments, emailCurrent ]; diff --git a/lib/features/email/presentation/widgets/bottom_bar_mail_widget_builder.dart b/lib/features/email/presentation/widgets/bottom_bar_mail_widget_builder.dart index 976466417..d8a0c78dc 100644 --- a/lib/features/email/presentation/widgets/bottom_bar_mail_widget_builder.dart +++ b/lib/features/email/presentation/widgets/bottom_bar_mail_widget_builder.dart @@ -9,7 +9,10 @@ import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/presentation_email_extension.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -typedef OnPressEmailActionClick = void Function(EmailActionType emailActionType); +typedef OnPressEmailActionClick = void Function( + EmailActionType emailActionType, + PresentationEmail presentationEmail +); class BottomBarMailWidgetBuilder extends StatelessWidget { @@ -60,7 +63,7 @@ class BottomBarMailWidgetBuilder extends StatelessWidget { color: AppColor.colorTextButton ), verticalDirection: responsiveUtils.isPortraitMobile(context), - onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.replyAll), + onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.replyAll, _presentationEmail), ), ), Expanded( @@ -79,7 +82,7 @@ class BottomBarMailWidgetBuilder extends StatelessWidget { color: AppColor.colorTextButton ), verticalDirection: responsiveUtils.isPortraitMobile(context), - onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.reply), + onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.reply, _presentationEmail), ), ), Expanded( @@ -98,7 +101,7 @@ class BottomBarMailWidgetBuilder extends StatelessWidget { color: AppColor.colorTextButton ), verticalDirection: responsiveUtils.isPortraitMobile(context), - onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.forward), + onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.forward, _presentationEmail), ), ), Expanded( @@ -117,7 +120,7 @@ class BottomBarMailWidgetBuilder extends StatelessWidget { color: AppColor.colorTextButton ), verticalDirection: responsiveUtils.isPortraitMobile(context), - onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.compose), + onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.compose, _presentationEmail), ), ), ] diff --git a/lib/features/mailbox_dashboard/data/datasource_impl/session_storage_composer_datasoure_impl.dart b/lib/features/mailbox_dashboard/data/datasource_impl/session_storage_composer_datasoure_impl.dart index eb55124b6..1b01fd42d 100644 --- a/lib/features/mailbox_dashboard/data/datasource_impl/session_storage_composer_datasoure_impl.dart +++ b/lib/features/mailbox_dashboard/data/datasource_impl/session_storage_composer_datasoure_impl.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:core/core.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; @@ -11,13 +12,9 @@ class SessionStorageComposerDatasourceImpl @override ComposerCache getComposerCacheOnWeb() { try { - final result = html.window.sessionStorage.entries - .where((e) => e.key == EmailActionType.edit.name) - .toList(); - if (result.isNotEmpty) { - final jsonHandle = - json.decode(result.first.value) as Map; - final emailCache = ComposerCache.fromJson(jsonHandle); + final result = html.window.sessionStorage.entries.firstWhereOrNull((e) => e.key == EmailActionType.reopenComposerBrowser.name); + if (result != null) { + final emailCache = ComposerCache.fromJson(jsonDecode(result.value)); return emailCache; } else { throw NotFoundInWebSessionException(); @@ -30,8 +27,7 @@ class SessionStorageComposerDatasourceImpl @override void removeComposerCacheOnWeb() { try { - html.window.sessionStorage - .removeWhere((key, value) => key == EmailActionType.edit.name); + html.window.sessionStorage.removeWhere((key, value) => key == EmailActionType.reopenComposerBrowser.name); } catch (e) { throw NotFoundInWebSessionException(errorMessage: e.toString()); } @@ -41,7 +37,7 @@ class SessionStorageComposerDatasourceImpl void saveComposerCacheOnWeb(Email email) { try { Map entries = { - EmailActionType.edit.name: json.encode(email.toJson()) + EmailActionType.reopenComposerBrowser.name: email.asString() }; html.window.sessionStorage.addAll(entries); } catch (e) { 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 055646117..e044de9ab 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -204,7 +204,8 @@ class MailboxDashBoardController extends ReloadableController { ComposerArguments? composerArguments; NavigationRouter? navigationRouter; - late StreamSubscription _emailReceiveManagerStreamSubscription; + late StreamSubscription _emailAddressStreamSubscription; + late StreamSubscription _emailContentStreamSubscription; late StreamSubscription _fileReceiveManagerStreamSubscription; final StreamController> _progressStateController = @@ -267,19 +268,7 @@ class MailboxDashBoardController extends ReloadableController { (failure) {}, (success) { if(success is GetComposerCacheSuccess){ - final ComposerArguments composerArguments = ComposerArguments( - emailActionType: EmailActionType.edit, - presentationEmail: PresentationEmail( - id: success.composerCache.id, - subject: success.composerCache.subject, - from: success.composerCache.from, - to: success.composerCache.to, - cc: success.composerCache.cc, - bcc: success.composerCache.bcc, - ), - emailContents: success.composerCache.emailContentList.asHtmlString, - ); - openComposerOverlay(composerArguments); + openComposerOverlay(ComposerArguments.fromSessionStorageBrowser(success.composerCache)); } }, ); @@ -373,49 +362,33 @@ class MailboxDashBoardController extends ReloadableController { } void _registerPendingEmailAddress() { - _emailReceiveManagerStreamSubscription = - _emailReceiveManager.pendingEmailAddressInfo.stream.listen((emailAddress) { - log('MailboxDashBoardController::_registerPendingEmailAddress(): ${emailAddress?.email}'); - if (emailAddress != null && emailAddress.email?.isNotEmpty == true) { - _emailReceiveManager.clearPendingEmailAddress(); - final arguments = ComposerArguments( - emailActionType: EmailActionType.composeFromEmailAddress, - emailAddress: emailAddress, - mailboxRole: selectedMailbox.value?.role); - goToComposer(arguments); - } - }); + _emailAddressStreamSubscription = + _emailReceiveManager.pendingEmailAddressInfo.stream.listen((emailAddress) { + if (emailAddress?.email?.isNotEmpty == true) { + _emailReceiveManager.clearPendingEmailAddress(); + goToComposer(ComposerArguments.fromEmailAddress(emailAddress!)); + } + }); } void _registerPendingEmailContents() { - _emailReceiveManagerStreamSubscription = + _emailContentStreamSubscription = _emailReceiveManager.pendingEmailContentInfo.stream.listen((emailContent) { - log('MailboxDashBoardController::_registerPendingEmailContents(): ${emailContent?.content}'); - if (emailContent != null && emailContent.content.isNotEmpty == true) { + if (emailContent?.content.isNotEmpty == true) { _emailReceiveManager.clearPendingEmailContent(); - final arguments = ComposerArguments( - emailActionType: EmailActionType.edit, - emailContents: [emailContent].asHtmlString, - mailboxRole: selectedMailbox.value?.role); - goToComposer(arguments); - } - }); + goToComposer(ComposerArguments.fromContentShared([emailContent!].asHtmlString)); + } + }); } void _registerPendingFileInfo() { _fileReceiveManagerStreamSubscription = - _emailReceiveManager.pendingFileInfo.stream.listen((listFile) { - log('MailboxDashBoardController::_registerPendingFileInfo(): ${listFile.length}'); - if (listFile.isNotEmpty && sessionCurrent != null) { - _emailReceiveManager.clearPendingFileInfo(); - final arguments = ComposerArguments( - emailActionType: EmailActionType.edit, - mailboxRole: selectedMailbox.value?.role, - listSharedMediaFile: listFile, - ); - goToComposer(arguments); - } - }); + _emailReceiveManager.pendingFileInfo.stream.listen((listFile) { + if (listFile.isNotEmpty) { + _emailReceiveManager.clearPendingFileInfo(); + goToComposer(ComposerArguments.fromFileShared(listFile)); + } + }); } void _registerStreamListener() { @@ -2071,11 +2044,14 @@ class MailboxDashBoardController extends ReloadableController { void selectAllEmailAction() { dispatchAction(SelectionAllEmailAction()); } + + String get baseDownloadUrl => sessionCurrent?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl) ?? ''; @override void onClose() { _emailReceiveManager.closeEmailReceiveManagerStream(); - _emailReceiveManagerStreamSubscription.cancel(); + _emailAddressStreamSubscription.cancel(); + _emailContentStreamSubscription.cancel(); _fileReceiveManagerStreamSubscription.cancel(); _progressStateController.close(); _refreshActionEventController.close(); diff --git a/lib/features/manage_account/data/datasource_impl/identity_data_source_impl.dart b/lib/features/manage_account/data/datasource_impl/identity_data_source_impl.dart index e271d5151..7cbf82872 100644 --- a/lib/features/manage_account/data/datasource_impl/identity_data_source_impl.dart +++ b/lib/features/manage_account/data/datasource_impl/identity_data_source_impl.dart @@ -62,7 +62,7 @@ class IdentityDataSourceImpl extends IdentityDataSource { Future transformHtmlSignature(String signature) { return Future.sync(() async { final signatureUnescape = await _htmlTransform.transformToHtml( - contentHtml: signature, + htmlContent: signature, transformConfiguration: TransformConfiguration.create(customDomTransformers: [ const RemoveScriptTransformer(), const BlockQuotedTransformer(), diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index 7d3b22dc3..b7b729606 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -664,7 +664,7 @@ class SearchEmailController extends BaseController switch(actionType) { case EmailActionType.preview: if (mailboxContain?.isDrafts == true) { - editEmail(selectedEmail); + editDraftEmail(selectedEmail); } else { previewEmail(selectedEmail); } diff --git a/lib/features/sending_queue/presentation/sending_queue_controller.dart b/lib/features/sending_queue/presentation/sending_queue_controller.dart index a5a6642c5..c1c9112b4 100644 --- a/lib/features/sending_queue/presentation/sending_queue_controller.dart +++ b/lib/features/sending_queue/presentation/sending_queue_controller.dart @@ -10,8 +10,6 @@ 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/user_name.dart'; -import 'package:model/email/email_action_type.dart'; -import 'package:model/extensions/list_email_content_extension.dart'; import 'package:model/mailbox/select_mode.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.dart'; @@ -270,16 +268,7 @@ class SendingQueueController extends BaseController with MessageDialogActionMixi void _editSendingEmailAction(SendingEmail sendingEmail) { disableSelectionMode(); - - final arguments = ComposerArguments( - emailActionType: EmailActionType.edit, - presentationEmail: sendingEmail.presentationEmail, - mailboxRole: dashboardController?.selectedMailbox.value?.role, - emailContents: sendingEmail.presentationEmail.emailContentList.asHtmlString, - sendingEmail: sendingEmail - ); - - dashboardController?.goToComposer(arguments); + dashboardController?.goToComposer(ComposerArguments.fromSendingEmail(sendingEmail)); } void _resendSendingEmailAction(List listSendingEmails) async { diff --git a/lib/features/thread/presentation/mixin/email_action_controller.dart b/lib/features/thread/presentation/mixin/email_action_controller.dart index 25b39d71c..faa25cfa4 100644 --- a/lib/features/thread/presentation/mixin/email_action_controller.dart +++ b/lib/features/thread/presentation/mixin/email_action_controller.dart @@ -37,13 +37,8 @@ mixin EmailActionController { final responsiveUtils = Get.find(); final imagePaths = Get.find(); - void editEmail(PresentationEmail presentationEmail) { - final arguments = ComposerArguments( - emailActionType: EmailActionType.edit, - presentationEmail: presentationEmail, - mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role); - - mailboxDashBoardController.goToComposer(arguments); + void editDraftEmail(PresentationEmail presentationEmail) { + mailboxDashBoardController.goToComposer(ComposerArguments.editDraftEmail(presentationEmail)); } void previewEmail(PresentationEmail presentationEmail) { diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 4f11a7a8a..9e9c6b961 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -873,7 +873,7 @@ class ThreadController extends BaseController with EmailActionController { switch(actionType) { case EmailActionType.preview: if (mailboxContain?.isDrafts == true) { - editEmail(selectedEmail); + editDraftEmail(selectedEmail); } else { previewEmail(selectedEmail); } diff --git a/lib/main/bindings/network/network_bindings.dart b/lib/main/bindings/network/network_bindings.dart index f8684bd00..fbe688ed9 100644 --- a/lib/main/bindings/network/network_bindings.dart +++ b/lib/main/bindings/network/network_bindings.dart @@ -110,7 +110,7 @@ class NetworkBindings extends Bindings { void _bindingTransformer() { Get.put(const HtmlEscape()); - Get.put(HtmlTransform(Get.find())); - Get.put(HtmlAnalyzer(Get.find(), Get.find())); + Get.put(HtmlTransform(Get.find(), Get.find())); + Get.put(HtmlAnalyzer(Get.find())); } } \ No newline at end of file diff --git a/model/lib/email/email_action_type.dart b/model/lib/email/email_action_type.dart index 161ee58fb..ef5bdf4fb 100644 --- a/model/lib/email/email_action_type.dart +++ b/model/lib/email/email_action_type.dart @@ -9,8 +9,12 @@ enum EmailActionType { markAsStarred, unMarkAsStarred, moveToMailbox, - edit, + editDraft, + editSendingEmail, + composeFromContentShared, + composeFromFileShared, composeFromEmailAddress, + reopenComposerBrowser, moveToTrash, deletePermanently, preview, diff --git a/model/lib/extensions/email_extension.dart b/model/lib/extensions/email_extension.dart index e6b4f119b..9d440f07f 100644 --- a/model/lib/extensions/email_extension.dart +++ b/model/lib/extensions/email_extension.dart @@ -19,6 +19,8 @@ extension EmailExtension on Email { bool get hasMdnSent => keywords?.containsKey(KeyWordIdentifier.mdnSent) == true; + bool get isDraft => keywords?.containsKey(KeyWordIdentifier.emailDraft) == true; + bool get withAttachments => hasAttachment == true; bool hasReadReceipt(Map mapMailbox) { @@ -127,6 +129,13 @@ extension EmailExtension on Email { return []; } + List get attachmentsWithCid { + return attachments + ?.where((element) => element.disposition != null && element.cid?.isNotEmpty == true) + .map((item) => item.toAttachment()) + .toList() ?? []; + } + PresentationMailbox? findMailboxContain(Map mapMailbox) { final newMailboxIds = mailboxIds; newMailboxIds?.removeWhere((key, value) => !value); diff --git a/model/lib/extensions/list_attachment_extension.dart b/model/lib/extensions/list_attachment_extension.dart index dd8f7ed72..8f3b715ab 100644 --- a/model/lib/extensions/list_attachment_extension.dart +++ b/model/lib/extensions/list_attachment_extension.dart @@ -1,4 +1,6 @@ -import 'package:model/model.dart'; + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:model/email/attachment.dart'; extension ListAttachmentExtension on List { @@ -20,4 +22,15 @@ extension ListAttachmentExtension on List { return where((attachment) => attachment.hasCid()) .toList(); } + + Map toMapCidImageDownloadUrl({ + required AccountId accountId, + required String downloadUrl + }) { + final mapUrlDownloadCID = { + for (var attachment in listAttachmentsDisplayedInContent) + attachment.cid! : attachment.getDownloadUrl(downloadUrl, accountId) + }; + return mapUrlDownloadCID; + } } \ No newline at end of file diff --git a/model/lib/extensions/list_email_address_extension.dart b/model/lib/extensions/list_email_address_extension.dart index 21062319a..41be64533 100644 --- a/model/lib/extensions/list_email_address_extension.dart +++ b/model/lib/extensions/list_email_address_extension.dart @@ -29,9 +29,9 @@ extension ListEmailAddressExtension on Set? { int numberEmailAddress() => this != null ? this!.length : 0; - List filterEmailAddress(EmailAddress emailAddressNotExist) { + List filterEmailAddress(String emailAddressNotExist) { return this != null - ? this!.where((emailAddress) => emailAddress.email != emailAddressNotExist.email).toList() + ? this!.where((emailAddress) => emailAddress.email != emailAddressNotExist).toList() : List.empty(); } } \ 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 82f81d092..a14b36733 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -110,10 +110,10 @@ extension PresentationEmailExtension on PresentationEmail { return allEmailAddress.isNotEmpty ? allEmailAddress.join(', ') : ''; } - Tuple3, List, List> generateRecipientsEmailAddressForComposer( - EmailActionType? emailActionType, - Role? mailboxRole - ) { + Tuple3, List, List> generateRecipientsEmailAddressForComposer({ + required EmailActionType emailActionType, + Role? mailboxRole + }) { switch(emailActionType) { case EmailActionType.reply: if (mailboxRole == PresentationMailbox.roleSent) { @@ -128,10 +128,8 @@ extension PresentationEmailExtension on PresentationEmail { } else { return Tuple3(to.asList() + from.asList(), cc.asList(), bcc.asList()); } - case EmailActionType.edit: - return Tuple3(to.asList(), cc.asList(), bcc.asList()); default: - return const Tuple3([], [], []); + return Tuple3(to.asList(), cc.asList(), bcc.asList()); } }