From 4e0e92c35b837eeb483ad87bd3b67ac398c879ba Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 31 Mar 2025 10:01:25 +0700 Subject: [PATCH] Hot fix after some reload minimized composer should be lost body Signed-off-by: dab246 --- .../dom/hide_draft_signature_transformer.dart | 2 + core/lib/utils/string_convert.dart | 13 + .../repository/composer_repository_impl.dart | 5 +- .../repository/composer_repository.dart | 2 + ...save_composer_cache_on_web_interactor.dart | 3 + .../presentation/composer_bindings.dart | 1 + .../presentation/composer_controller.dart | 744 ++------- .../composer/presentation/composer_view.dart | 24 +- .../presentation/composer_view_web.dart | 42 +- ..._signature_in_email_content_extension.dart | 39 + .../setup_email_attachments_extension.dart | 67 + .../setup_email_content_extension.dart | 223 +++ .../setup_email_important_flag_extension.dart | 21 + ...etup_email_other_components_extension.dart | 25 + .../setup_email_recipients_extension.dart | 73 + ...l_request_read_receipt_flag_extension.dart | 31 + ...receipt_flag_for_edit_draft_extension.dart | 9 + .../setup_email_subject_extension.dart | 44 + .../setup_list_identities_extension.dart | 41 + .../setup_selected_identity_extension.dart | 72 + .../model/create_email_request.dart | 9 + .../presentation/model/saved_email_draft.dart | 33 +- .../presentation/model/signature_status.dart | 4 - .../view/mobile/mobile_editor_view.dart | 7 +- .../view/web/web_editor_view.dart | 19 +- .../email/data/local/html_analyzer.dart | 33 +- .../domain/state/get_email_content_state.dart | 4 +- .../controller/single_email_controller.dart | 10 +- .../model/composer_arguments.dart | 18 + .../data/model/composer_cache.dart | 19 +- model/lib/email/attachment.dart | 21 + model/lib/email/email_content.dart | 28 +- .../composer_controller_test.dart | 1345 +++-------------- .../model/saved_email_draft_test.dart | 15 +- 34 files changed, 1144 insertions(+), 1902 deletions(-) create mode 100644 lib/features/composer/presentation/extensions/sanitize_signature_in_email_content_extension.dart create mode 100644 lib/features/composer/presentation/extensions/setup_email_attachments_extension.dart create mode 100644 lib/features/composer/presentation/extensions/setup_email_content_extension.dart create mode 100644 lib/features/composer/presentation/extensions/setup_email_important_flag_extension.dart create mode 100644 lib/features/composer/presentation/extensions/setup_email_other_components_extension.dart create mode 100644 lib/features/composer/presentation/extensions/setup_email_recipients_extension.dart create mode 100644 lib/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_extension.dart create mode 100644 lib/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_for_edit_draft_extension.dart create mode 100644 lib/features/composer/presentation/extensions/setup_email_subject_extension.dart create mode 100644 lib/features/composer/presentation/extensions/setup_list_identities_extension.dart create mode 100644 lib/features/composer/presentation/extensions/setup_selected_identity_extension.dart delete mode 100644 lib/features/composer/presentation/model/signature_status.dart diff --git a/core/lib/presentation/utils/html_transformer/dom/hide_draft_signature_transformer.dart b/core/lib/presentation/utils/html_transformer/dom/hide_draft_signature_transformer.dart index 572cbc221..3eb9ab930 100644 --- a/core/lib/presentation/utils/html_transformer/dom/hide_draft_signature_transformer.dart +++ b/core/lib/presentation/utils/html_transformer/dom/hide_draft_signature_transformer.dart @@ -19,6 +19,8 @@ class HideDraftSignatureTransformer extends DomTransformer { final currentStyle = signature.attributes['style']?.trim(); if (currentStyle == null) { signature.attributes['style'] = 'display: none;'; + } else if (currentStyle.contains('display: block;') == true) { + signature.attributes['style'] = currentStyle.replaceFirst('display: block;', 'display: none;'); } else if (currentStyle.endsWith(';')) { signature.attributes['style'] = '$currentStyle display: none;'; } else { diff --git a/core/lib/utils/string_convert.dart b/core/lib/utils/string_convert.dart index 6b87a44d5..6da89081a 100644 --- a/core/lib/utils/string_convert.dart +++ b/core/lib/utils/string_convert.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:core/utils/app_logger.dart'; import 'package:core/domain/exceptions/string_exception.dart'; +import 'package:html/parser.dart'; import 'package:http_parser/http_parser.dart'; class StringConvert { @@ -107,4 +108,16 @@ class StringConvert { return null; } } + + static String getContentOriginal(String content) { + try { + final emailDocument = parse(content); + final contentOriginal = emailDocument.body?.innerHtml ?? content; + log('StringConvert::getContentOriginal:contentOriginal = $contentOriginal'); + return contentOriginal; + } catch (e) { + logError('StringConvert::getContentOriginal:Exception = $e'); + return content; + } + } } diff --git a/lib/features/composer/data/repository/composer_repository_impl.dart b/lib/features/composer/data/repository/composer_repository_impl.dart index 6cfc4f016..ba5160561 100644 --- a/lib/features/composer/data/repository/composer_repository_impl.dart +++ b/lib/features/composer/data/repository/composer_repository_impl.dart @@ -63,7 +63,7 @@ class ComposerRepositoryImpl extends ComposerRepository { emailContent = tupleContentInlineAttachments.value1; emailAttachments.addAll(tupleContentInlineAttachments.value2); - emailContent = await _removeCollapsedExpandedSignatureEffect(emailContent: emailContent); + emailContent = await removeCollapsedExpandedSignatureEffect(emailContent: emailContent); final userAgent = await _applicationManager.generateApplicationUserAgent(); final emailBodyPartId = PartId(_uuid.v1()); @@ -104,7 +104,8 @@ class ComposerRepositoryImpl extends ComposerRepository { } } - Future _removeCollapsedExpandedSignatureEffect({required String emailContent}) { + @override + Future removeCollapsedExpandedSignatureEffect({required String emailContent}) { try { return _htmlDataSource.removeCollapsedExpandedSignatureEffect(emailContent: emailContent); } catch (e) { diff --git a/lib/features/composer/domain/repository/composer_repository.dart b/lib/features/composer/domain/repository/composer_repository.dart index 767104011..5a28d848d 100644 --- a/lib/features/composer/domain/repository/composer_repository.dart +++ b/lib/features/composer/domain/repository/composer_repository.dart @@ -15,4 +15,6 @@ abstract class ComposerRepository { Future uploadAttachment(FileInfo fileInfo, Uri uploadUri, {CancelToken? cancelToken}); Future downloadImageAsBase64(String url, String cid, FileInfo fileInfo, {double? maxWidth, bool? compress}); + + Future removeCollapsedExpandedSignatureEffect({required String emailContent}); } \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart b/lib/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart index 7e11038a5..27ea726c6 100644 --- a/lib/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart +++ b/lib/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart @@ -39,6 +39,9 @@ class SaveComposerCacheOnWebInteractor { displayMode: createEmailRequest.displayMode, composerIndex: createEmailRequest.composerIndex, composerId: createEmailRequest.composerId, + draftHash: createEmailRequest.savedDraftHash, + actionType: createEmailRequest.savedActionType, + draftEmailId: createEmailRequest.draftsEmailId, )); return Right(SaveComposerCacheSuccess()); } catch (exception) { diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index 7864fe746..dee60c914 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -329,6 +329,7 @@ class ComposerBindings extends BaseBindings { Get.find(tag: composerId), Get.find(tag: composerId), Get.find(tag: composerId), + Get.find(tag: composerId), composerId: composerId, composerArgs: composerArguments, ), tag: composerId); diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 2aaf68de1..5e07cd079 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:math'; -import 'package:collection/collection.dart'; import 'package:core/core.dart'; import 'package:custom_pop_up_menu/custom_pop_up_menu.dart'; import 'package:dartz/dartz.dart'; @@ -15,21 +14,16 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:html/parser.dart'; import 'package:html_editor_enhanced/html_editor.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; import 'package:model/model.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; -import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; -import 'package:server_settings/server_settings/tmail_server_settings_extension.dart'; import 'package:super_tag_editor/tag_editor.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/before_reconnect_handler.dart'; @@ -39,9 +33,9 @@ import 'package:tmail_ui_user/features/base/state/base_ui_state.dart'; import 'package:tmail_ui_user/features/base/state/button_state.dart'; import 'package:tmail_ui_user/features/composer/domain/exceptions/compose_email_exception.dart'; import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.dart'; +import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; import 'package:tmail_ui_user/features/composer/domain/state/download_image_as_base64_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/restore_email_inline_images_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; @@ -63,12 +57,20 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/get_outb import 'package:tmail_ui_user/features/composer/presentation/extensions/get_sent_mailbox_id_for_composer_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_message_failure_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart'; -import 'package:tmail_ui_user/features/composer/presentation/extensions/list_shared_media_file_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/sanitize_signature_in_email_content_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_attachments_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_content_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_important_flag_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_other_components_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_recipients_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_subject_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_list_identities_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_selected_identity_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/update_screen_display_mode_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/mixin/drag_drog_file_mixin.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/saved_email_draft.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/signature_status.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; @@ -77,16 +79,13 @@ import 'package:tmail_ui_user/features/composer/presentation/view/editor_view_mi import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from_composer_bottom_sheet_builder.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/saving_message_dialog_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/sending_message_dialog_view.dart'; -import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/print_email_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/extensions/presentation_email_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; -import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_by_id_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/open_and_close_composer_extension.dart'; @@ -96,8 +95,6 @@ import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_id import 'package:tmail_ui_user/features/manage_account/presentation/extensions/identity_extension.dart'; import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart' if (dart.library.html) 'package:tmail_ui_user/features/network_connection/presentation/web_network_connection_controller.dart'; -import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; -import 'package:tmail_ui_user/features/server_settings/domain/state/get_server_setting_state.dart'; import 'package:tmail_ui_user/features/server_settings/domain/usecases/get_server_setting_interactor.dart'; import 'package:tmail_ui_user/features/upload/domain/exceptions/pick_file_exception.dart'; import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_extension.dart'; @@ -110,10 +107,10 @@ import 'package:tmail_ui_user/features/upload/domain/state/local_image_picker_st import 'package:tmail_ui_user/features/upload/domain/usecases/local_file_picker_interactor.dart'; import 'package:tmail_ui_user/features/upload/domain/usecases/local_image_picker_interactor.dart'; import 'package:tmail_ui_user/features/upload/presentation/controller/upload_controller.dart'; -import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/universal_import/html_stub.dart' as html; +import 'package:tmail_ui_user/main/utils/app_config.dart'; class ComposerController extends BaseController with DragDropFileMixin, AutoCompleteResultMixin, EditorViewMixin @@ -156,13 +153,14 @@ class ComposerController extends BaseController final CreateNewAndSendEmailInteractor _createNewAndSendEmailInteractor; final CreateNewAndSaveEmailToDraftsInteractor _createNewAndSaveEmailToDraftsInteractor; final PrintEmailInteractor printEmailInteractor; + final ComposerRepository _composerRepository; final String? composerId; final ComposerArguments? composerArgs; GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; GetDeviceContactSuggestionsInteractor? _getDeviceContactSuggestionsInteractor; - RestoreEmailInlineImagesInteractor? _restoreEmailInlineImagesInteractor; + RestoreEmailInlineImagesInteractor? restoreEmailInlineImagesInteractor; List listToEmailAddress = []; List listCcEmailAddress = []; @@ -214,24 +212,31 @@ class ComposerController extends BaseController List initialAttachments = []; String? _textEditorWeb; double? maxWithEditor; - EmailId? _emailIdEditing; + EmailId? emailIdEditing; bool isAttachmentCollapsed = false; ButtonState _closeComposerButtonState = ButtonState.enabled; ButtonState _saveToDraftButtonState = ButtonState.enabled; ButtonState _sendButtonState = ButtonState.enabled; ButtonState printDraftButtonState = ButtonState.enabled; - SignatureStatus _identityContentOnOpenPolicy = SignatureStatus.editedAvailable; int? _savedEmailDraftHash; - bool _restoringSignatureButton = false; + bool restoringSignatureButton = false; + bool synchronizeInitDraftHash = false; GlobalKey? responsiveContainerKey; - Worker? emailContentViewStateWorker; - - @visibleForTesting - bool get restoringSignatureButton => _restoringSignatureButton; + EmailActionType? currentEmailActionType; + EmailActionType? savedActionType; + int minInputLengthAutocomplete = AppConfig.defaultMinInputLengthAutocomplete; @visibleForTesting int? get savedEmailDraftHash => _savedEmailDraftHash; + GetEmailContentInteractor get getEmailContentInteractor => _getEmailContentInteractor; + + GetServerSettingInteractor get getServerSettingInteractor => _getServerSettingInteractor; + + GetAllIdentitiesInteractor get getAllIdentitiesInteractor => _getAllIdentitiesInteractor; + + TransformHtmlEmailContentInteractor get transformHtmlEmailContentInteractor => _transformHtmlEmailContentInteractor; + late Worker uploadInlineImageWorker; late Worker dashboardViewStateWorker; late bool _isEmailBodyLoaded; @@ -250,6 +255,7 @@ class ComposerController extends BaseController this._createNewAndSendEmailInteractor, this._createNewAndSaveEmailToDraftsInteractor, this.printEmailInteractor, + this._composerRepository, { this.composerId, this.composerArgs, @@ -262,6 +268,7 @@ class ComposerController extends BaseController if (PlatformInfo.isWeb) { responsiveContainerKey = GlobalKey(); richTextWebController = getBinding(tag: composerId); + restoreEmailInlineImagesInteractor = getBinding(tag: composerId); menuMoreOptionController = CustomPopupMenuController(); } else { richTextMobileTabletController = getBinding(tag: composerId); @@ -270,6 +277,7 @@ class ComposerController extends BaseController scrollControllerEmailAddress.addListener(_scrollControllerEmailAddressListener); _listenStreamEvent(); _beforeReconnectManager.addListener(onBeforeReconnect); + _injectBinding(); } @override @@ -277,7 +285,7 @@ class ComposerController extends BaseController if (PlatformInfo.isWeb) { _triggerBrowserEventListener(); } - _initEmail(); + setupComposer(); if (PlatformInfo.isMobile) { Future.delayed(const Duration(milliseconds: 500), _checkContactPermission); } @@ -287,6 +295,12 @@ class ComposerController extends BaseController @override void onClose() { _textEditorWeb = null; + savedActionType = null; + _savedEmailDraftHash = null; + currentEmailActionType = null; + emailIdEditing = null; + maxWithEditor = null; + initialAttachments.clear(); dispatchState(Right(UIClosedState())); composerArguments.value = null; emailContentsViewState.value = Right(UIClosedState()); @@ -304,12 +318,9 @@ class ComposerController extends BaseController responsiveContainerKey = null; menuMoreOptionController?.dispose(); menuMoreOptionController = null; - emailContentViewStateWorker?.dispose(); - emailContentViewStateWorker = null; } else { richTextMobileTabletController = null; } - _identityContentOnOpenPolicy = SignatureStatus.editedAvailable; super.onClose(); } @@ -352,20 +363,10 @@ class ComposerController extends BaseController @override void handleSuccessViewState(Success success) { - super.handleSuccessViewState(success); - if (success is GetEmailContentLoading || - success is TransformHtmlEmailContentLoading || - success is TransformHtmlEmailContentSuccess || - success is RestoringEmailInlineImages) { - emailContentsViewState.value = Right(success); - } else if (success is LocalFilePickerSuccess) { + if (success is LocalFilePickerSuccess) { _handlePickFileSuccess(success); } else if (success is LocalImagePickerSuccess) { _handlePickImageSuccess(success); - } else if (success is GetEmailContentSuccess) { - _getEmailContentSuccess(success); - } else if (success is GetEmailContentFromCacheSuccess) { - _getEmailContentOffLineSuccess(success); } else if (success is GetAllIdentitiesSuccess) { _handleGetAllIdentitiesSuccess(success); } else if (success is DownloadImageAsBase64Success) { @@ -376,49 +377,22 @@ class ComposerController extends BaseController richTextMobileTabletController?.insertImage(inlineImage); } maxWithEditor = null; - } else if (success is GetServerSettingSuccess) { - hasRequestReadReceipt.value = success.settingOption.isAlwaysReadReceipts; - _initEmailDraftHash(); - } else if (success is RestoreEmailInlineImagesSuccess) { - _updateEditorContent(success); + } else { + super.handleSuccessViewState(success); } } - void _updateEditorContent(RestoreEmailInlineImagesSuccess success) { - richTextWebController?.editorController.setText(success.emailContent); - consumeState(Stream.value(Right(GetEmailContentSuccess(htmlEmailContent: success.emailContent)))); - } - @override void handleFailureViewState(Failure failure) { - super.handleFailureViewState(failure); if (failure is LocalFilePickerFailure) { _handlePickFileFailure(failure); } else if (failure is LocalImagePickerFailure) { _handlePickImageFailure(failure); - } else if (failure is GetEmailContentFailure) { - _handleGetEmailContentFailure(failure); - } else if (failure is TransformHtmlEmailContentFailure - || failure is RestoreEmailInlineImagesFailure) { - emailContentsViewState.value = Left(failure); - } else if (failure is GetAllIdentitiesFailure) { - if (identitySelected.value == null) { - _autoFocusFieldWhenLauncher(); - } - } else if (failure is GetServerSettingFailure) { - hasRequestReadReceipt.value = false; - _initEmailDraftHash(); + } else { + super.handleFailureViewState(failure); } } - @override - void handleUrgentExceptionOnMobile({Failure? failure, Exception? exception}) { - if (failure is GetAllIdentitiesFailure && exception is! BadCredentialsException) { - _handleGetAllIdentitiesFailure(); - } - super.handleUrgentExceptionOnMobile(failure: failure, exception: exception); - } - @override Future onUnloadBrowserListener(html.Event event) async { final username = mailboxDashBoardController.sessionCurrent?.username; @@ -442,39 +416,6 @@ class ComposerController extends BaseController } }); }); - - if (PlatformInfo.isWeb) { - emailContentViewStateWorker = ever(emailContentsViewState, (state) { - state?.fold((_) => null, (success) { - if (success is GetEmailContentSuccess) { - onChangeTextEditorWeb(success.htmlEmailContent); - } else if (success is TransformHtmlEmailContentSuccess) { - final arguments = composerArguments.value; - if (arguments == null || - currentContext == null || - arguments.presentationEmail == null) return; - - final emailActionType = arguments.emailActionType; - - if (emailActionType == EmailActionType.reply || - emailActionType == EmailActionType.replyAll || - emailActionType == EmailActionType.replyToList || - emailActionType == EmailActionType.forward) { - - final emailContentQuoted = getEmailContentQuotedAsHtml( - locale: Localizations.localeOf(currentContext!), - appLocalizations: AppLocalizations.of(currentContext!), - emailContent: success.htmlContent, - emailActionType: arguments.emailActionType, - presentationEmail: arguments.presentationEmail! - ); - - onChangeTextEditorWeb(emailContentQuoted); - } - } - }); - }); - } } void _triggerBrowserEventListener() { @@ -514,7 +455,7 @@ class ComposerController extends BaseController Future _saveComposerCacheOnWebAction() async { autoCreateEmailTag(); - final createEmailRequest = await _generateCreateEmailRequest(); + final createEmailRequest = await _generateCreateEmailRequestToSaveAsCache(); if (createEmailRequest == null) return; await _saveComposerCacheOnWebInteractor.execute( @@ -532,7 +473,7 @@ class ComposerController extends BaseController } } - Future _generateCreateEmailRequest() async { + Future _generateCreateEmailRequestToSaveAsCache() async { final arguments = composerArguments.value; final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; @@ -542,7 +483,12 @@ class ComposerController extends BaseController return null; } - final emailContent = await getContentInEditor(); + String emailContent = await getContentInEditor(); + if (currentEmailActionType == EmailActionType.compose) { + emailContent = await _composerRepository.removeCollapsedExpandedSignatureEffect( + emailContent: emailContent, + ); + } final uploadUri = _getUploadUriFromSession(session, accountId); final composerIndex = composerId != null @@ -578,6 +524,9 @@ class ComposerController extends BaseController uploadUri: uploadUri, composerIndex: composerIndex, composerId: composerId, + savedDraftHash: arguments.savedDraftHash ?? _savedEmailDraftHash, + savedActionType: savedActionType ?? currentEmailActionType, + savedEmailDraftId: emailIdEditing, ); } @@ -646,206 +595,47 @@ class ComposerController extends BaseController ); } - void onLoadCompletedMobileEditorAction(HtmlEditorApi editorApi, WebUri? url) async { + Future onLoadCompletedMobileEditorAction(HtmlEditorApi editorApi, WebUri? url) async { _isEmailBodyLoaded = true; - if (identitySelected.value == null) { - _getAllIdentities(); - } else { - await _selectIdentity(identitySelected.value); - _autoFocusFieldWhenLauncher(); - } + await setupSelectedIdentity(); + _autoFocusFieldWhenLauncher(); } - void _initEmail() { + void _injectBinding() { + injectAutoCompleteBindings( + mailboxDashBoardController.sessionCurrent, + mailboxDashBoardController.accountId.value, + ); + } + + Future setupComposer() async { _isEmailBodyLoaded = false; - final arguments = PlatformInfo.isWeb - ? composerArgs - : Get.arguments; - if (arguments is ComposerArguments) { - composerArguments.value = arguments; + final arguments = PlatformInfo.isWeb ? composerArgs : Get.arguments; - _initIdentities(arguments); + if (arguments is! ComposerArguments) return; - injectAutoCompleteBindings( - mailboxDashBoardController.sessionCurrent, - mailboxDashBoardController.accountId.value - ); + composerArguments.value = arguments; + currentEmailActionType = arguments.emailActionType; + savedActionType = arguments.savedActionType; + emailIdEditing = arguments.savedEmailDraftId; + emailContentsViewState.value = Right(GetEmailContentLoading()); - switch(arguments.emailActionType) { - case EmailActionType.editAsNewEmail: - _initEmailAddress( - presentationEmail: arguments.presentationEmail!, - actionType: EmailActionType.editAsNewEmail - ); - _initSubjectEmail( - presentationEmail: arguments.presentationEmail!, - actionType: EmailActionType.editAsNewEmail - ); - _getEmailContentOfEmailDrafts( - emailId: arguments.presentationEmail!.id!, - ); - isMarkAsImportant.value = arguments.presentationEmail!.isMarkAsImportant; - break; - case EmailActionType.editDraft: - _initEmailAddress( - presentationEmail: arguments.presentationEmail!, - actionType: EmailActionType.editDraft - ); - _initSubjectEmail( - presentationEmail: arguments.presentationEmail!, - actionType: EmailActionType.editDraft - ); - _getEmailContentOfEmailDrafts( - emailId: arguments.presentationEmail!.id!, - ); - _emailIdEditing = arguments.presentationEmail!.id!; - isMarkAsImportant.value = arguments.presentationEmail!.isMarkAsImportant; - break; - case EmailActionType.editSendingEmail: - _initEmailAddress( - presentationEmail: arguments.sendingEmail!.presentationEmail, - actionType: EmailActionType.editSendingEmail - ); - _initSubjectEmail( - presentationEmail: arguments.sendingEmail!.presentationEmail, - actionType: EmailActionType.editSendingEmail - ); - final allAttachments = arguments.sendingEmail!.email.allAttachments; - _initAttachmentsAndInlineImages( - attachments: allAttachments.getListAttachmentsDisplayedOutside( - arguments.sendingEmail!.email.htmlBodyAttachments), - inlineImages: allAttachments.listAttachmentsDisplayedInContent); + setupEmailSubject(arguments); + setupEmailRecipients(arguments); + setupEmailImportantFlag(arguments); + setupEmailAttachments(arguments); + setupEmailOtherComponents(arguments); + setupEmailRequestReadReceiptFlag(arguments); - _getEmailContentFromSendingEmail(arguments.sendingEmail!); - _emailIdEditing = arguments.sendingEmail!.presentationEmail.id!; - break; - case EmailActionType.composeFromContentShared: - _getEmailContentFromContentShared(arguments.emailContents!); - break; - case EmailActionType.composeFromFileShared: - _addAttachmentFromFileShare(arguments.listSharedMediaFile!); - break; - case EmailActionType.composeFromEmailAddress: - listToEmailAddress.addAll(arguments.listEmailAddress ?? []); - isInitialRecipient.value = true; - toAddressExpandMode.value = ExpandMode.COLLAPSE; - updateStatusEmailSendButton(); - break; - case EmailActionType.composeFromMailtoUri: - if (arguments.subject != null) { - setSubjectEmail(arguments.subject!); - subjectEmailInputController.text = arguments.subject!; - } - if (arguments.listEmailAddress?.isNotEmpty == true) { - listToEmailAddress.addAll(arguments.listEmailAddress!); - isInitialRecipient.value = true; - toAddressExpandMode.value = ExpandMode.COLLAPSE; - } - if (arguments.cc?.isNotEmpty == true) { - listCcEmailAddress = arguments.cc!; - ccRecipientState.value = PrefixRecipientState.enabled; - ccAddressExpandMode.value = ExpandMode.COLLAPSE; - } - if (arguments.bcc?.isNotEmpty == true) { - bccRecipientState.value = PrefixRecipientState.enabled; - bccAddressExpandMode.value = ExpandMode.COLLAPSE; - listBccEmailAddress = arguments.bcc!; - } - _getEmailContentFromMailtoUri(arguments.body ?? ''); - updateStatusEmailSendButton(); - break; - case EmailActionType.reply: - case EmailActionType.replyToList: - case EmailActionType.replyAll: - log('ComposerController::_initEmail:listPost = ${arguments.listPost}'); - _initEmailAddress( - presentationEmail: arguments.presentationEmail!, - actionType: arguments.emailActionType, - listPost: arguments.listPost, - ); - _initSubjectEmail( - presentationEmail: arguments.presentationEmail!, - actionType: arguments.emailActionType - ); - _initAttachmentsAndInlineImages( - attachments: arguments.attachments, - inlineImages: arguments.inlineImages); + await setupListIdentities(arguments); + await setupEmailContent(arguments); - _transformHtmlEmailContent(arguments.emailContents); - break; - case EmailActionType.forward: - _initSubjectEmail( - presentationEmail: arguments.presentationEmail!, - actionType: arguments.emailActionType - ); - _initAttachmentsAndInlineImages( - attachments: arguments.attachments, - inlineImages: arguments.inlineImages); - _transformHtmlEmailContent(arguments.emailContents); - break; - case EmailActionType.reopenComposerBrowser: - if (!PlatformInfo.isWeb) return; - - screenDisplayMode.value = arguments.displayMode; - isMarkAsImportant.value = arguments.isMarkAsImportant ?? false; - - _initEmailAddress( - presentationEmail: arguments.presentationEmail!, - actionType: EmailActionType.reopenComposerBrowser - ); - _initSubjectEmail( - presentationEmail: arguments.presentationEmail!, - actionType: EmailActionType.reopenComposerBrowser - ); - _initAttachmentsAndInlineImages( - attachments: arguments.attachments, - inlineImages: arguments.inlineImages); - - final accountId = mailboxDashBoardController.accountId.value; - dynamic downloadUrl; - try { - downloadUrl = mailboxDashBoardController.sessionCurrent - ?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl); - } catch (e) { - logError('ComposerController::_initEmail(): $e'); - downloadUrl = null; - } - - if (accountId == null || downloadUrl == null) return; - _getEmailContentFromSessionStorageBrowser( - htmlContent: arguments.emailContents ?? '', - inlineImages: arguments.inlineImages ?? [], - accountId: accountId, - downloadUrl: downloadUrl - ); - break; - case EmailActionType.composeFromUnsubscribeMailtoLink: - if (arguments.subject != null) { - setSubjectEmail(arguments.subject!); - subjectEmailInputController.text = arguments.subject!; - } - if (arguments.listEmailAddress?.isNotEmpty == true) { - listToEmailAddress.addAll(arguments.listEmailAddress!); - isInitialRecipient.value = true; - toAddressExpandMode.value = ExpandMode.COLLAPSE; - } - _getEmailContentFromUnsubscribeMailtoLink(arguments.body ?? ''); - updateStatusEmailSendButton(); - break; - default: - break; - } - - if (composerArguments.value?.emailActionType == EmailActionType.reopenComposerBrowser) { - log('ComposerController::_initEmail: hasRequestReadReceipt = ${arguments.hasRequestReadReceipt}'); - hasRequestReadReceipt.value = arguments.hasRequestReadReceipt ?? false; - } else if (composerArguments.value?.emailActionType != EmailActionType.editDraft) { - _getServerSetting(); - } + if (screenDisplayMode.value.isNotContentVisible()) { + await setupSelectedIdentityWithoutApplySignature(); } } - void _initSubjectEmail({ + void initSubjectEmail({ required PresentationEmail presentationEmail, required EmailActionType actionType }) { @@ -855,7 +645,7 @@ class ComposerController extends BaseController subjectEmailInputController.text = newSubject; } - void _initAttachmentsAndInlineImages({ + void initAttachmentsAndInlineImages({ List? attachments, List? inlineImages }) { @@ -868,28 +658,6 @@ class ComposerController extends BaseController } } - Identity? _selectIdentityFromId(IdentityId? identityId) { - if (identityId == null) return null; - - return listFromIdentities.firstWhereOrNull( - (identity) => identity.id == identityId); - } - - Future _initIdentities(ComposerArguments composerArguments) async { - listFromIdentities.value = composerArguments.identities ?? []; - final selectedIdentityFromId = _selectIdentityFromId( - composerArguments.selectedIdentityId); - if (listFromIdentities.isEmpty) { - _getAllIdentities(); - } else if (selectedIdentityFromId != null) { - await _selectIdentity(selectedIdentityFromId); - _initEmailDraftHash(); - } else if (composerArguments.identities?.isNotEmpty == true) { - await _selectIdentity(composerArguments.identities!.first); - _initEmailDraftHash(); - } - } - void _getAllIdentities() { log('ComposerController::_getAllIdentities: Fetch again identity !'); final accountId = mailboxDashBoardController.accountId.value; @@ -899,27 +667,14 @@ class ComposerController extends BaseController } } - void _handleGetAllIdentitiesSuccess(GetAllIdentitiesSuccess success) async { + void _handleGetAllIdentitiesSuccess(GetAllIdentitiesSuccess success) { final listIdentitiesMayDeleted = success.identities?.toListMayDeleted() ?? []; if (listIdentitiesMayDeleted.isNotEmpty) { listFromIdentities.value = listIdentitiesMayDeleted; - - if (identitySelected.value == null) { - final selectedIdentityFromId = _selectIdentityFromId( - composerArguments.value?.selectedIdentityId); - if (selectedIdentityFromId != null) { - await _selectIdentity(selectedIdentityFromId); - _initEmailDraftHash(); - } else { - await _selectIdentity(listIdentitiesMayDeleted.firstOrNull); - _initEmailDraftHash(); - } - } } - _autoFocusFieldWhenLauncher(); } - void _initEmailAddress({ + void initEmailAddress({ required PresentationEmail presentationEmail, required EmailActionType actionType, String? listPost, @@ -1322,19 +1077,18 @@ class ComposerController extends BaseController void _handlePickFileSuccess(LocalFilePickerSuccess success) { uploadController.validateTotalSizeAttachmentsBeforeUpload( totalSizePreparedFiles: success.pickedFiles.totalSize, - onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: success.pickedFiles) + onValidationSuccess: () => uploadAttachmentsAction(pickedFiles: success.pickedFiles) ); } void _handlePickImageSuccess(LocalImagePickerSuccess success) { uploadController.validateTotalSizeInlineAttachmentsBeforeUpload( totalSizePreparedFiles: success.fileInfo.fileSize, - onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: [success.fileInfo.withInline()]) + onValidationSuccess: () => uploadAttachmentsAction(pickedFiles: [success.fileInfo.withInline()]) ); } - - void _uploadAttachmentsAction({required List pickedFiles}) { + void uploadAttachmentsAction({required List pickedFiles}) { final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; if (session != null && accountId != null) { @@ -1345,11 +1099,11 @@ class ComposerController extends BaseController uploadUri: uploadUri, ); } catch (e) { - log('ComposerController::_uploadAttachmentsAction: $e'); + logError('ComposerController::uploadAttachmentsAction: $e'); uploadController.consumeState(Stream.value(Left(UploadAttachmentFailure(e, pickedFiles[0])))); } } else { - log('ComposerController::_uploadAttachmentsAction: SESSION OR ACCOUNT_ID is NULL'); + logError('ComposerController::uploadAttachmentsAction: SESSION OR ACCOUNT_ID is NULL'); } } @@ -1359,12 +1113,16 @@ class ComposerController extends BaseController Future _validateEmailChange() async { final newDraftHash = await _hashDraftEmail(); - + log('ComposerController::_validateEmailChange:newDraftHash = $newDraftHash | _savedEmailDraftHash = $_savedEmailDraftHash'); return _savedEmailDraftHash != newDraftHash; } Future _hashDraftEmail() async { - final emailContent = await getContentInEditor(); + String emailContent = await getContentInEditor(); + + emailContent = await _composerRepository.removeCollapsedExpandedSignatureEffect( + emailContent: emailContent, + ); final savedEmailDraft = SavedEmailDraft( subject: subjectEmail.value ?? '', @@ -1378,28 +1136,30 @@ class ComposerController extends BaseController hasReadReceipt: hasRequestReadReceipt.value, isMarkAsImportant: isMarkAsImportant.value, ); - - return savedEmailDraft.hashCode; + final draftAsString = savedEmailDraft.asString(); + final draftAsHasCode = draftAsString.hashCode; + log('ComposerController::_hashDraftEmail:draftAsString = $draftAsString | draftAsHasCode = $draftAsHasCode'); + return draftAsHasCode; } - int get emptyDraftEmailHash => SavedEmailDraft.empty().hashCode; - Future _updateSavedEmailDraftHash() async { _savedEmailDraftHash = await _hashDraftEmail(); } - Future _initEmailDraftHash() async { - final draftEmailHash = await _hashDraftEmail(); + Future initEmailDraftHash() async { + final currentDraftHash = await _hashDraftEmail(); - isEmailChanged.value = draftEmailHash != emptyDraftEmailHash; + final oldSavedDraftHash = composerArguments.value?.savedDraftHash; - final emailActionType = composerArguments.value?.emailActionType; - if (emailActionType == EmailActionType.compose - || emailActionType == EmailActionType.editDraft) { - _savedEmailDraftHash = draftEmailHash; - } else if (emailActionType == EmailActionType.reopenComposerBrowser) { - _savedEmailDraftHash = emptyDraftEmailHash; + if (currentEmailActionType == EmailActionType.compose || + currentEmailActionType == EmailActionType.editDraft) { + _savedEmailDraftHash = currentDraftHash; + } else if (currentEmailActionType == EmailActionType.reopenComposerBrowser) { + _savedEmailDraftHash = oldSavedDraftHash; } + log('ComposerController::initEmailDraftHash:oldSavedDraftHash = $oldSavedDraftHash | currentDraftHash = $currentDraftHash | _savedEmailDraftHash = $_savedEmailDraftHash'); + + isEmailChanged.value = currentDraftHash != _savedEmailDraftHash; } void handleClickSaveAsDraftsButton(BuildContext context) async { @@ -1433,18 +1193,18 @@ class ComposerController extends BaseController arguments: arguments, emailContent: emailContent, uploadUri: uploadUri, - draftEmailId: _emailIdEditing, + draftEmailId: emailIdEditing, cancelToken: cancelToken ); if (resultState is SaveEmailAsDraftsSuccess) { _saveToDraftButtonState = ButtonState.enabled; - _emailIdEditing = resultState.emailId; + emailIdEditing = resultState.emailId; mailboxDashBoardController.consumeState(Stream.value(Right(resultState))); _updateSavedEmailDraftHash(); } else if (resultState is UpdateEmailDraftsSuccess) { _saveToDraftButtonState = ButtonState.enabled; - _emailIdEditing = resultState.emailId; + emailIdEditing = resultState.emailId; mailboxDashBoardController.consumeState(Stream.value(Right(resultState))); _updateSavedEmailDraftHash(); } else if ((resultState is SaveEmailAsDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException) || @@ -1473,158 +1233,6 @@ class ComposerController extends BaseController } } - void _addAttachmentFromFileShare(List listSharedMediaFile) { - final listFileInfo = listSharedMediaFile.toListFileInfo(isShared: true); - - final tupleListFileInfo = partition(listFileInfo, (fileInfo) => fileInfo.isInline == true); - final listAttachments = tupleListFileInfo.value2; - - uploadController.validateTotalSizeAttachmentsBeforeUpload( - totalSizePreparedFiles: listFileInfo.totalSize, - totalSizePreparedFilesWithDispositionAttachment: listAttachments.totalSize, - onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo) - ); - } - - void _getEmailContentFromSendingEmail(SendingEmail sendingEmail) { - consumeState(Stream.value( - Right(GetEmailContentSuccess( - htmlEmailContent: sendingEmail.presentationEmail.emailContentList.asHtmlString, - emailCurrent: sendingEmail.email - )) - )); - } - - void _getEmailContentFromSessionStorageBrowser({ - required String htmlContent, - required List inlineImages, - required AccountId accountId, - required String downloadUrl - }) { - _restoreEmailInlineImagesInteractor = getBinding(tag: composerId); - if (_restoreEmailInlineImagesInteractor == null) return; - consumeState(_restoreEmailInlineImagesInteractor!.execute( - htmlContent: htmlContent, - transformConfiguration: TransformConfiguration.forRestoreEmail(), - mapUrlDownloadCID: inlineImages.toMapCidImageDownloadUrl( - accountId: accountId, - downloadUrl: downloadUrl))); - } - - void _getEmailContentFromContentShared(String content) { - consumeState(Stream.value(Right(GetEmailContentSuccess(htmlEmailContent: content)))); - } - - void _getEmailContentFromMailtoUri(String content) { - log('ComposerController::_getEmailContentFromMailtoUri:content: $content'); - consumeState(Stream.value(Right(GetEmailContentSuccess(htmlEmailContent: content)))); - } - - void _getEmailContentFromUnsubscribeMailtoLink(String content) { - log('ComposerController::_getEmailContentFromUnsubscribeMailtoLink:content: $content'); - consumeState(Stream.value(Right(GetEmailContentSuccess(htmlEmailContent: content)))); - } - - void _getEmailContentOfEmailDrafts({required EmailId emailId}) { - final session = mailboxDashBoardController.sessionCurrent; - final accountId = mailboxDashBoardController.accountId.value; - - if (session == null || accountId == null) { - consumeState(Stream.value(Left(GetEmailContentFailure(NotFoundSessionException())))); - return; - } - - try { - consumeState(_getEmailContentInteractor.execute( - session, - accountId, - emailId, - mailboxDashBoardController.baseDownloadUrl, - TransformConfiguration.forEditDraftsEmail(), - additionalProperties: Properties({ - IndividualHeaderIdentifier.identityHeader.value}), - )); - } catch (e) { - logError('ComposerController::_handleUploadInlineSuccess(): $e'); - consumeState(Stream.value(Left(GetEmailContentFailure(e)))); - } - } - - void _getEmailContentOffLineSuccess(GetEmailContentFromCacheSuccess success) { - _initAttachmentsAndInlineImages( - attachments: success.attachments, - inlineImages: success.inlineImages); - emailContentsViewState.value = Right(success); - - if (composerArguments.value?.emailActionType == EmailActionType.editDraft) { - _setUpRequestReadReceiptForDraftEmail(success.emailCurrent); - } - } - - void _getEmailContentSuccess(GetEmailContentSuccess success) { - _initAttachmentsAndInlineImages( - attachments: success.attachments, - inlineImages: success.inlineImages); - emailContentsViewState.value = Right(success); - - if (composerArguments.value?.emailActionType == EmailActionType.editDraft) { - _setUpRequestReadReceiptForDraftEmail(success.emailCurrent); - _restoreIdentityFromHeader(success.emailCurrent); - } - } - - void _restoreIdentityFromHeader(Email? email) { - final identityIdFromHeader = email?.identityIdFromHeader; - if (identityIdFromHeader == null) return; - final selectedIdentityFromHeader = _selectIdentityFromId(identityIdFromHeader); - if (selectedIdentityFromHeader == null) return; - identitySelected.value = selectedIdentityFromHeader; - - _initEmailDraftHash(); - } - - Future restoreCollapsibleButton(String? emailContent) async { - try { - if (emailContent == null) return; - final emailDocument = parse(emailContent); - - final existedSignatureButton = emailDocument.querySelector( - 'button.tmail-signature-button'); - if (existedSignatureButton != null) return; - - final signature = emailDocument.querySelector('div.tmail-signature'); - if (signature == null) return; - _restoringSignatureButton = true; - await _applySignature(signature.innerHtml); - } catch (e) { - logError('ComposerController::_restoreCollapsibleButton: $e'); - } - } - - 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.editDraft) { - return arguments.presentationEmail?.firstEmailAddressInFrom ?? ''; - } else { - return mailboxDashBoardController.sessionCurrent?.getOwnEmailAddress() ?? ''; - } - } - return ''; - } - void clearFocus(BuildContext context) { log('ComposerController::clearFocus:'); if (PlatformInfo.isMobile) { @@ -1804,24 +1412,11 @@ class ComposerController extends BaseController } } - Future _selectIdentity(Identity? newIdentity) async { + Future selectIdentity(Identity? newIdentity) async { final formerIdentity = identitySelected.value; identitySelected.value = newIdentity; if (newIdentity == null) return; - - final emailActionType = composerArguments.value?.emailActionType; - switch (emailActionType) { - case EmailActionType.reopenComposerBrowser: - case EmailActionType.editDraft: - if (_identityContentOnOpenPolicy == SignatureStatus.editedAvailable) { - _identityContentOnOpenPolicy = SignatureStatus.editedApplied; - } else { - await _applyIdentityForAllFieldComposer(formerIdentity, newIdentity); - } - break; - default: - await _applyIdentityForAllFieldComposer(formerIdentity, newIdentity); - } + await _applyIdentityForAllFieldComposer(formerIdentity, newIdentity); } Future _applyIdentityForAllFieldComposer( @@ -1840,7 +1435,7 @@ class ComposerController extends BaseController } if (newIdentity.signatureAsString.isNotEmpty == true) { - await _applySignature(newIdentity.signatureAsString.asSignatureHtml()); + await applySignature(newIdentity.signatureAsString.asSignatureHtml()); } if (PlatformInfo.isMobile) { @@ -1877,7 +1472,7 @@ class ComposerController extends BaseController updateStatusEmailSendButton(); } - Future _applySignature(String signature) async { + Future applySignature(String signature) async { if (PlatformInfo.isWeb) { richTextWebController?.editorController.insertSignature(signature); } else { @@ -1886,7 +1481,6 @@ class ComposerController extends BaseController } Future _removeSignature() async { - log('ComposerController::_removeSignature():'); if (PlatformInfo.isWeb) { richTextWebController?.editorController.removeSignature(); } else { @@ -2049,22 +1643,19 @@ class ComposerController extends BaseController return false; } - void handleInitHtmlEditorWeb(String initContent) async { + void handleInitHtmlEditorWeb(String initContent) { if (_isEmailBodyLoaded) return; - log('ComposerController::handleInitHtmlEditorWeb:'); _isEmailBodyLoaded = true; richTextWebController?.editorController.setFullScreen(); richTextWebController?.editorController.setOnDragDropEvent(); - onChangeTextEditorWeb(initContent); richTextWebController?.setEnableCodeView(); - if (identitySelected.value == null) { - _getAllIdentities(); - } else { - if (composerArguments.value?.emailActionType != EmailActionType.editDraft) { - await _selectIdentity(identitySelected.value); - } - _autoFocusFieldWhenLauncher(); - } + setTextEditorWeb(initContent); + } + + Future onInitialContentLoadCompleteWeb(String? initContent) async { + await restoreCollapsibleSignatureButton(initContent); + await setupSelectedIdentity(); + _autoFocusFieldWhenLauncher(); } void handleOnFocusHtmlEditorWeb() { @@ -2119,21 +1710,17 @@ class ComposerController extends BaseController String? get textEditorWeb => _textEditorWeb; + void setTextEditorWeb(String value) => _textEditorWeb = value; + HtmlEditorApi? get htmlEditorApi => richTextMobileTabletController?.htmlEditorApi; void onChangeTextEditorWeb(String? text) { _textEditorWeb = text; - _initEmailDraftHashAfterSignatureButtonRestored(text); - } - void _initEmailDraftHashAfterSignatureButtonRestored(String? emailContent) { - if (!_restoringSignatureButton) return; - final emailDocument = parse(emailContent); - final signatureButton = emailDocument.querySelector('button.tmail-signature-button'); - if (signatureButton == null) return; - - _restoringSignatureButton = false; - _initEmailDraftHash(); + if (restoringSignatureButton || + (currentEmailActionType == EmailActionType.compose && !synchronizeInitDraftHash)) { + synchronizeInitEmailDraftHash(text); + } } void setSubjectEmail(String subject) => subjectEmail.value = subject; @@ -2146,18 +1733,8 @@ class ComposerController extends BaseController ); } - Future _handleGetAllIdentitiesFailure() async { - if (composerArguments.value?.emailActionType == EmailActionType.editSendingEmail) { - final signatureContent = await htmlEditorApi?.getSignatureContent(); - log('ComposerController::_handleGetAllIdentitiesFailure:signatureContent: $signatureContent'); - if (signatureContent?.isNotEmpty == true) { - await _applySignature(signatureContent!); - } - } - } - Future onChangeIdentity(Identity? newIdentity) async { - await _selectIdentity(newIdentity); + await selectIdentity(newIdentity); } void _searchIdentities(String searchText) { @@ -2197,7 +1774,7 @@ class ComposerController extends BaseController _closeComposerButtonState = ButtonState.disabled; - if (composerArguments.value == null || !_isEmailBodyLoaded) { + if (_validateCloseComposerWithoutSave()) { log('ComposerController::handleClickCloseComposer: ARGUMENTS is NULL or EMAIL NOT LOADED'); _closeComposerButtonState = ButtonState.enabled; clearFocus(context); @@ -2220,6 +1797,18 @@ class ComposerController extends BaseController } } + bool _validateCloseComposerWithoutSave() { + if (composerArguments.value == null) return true; + + if (PlatformInfo.isWeb && + !_isEmailBodyLoaded && + !screenDisplayMode.value.isNotContentVisible()) return true; + + if (PlatformInfo.isMobile && !_isEmailBodyLoaded) return true; + + return false; + } + Future _showConfirmDialogSaveMessage(BuildContext context) async { await showConfirmDialogAction( context, @@ -2254,13 +1843,6 @@ class ComposerController extends BaseController ); } - void _getServerSetting() { - final accountId = mailboxDashBoardController.accountId.value; - if (accountId != null) { - consumeState(_getServerSettingInteractor.execute(accountId)); - } - } - void handleOnDragEnterHtmlEditorWeb(List? types) { if (types.validateFilesTransfer) { mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active; @@ -2293,7 +1875,7 @@ class ComposerController extends BaseController uploadController.validateTotalSizeAttachmentsBeforeUpload( totalSizePreparedFiles: listFileInfo.totalSize, totalSizePreparedFilesWithDispositionAttachment: listFileInfo.listAttachmentFiles.totalSize, - onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo) + onValidationSuccess: () => uploadAttachmentsAction(pickedFiles: listFileInfo) ); } @@ -2351,10 +1933,10 @@ class ComposerController extends BaseController } EmailId? getDraftEmailId() { - if (_emailIdEditing != null && - _emailIdEditing != composerArguments.value!.presentationEmail?.id) { - return _emailIdEditing; - } else if (composerArguments.value!.emailActionType == EmailActionType.editDraft) { + if (emailIdEditing != null && + emailIdEditing != composerArguments.value!.presentationEmail?.id) { + return emailIdEditing; + } else if (currentEmailActionType == EmailActionType.editDraft) { return composerArguments.value!.presentationEmail?.id; } else { return null; @@ -2456,22 +2038,6 @@ class ComposerController extends BaseController replyToRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; } - void _handleGetEmailContentFailure(GetEmailContentFailure failure) { - emailContentsViewState.value = Left(failure); - if (composerArguments.value?.emailActionType == EmailActionType.editDraft) { - _getServerSetting(); - } - } - - void _setUpRequestReadReceiptForDraftEmail(Email? email) { - if (email?.hasRequestReadReceipt == true) { - hasRequestReadReceipt.value = true; - _initEmailDraftHash(); - } else { - _getServerSetting(); - } - } - @override Future onBeforeReconnect() async { if (mailboxDashBoardController.accountId.value != null && @@ -2504,7 +2070,7 @@ class ComposerController extends BaseController uploadController.validateTotalSizeAttachmentsBeforeUpload( totalSizePreparedFiles: listFileInfo.totalSize, - onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo) + onValidationSuccess: () => uploadAttachmentsAction(pickedFiles: listFileInfo) ); } @@ -2519,4 +2085,8 @@ class ComposerController extends BaseController context, AppLocalizations.of(context).thisImageCannotBePastedIntoTheEditor); } + + void onCompleteSetupComposer() { + initEmailDraftHash(); + } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index fbd703b71..357ef5700 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -127,9 +127,7 @@ class ComposerView extends GetWidget { listEmailAddress: controller.listToEmailAddress, imagePaths: controller.imagePaths, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller - .mailboxDashBoardController - .minInputLengthAutocomplete, + minInputLengthAutocomplete: controller.minInputLengthAutocomplete, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, @@ -157,9 +155,7 @@ class ComposerView extends GetWidget { listEmailAddress: controller.listCcEmailAddress, imagePaths: controller.imagePaths, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller - .mailboxDashBoardController - .minInputLengthAutocomplete, + minInputLengthAutocomplete: controller.minInputLengthAutocomplete, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, @@ -186,9 +182,7 @@ class ComposerView extends GetWidget { listEmailAddress: controller.listBccEmailAddress, imagePaths: controller.imagePaths, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller - .mailboxDashBoardController - .minInputLengthAutocomplete, + minInputLengthAutocomplete: controller.minInputLengthAutocomplete, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, @@ -320,9 +314,7 @@ class ComposerView extends GetWidget { listEmailAddress: controller.listToEmailAddress, imagePaths: controller.imagePaths, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller - .mailboxDashBoardController - .minInputLengthAutocomplete, + minInputLengthAutocomplete: controller.minInputLengthAutocomplete, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, @@ -349,9 +341,7 @@ class ComposerView extends GetWidget { listEmailAddress: controller.listCcEmailAddress, imagePaths: controller.imagePaths, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller - .mailboxDashBoardController - .minInputLengthAutocomplete, + minInputLengthAutocomplete: controller.minInputLengthAutocomplete, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, @@ -373,9 +363,7 @@ class ComposerView extends GetWidget { listEmailAddress: controller.listBccEmailAddress, imagePaths: controller.imagePaths, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller - .mailboxDashBoardController - .minInputLengthAutocomplete, + minInputLengthAutocomplete: controller.minInputLengthAutocomplete, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index ef3e060e2..726292564 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -98,9 +98,7 @@ class ComposerView extends GetWidget { listEmailAddress: controller.listToEmailAddress, imagePaths: controller.imagePaths, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller - .mailboxDashBoardController - .minInputLengthAutocomplete, + minInputLengthAutocomplete: controller.minInputLengthAutocomplete, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, @@ -129,9 +127,7 @@ class ComposerView extends GetWidget { listEmailAddress: controller.listCcEmailAddress, imagePaths: controller.imagePaths, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller - .mailboxDashBoardController - .minInputLengthAutocomplete, + minInputLengthAutocomplete: controller.minInputLengthAutocomplete, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, @@ -156,9 +152,7 @@ class ComposerView extends GetWidget { listEmailAddress: controller.listBccEmailAddress, imagePaths: controller.imagePaths, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller - .mailboxDashBoardController - .minInputLengthAutocomplete, + minInputLengthAutocomplete: controller.minInputLengthAutocomplete, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, @@ -248,7 +242,7 @@ class ComposerView extends GetWidget { base64: base64, uploadError: uploadError ), - onInitialContentLoadComplete: controller.restoreCollapsibleButton, + onInitialContentLoadComplete: controller.onInitialContentLoadCompleteWeb, )), ), ), @@ -375,9 +369,7 @@ class ComposerView extends GetWidget { listEmailAddress: controller.listToEmailAddress, imagePaths: controller.imagePaths, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller - .mailboxDashBoardController - .minInputLengthAutocomplete, + minInputLengthAutocomplete: controller.minInputLengthAutocomplete, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, @@ -406,9 +398,7 @@ class ComposerView extends GetWidget { listEmailAddress: controller.listCcEmailAddress, imagePaths: controller.imagePaths, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller - .mailboxDashBoardController - .minInputLengthAutocomplete, + minInputLengthAutocomplete: controller.minInputLengthAutocomplete, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, @@ -433,9 +423,7 @@ class ComposerView extends GetWidget { listEmailAddress: controller.listBccEmailAddress, imagePaths: controller.imagePaths, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller - .mailboxDashBoardController - .minInputLengthAutocomplete, + minInputLengthAutocomplete: controller.minInputLengthAutocomplete, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, @@ -539,7 +527,7 @@ class ComposerView extends GetWidget { base64: base64, uploadError: uploadError ), - onInitialContentLoadComplete: controller.restoreCollapsibleButton, + onInitialContentLoadComplete: controller.onInitialContentLoadCompleteWeb, ); }), ), @@ -694,9 +682,7 @@ class ComposerView extends GetWidget { listEmailAddress: controller.listToEmailAddress, imagePaths: controller.imagePaths, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller - .mailboxDashBoardController - .minInputLengthAutocomplete, + minInputLengthAutocomplete: controller.minInputLengthAutocomplete, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, @@ -725,9 +711,7 @@ class ComposerView extends GetWidget { listEmailAddress: controller.listCcEmailAddress, imagePaths: controller.imagePaths, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller - .mailboxDashBoardController - .minInputLengthAutocomplete, + minInputLengthAutocomplete: controller.minInputLengthAutocomplete, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, @@ -752,9 +736,7 @@ class ComposerView extends GetWidget { listEmailAddress: controller.listBccEmailAddress, imagePaths: controller.imagePaths, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller - .mailboxDashBoardController - .minInputLengthAutocomplete, + minInputLengthAutocomplete: controller.minInputLengthAutocomplete, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, @@ -857,7 +839,7 @@ class ComposerView extends GetWidget { base64: base64, uploadError: uploadError ), - onInitialContentLoadComplete: controller.restoreCollapsibleButton, + onInitialContentLoadComplete: controller.onInitialContentLoadCompleteWeb, )), ), ), diff --git a/lib/features/composer/presentation/extensions/sanitize_signature_in_email_content_extension.dart b/lib/features/composer/presentation/extensions/sanitize_signature_in_email_content_extension.dart new file mode 100644 index 000000000..039c12b9d --- /dev/null +++ b/lib/features/composer/presentation/extensions/sanitize_signature_in_email_content_extension.dart @@ -0,0 +1,39 @@ + +import 'package:html/parser.dart'; +import 'package:rich_text_composer/views/commons/logger.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; + +extension SanitizeSignatureInEmailContentExtension on ComposerController { + + Future restoreCollapsibleSignatureButton(String? emailContent) async { + try { + if (emailContent == null) return; + + final emailDocument = parse(emailContent); + final existedSignatureButton = emailDocument.querySelector('button.tmail-signature-button'); + if (existedSignatureButton != null) return; + + final signature = emailDocument.querySelector('div.tmail-signature'); + if (signature == null) return; + + restoringSignatureButton = true; + await applySignature(signature.innerHtml); + } catch (e) { + logError('SanitizeSignatureInEmailContentExtension::restoreCollapsibleSignatureButton:Exception = $e'); + } + } + + void synchronizeInitEmailDraftHash(String? emailContent) { + try { + final emailDocument = parse(emailContent); + final signatureButton = emailDocument.querySelector('button.tmail-signature-button'); + if (signatureButton == null) return; + + restoringSignatureButton = false; + synchronizeInitDraftHash = true; + initEmailDraftHash(); + } catch (e) { + logError('SanitizeSignatureInEmailContentExtension::synchronizeInitEmailDraftHash:Exception = $e'); + } + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/setup_email_attachments_extension.dart b/lib/features/composer/presentation/extensions/setup_email_attachments_extension.dart new file mode 100644 index 000000000..f75a3cf2f --- /dev/null +++ b/lib/features/composer/presentation/extensions/setup_email_attachments_extension.dart @@ -0,0 +1,67 @@ + +import 'package:core/utils/list_utils.dart'; +import 'package:model/email/attachment.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:model/extensions/email_extension.dart'; +import 'package:model/extensions/list_attachment_extension.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/list_shared_media_file_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_extension.dart'; + +extension SetupEmailAttachmentsExtension on ComposerController { + + void setupEmailAttachments(ComposerArguments arguments) { + List? attachments; + List? inlineImages; + + switch(currentEmailActionType) { + case EmailActionType.editSendingEmail: + final sendingEmail = arguments.sendingEmail; + final allAttachments = sendingEmail?.email.allAttachments; + attachments = allAttachments?.getListAttachmentsDisplayedOutside( + sendingEmail?.email.htmlBodyAttachments ?? [], + ); + inlineImages = allAttachments?.listAttachmentsDisplayedInContent; + break; + case EmailActionType.composeFromFileShared: + _uploadAttachmentFromFileShare(arguments.listSharedMediaFile!); + break; + case EmailActionType.reply: + case EmailActionType.replyToList: + case EmailActionType.replyAll: + case EmailActionType.forward: + attachments = arguments.attachments; + inlineImages = arguments.inlineImages; + break; + case EmailActionType.reopenComposerBrowser: + attachments = arguments.attachments; + inlineImages = arguments.inlineImages; + break; + default: + break; + } + + initAttachmentsAndInlineImages( + attachments: attachments, + inlineImages: inlineImages, + ); + } + + void _uploadAttachmentFromFileShare(List listSharedMediaFile) { + final listFileInfo = listSharedMediaFile.toListFileInfo(isShared: true); + + final tupleListFileInfo = partition( + listFileInfo, + (fileInfo) => fileInfo.isInline == true, + ); + final listAttachments = tupleListFileInfo.value2; + + uploadController.validateTotalSizeAttachmentsBeforeUpload( + totalSizePreparedFiles: listFileInfo.totalSize, + totalSizePreparedFilesWithDispositionAttachment: listAttachments.totalSize, + onValidationSuccess: () => uploadAttachmentsAction(pickedFiles: listFileInfo), + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/setup_email_content_extension.dart b/lib/features/composer/presentation/extensions/setup_email_content_extension.dart new file mode 100644 index 000000000..10fc4b6af --- /dev/null +++ b/lib/features/composer/presentation/extensions/setup_email_content_extension.dart @@ -0,0 +1,223 @@ + +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:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:model/extensions/email_extension.dart'; +import 'package:model/extensions/list_attachment_extension.dart'; +import 'package:model/extensions/list_email_content_extension.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/restore_email_inline_images_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_for_edit_draft_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_selected_identity_extension.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/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +extension SetupEmailContentExtension on ComposerController { + + Future setupEmailContent(ComposerArguments arguments) async { + switch(currentEmailActionType) { + case EmailActionType.editAsNewEmail: + case EmailActionType.editDraft: + final session = mailboxDashBoardController.sessionCurrent; + final accountId = mailboxDashBoardController.accountId.value; + final emailId = arguments.presentationEmail?.id; + + if (session == null) { + emailContentsViewState.value = Left(GetEmailContentFailure(NotFoundSessionException())); + return; + } + + if (accountId == null) { + emailContentsViewState.value = Left(GetEmailContentFailure(NotFoundAccountIdException())); + return; + } + + if (emailId == null) { + emailContentsViewState.value = Left(GetEmailContentFailure(NotFoundEmailException())); + return; + } + + final resultState = await getEmailContentInteractor.execute( + session, + accountId, + emailId, + mailboxDashBoardController.baseDownloadUrl, + TransformConfiguration.forEditDraftsEmail(), + additionalProperties: Properties({ + IndividualHeaderIdentifier.identityHeader.value, + }), + ).last; + + final uiState = resultState.fold((failure) => failure, (success) => success); + + if (uiState is GetEmailContentSuccess) { + initAttachmentsAndInlineImages( + attachments: uiState.attachments, + inlineImages: uiState.inlineImages, + ); + + if (currentEmailActionType == EmailActionType.editDraft) { + setupEmailRequestReadReceiptFlagForEditDraft( + uiState.emailCurrent!.hasRequestReadReceipt, + ); + setupSelectedIdentityForEditDraft( + uiState.emailCurrent!.identityIdFromHeader, + ); + } + + emailContentsViewState.value = Right(uiState); + } else if (uiState is GetEmailContentFromCacheSuccess) { + initAttachmentsAndInlineImages( + attachments: uiState.attachments, + inlineImages: uiState.inlineImages, + ); + + if (currentEmailActionType == EmailActionType.editDraft) { + setupEmailRequestReadReceiptFlagForEditDraft( + uiState.emailCurrent.hasRequestReadReceipt, + ); + } + + emailContentsViewState.value = Right(uiState); + } else if (uiState is GetEmailContentFailure) { + emailContentsViewState.value = Left(uiState); + consumeState(Stream.value(Left(uiState))); + } else { + emailContentsViewState.value = Right(UIState.idle); + } + break; + case EmailActionType.editSendingEmail: + final sendingEmail = arguments.sendingEmail; + final successState = GetEmailContentSuccess( + htmlEmailContent: sendingEmail?.presentationEmail.emailContentList.asHtmlString ?? '', + emailCurrent: arguments.sendingEmail?.email, + ); + if (PlatformInfo.isWeb) { + setTextEditorWeb(successState.htmlEmailContent); + } + emailContentsViewState.value = Right(successState); + break; + case EmailActionType.composeFromContentShared: + final successState = GetEmailContentSuccess(htmlEmailContent: arguments.emailContents ?? ''); + if (PlatformInfo.isWeb) { + setTextEditorWeb(successState.htmlEmailContent); + } + emailContentsViewState.value = Right(successState); + break; + case EmailActionType.composeFromMailtoUri: + final successState = GetEmailContentSuccess(htmlEmailContent: arguments.body ?? ''); + if (PlatformInfo.isWeb) { + setTextEditorWeb(successState.htmlEmailContent); + } + emailContentsViewState.value = Right(successState); + break; + case EmailActionType.reply: + case EmailActionType.replyToList: + case EmailActionType.replyAll: + case EmailActionType.forward: + if (arguments.emailContents?.trim().isNotEmpty != true) { + emailContentsViewState.value = Left(GetEmailContentFailure(EmptyEmailContentException())); + } else { + final resultState = await transformHtmlEmailContentInteractor.execute( + arguments.emailContents ?? '', + TransformConfiguration.forReplyForwardEmail(), + ).last; + + final uiState = resultState.fold((failure) => failure, (success) => success); + + if (uiState is TransformHtmlEmailContentSuccess) { + final emailContent = uiState.htmlContent; + + if (PlatformInfo.isWeb && + currentContext != null && + arguments.presentationEmail != null) { + final emailContentQuoted = getEmailContentQuotedAsHtml( + locale: Localizations.localeOf(currentContext!), + appLocalizations: AppLocalizations.of(currentContext!), + emailContent: emailContent, + emailActionType: currentEmailActionType!, + presentationEmail: arguments.presentationEmail!, + ); + + setTextEditorWeb(emailContentQuoted); + } + + final successState = GetEmailContentSuccess(htmlEmailContent: emailContent); + emailContentsViewState.value = Right(successState); + } else if (uiState is TransformHtmlEmailContentFailure) { + emailContentsViewState.value = Left(GetEmailContentFailure(uiState.exception)); + consumeState(Stream.value(Left(GetEmailContentFailure(uiState.exception)))); + } else { + emailContentsViewState.value = Right(UIState.idle); + } + } + break; + case EmailActionType.reopenComposerBrowser: + final inlineImages = arguments.inlineImages ?? []; + final content = arguments.emailContents ?? ''; + final displayMode = arguments.displayMode; + + if (content.trim().isEmpty) { + emailContentsViewState.value = Left(GetEmailContentFailure(EmptyEmailContentException())); + } else if (displayMode.isNotContentVisible()) { + final successState = GetEmailContentSuccess(htmlEmailContent: content); + setTextEditorWeb(content); + emailContentsViewState.value = Right(successState); + } else { + final accountId = mailboxDashBoardController.accountId.value; + final downloadUrl = mailboxDashBoardController.baseDownloadUrl; + + if (restoreEmailInlineImagesInteractor == null || + accountId == null || + downloadUrl.isEmpty) { + emailContentsViewState.value = Left(GetEmailContentFailure(NotFoundAccountIdException())); + return; + } + + final resultState = await restoreEmailInlineImagesInteractor!.execute( + htmlContent: content, + transformConfiguration: TransformConfiguration.forRestoreEmail(), + mapUrlDownloadCID: inlineImages.toMapCidImageDownloadUrl( + accountId: accountId, + downloadUrl: downloadUrl, + ), + ).last; + + final uiState = resultState.fold((failure) => failure, (success) => success); + + if (uiState is RestoreEmailInlineImagesSuccess) { + final emailContent = uiState.emailContent; + final successState = GetEmailContentSuccess(htmlEmailContent: emailContent); + setTextEditorWeb(emailContent); + emailContentsViewState.value = Right(successState); + } else if (uiState is RestoreEmailInlineImagesFailure) { + emailContentsViewState.value = Left(GetEmailContentFailure(uiState.exception)); + consumeState(Stream.value(Left(GetEmailContentFailure(uiState.exception)))); + } else { + emailContentsViewState.value = Right(UIState.idle); + } + } + break; + case EmailActionType.composeFromUnsubscribeMailtoLink: + final successState = GetEmailContentSuccess(htmlEmailContent: arguments.body ?? ''); + if (PlatformInfo.isWeb) { + setTextEditorWeb(successState.htmlEmailContent); + } + emailContentsViewState.value = Right(successState); + break; + default: + emailContentsViewState.value = Right(UIState.idle); + break; + } + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/setup_email_important_flag_extension.dart b/lib/features/composer/presentation/extensions/setup_email_important_flag_extension.dart new file mode 100644 index 000000000..c3921306b --- /dev/null +++ b/lib/features/composer/presentation/extensions/setup_email_important_flag_extension.dart @@ -0,0 +1,21 @@ + +import 'package:model/email/email_action_type.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; + +extension SetupEmailImportantFlagExtension on ComposerController { + + void setupEmailImportantFlag(ComposerArguments arguments) { + switch(currentEmailActionType) { + case EmailActionType.editAsNewEmail: + case EmailActionType.editDraft: + isMarkAsImportant.value = arguments.presentationEmail?.isMarkAsImportant ?? false; + break; + case EmailActionType.reopenComposerBrowser: + isMarkAsImportant.value = arguments.isMarkAsImportant ?? false; + break; + default: + break; + } + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/setup_email_other_components_extension.dart b/lib/features/composer/presentation/extensions/setup_email_other_components_extension.dart new file mode 100644 index 000000000..ab321761b --- /dev/null +++ b/lib/features/composer/presentation/extensions/setup_email_other_components_extension.dart @@ -0,0 +1,25 @@ + +import 'package:model/email/email_action_type.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; + +extension SetupEmailOtherComponentsExtension on ComposerController { + + void setupEmailOtherComponents(ComposerArguments arguments) { + switch(currentEmailActionType) { + case EmailActionType.editDraft: + emailIdEditing = arguments.presentationEmail?.id; + break; + case EmailActionType.editSendingEmail: + emailIdEditing = arguments.sendingEmail?.presentationEmail.id; + break; + case EmailActionType.reopenComposerBrowser: + screenDisplayMode.value = arguments.displayMode; + break; + default: + break; + } + + minInputLengthAutocomplete = mailboxDashBoardController.minInputLengthAutocomplete; + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/setup_email_recipients_extension.dart b/lib/features/composer/presentation/extensions/setup_email_recipients_extension.dart new file mode 100644 index 000000000..03dc63bca --- /dev/null +++ b/lib/features/composer/presentation/extensions/setup_email_recipients_extension.dart @@ -0,0 +1,73 @@ + +import 'package:model/email/email_action_type.dart'; +import 'package:model/mailbox/expand_mode.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; + +extension SetupEmailRecipientsExtension on ComposerController { + + void setupEmailRecipients(ComposerArguments arguments) { + switch(currentEmailActionType) { + case EmailActionType.editAsNewEmail: + case EmailActionType.editDraft: + case EmailActionType.reopenComposerBrowser: + initEmailAddress( + presentationEmail: arguments.presentationEmail!, + actionType: currentEmailActionType!, + ); + break; + case EmailActionType.editSendingEmail: + initEmailAddress( + presentationEmail: arguments.sendingEmail!.presentationEmail, + actionType: currentEmailActionType!, + ); + break; + case EmailActionType.composeFromEmailAddress: + case EmailActionType.composeFromUnsubscribeMailtoLink: + final emailAddressOfTo = arguments.listEmailAddress ?? []; + if (emailAddressOfTo.isNotEmpty) { + listToEmailAddress.addAll(emailAddressOfTo); + isInitialRecipient.value = true; + toAddressExpandMode.value = ExpandMode.COLLAPSE; + } + break; + case EmailActionType.composeFromMailtoUri: + final emailAddressOfTo = arguments.listEmailAddress ?? []; + final emailAddressOfCc = arguments.cc ?? []; + final emailAddressOfBc = arguments.bcc ?? []; + + if (emailAddressOfTo.isNotEmpty) { + listToEmailAddress.addAll(emailAddressOfTo); + isInitialRecipient.value = true; + toAddressExpandMode.value = ExpandMode.COLLAPSE; + } + + if (emailAddressOfCc.isNotEmpty) { + listCcEmailAddress = emailAddressOfCc; + ccRecipientState.value = PrefixRecipientState.enabled; + ccAddressExpandMode.value = ExpandMode.COLLAPSE; + } + + if (emailAddressOfBc.isNotEmpty) { + listBccEmailAddress = emailAddressOfBc; + bccRecipientState.value = PrefixRecipientState.enabled; + bccAddressExpandMode.value = ExpandMode.COLLAPSE; + } + break; + case EmailActionType.reply: + case EmailActionType.replyToList: + case EmailActionType.replyAll: + initEmailAddress( + presentationEmail: arguments.presentationEmail!, + actionType: currentEmailActionType!, + listPost: arguments.listPost, + ); + break; + default: + break; + } + + updateStatusEmailSendButton(); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_extension.dart b/lib/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_extension.dart new file mode 100644 index 000000000..44a62ea11 --- /dev/null +++ b/lib/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_extension.dart @@ -0,0 +1,31 @@ + +import 'package:model/email/email_action_type.dart'; +import 'package:server_settings/server_settings/tmail_server_settings_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/features/server_settings/domain/state/get_server_setting_state.dart'; + +extension SetupEmailRequestReadReceiptFlagExtension on ComposerController { + + void setupEmailRequestReadReceiptFlag(ComposerArguments arguments) { + if (currentEmailActionType == EmailActionType.reopenComposerBrowser) { + hasRequestReadReceipt.value = arguments.hasRequestReadReceipt ?? false; + } else if (currentEmailActionType != EmailActionType.editDraft) { + getServerSetting(); + } + } + + Future getServerSetting() async { + final accountId = mailboxDashBoardController.accountId.value; + if (accountId == null) return; + + final resultState = await getServerSettingInteractor.execute(accountId).last; + + final uiState = resultState.fold((failure) => failure, (success) => success); + + if (uiState is GetServerSettingSuccess) { + hasRequestReadReceipt.value = uiState.settingOption.isAlwaysReadReceipts; + initEmailDraftHash(); + } + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_for_edit_draft_extension.dart b/lib/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_for_edit_draft_extension.dart new file mode 100644 index 000000000..ae0eeb169 --- /dev/null +++ b/lib/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_for_edit_draft_extension.dart @@ -0,0 +1,9 @@ + +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; + +extension SetupEmailRequestReadReceiptFlagExtension on ComposerController { + + void setupEmailRequestReadReceiptFlagForEditDraft(bool isRequestReadReceipt) { + hasRequestReadReceipt.value = isRequestReadReceipt; + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/setup_email_subject_extension.dart b/lib/features/composer/presentation/extensions/setup_email_subject_extension.dart new file mode 100644 index 000000000..ff5a8c505 --- /dev/null +++ b/lib/features/composer/presentation/extensions/setup_email_subject_extension.dart @@ -0,0 +1,44 @@ + +import 'package:model/email/email_action_type.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +extension SetupEmailSubjectExtension on ComposerController { + + void setupEmailSubject(ComposerArguments arguments) { + String subject = ''; + + switch(currentEmailActionType!) { + case EmailActionType.editAsNewEmail: + case EmailActionType.editDraft: + case EmailActionType.reply: + case EmailActionType.replyToList: + case EmailActionType.replyAll: + case EmailActionType.forward: + case EmailActionType.reopenComposerBrowser: + subject = arguments.presentationEmail!.getEmailTitle().trim(); + break; + case EmailActionType.editSendingEmail: + subject = arguments.sendingEmail!.presentationEmail.getEmailTitle().trim(); + break; + case EmailActionType.composeFromMailtoUri: + case EmailActionType.composeFromUnsubscribeMailtoLink: + subject = arguments.subject ?? ''; + break; + default: + break; + } + + final newSubject = currentEmailActionType!.getSubjectComposer( + currentContext, + subject, + ); + + if (newSubject.isNotEmpty) { + setSubjectEmail(subject); + subjectEmailInputController.text = subject; + } + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/setup_list_identities_extension.dart b/lib/features/composer/presentation/extensions/setup_list_identities_extension.dart new file mode 100644 index 000000000..1326cf30d --- /dev/null +++ b/lib/features/composer/presentation/extensions/setup_list_identities_extension.dart @@ -0,0 +1,41 @@ + +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; + +extension SetupListIdentitiesExtension on ComposerController { + + Future setupListIdentities(ComposerArguments arguments) async { + final identities = arguments.identities ?? []; + if (identities.isEmpty) { + await getAllIdentitiesAsSynchronize(); + } else { + listFromIdentities.value = identities; + } + } + + Future getAllIdentitiesAsSynchronize() async { + final accountId = mailboxDashBoardController.accountId.value; + final session = mailboxDashBoardController.sessionCurrent; + + if (accountId == null || session == null) return; + + final resultState = await getAllIdentitiesInteractor.execute( + session, + accountId, + ).last; + + final uiState = resultState.fold((failure) => failure, (success) => success); + + if (uiState is GetAllIdentitiesSuccess) { + final identitiesMayDeleted = uiState.identities?.toListMayDeleted() ?? []; + if (identitiesMayDeleted.isNotEmpty) { + listFromIdentities.value = identitiesMayDeleted; + } + } else if (uiState is GetAllIdentitiesFailure) { + consumeState(Stream.value(Left(uiState))); + } + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/setup_selected_identity_extension.dart b/lib/features/composer/presentation/extensions/setup_selected_identity_extension.dart new file mode 100644 index 000000000..5203ec22c --- /dev/null +++ b/lib/features/composer/presentation/extensions/setup_selected_identity_extension.dart @@ -0,0 +1,72 @@ + +import 'package:collection/collection.dart'; +import 'package:core/core.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; + +extension SetupSelectedIdentityExtension on ComposerController { + + Future setupSelectedIdentity() async { + if (identitySelected.value != null) { + if (PlatformInfo.isMobile && currentEmailActionType == EmailActionType.editDraft) { + await selectIdentity(identitySelected.value); + onCompleteSetupComposer(); + } + return; + } + + if (listFromIdentities.isEmpty && + currentEmailActionType == EmailActionType.editSendingEmail) { + final signatureContent = await htmlEditorApi?.getSignatureContent(); + if (signatureContent?.trim().isNotEmpty == true) { + await applySignature(signatureContent!); + } + } + + if (listFromIdentities.isNotEmpty) { + final currentIdentity = _findIdentityById( + composerArguments.value?.selectedIdentityId, + ) ?? listFromIdentities.first; + + if (currentEmailActionType == EmailActionType.editDraft || + currentEmailActionType == EmailActionType.reopenComposerBrowser && + savedActionType == EmailActionType.editDraft) { + identitySelected.value = currentIdentity; + } else if (currentEmailActionType == EmailActionType.editAsNewEmail) { + identitySelected.value = currentIdentity; + await selectIdentity(currentIdentity); + } else { + await selectIdentity(currentIdentity); + } + } + + onCompleteSetupComposer(); + } + + Future setupSelectedIdentityWithoutApplySignature() async { + if (identitySelected.value == null && listFromIdentities.isNotEmpty) { + final currentIdentity = _findIdentityById( + composerArguments.value?.selectedIdentityId, + ) ?? listFromIdentities.first; + + identitySelected.value = currentIdentity; + } + + onCompleteSetupComposer(); + } + + void setupSelectedIdentityForEditDraft(IdentityId? identityId) { + if (identityId == null) return; + + if (listFromIdentities.isNotEmpty) { + final currentIdentity = _findIdentityById(identityId); + identitySelected.value = currentIdentity; + } + } + + Identity? _findIdentityById(IdentityId? identityId) { + return listFromIdentities + .firstWhereOrNull((identity) => identity.id == identityId); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/model/create_email_request.dart b/lib/features/composer/presentation/model/create_email_request.dart index a4236833d..2ffb786d5 100644 --- a/lib/features/composer/presentation/model/create_email_request.dart +++ b/lib/features/composer/presentation/model/create_email_request.dart @@ -41,6 +41,9 @@ class CreateEmailRequest with EquatableMixin { final Uri? uploadUri; final int? composerIndex; final String? composerId; + final int? savedDraftHash; + final EmailActionType? savedActionType; + final EmailId? savedEmailDraftId; CreateEmailRequest({ required this.session, @@ -71,6 +74,9 @@ class CreateEmailRequest with EquatableMixin { this.uploadUri, this.composerIndex, this.composerId, + this.savedDraftHash, + this.savedActionType, + this.savedEmailDraftId, }); @override @@ -103,5 +109,8 @@ class CreateEmailRequest with EquatableMixin { uploadUri, composerIndex, composerId, + savedDraftHash, + savedActionType, + savedEmailDraftId, ]; } \ No newline at end of file diff --git a/lib/features/composer/presentation/model/saved_email_draft.dart b/lib/features/composer/presentation/model/saved_email_draft.dart index a0ed39604..ec7b7845d 100644 --- a/lib/features/composer/presentation/model/saved_email_draft.dart +++ b/lib/features/composer/presentation/model/saved_email_draft.dart @@ -1,8 +1,14 @@ +import 'dart:convert'; + import 'package:equatable/equatable.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:model/email/attachment.dart'; +part 'saved_email_draft.g.dart'; + +@JsonSerializable(includeIfNull: false, explicitToJson: true) class SavedEmailDraft with EquatableMixin { final String content; final String subject; @@ -28,29 +34,20 @@ class SavedEmailDraft with EquatableMixin { this.isMarkAsImportant = false, }); - factory SavedEmailDraft.empty() { - return SavedEmailDraft( - subject: '', - content: '', - toRecipients: {}, - ccRecipients: {}, - bccRecipients: {}, - replyToRecipients: {}, - attachments: [], - identity: null, - hasReadReceipt: false, - ); - } + factory SavedEmailDraft.fromJson(Map json) => _$SavedEmailDraftFromJson(json); + + Map toJson() => _$SavedEmailDraftToJson(this); + + String asString() => jsonEncode(toJson()); @override List get props => [ content, subject, - // Prevent identical Set - {0: toRecipients}, - {1: ccRecipients}, - {2: bccRecipients}, - {3: replyToRecipients}, + toRecipients, + ccRecipients, + bccRecipients, + replyToRecipients, attachments, identity, hasReadReceipt, diff --git a/lib/features/composer/presentation/model/signature_status.dart b/lib/features/composer/presentation/model/signature_status.dart deleted file mode 100644 index 773978344..000000000 --- a/lib/features/composer/presentation/model/signature_status.dart +++ /dev/null @@ -1,4 +0,0 @@ -enum SignatureStatus { - editedAvailable, - editedApplied, -} \ No newline at end of file diff --git a/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart b/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart index bb0640baf..03531bbbe 100644 --- a/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart +++ b/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart @@ -8,7 +8,6 @@ import 'package:model/email/email_action_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/view/editor_view_mixin.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/mobile_editor_widget.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/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; @@ -107,14 +106,14 @@ class MobileEditorView extends StatelessWidget with EditorViewMixin { ); }, (success) { - if (success is TransformHtmlEmailContentLoading) { + if (success is GetEmailContentLoading) { return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); } else { final emailContentQuoted = getEmailContentQuotedAsHtml( locale: Localizations.localeOf(context), appLocalizations: AppLocalizations.of(context), - emailContent: success is TransformHtmlEmailContentSuccess - ? success.htmlContent + emailContent: success is GetEmailContentSuccess + ? success.htmlEmailContent : '', emailActionType: arguments!.emailActionType, presentationEmail: arguments!.presentationEmail! diff --git a/lib/features/composer/presentation/view/web/web_editor_view.dart b/lib/features/composer/presentation/view/web/web_editor_view.dart index 0cf333227..68be4c95d 100644 --- a/lib/features/composer/presentation/view/web/web_editor_view.dart +++ b/lib/features/composer/presentation/view/web/web_editor_view.dart @@ -7,11 +7,9 @@ import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:html_editor_enhanced/html_editor.dart'; import 'package:model/email/email_action_type.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/restore_email_inline_images_state.dart'; import 'package:tmail_ui_user/features/composer/presentation/view/editor_view_mixin.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/web_editor_widget.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/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; @@ -115,12 +113,15 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onInitialContentLoadComplete: onInitialContentLoadComplete, ), (success) { - if (success is GetEmailContentLoading || success is RestoringEmailInlineImages) { + if (success is GetEmailContentLoading) { return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); } else { - var newContent = success is GetEmailContentSuccess - ? success.htmlEmailContent - : HtmlExtension.editorStartTags; + var newContent = ''; + if (success is GetEmailContentSuccess) { + newContent = success.htmlEmailContent; + } else if (success is GetEmailContentFromCacheSuccess) { + newContent = success.htmlEmailContent; + } if (newContent.isEmpty) { newContent = HtmlExtension.editorStartTags; } @@ -181,14 +182,14 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { ); }, (success) { - if (success is TransformHtmlEmailContentLoading) { + if (success is GetEmailContentLoading) { return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); } else { final emailContentQuoted = getEmailContentQuotedAsHtml( locale: Localizations.localeOf(context), appLocalizations: AppLocalizations.of(context), - emailContent: success is TransformHtmlEmailContentSuccess - ? success.htmlContent + emailContent: success is GetEmailContentSuccess + ? success.htmlEmailContent : '', emailActionType: arguments!.emailActionType, presentationEmail: arguments!.presentationEmail! diff --git a/lib/features/email/data/local/html_analyzer.dart b/lib/features/email/data/local/html_analyzer.dart index fdb1d14d1..0e14bec5b 100644 --- a/lib/features/email/data/local/html_analyzer.dart +++ b/lib/features/email/data/local/html_analyzer.dart @@ -190,23 +190,24 @@ class HtmlAnalyzer { } Future removeCollapsedExpandedSignatureEffect({required String emailContent}) async { - log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect: BEFORE = $emailContent'); - final document = parse(emailContent); - final signatureElements = document.querySelectorAll('div.tmail-signature'); - await Future.wait(signatureElements.map((signatureTag) async { - final signatureChildren = signatureTag.children; - for (var child in signatureChildren) { - log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect: CHILD = ${child.outerHtml}'); - if (child.attributes['class']?.contains('tmail-signature-button') == true) { - child.remove(); - } else if (child.attributes['class']?.contains('tmail-signature-content') == true) { - signatureTag.innerHtml = child.innerHtml; + try { + final document = parse(emailContent); + final signatureElements = document.querySelectorAll('div.tmail-signature'); + await Future.wait(signatureElements.map((signatureTag) async { + final signatureChildren = signatureTag.children; + for (var child in signatureChildren) { + if (child.attributes['class']?.contains('tmail-signature-button') == true) { + child.remove(); + } else if (child.attributes['class']?.contains('tmail-signature-content') == true) { + signatureTag.innerHtml = child.innerHtml; + } } - } - })); - final newContent = document.body?.innerHtml ?? emailContent; - log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect: AFTER = $newContent'); - return newContent; + })); + return document.body?.innerHtml ?? emailContent; + } catch (e) { + logError('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect:Exception = $e'); + return emailContent; + } } Future<(Attachment attachment, String taskId)?> _retrieveAttachmentFromUpload({ 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 57250a68f..af729942d 100644 --- a/lib/features/email/domain/state/get_email_content_state.dart +++ b/lib/features/email/domain/state/get_email_content_state.dart @@ -31,13 +31,13 @@ class GetEmailContentFromCacheSuccess extends UIState { final String htmlEmailContent; final List? attachments; final List? inlineImages; - final Email? emailCurrent; + final Email emailCurrent; GetEmailContentFromCacheSuccess({ required this.htmlEmailContent, + required this.emailCurrent, this.attachments, this.inlineImages, - this.emailCurrent }); @override diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index ca810231c..e6d174e31 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -619,7 +619,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { if (emailSupervisorController.presentationEmailsLoaded.length > ThreadConstants.defaultLimit.value.toInt()) { emailSupervisorController.popFirstEmailQueue(); } - emailSupervisorController.popEmailQueue(success.emailCurrent?.id); + emailSupervisorController.popEmailQueue(success.emailCurrent.id); currentEmailLoaded.value = EmailLoaded( htmlContent: success.htmlEmailContent, @@ -629,7 +629,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { ); emailSupervisorController.pushEmailQueue(currentEmailLoaded.value!); - if (success.emailCurrent?.id == currentEmail?.id) { + if (success.emailCurrent.id == currentEmail?.id) { attachments.value = success.attachments ?? []; attachmentsViewState.value = { for (var attachment in attachments.where((item) => item.blobId != null)) @@ -645,13 +645,13 @@ class SingleEmailController extends BaseController with AppLoaderMixin { emailContents.value = success.htmlEmailContent; } - final isShowMessageReadReceipt = success.emailCurrent?.hasReadReceipt(mailboxDashBoardController.mapMailboxById) == true; + final isShowMessageReadReceipt = success.emailCurrent.hasReadReceipt(mailboxDashBoardController.mapMailboxById) == true; if (isShowMessageReadReceipt) { _handleReadReceipt(); } - if (currentEmail?.isSubscribed == false && success.emailCurrent?.hasListUnsubscribe == true) { - _handleUnsubscribe(success.emailCurrent!.listUnsubscribe); + if (currentEmail?.isSubscribed == false && success.emailCurrent.hasListUnsubscribe == true) { + _handleUnsubscribe(success.emailCurrent.listUnsubscribe); } else { emailUnsubscribe.value = null; } diff --git a/lib/features/email/presentation/model/composer_arguments.dart b/lib/features/email/presentation/model/composer_arguments.dart index 0937c9b7d..de16c586d 100644 --- a/lib/features/email/presentation/model/composer_arguments.dart +++ b/lib/features/email/presentation/model/composer_arguments.dart @@ -35,6 +35,9 @@ class ComposerArguments extends RouterArguments { final List? bcc; final String? listPost; final String? composerId; + final int? savedDraftHash; + final EmailActionType? savedActionType; + final EmailId? savedEmailDraftId; ComposerArguments({ this.emailActionType = EmailActionType.compose, @@ -60,6 +63,9 @@ class ComposerArguments extends RouterArguments { this.listPost, this.isMarkAsImportant, this.composerId, + this.savedDraftHash, + this.savedActionType, + this.savedEmailDraftId, }); factory ComposerArguments.fromSendingEmail(SendingEmail sendingEmail) => @@ -125,6 +131,9 @@ class ComposerArguments extends RouterArguments { displayMode: composerCache.displayMode, isMarkAsImportant: composerCache.isMarkAsImportant, composerId: composerCache.composerId, + savedDraftHash: composerCache.draftHash, + savedActionType: composerCache.actionType, + savedEmailDraftId: composerCache.draftEmailId, ); factory ComposerArguments.replyEmail({ @@ -246,6 +255,9 @@ class ComposerArguments extends RouterArguments { bcc, listPost, composerId, + savedDraftHash, + savedActionType, + savedEmailDraftId, ]; ComposerArguments copyWith({ @@ -272,6 +284,9 @@ class ComposerArguments extends RouterArguments { List? bcc, String? listPost, String? composerId, + int? savedDraftHash, + EmailActionType? savedActionType, + EmailId? savedEmailDraftId, }) { return ComposerArguments( emailActionType: emailActionType ?? this.emailActionType, @@ -297,6 +312,9 @@ class ComposerArguments extends RouterArguments { bcc: bcc ?? this.bcc, listPost: listPost ?? this.listPost, composerId: composerId ?? this.composerId, + savedDraftHash: savedDraftHash ?? this.savedDraftHash, + savedActionType: savedActionType ?? this.savedActionType, + savedEmailDraftId: savedEmailDraftId ?? this.savedEmailDraftId, ); } } diff --git a/lib/features/mailbox_dashboard/data/model/composer_cache.dart b/lib/features/mailbox_dashboard/data/model/composer_cache.dart index afe7178ab..30f6b9ed2 100644 --- a/lib/features/mailbox_dashboard/data/model/composer_cache.dart +++ b/lib/features/mailbox_dashboard/data/model/composer_cache.dart @@ -1,11 +1,19 @@ import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/http/converter/email_id_nullable_converter.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:model/email/email_action_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; part 'composer_cache.g.dart'; -@JsonSerializable(explicitToJson: true, includeIfNull: false) +@JsonSerializable( + explicitToJson: true, + includeIfNull: false, + converters: [ + EmailIdNullableConverter(), + ] +) class ComposerCache with EquatableMixin { final Email? email; @@ -14,6 +22,9 @@ class ComposerCache with EquatableMixin { final ScreenDisplayMode displayMode; final int? composerIndex; final String? composerId; + final int? draftHash; + final EmailActionType? actionType; + final EmailId? draftEmailId; ComposerCache({ this.email, @@ -22,6 +33,9 @@ class ComposerCache with EquatableMixin { this.displayMode = ScreenDisplayMode.normal, this.composerIndex, this.composerId, + this.draftHash, + this.actionType, + this.draftEmailId, }); factory ComposerCache.fromJson(Map json) => _$ComposerCacheFromJson(json); @@ -36,5 +50,8 @@ class ComposerCache with EquatableMixin { displayMode, composerIndex, composerId, + draftHash, + actionType, + draftEmailId, ]; } diff --git a/model/lib/email/attachment.dart b/model/lib/email/attachment.dart index bccb38d0f..db1f91f37 100644 --- a/model/lib/email/attachment.dart +++ b/model/lib/email/attachment.dart @@ -1,12 +1,29 @@ import 'package:equatable/equatable.dart'; import 'package:http_parser/http_parser.dart'; +import 'package:jmap_dart_client/http/converter/id_nullable_converter.dart'; +import 'package:jmap_dart_client/http/converter/media_type_nullable_converter.dart'; +import 'package:jmap_dart_client/http/converter/part_id_nullable_converter.dart'; +import 'package:jmap_dart_client/http/converter/unsigned_int_nullable_converter.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/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:uri/uri.dart'; +part 'attachment.g.dart'; + +@JsonSerializable( + includeIfNull: false, + explicitToJson: true, + converters: [ + PartIdNullableConverter(), + IdNullableConverter(), + UnsignedIntNullableConverter(), + MediaTypeNullableConverter(), + ], +) class Attachment with EquatableMixin { static const String eventICSSubtype = 'ics'; @@ -62,6 +79,10 @@ class Attachment with EquatableMixin { } } + factory Attachment.fromJson(Map json) => _$AttachmentFromJson(json); + + Map toJson() => _$AttachmentToJson(this); + @override List get props => [ partId, diff --git a/model/lib/email/email_content.dart b/model/lib/email/email_content.dart index be3108c83..78ad7a250 100644 --- a/model/lib/email/email_content.dart +++ b/model/lib/email/email_content.dart @@ -1,10 +1,8 @@ +import 'package:core/utils/string_convert.dart'; import 'package:equatable/equatable.dart'; import 'package:model/email/email_content_type.dart'; class EmailContent with EquatableMixin { - - static const defaultHtmlTagsWrapContent = ''; - final String content; final EmailContentType type; @@ -23,28 +21,6 @@ extension EmailContentExtension on EmailContent { .replaceAll('\n', '
') .replaceAll('\n\n', '
'); } - final newContent = _getContentOriginal(content); - return newContent; - } - - String _getContentOriginal(String content) { - if (content == EmailContent.defaultHtmlTagsWrapContent || - content.trim().isEmpty) { - return ''; - } - - const firstTags = ''; - const latestTags = ''; - if (content.startsWith(firstTags) && content.endsWith(latestTags)) { - const firstIndex = firstTags.length; - final latestIndex = content.length - latestTags.length; - - if (latestIndex > firstIndex) { - final contentOriginal = content.substring(firstIndex, latestIndex); - return contentOriginal; - } - } - - return content; + return StringConvert.getContentOriginal(content); } } \ No newline at end of file diff --git a/test/features/composer/presentation/composer_controller_test.dart b/test/features/composer/presentation/composer_controller_test.dart index e3562f18b..bd1b7db0a 100644 --- a/test/features/composer/presentation/composer_controller_test.dart +++ b/test/features/composer/presentation/composer_controller_test.dart @@ -17,20 +17,16 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email_header.dart'; -import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:model/email/attachment.dart'; import 'package:model/email/email_action_type.dart'; -import 'package:model/email/email_property.dart'; -import 'package:model/email/presentation_email.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; -import 'package:server_settings/server_settings/tmail_server_settings.dart'; import 'package:tmail_ui_user/features/base/before_reconnect_manager.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; +import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart'; @@ -41,9 +37,10 @@ import 'package:tmail_ui_user/features/composer/presentation/composer_controller import 'package:tmail_ui_user/features/composer/presentation/composer_view_web.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_selected_identity_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/formatting_options_state.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/saved_email_draft.dart'; -import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart'; @@ -55,11 +52,9 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; -import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart'; -import 'package:tmail_ui_user/features/server_settings/domain/state/get_server_setting_state.dart'; import 'package:tmail_ui_user/features/server_settings/domain/usecases/get_server_setting_interactor.dart'; import 'package:tmail_ui_user/features/upload/domain/usecases/local_file_picker_interactor.dart'; import 'package:tmail_ui_user/features/upload/domain/usecases/local_image_picker_interactor.dart'; @@ -164,6 +159,7 @@ class MockMailboxDashBoardController extends Mock implements MailboxDashBoardCon MockSpec(), MockSpec(), MockSpec(), + MockSpec(), // Additional Getx dependencies mock specs MockSpec(fallbackGenerators: fallbackGenerators), @@ -207,6 +203,7 @@ void main() { late MockCreateNewAndSendEmailInteractor mockCreateNewAndSendEmailInteractor; late MockCreateNewAndSaveEmailToDraftsInteractor mockCreateNewAndSaveEmailToDraftsInteractor; late MockPrintEmailInteractor mockPrintEmailInteractor; + late MockComposerRepository mockComposerRepository; // Declaration Getx dependencies final mockMailboxDashBoardController = MockMailboxDashBoardController(); @@ -275,6 +272,7 @@ void main() { mockCreateNewAndSendEmailInteractor = MockCreateNewAndSendEmailInteractor(); mockCreateNewAndSaveEmailToDraftsInteractor = MockCreateNewAndSaveEmailToDraftsInteractor(); mockPrintEmailInteractor = MockPrintEmailInteractor(); + mockComposerRepository = MockComposerRepository(); composerController = ComposerController( mockLocalFilePickerInteractor, @@ -290,6 +288,7 @@ void main() { mockCreateNewAndSendEmailInteractor, mockCreateNewAndSaveEmailToDraftsInteractor, mockPrintEmailInteractor, + mockComposerRepository, ); mockHtmlEditorApi = MockHtmlEditorApi(); @@ -310,33 +309,40 @@ void main() { final replyToRecipient = EmailAddress('replyTo', 'replyTo@linagora.com'); final identity = Identity(); final attachment = Attachment(); - const alwaysReadReceiptEnabled = true; + const alwaysReadReceiptEnabled = false; + const isMarkAsImportant = false; group('email action type is EmailActionType.compose:', () { - setUp(() { - composerController?.composerArguments.value = ComposerArguments( - emailActionType: EmailActionType.compose); - }); - test( - 'should update _savedEmailDraftHash ' - 'when there is a new view state ' - 'and the state is GetAlwaysReadReceiptSettingSuccess', + 'Should update _savedEmailDraftHash\n' + 'When screenDisplayMode is normal', () async { // arrange + final composerArguments = ComposerArguments( + emailActionType: EmailActionType.compose, + displayMode: ScreenDisplayMode.normal, + identities: [identity], + selectedIdentityId: identity.id, + ); + composerController?.composerArguments.value = composerArguments; composerController?.richTextMobileTabletController = mockRichTextMobileTabletController; - when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn( - mockHtmlEditorApi); - - when(mockHtmlEditorApi.getText()).thenAnswer((_) async => emailContent); composerController?.subjectEmail.value = emailSubject; composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; composerController?.listReplyToEmailAddress = [replyToRecipient]; - composerController?.identitySelected.value = identity; + composerController?.hasRequestReadReceipt.value = alwaysReadReceiptEnabled; + composerController?.isMarkAsImportant.value = isMarkAsImportant; + composerController?.screenDisplayMode.value = composerArguments.displayMode; + composerController?.currentEmailActionType = composerArguments.emailActionType; + composerController?.listFromIdentities.value = composerArguments.identities!; + + when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn(mockHtmlEditorApi); + when(mockHtmlEditorApi.getText()).thenAnswer((_) async => emailContent); when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - final state = GetServerSettingSuccess(TMailServerSettingOptions(alwaysReadReceipts: alwaysReadReceiptEnabled)); + when(mockComposerRepository.removeCollapsedExpandedSignatureEffect( + emailContent: anyNamed('emailContent'), + )).thenAnswer((_) async => emailContent); final savedEmailDraft = SavedEmailDraft( content: emailContent, @@ -348,35 +354,55 @@ void main() { identity: identity, attachments: [attachment], hasReadReceipt: alwaysReadReceiptEnabled, + isMarkAsImportant: isMarkAsImportant, ); // act - composerController?.handleSuccessViewState(state); + composerController?.setupSelectedIdentity(); + + await untilCalled(mockHtmlEditorApi.onDocumentChanged()); await untilCalled(mockHtmlEditorApi.getText()); - + await untilCalled( + mockComposerRepository.removeCollapsedExpandedSignatureEffect( + emailContent: anyNamed('emailContent'))); + // assert - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + expect( + composerController?.savedEmailDraftHash, + equals(savedEmailDraft.asString().hashCode), + ); }); test( - 'should update _savedEmailDraftHash ' - 'when there is a new view state ' - 'and the state is GetAlwaysReadReceiptSettingFailure', + 'Should update _savedEmailDraftHash\n' + 'When screenDisplayMode is minimize', () async { // arrange + final composerArguments = ComposerArguments( + emailActionType: EmailActionType.compose, + displayMode: ScreenDisplayMode.minimize, + identities: [identity], + selectedIdentityId: identity.id, + ); + composerController?.composerArguments.value = composerArguments; composerController?.richTextMobileTabletController = mockRichTextMobileTabletController; - when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn( - mockHtmlEditorApi); - + composerController?.subjectEmail.value = emailSubject; + composerController?.listToEmailAddress = [toRecipient]; + composerController?.listCcEmailAddress = [ccRecipient]; + composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.listReplyToEmailAddress = [replyToRecipient]; + composerController?.hasRequestReadReceipt.value = alwaysReadReceiptEnabled; + composerController?.isMarkAsImportant.value = isMarkAsImportant; + composerController?.screenDisplayMode.value = composerArguments.displayMode; + composerController?.currentEmailActionType = composerArguments.emailActionType; + composerController?.listFromIdentities.value = composerArguments.identities!; + + when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn(mockHtmlEditorApi); when(mockHtmlEditorApi.getText()).thenAnswer((_) async => emailContent); - composerController?.subjectEmail.value = emailSubject; - composerController?.listToEmailAddress = [toRecipient]; - composerController?.listCcEmailAddress = [ccRecipient]; - composerController?.listBccEmailAddress = [bccRecipient]; - composerController?.listReplyToEmailAddress = [replyToRecipient]; - composerController?.identitySelected.value = identity; when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - final state = GetServerSettingFailure(Exception()); + when(mockComposerRepository.removeCollapsedExpandedSignatureEffect( + emailContent: anyNamed('emailContent'), + )).thenAnswer((_) async => emailContent); final savedEmailDraft = SavedEmailDraft( content: emailContent, @@ -387,271 +413,29 @@ void main() { replyToRecipients: {replyToRecipient}, identity: identity, attachments: [attachment], - hasReadReceipt: false + hasReadReceipt: alwaysReadReceiptEnabled, + isMarkAsImportant: isMarkAsImportant, ); - + // act - composerController?.handleFailureViewState(state); + composerController?.setupSelectedIdentityWithoutApplySignature(); + await untilCalled(mockHtmlEditorApi.getText()); - + await untilCalled( + mockComposerRepository.removeCollapsedExpandedSignatureEffect( + emailContent: anyNamed('emailContent'))); + // assert - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); - }); - - test( - 'should update _savedEmailDraftHash ' - 'when _initIdentities is called ' - 'and listFromIdentities is not empty ' - 'and selectedIdentity is available', - () async { - // arrange - PlatformInfo.isTestingForWeb = true; - - final selectedIdentity = Identity(id: IdentityId(Id('alice'))); - - composerController = ComposerController( - mockLocalFilePickerInteractor, - mockLocalImagePickerInteractor, - mockGetEmailContentInteractor, - mockGetAllIdentitiesInteractor, - mockUploadController, - mockRemoveComposerCacheByIdOnWebInteractor, - mockSaveComposerCacheOnWebInteractor, - mockDownloadImageAsBase64Interactor, - mockTransformHtmlEmailContentInteractor, - mockGetServerSettingInteractor, - mockCreateNewAndSendEmailInteractor, - mockCreateNewAndSaveEmailToDraftsInteractor, - mockPrintEmailInteractor, - composerArgs: ComposerArguments( - selectedIdentityId: selectedIdentity.id, - identities: [selectedIdentity], - ), + expect( + composerController?.savedEmailDraftHash, + equals(savedEmailDraft.asString().hashCode), ); - - composerController?.onChangeTextEditorWeb(emailContent); - composerController?.subjectEmail.value = emailSubject; - composerController?.listToEmailAddress = [toRecipient]; - composerController?.listCcEmailAddress = [ccRecipient]; - composerController?.listBccEmailAddress = [bccRecipient]; - composerController?.listReplyToEmailAddress = [replyToRecipient]; - when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - - - final savedEmailDraft = SavedEmailDraft( - content: emailContent, - subject: emailSubject, - toRecipients: {toRecipient}, - ccRecipients: {ccRecipient}, - bccRecipients: {bccRecipient}, - replyToRecipients: {replyToRecipient}, - identity: selectedIdentity, - attachments: [attachment], - hasReadReceipt: false - ); - - // act - composerController?.onReady(); - await Future.delayed(Duration.zero); - - // assert - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); - - // tear down - PlatformInfo.isTestingForWeb = false; - }); - - test( - 'should update _savedEmailDraftHash ' - 'when _initIdentities is called ' - 'and listFromIdentities is not empty ' - 'and selectedIdentity is not available', - () async { - // arrange - PlatformInfo.isTestingForWeb = true; - - composerController = ComposerController( - mockLocalFilePickerInteractor, - mockLocalImagePickerInteractor, - mockGetEmailContentInteractor, - mockGetAllIdentitiesInteractor, - mockUploadController, - mockRemoveComposerCacheByIdOnWebInteractor, - mockSaveComposerCacheOnWebInteractor, - mockDownloadImageAsBase64Interactor, - mockTransformHtmlEmailContentInteractor, - mockGetServerSettingInteractor, - mockCreateNewAndSendEmailInteractor, - mockCreateNewAndSaveEmailToDraftsInteractor, - mockPrintEmailInteractor, - composerArgs: ComposerArguments(identities: [identity]), - ); - - composerController?.onChangeTextEditorWeb(emailContent); - composerController?.subjectEmail.value = emailSubject; - composerController?.listToEmailAddress = [toRecipient]; - composerController?.listCcEmailAddress = [ccRecipient]; - composerController?.listBccEmailAddress = [bccRecipient]; - composerController?.listReplyToEmailAddress = [replyToRecipient]; - when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - - final savedEmailDraft = SavedEmailDraft( - content: emailContent, - subject: emailSubject, - toRecipients: {toRecipient}, - ccRecipients: {ccRecipient}, - bccRecipients: {bccRecipient}, - replyToRecipients: {replyToRecipient}, - identity: identity, - attachments: [attachment], - hasReadReceipt: false - ); - - // act - composerController?.onReady(); - await Future.delayed(Duration.zero); - - // assert - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); - - // tear down - PlatformInfo.isTestingForWeb = false; - }); - - test( - 'should update _savedEmailDraftHash ' - 'when _initIdentities is called ' - 'and listFromIdentities is empty ' - 'and selectedIdentity is available', - () async { - // arrange - PlatformInfo.isTestingForWeb = true; - - final selectedIdentity = Identity( - id: IdentityId(Id('alice')), - mayDelete: true, - ); - - composerController = ComposerController( - mockLocalFilePickerInteractor, - mockLocalImagePickerInteractor, - mockGetEmailContentInteractor, - mockGetAllIdentitiesInteractor, - mockUploadController, - mockRemoveComposerCacheByIdOnWebInteractor, - mockSaveComposerCacheOnWebInteractor, - mockDownloadImageAsBase64Interactor, - mockTransformHtmlEmailContentInteractor, - mockGetServerSettingInteractor, - mockCreateNewAndSendEmailInteractor, - mockCreateNewAndSaveEmailToDraftsInteractor, - mockPrintEmailInteractor, - composerArgs: ComposerArguments(selectedIdentityId: selectedIdentity.id), - ); - - composerController?.onChangeTextEditorWeb(emailContent); - composerController?.subjectEmail.value = emailSubject; - composerController?.listToEmailAddress = [toRecipient]; - composerController?.listCcEmailAddress = [ccRecipient]; - composerController?.listBccEmailAddress = [bccRecipient]; - composerController?.listReplyToEmailAddress = [replyToRecipient]; - when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - - when(mockGetAllIdentitiesInteractor.execute(any, any)).thenAnswer( - (_) => Stream.value( - Right(GetAllIdentitiesSuccess([selectedIdentity], null)))); - - final savedEmailDraft = SavedEmailDraft( - content: emailContent, - subject: emailSubject, - toRecipients: {toRecipient}, - ccRecipients: {ccRecipient}, - bccRecipients: {bccRecipient}, - replyToRecipients: {replyToRecipient}, - identity: selectedIdentity, - attachments: [attachment], - hasReadReceipt: false - ); - - // act - composerController?.onReady(); - await Future.delayed(Duration.zero); - - // assert - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); - - // tear down - PlatformInfo.isTestingForWeb = false; - }); - - test( - 'should update _savedEmailDraftHash ' - 'when _initIdentities is called ' - 'and listFromIdentities is empty ' - 'and selectedIdentity is not available', - () async { - // arrange - PlatformInfo.isTestingForWeb = true; - - composerController = ComposerController( - mockLocalFilePickerInteractor, - mockLocalImagePickerInteractor, - mockGetEmailContentInteractor, - mockGetAllIdentitiesInteractor, - mockUploadController, - mockRemoveComposerCacheByIdOnWebInteractor, - mockSaveComposerCacheOnWebInteractor, - mockDownloadImageAsBase64Interactor, - mockTransformHtmlEmailContentInteractor, - mockGetServerSettingInteractor, - mockCreateNewAndSendEmailInteractor, - mockCreateNewAndSaveEmailToDraftsInteractor, - mockPrintEmailInteractor, - composerArgs: ComposerArguments(), - ); - - composerController?.onChangeTextEditorWeb(emailContent); - composerController?.subjectEmail.value = emailSubject; - composerController?.listToEmailAddress = [toRecipient]; - composerController?.listCcEmailAddress = [ccRecipient]; - composerController?.listBccEmailAddress = [bccRecipient]; - composerController?.listReplyToEmailAddress = [replyToRecipient]; - when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - - final identity = Identity( - id: IdentityId(Id('alice')), - mayDelete: true); - when(mockGetAllIdentitiesInteractor.execute(any, any)).thenAnswer( - (_) => Stream.value( - Right(GetAllIdentitiesSuccess([identity], null)))); - - final savedEmailDraft = SavedEmailDraft( - content: emailContent, - subject: emailSubject, - toRecipients: {toRecipient}, - ccRecipients: {ccRecipient}, - bccRecipients: {bccRecipient}, - replyToRecipients: {replyToRecipient}, - identity: identity, - attachments: [attachment], - hasReadReceipt: false - ); - - // act - composerController?.onReady(); - await Future.delayed(Duration.zero); - - // assert - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); - - // tear down - PlatformInfo.isTestingForWeb = false; }); testWidgets( - 'should update _savedEmailDraftHash ' - 'when user click save draft button ' - 'and SaveEmailAsDraftsSuccess is returned', + 'Should update _savedEmailDraftHash\n' + 'When user click save draft button\n' + 'And SaveEmailAsDraftsSuccess is returned', (tester) async { await tester.runAsync(() async { // arrange @@ -665,14 +449,17 @@ void main() { Get.put(composerController!); composerController?.richTextWebController = mockRichTextWebController; - - composerController?.onChangeTextEditorWeb(emailContent); + + composerController?.setTextEditorWeb(emailContent); composerController?.subjectEmail.value = emailSubject; composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; composerController?.listReplyToEmailAddress = [replyToRecipient]; when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + when(mockComposerRepository.removeCollapsedExpandedSignatureEffect( + emailContent: anyNamed('emailContent'), + )).thenAnswer((_) async => emailContent); final selectedIdentity = Identity(id: IdentityId(Id('alice'))); composerController?.identitySelected.value = selectedIdentity; @@ -693,7 +480,8 @@ void main() { replyToRecipients: {replyToRecipient}, identity: selectedIdentity, attachments: [attachment], - hasReadReceipt: false + hasReadReceipt: false, + isMarkAsImportant: false, ); await tester.pumpWidget(WidgetFixtures.makeTestableWidget( @@ -714,7 +502,10 @@ void main() { cancelToken: anyNamed('cancelToken'))); // assert - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + expect( + composerController?.savedEmailDraftHash, + equals(savedEmailDraft.asString().hashCode), + ); // tear down PlatformInfo.isTestingForWeb = false; @@ -722,9 +513,9 @@ void main() { }); testWidgets( - 'should update _savedEmailDraftHash ' - 'when user click save draft button ' - 'and UpdateEmailDraftsSuccess is returned', + 'Should update _savedEmailDraftHash\n' + 'When user click save draft button\n' + 'And UpdateEmailDraftsSuccess is returned', (tester) async { await tester.runAsync(() async { // arrange @@ -738,7 +529,7 @@ void main() { Get.put(composerController!); composerController?.richTextWebController = mockRichTextWebController; - + composerController?.onChangeTextEditorWeb(emailContent); composerController?.subjectEmail.value = emailSubject; composerController?.listToEmailAddress = [toRecipient]; @@ -746,6 +537,9 @@ void main() { composerController?.listBccEmailAddress = [bccRecipient]; composerController?.listReplyToEmailAddress = [replyToRecipient]; when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + when(mockComposerRepository.removeCollapsedExpandedSignatureEffect( + emailContent: anyNamed('emailContent'), + )).thenAnswer((_) async => emailContent); final selectedIdentity = Identity(id: IdentityId(Id('alice'))); composerController?.identitySelected.value = selectedIdentity; @@ -766,7 +560,8 @@ void main() { replyToRecipients: {replyToRecipient}, identity: selectedIdentity, attachments: [attachment], - hasReadReceipt: false + hasReadReceipt: false, + isMarkAsImportant: false, ); await tester.pumpWidget(WidgetFixtures.makeTestableWidget( @@ -787,7 +582,10 @@ void main() { cancelToken: anyNamed('cancelToken'))); // assert - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + expect( + composerController?.savedEmailDraftHash, + equals(savedEmailDraft.asString().hashCode), + ); // tear down PlatformInfo.isTestingForWeb = false; @@ -796,570 +594,37 @@ void main() { }); group('email action type is EmailActionType.editDraft:', () { - setUp(() { - composerController?.composerArguments.value = ComposerArguments( - emailActionType: EmailActionType.editDraft); - }); - test( - 'should update _savedEmailDraftHash ' - 'when there is a new view state ' - 'and the state is GetAlwaysReadReceiptSettingSuccess', + 'Should update _savedEmailDraftHash\n' + 'When screenDisplayMode is normal', () async { // arrange + final composerArguments = ComposerArguments( + emailActionType: EmailActionType.editDraft, + displayMode: ScreenDisplayMode.normal, + identities: [identity], + selectedIdentityId: identity.id, + ); + composerController?.composerArguments.value = composerArguments; composerController?.richTextMobileTabletController = mockRichTextMobileTabletController; - when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn( - mockHtmlEditorApi); - - when(mockHtmlEditorApi.getText()).thenAnswer((_) async => emailContent); - composerController?.subjectEmail.value = emailSubject; - composerController?.listToEmailAddress = [toRecipient]; - composerController?.listCcEmailAddress = [ccRecipient]; - composerController?.listBccEmailAddress = [bccRecipient]; - composerController?.listReplyToEmailAddress = [replyToRecipient]; - composerController?.identitySelected.value = identity; - when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - - const alwaysReadReceiptEnabled = true; - final state = GetServerSettingSuccess(TMailServerSettingOptions(alwaysReadReceipts: alwaysReadReceiptEnabled)); - - final savedEmailDraft = SavedEmailDraft( - content: emailContent, - subject: emailSubject, - toRecipients: {toRecipient}, - ccRecipients: {ccRecipient}, - bccRecipients: {bccRecipient}, - replyToRecipients: {replyToRecipient}, - identity: identity, - attachments: [attachment], - hasReadReceipt: alwaysReadReceiptEnabled - ); - - // act - composerController?.handleSuccessViewState(state); - await untilCalled(mockHtmlEditorApi.getText()); - - // assert - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); - }); - - test( - 'should update _savedEmailDraftHash ' - 'when there is a new view state ' - 'and the state is GetAlwaysReadReceiptSettingFailure', - () async { - // arrange - composerController?.richTextMobileTabletController = mockRichTextMobileTabletController; - when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn( - mockHtmlEditorApi); - - when(mockHtmlEditorApi.getText()).thenAnswer((_) async => emailContent); - composerController?.subjectEmail.value = emailSubject; - composerController?.listToEmailAddress = [toRecipient]; - composerController?.listCcEmailAddress = [ccRecipient]; - composerController?.listBccEmailAddress = [bccRecipient]; - composerController?.listReplyToEmailAddress = [replyToRecipient]; - composerController?.identitySelected.value = identity; - when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - - final state = GetServerSettingFailure(Exception()); - - final savedEmailDraft = SavedEmailDraft( - content: emailContent, - subject: emailSubject, - toRecipients: {toRecipient}, - ccRecipients: {ccRecipient}, - bccRecipients: {bccRecipient}, - replyToRecipients: {replyToRecipient}, - identity: identity, - attachments: [attachment], - hasReadReceipt: false - ); - - // act - composerController?.handleFailureViewState(state); - await untilCalled(mockHtmlEditorApi.getText()); - - // assert - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); - }); - - test( - 'should update _savedEmailDraftHash ' - 'when _initIdentities is called ' - 'and listFromIdentities is not empty ' - 'and selectedIdentity is available', - () async { - // arrange - PlatformInfo.isTestingForWeb = true; - - final selectedIdentity = Identity(id: IdentityId(Id('alice'))); - - composerController = ComposerController( - mockLocalFilePickerInteractor, - mockLocalImagePickerInteractor, - mockGetEmailContentInteractor, - mockGetAllIdentitiesInteractor, - mockUploadController, - mockRemoveComposerCacheByIdOnWebInteractor, - mockSaveComposerCacheOnWebInteractor, - mockDownloadImageAsBase64Interactor, - mockTransformHtmlEmailContentInteractor, - mockGetServerSettingInteractor, - mockCreateNewAndSendEmailInteractor, - mockCreateNewAndSaveEmailToDraftsInteractor, - mockPrintEmailInteractor, - composerArgs: ComposerArguments( - emailActionType: EmailActionType.editDraft, - emailContents: emailContent, - presentationEmail: PresentationEmail( - id: EmailId(Id('some-email-id')), - subject: emailSubject, - to: {toRecipient}, - cc: {ccRecipient}, - bcc: {bccRecipient}, - replyTo: {replyToRecipient}, - mailboxContain: PresentationMailbox( - MailboxId(Id('some-mailbox-id')), - role: PresentationMailbox.roleJunk, - ), - ), - selectedIdentityId: selectedIdentity.id, - identities: [selectedIdentity], - ), - ); - - composerController?.onChangeTextEditorWeb(emailContent); - - when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - - final savedEmailDraft = SavedEmailDraft( - content: emailContent, - subject: emailSubject, - toRecipients: {toRecipient}, - ccRecipients: {ccRecipient}, - bccRecipients: {bccRecipient}, - replyToRecipients: {replyToRecipient}, - identity: selectedIdentity, - attachments: [attachment], - hasReadReceipt: false - ); - - // act - composerController?.onReady(); - await Future.delayed(Duration.zero); - - // assert - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); - - // tear down - PlatformInfo.isTestingForWeb = false; - }); - - test( - 'should update _savedEmailDraftHash ' - 'when _initIdentities is called ' - 'and listFromIdentities is not empty ' - 'and selectedIdentity is not available', - () async { - // arrange - PlatformInfo.isTestingForWeb = true; - - final identity = Identity(id: IdentityId(Id('alice'))); - - composerController = ComposerController( - mockLocalFilePickerInteractor, - mockLocalImagePickerInteractor, - mockGetEmailContentInteractor, - mockGetAllIdentitiesInteractor, - mockUploadController, - mockRemoveComposerCacheByIdOnWebInteractor, - mockSaveComposerCacheOnWebInteractor, - mockDownloadImageAsBase64Interactor, - mockTransformHtmlEmailContentInteractor, - mockGetServerSettingInteractor, - mockCreateNewAndSendEmailInteractor, - mockCreateNewAndSaveEmailToDraftsInteractor, - mockPrintEmailInteractor, - composerArgs: ComposerArguments( - emailActionType: EmailActionType.editDraft, - emailContents: emailContent, - presentationEmail: PresentationEmail( - id: EmailId(Id('some-email-id')), - subject: emailSubject, - to: {toRecipient}, - cc: {ccRecipient}, - bcc: {bccRecipient}, - replyTo: {replyToRecipient}, - mailboxContain: PresentationMailbox( - MailboxId(Id('some-mailbox-id')), - role: PresentationMailbox.roleJunk, - ), - ), - identities: [identity], - ), - ); - - composerController?.onChangeTextEditorWeb(emailContent); - - when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - - final savedEmailDraft = SavedEmailDraft( - content: emailContent, - subject: emailSubject, - toRecipients: {toRecipient}, - ccRecipients: {ccRecipient}, - bccRecipients: {bccRecipient}, - replyToRecipients: {replyToRecipient}, - identity: identity, - attachments: [attachment], - hasReadReceipt: false - ); - - // act - composerController?.onReady(); - await Future.delayed(Duration.zero); - - // assert - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); - - // tear down - PlatformInfo.isTestingForWeb = false; - }); - - test( - 'should update _savedEmailDraftHash ' - 'when _initIdentities is called ' - 'and listFromIdentities is empty ' - 'and selectedIdentity is available', - () async { - // arrange - PlatformInfo.isTestingForWeb = true; - - final selectedIdentity = Identity( - id: IdentityId(Id('alice')), - mayDelete: true, - ); - - composerController = ComposerController( - mockLocalFilePickerInteractor, - mockLocalImagePickerInteractor, - mockGetEmailContentInteractor, - mockGetAllIdentitiesInteractor, - mockUploadController, - mockRemoveComposerCacheByIdOnWebInteractor, - mockSaveComposerCacheOnWebInteractor, - mockDownloadImageAsBase64Interactor, - mockTransformHtmlEmailContentInteractor, - mockGetServerSettingInteractor, - mockCreateNewAndSendEmailInteractor, - mockCreateNewAndSaveEmailToDraftsInteractor, - mockPrintEmailInteractor, - composerArgs: ComposerArguments( - emailActionType: EmailActionType.editDraft, - emailContents: emailContent, - presentationEmail: PresentationEmail( - id: EmailId(Id('some-email-id')), - subject: emailSubject, - to: {toRecipient}, - cc: {ccRecipient}, - bcc: {bccRecipient}, - replyTo: {replyToRecipient}, - mailboxContain: PresentationMailbox( - MailboxId(Id('some-mailbox-id')), - role: PresentationMailbox.roleJunk, - ), - ), - selectedIdentityId: selectedIdentity.id, - ), - ); - - composerController?.onChangeTextEditorWeb(emailContent); - - when(mockGetAllIdentitiesInteractor.execute(any, any)).thenAnswer( - (_) => Stream.value( - Right(GetAllIdentitiesSuccess([selectedIdentity], null)))); - - when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - - final savedEmailDraft = SavedEmailDraft( - content: emailContent, - subject: emailSubject, - toRecipients: {toRecipient}, - ccRecipients: {ccRecipient}, - bccRecipients: {bccRecipient}, - replyToRecipients: {replyToRecipient}, - identity: selectedIdentity, - attachments: [attachment], - hasReadReceipt: false - ); - - // act - composerController?.onReady(); - await Future.delayed(Duration.zero); - - // assert - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); - - // tear down - PlatformInfo.isTestingForWeb = false; - }); - - test( - 'should update _savedEmailDraftHash ' - 'when _initIdentities is called ' - 'and listFromIdentities is empty ' - 'and selectedIdentity is not available', - () async { - // arrange - PlatformInfo.isTestingForWeb = true; - - final identity = Identity( - id: IdentityId(Id('alice')), - mayDelete: true, - ); - - composerController = ComposerController( - mockLocalFilePickerInteractor, - mockLocalImagePickerInteractor, - mockGetEmailContentInteractor, - mockGetAllIdentitiesInteractor, - mockUploadController, - mockRemoveComposerCacheByIdOnWebInteractor, - mockSaveComposerCacheOnWebInteractor, - mockDownloadImageAsBase64Interactor, - mockTransformHtmlEmailContentInteractor, - mockGetServerSettingInteractor, - mockCreateNewAndSendEmailInteractor, - mockCreateNewAndSaveEmailToDraftsInteractor, - mockPrintEmailInteractor, - composerArgs: ComposerArguments( - emailActionType: EmailActionType.editDraft, - emailContents: emailContent, - presentationEmail: PresentationEmail( - id: EmailId(Id('some-email-id')), - subject: emailSubject, - to: {toRecipient}, - cc: {ccRecipient}, - bcc: {bccRecipient}, - replyTo: {replyToRecipient}, - mailboxContain: PresentationMailbox( - MailboxId(Id('some-mailbox-id')), - role: PresentationMailbox.roleJunk, - ), - ), - ), - ); - - composerController?.onChangeTextEditorWeb(emailContent); - - when(mockGetAllIdentitiesInteractor.execute(any, any)).thenAnswer( - (_) => Stream.value( - Right(GetAllIdentitiesSuccess([identity], null)))); - - when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - - final savedEmailDraft = SavedEmailDraft( - content: emailContent, - subject: emailSubject, - toRecipients: {toRecipient}, - ccRecipients: {ccRecipient}, - bccRecipients: {bccRecipient}, - replyToRecipients: {replyToRecipient}, - identity: identity, - attachments: [attachment], - hasReadReceipt: false - ); - - // act - composerController?.onReady(); - await Future.delayed(Duration.zero); - - // assert - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); - - // tear down - PlatformInfo.isTestingForWeb = false; - }); - - testWidgets( - 'should update _savedEmailDraftHash ' - 'when user click save draft button ' - 'and SaveEmailAsDraftsSuccess is returned', - (tester) async { - await tester.runAsync(() async { - // arrange - PlatformInfo.isTestingForWeb = true; - InAppWebViewPlatform.instance = MockWebViewPlatform(); - - when(mockUploadController.uploadInlineViewState).thenReturn( - Rx(Right(UIState.idle))); - when(mockUploadController.listUploadAttachments).thenReturn( - RxList()); - - Get.put(composerController!); - composerController?.richTextWebController = mockRichTextWebController; - - composerController?.onChangeTextEditorWeb(emailContent); - composerController?.subjectEmail.value = emailSubject; - composerController?.listToEmailAddress = [toRecipient]; - composerController?.listCcEmailAddress = [ccRecipient]; - composerController?.listBccEmailAddress = [bccRecipient]; - composerController?.listReplyToEmailAddress = [replyToRecipient]; - final selectedIdentity = Identity(id: IdentityId(Id('alice'))); - composerController?.identitySelected.value = selectedIdentity; - when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - - composerController?.composerArguments.value = ComposerArguments( - emailActionType: EmailActionType.editDraft); - when( - mockCreateNewAndSaveEmailToDraftsInteractor.execute( - createEmailRequest: anyNamed('createEmailRequest'), - cancelToken: anyNamed('cancelToken'))) - .thenAnswer((_) => Stream.value( - Right(SaveEmailAsDraftsSuccess(EmailId(Id('123')), null)))); - - final savedEmailDraft = SavedEmailDraft( - content: emailContent, - subject: emailSubject, - toRecipients: {toRecipient}, - ccRecipients: {ccRecipient}, - bccRecipients: {bccRecipient}, - replyToRecipients: {replyToRecipient}, - identity: selectedIdentity, - attachments: [attachment], - hasReadReceipt: false - ); - - await tester.pumpWidget(WidgetFixtures.makeTestableWidget( - child: const Stack(children: [ComposerView()]))); - await tester.pump(); - - // act - final saveAsDraftButton = find.ancestor( - of: find.byType(InkWell), - matching: find.byWidgetPredicate( - (widget) => widget is TMailButtonWidget - && widget.icon == ImagePaths().icSaveToDraft)); - await tester.tap(saveAsDraftButton); - await tester.pump(); - await untilCalled( - mockCreateNewAndSaveEmailToDraftsInteractor.execute( - createEmailRequest: anyNamed('createEmailRequest'), - cancelToken: anyNamed('cancelToken'))); - - // assert - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); - - // tear down - PlatformInfo.isTestingForWeb = false; - }); - }); - - testWidgets( - 'should update _savedEmailDraftHash ' - 'when user click save draft button ' - 'and UpdateEmailDraftsSuccess is returned', - (tester) async { - await tester.runAsync(() async { - // arrange - PlatformInfo.isTestingForWeb = true; - InAppWebViewPlatform.instance = MockWebViewPlatform(); - - when(mockUploadController.uploadInlineViewState).thenReturn( - Rx(Right(UIState.idle))); - when(mockUploadController.listUploadAttachments).thenReturn( - RxList()); - - Get.put(composerController!); - composerController?.richTextWebController = mockRichTextWebController; - - composerController?.onChangeTextEditorWeb(emailContent); - composerController?.subjectEmail.value = emailSubject; - composerController?.listToEmailAddress = [toRecipient]; - composerController?.listCcEmailAddress = [ccRecipient]; - composerController?.listBccEmailAddress = [bccRecipient]; - composerController?.listReplyToEmailAddress = [replyToRecipient]; - final selectedIdentity = Identity(id: IdentityId(Id('alice'))); - composerController?.identitySelected.value = selectedIdentity; - when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - - composerController?.composerArguments.value = ComposerArguments( - emailActionType: EmailActionType.editDraft); - when( - mockCreateNewAndSaveEmailToDraftsInteractor.execute( - createEmailRequest: anyNamed('createEmailRequest'), - cancelToken: anyNamed('cancelToken'))) - .thenAnswer((_) => Stream.value( - Right(UpdateEmailDraftsSuccess(EmailId(Id('123')))))); - - final savedEmailDraft = SavedEmailDraft( - content: emailContent, - subject: emailSubject, - toRecipients: {toRecipient}, - ccRecipients: {ccRecipient}, - bccRecipients: {bccRecipient}, - replyToRecipients: {replyToRecipient}, - identity: selectedIdentity, - attachments: [attachment], - hasReadReceipt: false - ); - - await tester.pumpWidget(WidgetFixtures.makeTestableWidget( - child: const Stack(children: [ComposerView()]))); - await tester.pump(); - - // act - final saveAsDraftButton = find.ancestor( - of: find.byType(InkWell), - matching: find.byWidgetPredicate( - (widget) => widget is TMailButtonWidget - && widget.icon == ImagePaths().icSaveToDraft)); - await tester.tap(saveAsDraftButton); - await tester.pump(); - await untilCalled( - mockCreateNewAndSaveEmailToDraftsInteractor.execute( - createEmailRequest: anyNamed('createEmailRequest'), - cancelToken: anyNamed('cancelToken'))); - - // assert - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); - - // tear down - PlatformInfo.isTestingForWeb = false; - }); - }); - - test( - 'should update _savedEmailDraftHash with the same value' - 'and call _updateSavedEmailDraftHash twice ' - 'when there is a new view state ' - 'and the state is GetEmailContentSuccess', - () async { - // arrange - composerController?.richTextMobileTabletController = mockRichTextMobileTabletController; - when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn( - mockHtmlEditorApi); - - when(mockHtmlEditorApi.getText()).thenAnswer((_) async => emailContent); composerController?.subjectEmail.value = emailSubject; composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; composerController?.listReplyToEmailAddress = [replyToRecipient]; composerController?.hasRequestReadReceipt.value = alwaysReadReceiptEnabled; + composerController?.isMarkAsImportant.value = isMarkAsImportant; + composerController?.screenDisplayMode.value = composerArguments.displayMode; + composerController?.currentEmailActionType = composerArguments.emailActionType; + composerController?.listFromIdentities.value = composerArguments.identities!; + composerController?.identitySelected.value = null; - const idenityId = 'some-identity-id'; - final identity = Identity(id: IdentityId(Id(idenityId))); - composerController?.identitySelected.value = identity; - composerController?.listFromIdentities.add(identity); + when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn(mockHtmlEditorApi); + when(mockHtmlEditorApi.getText()).thenAnswer((_) async => emailContent); when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - - final state = GetEmailContentSuccess( - htmlEmailContent: '', - emailCurrent: Email( - identityHeader: {IndividualHeaderIdentifier.identityHeader: idenityId}, - headers: {EmailHeader(EmailProperty.headerMdnKey, 'value')})); + when(mockComposerRepository.removeCollapsedExpandedSignatureEffect( + emailContent: anyNamed('emailContent'), + )).thenAnswer((_) async => emailContent); final savedEmailDraft = SavedEmailDraft( content: emailContent, @@ -1370,40 +635,59 @@ void main() { replyToRecipients: {replyToRecipient}, identity: identity, attachments: [attachment], - hasReadReceipt: alwaysReadReceiptEnabled + hasReadReceipt: alwaysReadReceiptEnabled, + isMarkAsImportant: isMarkAsImportant, ); - + // act - composerController?.handleSuccessViewState(state); + composerController?.setupSelectedIdentity(); + await untilCalled(mockHtmlEditorApi.getText()); - + await untilCalled( + mockComposerRepository.removeCollapsedExpandedSignatureEffect( + emailContent: anyNamed('emailContent'))); + // assert - verify(mockHtmlEditorApi.getText()).called(2); - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + expect( + composerController?.savedEmailDraftHash, + equals(savedEmailDraft.asString().hashCode), + ); }); test( - 'should update _savedEmailDraftHash ' - 'when restoring signature button finished', + 'Should update _savedEmailDraftHash\n' + 'When screenDisplayMode is minimize', () async { // arrange - PlatformInfo.isTestingForWeb = true; - - const emailContentWithSignature = '
'; - const emailContentWithSignatureButton = '
'; - - when(mockHtmlEditorApi.getText()).thenAnswer((_) async => emailContentWithSignatureButton); + final composerArguments = ComposerArguments( + emailActionType: EmailActionType.editDraft, + displayMode: ScreenDisplayMode.minimize, + identities: [identity], + selectedIdentityId: identity.id, + ); + composerController?.composerArguments.value = composerArguments; + composerController?.richTextMobileTabletController = mockRichTextMobileTabletController; composerController?.subjectEmail.value = emailSubject; composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; composerController?.listReplyToEmailAddress = [replyToRecipient]; - composerController?.identitySelected.value = identity; - when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); composerController?.hasRequestReadReceipt.value = alwaysReadReceiptEnabled; + composerController?.isMarkAsImportant.value = isMarkAsImportant; + composerController?.screenDisplayMode.value = composerArguments.displayMode; + composerController?.currentEmailActionType = composerArguments.emailActionType; + composerController?.listFromIdentities.value = composerArguments.identities!; + composerController?.identitySelected.value = null; + + when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn(mockHtmlEditorApi); + when(mockHtmlEditorApi.getText()).thenAnswer((_) async => emailContent); + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + when(mockComposerRepository.removeCollapsedExpandedSignatureEffect( + emailContent: anyNamed('emailContent'), + )).thenAnswer((_) async => emailContent); final savedEmailDraft = SavedEmailDraft( - content: emailContentWithSignatureButton, + content: emailContent, subject: emailSubject, toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, @@ -1411,310 +695,23 @@ void main() { replyToRecipients: {replyToRecipient}, identity: identity, attachments: [attachment], - hasReadReceipt: alwaysReadReceiptEnabled - ); - - // act - await composerController?.restoreCollapsibleButton(emailContentWithSignature); - expect(composerController?.restoringSignatureButton, true); - composerController?.onChangeTextEditorWeb(emailContentWithSignatureButton); - await Future.delayed(Duration.zero); - - // assert - expect(composerController?.restoringSignatureButton, false); - expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); - - // tear down - PlatformInfo.isTestingForWeb = false; - }); - }); - - group('email action type is neither EmailActionType.compose nor EmailActionType.editDraft:', () { - setUp(() { - composerController?.composerArguments.value = ComposerArguments( - emailActionType: EmailActionType.reply); - }); - - test( - 'should not update _savedEmailDraftHash ' - 'when there is a new view state ' - 'and the state is GetAlwaysReadReceiptSettingSuccess', - () async { - // arrange - composerController?.richTextMobileTabletController = mockRichTextMobileTabletController; - when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn( - mockHtmlEditorApi); - - final state = GetServerSettingSuccess(TMailServerSettingOptions(alwaysReadReceipts: true)); - - // act - composerController?.handleSuccessViewState(state); - - // assert - expect(composerController?.savedEmailDraftHash, isNull); - }); - - test( - 'should not update _savedEmailDraftHash ' - 'when there is a new view state ' - 'and the state is GetAlwaysReadReceiptSettingFailure', - () async { - // arrange - composerController?.richTextMobileTabletController = mockRichTextMobileTabletController; - when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn( - mockHtmlEditorApi); - - final state = GetServerSettingFailure(Exception()); - - // act - composerController?.handleFailureViewState(state); - - // assert - expect(composerController?.savedEmailDraftHash, isNull); - }); - - test( - 'should not update _savedEmailDraftHash ' - 'when _initIdentities is called ' - 'and listFromIdentities is not empty ' - 'and selectedIdentity is available', - () async { - // arrange - PlatformInfo.isTestingForWeb = true; - - final selectedIdentity = Identity(id: IdentityId(Id('alice'))); - - composerController = ComposerController( - mockLocalFilePickerInteractor, - mockLocalImagePickerInteractor, - mockGetEmailContentInteractor, - mockGetAllIdentitiesInteractor, - mockUploadController, - mockRemoveComposerCacheByIdOnWebInteractor, - mockSaveComposerCacheOnWebInteractor, - mockDownloadImageAsBase64Interactor, - mockTransformHtmlEmailContentInteractor, - mockGetServerSettingInteractor, - mockCreateNewAndSendEmailInteractor, - mockCreateNewAndSaveEmailToDraftsInteractor, - mockPrintEmailInteractor, - composerArgs: ComposerArguments( - emailActionType: EmailActionType.reply, - emailContents: emailContent, - presentationEmail: PresentationEmail( - mailboxContain: PresentationMailbox( - MailboxId(Id('some-mailbox-id')), - role: PresentationMailbox.roleJunk, - ), - ), - selectedIdentityId: selectedIdentity.id, - identities: [selectedIdentity], - ), + hasReadReceipt: alwaysReadReceiptEnabled, + isMarkAsImportant: isMarkAsImportant, ); - when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - // act - composerController?.onReady(); - await Future.delayed(Duration.zero); - + composerController?.setupSelectedIdentityWithoutApplySignature(); + + await untilCalled(mockHtmlEditorApi.getText()); + await untilCalled( + mockComposerRepository.removeCollapsedExpandedSignatureEffect( + emailContent: anyNamed('emailContent'))); + // assert - expect(composerController?.savedEmailDraftHash, isNull); - - // tear down - PlatformInfo.isTestingForWeb = false; - }); - - test( - 'should not update _savedEmailDraftHash ' - 'when _initIdentities is called ' - 'and listFromIdentities is not empty ' - 'and selectedIdentity is not available', - () async { - // arrange - PlatformInfo.isTestingForWeb = true; - - final identity = Identity(id: IdentityId(Id('alice'))); - - composerController = ComposerController( - mockLocalFilePickerInteractor, - mockLocalImagePickerInteractor, - mockGetEmailContentInteractor, - mockGetAllIdentitiesInteractor, - mockUploadController, - mockRemoveComposerCacheByIdOnWebInteractor, - mockSaveComposerCacheOnWebInteractor, - mockDownloadImageAsBase64Interactor, - mockTransformHtmlEmailContentInteractor, - mockGetServerSettingInteractor, - mockCreateNewAndSendEmailInteractor, - mockCreateNewAndSaveEmailToDraftsInteractor, - mockPrintEmailInteractor, - composerArgs: ComposerArguments( - emailActionType: EmailActionType.reply, - emailContents: emailContent, - presentationEmail: PresentationEmail( - mailboxContain: PresentationMailbox( - MailboxId(Id('some-mailbox-id')), - role: PresentationMailbox.roleJunk, - ), - ), - identities: [identity], - ), + expect( + composerController?.savedEmailDraftHash, + equals(savedEmailDraft.asString().hashCode), ); - - when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - - // act - composerController?.onReady(); - await Future.delayed(Duration.zero); - - // assert - expect(composerController?.savedEmailDraftHash, isNull); - - // tear down - PlatformInfo.isTestingForWeb = false; - }); - - test( - 'should not update _savedEmailDraftHash ' - 'when _initIdentities is called ' - 'and listFromIdentities is empty ' - 'and selectedIdentity is available', - () async { - // arrange - PlatformInfo.isTestingForWeb = true; - - final selectedIdentity = Identity( - id: IdentityId(Id('alice')), - mayDelete: true); - - composerController = ComposerController( - mockLocalFilePickerInteractor, - mockLocalImagePickerInteractor, - mockGetEmailContentInteractor, - mockGetAllIdentitiesInteractor, - mockUploadController, - mockRemoveComposerCacheByIdOnWebInteractor, - mockSaveComposerCacheOnWebInteractor, - mockDownloadImageAsBase64Interactor, - mockTransformHtmlEmailContentInteractor, - mockGetServerSettingInteractor, - mockCreateNewAndSendEmailInteractor, - mockCreateNewAndSaveEmailToDraftsInteractor, - mockPrintEmailInteractor, - composerArgs: ComposerArguments( - emailActionType: EmailActionType.reply, - emailContents: emailContent, - presentationEmail: PresentationEmail( - mailboxContain: PresentationMailbox( - MailboxId(Id('some-mailbox-id')), - role: PresentationMailbox.roleJunk, - ), - ), - selectedIdentityId: selectedIdentity.id, - ), - ); - - when(mockGetAllIdentitiesInteractor.execute(any, any)).thenAnswer( - (_) => Stream.value( - Right(GetAllIdentitiesSuccess([selectedIdentity], null)))); - - when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - - // act - composerController?.onReady(); - await Future.delayed(Duration.zero); - - // assert - expect(composerController?.savedEmailDraftHash, isNull); - - // tear down - PlatformInfo.isTestingForWeb = false; - }); - - test( - 'should not update _savedEmailDraftHash ' - 'when _initIdentities is called ' - 'and listFromIdentities is empty ' - 'and selectedIdentity is not available', - () async { - // arrange - PlatformInfo.isTestingForWeb = true; - - final identity = Identity( - id: IdentityId(Id('alice')), - mayDelete: true); - - composerController = ComposerController( - mockLocalFilePickerInteractor, - mockLocalImagePickerInteractor, - mockGetEmailContentInteractor, - mockGetAllIdentitiesInteractor, - mockUploadController, - mockRemoveComposerCacheByIdOnWebInteractor, - mockSaveComposerCacheOnWebInteractor, - mockDownloadImageAsBase64Interactor, - mockTransformHtmlEmailContentInteractor, - mockGetServerSettingInteractor, - mockCreateNewAndSendEmailInteractor, - mockCreateNewAndSaveEmailToDraftsInteractor, - mockPrintEmailInteractor, - composerArgs: ComposerArguments( - emailActionType: EmailActionType.reply, - emailContents: emailContent, - presentationEmail: PresentationEmail( - mailboxContain: PresentationMailbox( - MailboxId(Id('some-mailbox-id')), - role: PresentationMailbox.roleJunk, - ), - ), - ), - ); - - when(mockGetAllIdentitiesInteractor.execute(any, any)).thenAnswer( - (_) => Stream.value( - Right(GetAllIdentitiesSuccess([identity], null)))); - - when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); - - // act - composerController?.onReady(); - await Future.delayed(Duration.zero); - - // assert - expect(composerController?.savedEmailDraftHash, isNull); - - // tear down - PlatformInfo.isTestingForWeb = false; - }); - - test( - 'should not update _savedEmailDraftHash ' - 'when there is a new view state ' - 'and the state is GetEmailContentSuccess', - () async { - // arrange - composerController?.richTextMobileTabletController = mockRichTextMobileTabletController; - when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn( - mockHtmlEditorApi); - - const idenityId = 'some-identity-id'; - final identity = Identity(id: IdentityId(Id(idenityId))); - composerController?.identitySelected.value = identity; - composerController?.listFromIdentities.add(identity); - - final state = GetEmailContentSuccess( - htmlEmailContent: emailContent, - emailCurrent: Email( - identityHeader: {IndividualHeaderIdentifier.identityHeader: idenityId})); - - // act - composerController?.handleSuccessViewState(state); - - // assert - verifyNever(mockHtmlEditorApi.getText()); - expect(composerController?.savedEmailDraftHash, isNull); }); }); }); diff --git a/test/features/composer/presentation/model/saved_email_draft_test.dart b/test/features/composer/presentation/model/saved_email_draft_test.dart index cbc1dcf8a..2ed46f3b7 100644 --- a/test/features/composer/presentation/model/saved_email_draft_test.dart +++ b/test/features/composer/presentation/model/saved_email_draft_test.dart @@ -26,9 +26,9 @@ void main() { final props = savedEmailDraft.props; // assert - expect(props[2], equals({0: savedEmailDraft.toRecipients})); - expect(props[3], equals({1: savedEmailDraft.ccRecipients})); - expect(props[4], equals({2: savedEmailDraft.bccRecipients})); + expect(props[2], equals(savedEmailDraft.toRecipients)); + expect(props[3], equals(savedEmailDraft.ccRecipients)); + expect(props[4], equals(savedEmailDraft.bccRecipients)); }); test( @@ -123,11 +123,11 @@ void main() { attachments: [], hasReadReceipt: false ); - final hashCodeBeforeChange = savedEmailDraft.hashCode; + final hashCodeBeforeChange = savedEmailDraft.asString().hashCode; // act listToRecipients.add(EmailAddress('to name 2', 'to email 2')); - final hashCodeAfterChange = savedEmailDraft.hashCode; + final hashCodeAfterChange = savedEmailDraft.asString().hashCode; // assert expect(hashCodeBeforeChange, isNot(hashCodeAfterChange)); @@ -163,7 +163,10 @@ void main() { ); // assert - expect(savedEmailDraft.hashCode, equals(savedEmailDraft2.hashCode)); + expect( + savedEmailDraft.asString().hashCode, + equals(savedEmailDraft2.asString().hashCode), + ); }); }); } \ No newline at end of file