From 49c029883db17d6cda0c93ed142e4db086efec2b Mon Sep 17 00:00:00 2001 From: DatDang Date: Tue, 18 Jun 2024 15:23:06 +0700 Subject: [PATCH 1/3] TF-2901 Create before unload manager to handle before log out callbacks --- lib/features/base/base_controller.dart | 13 +++ lib/features/base/before_unload_handler.dart | 3 + lib/features/base/before_unload_manager.dart | 24 +++++ lib/main/bindings/core/core_bindings.dart | 2 + .../base/before_unload_manager_test.dart | 96 +++++++++++++++++++ 5 files changed, 138 insertions(+) create mode 100644 lib/features/base/before_unload_handler.dart create mode 100644 lib/features/base/before_unload_manager.dart create mode 100644 test/features/base/before_unload_manager_test.dart diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index eacf5b8cb3..e44350eedf 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -23,6 +23,7 @@ import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:model/account/authentication_type.dart'; import 'package:rule_filter/rule_filter/capability_rule_filter.dart'; +import 'package:tmail_ui_user/features/base/before_unload_manager.dart'; import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixin.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; @@ -169,7 +170,14 @@ abstract class BaseController extends GetxController if (!authorizationInterceptors.isAppRunning) { return; } + _executeBeforeUnloadAndLogOut(exception); + } + + Future _executeBeforeUnloadAndLogOut(Exception? exception) async { if (exception is BadCredentialsException || exception is ConnectionError) { + if (PlatformInfo.isWeb) { + await executeBeforeUnload(); + } clearDataAndGoToLoginPage(); } } @@ -334,6 +342,11 @@ abstract class BaseController extends GetxController } } + Future executeBeforeUnload() async { + final beforeUnloadManager = getBinding(); + await beforeUnloadManager?.executeBeforeUnloadListeners(); + } + Future clearDataAndGoToLoginPage() async { log('BaseController::clearDataAndGoToLoginPage:'); await clearAllData(); diff --git a/lib/features/base/before_unload_handler.dart b/lib/features/base/before_unload_handler.dart new file mode 100644 index 0000000000..2c4bdc1108 --- /dev/null +++ b/lib/features/base/before_unload_handler.dart @@ -0,0 +1,3 @@ +abstract class BeforeUnloadHandler { + Future onBeforeUnload(); +} \ No newline at end of file diff --git a/lib/features/base/before_unload_manager.dart b/lib/features/base/before_unload_manager.dart new file mode 100644 index 0000000000..6b45ffad43 --- /dev/null +++ b/lib/features/base/before_unload_manager.dart @@ -0,0 +1,24 @@ +import 'package:core/utils/app_logger.dart'; + +typedef BeforeUnloadListener = Future Function(); + +class BeforeUnloadManager { + static final BeforeUnloadManager _instance = BeforeUnloadManager._(); + factory BeforeUnloadManager() => _instance; + BeforeUnloadManager._(); + + final _listeners = []; + + void addListener(BeforeUnloadListener listener) { + _listeners.add(listener); + } + + void removeListener(BeforeUnloadListener listener) { + _listeners.remove(listener); + } + + Future executeBeforeUnloadListeners() async { + await Future.wait(_listeners.map((listener) => listener.call())) + .onError((error, stackTrace) => [logError(error.toString())]); + } +} \ No newline at end of file diff --git a/lib/main/bindings/core/core_bindings.dart b/lib/main/bindings/core/core_bindings.dart index fea26aee3c..5feaf5f03c 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -12,6 +12,7 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tmail_ui_user/features/base/before_unload_manager.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/utils/sending_queue_isolate_manager.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; @@ -64,6 +65,7 @@ class CoreBindings extends Bindings { Get.put(FileUtils()); Get.put(PrintUtils()); Get.put(ApplicationManager(Get.find())); + Get.put(BeforeUnloadManager()); } void _bindingIsolate() { diff --git a/test/features/base/before_unload_manager_test.dart b/test/features/base/before_unload_manager_test.dart new file mode 100644 index 0000000000..d5c4fb1d3d --- /dev/null +++ b/test/features/base/before_unload_manager_test.dart @@ -0,0 +1,96 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tmail_ui_user/features/base/before_unload_manager.dart'; + +void main() { + + group('before unload manager test:', () { + test( + 'should finish all futures ' + 'when no future fails', + () async { + // arrange + List result = []; + future1Second() => Future.delayed(const Duration(seconds: 1), () => result.add(1)); + future2Second() => Future.delayed(const Duration(seconds: 2), () => result.add(2)); + future3Second() => Future.delayed(const Duration(seconds: 3), () => result.add(3)); + final beforeUnloadManager = BeforeUnloadManager(); + beforeUnloadManager.addListener(future1Second); + beforeUnloadManager.addListener(future2Second); + beforeUnloadManager.addListener(future3Second); + + // act + await beforeUnloadManager.executeBeforeUnloadListeners(); + + // assert + expect(result.length, 3); + }); + + test( + 'should finish all possible futures ' + 'when one future fail', + () async { + // arrange + List result = []; + final exception = Exception('error'); + future1Second() => Future.delayed(const Duration(seconds: 1), () => result.add(1)); + future2Second() => Future.delayed(const Duration(seconds: 2), () => throw exception); + future3Second() => Future.delayed(const Duration(seconds: 3), () => result.add(3)); + final beforeUnloadManager = BeforeUnloadManager(); + beforeUnloadManager.addListener(future1Second); + beforeUnloadManager.addListener(future2Second); + beforeUnloadManager.addListener(future3Second); + + // act + await beforeUnloadManager.executeBeforeUnloadListeners(); + + // assert + expect(result.length, 2); + }); + + test( + 'should finish all possible futures ' + 'when more than one future fail', + () async { + // arrange + List result = []; + final exception1 = Exception('error 1'); + final exception2 = Exception('error 2'); + future1Second() => Future.delayed(const Duration(seconds: 1), () => throw exception1); + future2Second() => Future.delayed(const Duration(seconds: 2), () => throw exception2); + future3Second() => Future.delayed(const Duration(seconds: 3), () => result.add(3)); + final beforeUnloadManager = BeforeUnloadManager(); + beforeUnloadManager.addListener(future1Second); + beforeUnloadManager.addListener(future2Second); + beforeUnloadManager.addListener(future3Second); + + // act + await beforeUnloadManager.executeBeforeUnloadListeners(); + + // assert + expect(result.length, 1); + }); + + test( + 'should finish all possible futures ' + 'when more than one future fail', + () async { + // arrange + List result = []; + final exception3 = Exception('error 3'); + final exception2 = Exception('error 2'); + future1Second() => Future.delayed(const Duration(seconds: 1), () => result.add(1)); + future2Second() => Future.delayed(const Duration(seconds: 2), () => throw exception2); + future3Second() => Future.delayed(const Duration(seconds: 3), () => throw exception3); + final beforeUnloadManager = BeforeUnloadManager(); + beforeUnloadManager.addListener(future1Second); + beforeUnloadManager.addListener(future2Second); + beforeUnloadManager.addListener(future3Second); + + // act + await beforeUnloadManager.executeBeforeUnloadListeners(); + + // assert + expect(result.length, 1); + }); + }); +} \ No newline at end of file From 06f9abe994e41bda71912f3db12f5076d90a47d3 Mon Sep 17 00:00:00 2001 From: DatDang Date: Tue, 18 Jun 2024 15:24:02 +0700 Subject: [PATCH 2/3] TF-2901 Implement caching composer on log out --- .../exceptions/web_session_exception.dart | 7 + .../dom/image_transformers.dart | 1 + .../transform_configuration.dart | 2 + .../restore_email_inline_images_state.dart | 18 ++ ...estore_email_inline_images_interactor.dart | 30 +++ ...save_composer_cache_on_web_interactor.dart | 19 +- .../presentation/composer_bindings.dart | 3 + .../presentation/composer_controller.dart | 202 +++++++++++++----- .../model/screen_display_mode.dart | 15 +- .../composer_arguments_extension.dart | 18 +- .../model/composer_arguments.dart | 72 ++++++- .../session_storage_composer_datasource.dart | 27 ++- ...ssion_storage_composer_datasoure_impl.dart | 89 ++++++-- .../data/model/composer_cache.dart | 95 +++----- .../composer_cache_repository_impl.dart | 44 +++- .../repository/composer_cache_repository.dart | 27 ++- .../get_composer_cache_on_web_interactor.dart | 10 +- .../controller/upload_controller.dart | 11 + 18 files changed, 506 insertions(+), 184 deletions(-) create mode 100644 lib/features/composer/domain/state/restore_email_inline_images_state.dart create mode 100644 lib/features/composer/domain/usecases/restore_email_inline_images_interactor.dart diff --git a/core/lib/domain/exceptions/web_session_exception.dart b/core/lib/domain/exceptions/web_session_exception.dart index 62a2535a36..8d118d831f 100644 --- a/core/lib/domain/exceptions/web_session_exception.dart +++ b/core/lib/domain/exceptions/web_session_exception.dart @@ -9,6 +9,13 @@ class NotFoundInWebSessionException with EquatableMixin implements Exception { List get props => []; } +class NotMatchInWebSessionException with EquatableMixin implements Exception { + const NotMatchInWebSessionException(); + + @override + List get props => []; +} + class SaveToWebSessionFailException with EquatableMixin implements Exception { final String? errorMessage; diff --git a/core/lib/presentation/utils/html_transformer/dom/image_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/image_transformers.dart index 8cfcce26ba..2c1dfbf5cd 100644 --- a/core/lib/presentation/utils/html_transformer/dom/image_transformers.dart +++ b/core/lib/presentation/utils/html_transformer/dom/image_transformers.dart @@ -48,6 +48,7 @@ class ImageTransformer extends DomTransformer { imageSource: src ); imageElement.attributes['src'] = imageBase64 ?? src; + imageElement.attributes['id'] ??= src; } else if (src.startsWith('https://') || src.startsWith('http://')) { if (!imageElement.attributes.containsKey('loading')) { imageElement.attributes['loading'] = 'lazy'; diff --git a/core/lib/presentation/utils/html_transformer/transform_configuration.dart b/core/lib/presentation/utils/html_transformer/transform_configuration.dart index 6c8ad45207..17fdea07ea 100644 --- a/core/lib/presentation/utils/html_transformer/transform_configuration.dart +++ b/core/lib/presentation/utils/html_transformer/transform_configuration.dart @@ -63,6 +63,8 @@ class TransformConfiguration { factory TransformConfiguration.forPreviewEmail() => TransformConfiguration.standardConfiguration; + factory TransformConfiguration.forRestoreEmail() => TransformConfiguration.fromDomTransformers([const ImageTransformer()]); + factory TransformConfiguration.forPrintEmail() => TransformConfiguration.fromDomTransformers([ if (PlatformInfo.isWeb) const RemoveTooltipLinkTransformer(), diff --git a/lib/features/composer/domain/state/restore_email_inline_images_state.dart b/lib/features/composer/domain/state/restore_email_inline_images_state.dart new file mode 100644 index 0000000000..221cd0fc26 --- /dev/null +++ b/lib/features/composer/domain/state/restore_email_inline_images_state.dart @@ -0,0 +1,18 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class RestoringEmailInlineImages extends LoadingState {} + +class RestoreEmailInlineImagesSuccess extends UIState { + final String emailContent; + + RestoreEmailInlineImagesSuccess(this.emailContent); + + @override + List get props => [emailContent]; +} + +class RestoreEmailInlineImagesFailure extends FeatureFailure { + + RestoreEmailInlineImagesFailure({super.exception}); +} \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/restore_email_inline_images_interactor.dart b/lib/features/composer/domain/usecases/restore_email_inline_images_interactor.dart new file mode 100644 index 0000000000..0d10be8191 --- /dev/null +++ b/lib/features/composer/domain/usecases/restore_email_inline_images_interactor.dart @@ -0,0 +1,30 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/restore_email_inline_images_state.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart'; + +class RestoreEmailInlineImagesInteractor { + RestoreEmailInlineImagesInteractor(this._composerCacheRepository); + + final ComposerCacheRepository _composerCacheRepository; + + Stream> execute({ + required String htmlContent, + required TransformConfiguration transformConfiguration, + required Map mapUrlDownloadCID + }) async* { + try { + yield Right(RestoringEmailInlineImages()); + + final emailContent = await _composerCacheRepository.restoreEmailInlineImages( + htmlContent, + transformConfiguration, + mapUrlDownloadCID); + yield Right(RestoreEmailInlineImagesSuccess(emailContent)); + } catch (exception) { + yield Left(RestoreEmailInlineImagesFailure(exception: exception)); + } + } +} \ 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 cbb6c924d1..086709ab73 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 @@ -1,8 +1,11 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/save_composer_cache_state.dart'; @@ -15,10 +18,22 @@ class SaveComposerCacheOnWebInteractor { this._composerRepository, ); - Future> execute(CreateEmailRequest createEmailRequest) async { + Future> execute( + CreateEmailRequest createEmailRequest, + AccountId accountId, + UserName userName, + {required ScreenDisplayMode displayMode} + ) async { try { final emailCreated = await _composerRepository.generateEmail(createEmailRequest); - _composerCacheRepository.saveComposerCacheOnWeb(emailCreated); + final identity = createEmailRequest.identity; + await _composerCacheRepository.saveComposerCacheOnWeb( + emailCreated, + accountId: accountId, + userName: userName, + displayMode: displayMode, + identity: identity, + readReceipentEnabled: createEmailRequest.isRequestReadReceipt); return Right(SaveComposerCacheSuccess()); } catch (exception) { return Left(SaveComposerCacheFailure(exception)); diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index b600963b6e..83a6556616 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -13,6 +13,7 @@ import 'package:tmail_ui_user/features/composer/domain/repository/contact_reposi import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/download_image_as_base64_interactor.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/restore_email_inline_images_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachment_interactor.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; @@ -208,6 +209,8 @@ class ComposerBindings extends BaseBindings { Get.find(), Get.find(), )); + Get.lazyPut(() => RestoreEmailInlineImagesInteractor( + Get.find())); IdentityInteractorsBindings().dependencies(); } diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 4bbf61c6c1..711c052ee1 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -15,6 +15,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; @@ -26,6 +27,8 @@ import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:super_tag_editor/tag_editor.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; +import 'package:tmail_ui_user/features/base/before_unload_handler.dart'; +import 'package:tmail_ui_user/features/base/before_unload_manager.dart'; import 'package:tmail_ui_user/features/base/state/base_ui_state.dart'; import 'package:tmail_ui_user/features/base/state/button_state.dart'; import 'package:tmail_ui_user/features/composer/domain/exceptions/compose_email_exception.dart'; @@ -34,6 +37,7 @@ import 'package:tmail_ui_user/features/composer/domain/state/download_image_as_b import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_device_contact_suggestions_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'; @@ -43,6 +47,7 @@ import 'package:tmail_ui_user/features/composer/domain/usecases/download_image_a import 'package:tmail_ui_user/features/composer/domain/usecases/get_all_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_device_contact_suggestions_interactor.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/restore_email_inline_images_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/save_composer_cache_on_web_interactor.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'; @@ -91,11 +96,12 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:universal_html/html.dart' as html; -class ComposerController extends BaseController with DragDropFileMixin { +class ComposerController extends BaseController with DragDropFileMixin implements BeforeUnloadHandler { final mailboxDashBoardController = Get.find(); final networkConnectionController = Get.find(); final _dynamicUrlInterceptors = Get.find(); + final _beforeUnloadManager = Get.find(); final composerArguments = Rxn(); final isEnableEmailSendButton = false.obs; @@ -129,6 +135,7 @@ class ComposerController extends BaseController with DragDropFileMixin { GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; GetDeviceContactSuggestionsInteractor? _getDeviceContactSuggestionsInteractor; + RestoreEmailInlineImagesInteractor? _restoreEmailInlineImagesInteractor; List listToEmailAddress = []; List listCcEmailAddress = []; @@ -162,6 +169,7 @@ class ComposerController extends BaseController with DragDropFileMixin { StreamSubscription? _subscriptionOnDragOver; StreamSubscription? _subscriptionOnDragLeave; StreamSubscription? _subscriptionOnDrop; + StreamSubscription? _composerCacheListener; RichTextMobileTabletController? richTextMobileTabletController; RichTextWebController? richTextWebController; @@ -215,6 +223,7 @@ class ComposerController extends BaseController with DragDropFileMixin { scrollControllerEmailAddress.addListener(_scrollControllerEmailAddressListener); _listenStreamEvent(); _getAlwaysReadReceiptSetting(); + _beforeUnloadManager.addListener(onBeforeUnload); } @override @@ -241,6 +250,8 @@ class ComposerController extends BaseController with DragDropFileMixin { _subscriptionOnDragLeave?.cancel(); _subscriptionOnDrop?.cancel(); subjectEmailInputFocusNode?.removeListener(_subjectEmailInputFocusListener); + _composerCacheListener?.cancel(); + _beforeUnloadManager.removeListener(onBeforeUnload); super.onClose(); } @@ -281,7 +292,8 @@ class ComposerController extends BaseController with DragDropFileMixin { super.handleSuccessViewState(success); if (success is GetEmailContentLoading || success is TransformHtmlEmailContentLoading || - success is TransformHtmlEmailContentSuccess) { + success is TransformHtmlEmailContentSuccess || + success is RestoringEmailInlineImages) { emailContentsViewState.value = Right(success); } else if (success is LocalFilePickerSuccess) { _handlePickFileSuccess(success); @@ -303,9 +315,18 @@ class ComposerController extends BaseController with DragDropFileMixin { maxWithEditor = null; } else if (success is GetAlwaysReadReceiptSettingSuccess) { hasRequestReadReceipt.value = success.alwaysReadReceiptEnabled; + } else if (success is RestoreEmailInlineImagesSuccess) { + _updateEditorContent(success); } } + void _updateEditorContent(RestoreEmailInlineImagesSuccess success) { + richTextWebController?.editorController.setText(success.emailContent); + consumeState(Stream.value(Right(GetEmailContentSuccess( + htmlEmailContent: success.emailContent, + attachments: [])))); + } + @override void handleFailureViewState(Failure failure) { super.handleFailureViewState(failure); @@ -314,7 +335,8 @@ class ComposerController extends BaseController with DragDropFileMixin { } else if (failure is LocalImagePickerFailure) { _handlePickImageFailure(failure); } else if (failure is GetEmailContentFailure || - failure is TransformHtmlEmailContentFailure) { + failure is TransformHtmlEmailContentFailure || + failure is RestoreEmailInlineImagesFailure) { emailContentsViewState.value = Left(failure); } else if (failure is GetAllIdentitiesFailure) { if (identitySelected.value == null) { @@ -348,40 +370,7 @@ class ComposerController extends BaseController with DragDropFileMixin { _subscriptionOnBeforeUnload = html.window.onBeforeUnload.listen((event) async { await _removeComposerCacheOnWebInteractor.execute(); - if (composerArguments.value == null || - mailboxDashBoardController.sessionCurrent == null || - mailboxDashBoardController.accountId.value == null - ) { - log('ComposerController::_listenBrowserTabRefresh: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); - return; - } - - final emailContent = await _getContentInEditor(); - - await _saveComposerCacheOnWebInteractor.execute(CreateEmailRequest( - session: mailboxDashBoardController.sessionCurrent!, - accountId: mailboxDashBoardController.accountId.value!, - emailActionType: composerArguments.value!.emailActionType, - subject: subjectEmail.value ?? '', - emailContent: emailContent, - fromSender: composerArguments.value!.presentationEmail?.from ?? {}, - toRecipients: listToEmailAddress.toSet(), - ccRecipients: listCcEmailAddress.toSet(), - bccRecipients: listBccEmailAddress.toSet(), - isRequestReadReceipt: hasRequestReadReceipt.value, - identity: identitySelected.value, - attachments: uploadController.attachmentsUploaded, - inlineAttachments: uploadController.mapInlineAttachments, - outboxMailboxId: mailboxDashBoardController.outboxMailbox?.mailboxId, - sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], - draftsMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts], - draftsEmailId: _getDraftEmailId(), - answerForwardEmailId: composerArguments.value!.presentationEmail?.id, - unsubscribeEmailId: composerArguments.value!.previousEmailId, - messageId: composerArguments.value!.messageId, - references: composerArguments.value!.references, - emailSendingQueue: composerArguments.value!.sendingEmail - )); + await _saveComposerCacheOnWebAction(); }); _subscriptionOnDragEnter = html.window.onDragEnter.listen((event) { @@ -401,6 +390,56 @@ class ComposerController extends BaseController with DragDropFileMixin { }); } + Future _saveComposerCacheOnWebAction() async { + _autoCreateEmailTag(); + + final createEmailRequest = await _generateCreateEmailRequest(); + if (createEmailRequest == null) return; + + await _saveComposerCacheOnWebInteractor.execute( + createEmailRequest, + mailboxDashBoardController.accountId.value!, + mailboxDashBoardController.sessionCurrent!.username, + displayMode: screenDisplayMode.value); + } + + Future _generateCreateEmailRequest() async { + if (composerArguments.value == null || + mailboxDashBoardController.sessionCurrent == null || + mailboxDashBoardController.accountId.value == null + ) { + log('ComposerController::_generateCreateEmailRequest: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); + return null; + } + + final emailContent = await _getContentInEditor(); + + return CreateEmailRequest( + session: mailboxDashBoardController.sessionCurrent!, + accountId: mailboxDashBoardController.accountId.value!, + emailActionType: composerArguments.value!.emailActionType, + subject: subjectEmail.value ?? '', + emailContent: emailContent, + fromSender: composerArguments.value!.presentationEmail?.from ?? {}, + toRecipients: listToEmailAddress.toSet(), + ccRecipients: listCcEmailAddress.toSet(), + bccRecipients: listBccEmailAddress.toSet(), + isRequestReadReceipt: hasRequestReadReceipt.value, + identity: identitySelected.value, + attachments: uploadController.attachmentsUploaded, + inlineAttachments: uploadController.mapInlineAttachments, + outboxMailboxId: mailboxDashBoardController.outboxMailbox?.mailboxId, + sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], + draftsMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts], + draftsEmailId: _getDraftEmailId(), + answerForwardEmailId: composerArguments.value!.presentationEmail?.id, + unsubscribeEmailId: composerArguments.value!.previousEmailId, + messageId: composerArguments.value!.messageId, + references: composerArguments.value!.references, + emailSendingQueue: composerArguments.value!.sendingEmail + ); + } + void _scrollControllerEmailAddressListener() { if (toEmailAddressController.text.isNotEmpty) { keyToEmailTagEditor.currentState?.closeSuggestionBox(); @@ -481,7 +520,7 @@ class ComposerController extends BaseController with DragDropFileMixin { if (arguments is ComposerArguments) { composerArguments.value = arguments; - _initIdentities(arguments.identities); + _initIdentities(arguments); injectAutoCompleteBindings( mailboxDashBoardController.sessionCurrent, @@ -563,6 +602,10 @@ class ComposerController extends BaseController with DragDropFileMixin { _transformHtmlEmailContent(arguments.emailContents); break; case EmailActionType.reopenComposerBrowser: + if (!PlatformInfo.isWeb) return; + + screenDisplayMode.value = arguments.displayMode; + _initEmailAddress( presentationEmail: arguments.presentationEmail!, actionType: EmailActionType.reopenComposerBrowser @@ -571,8 +614,22 @@ class ComposerController extends BaseController with DragDropFileMixin { presentationEmail: arguments.presentationEmail!, actionType: EmailActionType.reopenComposerBrowser ); - _initAttachments(arguments.attachments ?? []); - _getEmailContentFromSessionStorageBrowser(arguments.emailContents!); + _initAttachments( + arguments.attachments ?? [], + inlineAttachments: arguments.inlineImages); + + final accountId = mailboxDashBoardController.accountId.value; + final downloadUrl = mailboxDashBoardController.sessionCurrent + ?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl); + if (accountId == null || downloadUrl == null) return; + _getEmailContentFromSessionStorageBrowser( + htmlContent: arguments.emailContents ?? '', + inlineImages: arguments.inlineImages ?? [], + accountId: accountId, + downloadUrl: downloadUrl + ); + + hasRequestReadReceipt.value = arguments.readRecepientEnabled ?? false; break; case EmailActionType.composeFromUnsubscribeMailtoLink: if (arguments.subject != null) { @@ -603,17 +660,26 @@ class ComposerController extends BaseController with DragDropFileMixin { subjectEmailInputController.text = newSubject; } - void _initAttachments(List attachments) { + void _initAttachments(List attachments, {List? inlineAttachments}) { if (attachments.isNotEmpty) { initialAttachments = attachments; uploadController.initializeUploadAttachments(attachments); } + if (inlineAttachments != null) { + uploadController.initializeUploadInlineAttachments(inlineAttachments); + } } - void _initIdentities(List? identities) { - if (identities?.isNotEmpty == true) { - listFromIdentities.value = identities!; - identitySelected.value = identities.first; + void _initIdentities(ComposerArguments composerArguments) { + listFromIdentities.value = composerArguments.identities ?? []; + if (listFromIdentities.isEmpty) { + _getAllIdentities(); + } else if (composerArguments.selectedIdentity != null + && listFromIdentities.contains(composerArguments.selectedIdentity!) + ) { + _selectIdentity(composerArguments.selectedIdentity!); + } else if (composerArguments.identities?.isNotEmpty == true) { + _selectIdentity(composerArguments.identities!.first); } } @@ -632,7 +698,14 @@ class ComposerController extends BaseController with DragDropFileMixin { listFromIdentities.value = listIdentitiesMayDeleted; if (identitySelected.value == null) { - await _selectIdentity(listIdentitiesMayDeleted.first); + final selectedIdentityFromArguments = composerArguments.value?.selectedIdentity; + if (selectedIdentityFromArguments != null + && listFromIdentities.contains(selectedIdentityFromArguments) + ) { + await _selectIdentity(selectedIdentityFromArguments); + } else { + await _selectIdentity(listIdentitiesMayDeleted.firstOrNull); + } } } _autoFocusFieldWhenLauncher(); @@ -1226,13 +1299,20 @@ class ComposerController extends BaseController with DragDropFileMixin { )); } - void _getEmailContentFromSessionStorageBrowser(String content) { - consumeState(Stream.value( - Right(GetEmailContentSuccess( - htmlEmailContent: content, - attachments: [], - )) - )); + void _getEmailContentFromSessionStorageBrowser({ + required String htmlContent, + required List inlineImages, + required AccountId accountId, + required String downloadUrl + }) { + _restoreEmailInlineImagesInteractor = getBinding(); + if (_restoreEmailInlineImagesInteractor == null) return; + consumeState(_restoreEmailInlineImagesInteractor!.execute( + htmlContent: htmlContent, + transformConfiguration: TransformConfiguration.forRestoreEmail(), + mapUrlDownloadCID: inlineImages.toMapCidImageDownloadUrl( + accountId: accountId, + downloadUrl: downloadUrl))); } void _getEmailContentFromContentShared(String content) { @@ -1533,7 +1613,12 @@ class ComposerController extends BaseController with DragDropFileMixin { Future _selectIdentity(Identity? newIdentity) async { final formerIdentity = identitySelected.value; identitySelected.value = newIdentity; - if (newIdentity != null) { + if (newIdentity == null) return; + + if (composerArguments.value?.emailActionType == EmailActionType.reopenComposerBrowser) { + composerArguments.value = composerArguments.value?.copyWith( + emailActionType: EmailActionType.editDraft); + } else { await _applyIdentityForAllFieldComposer(formerIdentity, newIdentity); } } @@ -2178,4 +2263,13 @@ class ComposerController extends BaseController with DragDropFileMixin { ccRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; bccRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; } + + @override + Future onBeforeUnload() async { + if (mailboxDashBoardController.accountId.value != null && + mailboxDashBoardController.sessionCurrent?.username != null + ) { + await _saveComposerCacheOnWebAction(); + } + } } \ No newline at end of file diff --git a/lib/features/composer/presentation/model/screen_display_mode.dart b/lib/features/composer/presentation/model/screen_display_mode.dart index 074587b32c..f480c16974 100644 --- a/lib/features/composer/presentation/model/screen_display_mode.dart +++ b/lib/features/composer/presentation/model/screen_display_mode.dart @@ -2,5 +2,18 @@ enum ScreenDisplayMode { fullScreen, minimize, - normal + normal; + + factory ScreenDisplayMode.fromJson(String value) { + switch (value) { + case 'fullScreen': + return ScreenDisplayMode.fullScreen; + case 'minimize': + return ScreenDisplayMode.minimize; + default: + return ScreenDisplayMode.normal; + } + } + + String toJson() => name; } \ No newline at end of file diff --git a/lib/features/email/presentation/extensions/composer_arguments_extension.dart b/lib/features/email/presentation/extensions/composer_arguments_extension.dart index 625576a76e..3840d6ece5 100644 --- a/lib/features/email/presentation/extensions/composer_arguments_extension.dart +++ b/lib/features/email/presentation/extensions/composer_arguments_extension.dart @@ -3,22 +3,10 @@ import 'package:tmail_ui_user/features/email/presentation/model/composer_argumen extension ComposerArgumentsExtension on ComposerArguments { - ComposerArguments withIdentity({List? identities}) { - return ComposerArguments( - emailActionType: emailActionType, - presentationEmail: presentationEmail, - emailContents: emailContents, - attachments: attachments, - mailboxRole: mailboxRole, - listEmailAddress: listEmailAddress, - listSharedMediaFile: listSharedMediaFile, - sendingEmail: sendingEmail, - subject: subject, - body: body, - messageId: messageId, - references: references, - previousEmailId: previousEmailId, + ComposerArguments withIdentity({List? identities, Identity? selectedIdentity}) { + return copyWith( identities: identities, + selectedIdentity: selectedIdentity, ); } } \ No newline at end of file diff --git a/lib/features/email/presentation/model/composer_arguments.dart b/lib/features/email/presentation/model/composer_arguments.dart index 0b6291fd13..a7cc563d6a 100644 --- a/lib/features/email/presentation/model/composer_arguments.dart +++ b/lib/features/email/presentation/model/composer_arguments.dart @@ -4,6 +4,8 @@ import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; + +import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/composer_cache.dart'; import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_action_type.dart'; @@ -24,6 +26,10 @@ class ComposerArguments extends RouterArguments { final MessageIdsHeaderValue? references; final EmailId? previousEmailId; final List? identities; + final Identity? selectedIdentity; + final List? inlineImages; + final bool? readRecepientEnabled; + final ScreenDisplayMode displayMode; ComposerArguments({ this.emailActionType = EmailActionType.compose, @@ -40,6 +46,10 @@ class ComposerArguments extends RouterArguments { this.references, this.previousEmailId, this.identities, + this.selectedIdentity, + this.inlineImages, + this.readRecepientEnabled, + this.displayMode = ScreenDisplayMode.normal }); factory ComposerArguments.fromSendingEmail(SendingEmail sendingEmail) => @@ -83,15 +93,15 @@ class ComposerArguments extends RouterArguments { factory ComposerArguments.fromSessionStorageBrowser(ComposerCache composerCache) => ComposerArguments( emailActionType: EmailActionType.reopenComposerBrowser, - presentationEmail: PresentationEmail( - id: composerCache.id, - subject: composerCache.subject, - from: composerCache.from, - to: composerCache.to, - cc: composerCache.cc, - bcc: composerCache.bcc, - ), - emailContents: composerCache.emailContentList.asHtmlString, + presentationEmail: composerCache.email?.toPresentationEmail(), + emailContents: composerCache.email?.emailContentList.asHtmlString, + attachments: composerCache.email?.allAttachments + .where((attachment) => attachment.disposition != ContentDisposition.inline) + .toList(), + selectedIdentity: composerCache.identity, + inlineImages: composerCache.email?.attachmentsWithCid, + readRecepientEnabled: composerCache.readReceipentEnabled, + displayMode: composerCache.displayMode, ); factory ComposerArguments.replyEmail({ @@ -175,4 +185,46 @@ class ComposerArguments extends RouterArguments { references, identities, ]; -} \ No newline at end of file + + ComposerArguments copyWith({ + EmailActionType? emailActionType, + PresentationEmail? presentationEmail, + String? emailContents, + List? listSharedMediaFile, + List? listEmailAddress, + List? attachments, + Role? mailboxRole, + SendingEmail? sendingEmail, + String? subject, + String? body, + MessageIdsHeaderValue? messageId, + MessageIdsHeaderValue? references, + EmailId? previousEmailId, + List? identities, + Identity? selectedIdentity, + List? inlineImages, + bool? readRecepientEnabled, + ScreenDisplayMode? displayMode, + }) { + return ComposerArguments( + emailActionType: emailActionType ?? this.emailActionType, + presentationEmail: presentationEmail ?? this.presentationEmail, + emailContents: emailContents ?? this.emailContents, + listSharedMediaFile: listSharedMediaFile ?? this.listSharedMediaFile, + listEmailAddress: listEmailAddress ?? this.listEmailAddress, + attachments: attachments ?? this.attachments, + mailboxRole: mailboxRole ?? this.mailboxRole, + sendingEmail: sendingEmail ?? this.sendingEmail, + subject: subject ?? this.subject, + body: body ?? this.body, + messageId: messageId ?? this.messageId, + references: references ?? this.references, + previousEmailId: previousEmailId ?? this.previousEmailId, + identities: identities ?? this.identities, + selectedIdentity: selectedIdentity ?? this.selectedIdentity, + inlineImages: inlineImages ?? this.inlineImages, + readRecepientEnabled: readRecepientEnabled ?? this.readRecepientEnabled, + displayMode: displayMode ?? this.displayMode, + ); + } +} diff --git a/lib/features/mailbox_dashboard/data/datasource/session_storage_composer_datasource.dart b/lib/features/mailbox_dashboard/data/datasource/session_storage_composer_datasource.dart index 622506aa08..1c36acf85d 100644 --- a/lib/features/mailbox_dashboard/data/datasource/session_storage_composer_datasource.dart +++ b/lib/features/mailbox_dashboard/data/datasource/session_storage_composer_datasource.dart @@ -1,10 +1,31 @@ +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/composer_cache.dart'; abstract class SessionStorageComposerDatasource { - void saveComposerCacheOnWeb(Email email); + Future saveComposerCacheOnWeb( + Email email, + { + required AccountId accountId, + required UserName userName, + required ScreenDisplayMode displayMode, + Identity? identity, + bool? readReceipentEnabled, + } + ); - ComposerCache getComposerCacheOnWeb(); + Future getComposerCacheOnWeb( + AccountId accountId, + UserName userName); - void removeComposerCacheOnWeb(); + Future removeComposerCacheOnWeb(); + + Future restoreEmailInlineImages( + String htmlContent, + TransformConfiguration transformConfiguration, + Map mapUrlDownloadCID); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/data/datasource_impl/session_storage_composer_datasoure_impl.dart b/lib/features/mailbox_dashboard/data/datasource_impl/session_storage_composer_datasoure_impl.dart index 1b01fd42db..9ff3d3c88b 100644 --- a/lib/features/mailbox_dashboard/data/datasource_impl/session_storage_composer_datasoure_impl.dart +++ b/lib/features/mailbox_dashboard/data/datasource_impl/session_storage_composer_datasoure_impl.dart @@ -1,47 +1,94 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:core/core.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/composer_cache.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/session_storage_composer_datasource.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/composer_cache.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; import 'package:universal_html/html.dart' as html; class SessionStorageComposerDatasourceImpl extends SessionStorageComposerDatasource { + SessionStorageComposerDatasourceImpl(this._htmlTransform, this._exceptionThrower); + + final HtmlTransform _htmlTransform; + final ExceptionThrower _exceptionThrower; + @override - ComposerCache getComposerCacheOnWeb() { - try { - final result = html.window.sessionStorage.entries.firstWhereOrNull((e) => e.key == EmailActionType.reopenComposerBrowser.name); + Future getComposerCacheOnWeb( + AccountId accountId, + UserName userName + ) async { + return Future.sync(() async { + final keyWithIdentity = TupleKey( + EmailActionType.reopenComposerBrowser.name, + accountId.asString, + userName.value).toString(); + + final result = html.window.sessionStorage.entries.firstWhereOrNull( + (entry) => entry.key == keyWithIdentity); if (result != null) { - final emailCache = ComposerCache.fromJson(jsonDecode(result.value)); - return emailCache; + return ComposerCache.fromJson(jsonDecode(result.value)); } else { throw NotFoundInWebSessionException(); } - } catch (e) { - throw NotFoundInWebSessionException(errorMessage: e.toString()); - } + }).catchError(_exceptionThrower.throwException); } @override - void removeComposerCacheOnWeb() { - try { - html.window.sessionStorage.removeWhere((key, value) => key == EmailActionType.reopenComposerBrowser.name); - } catch (e) { - throw NotFoundInWebSessionException(errorMessage: e.toString()); - } + Future removeComposerCacheOnWeb() async { + return Future.sync(() { + html.window.sessionStorage.removeWhere( + (key, value) => key.startsWith(EmailActionType.reopenComposerBrowser.name)); + }).catchError(_exceptionThrower.throwException); } @override - void saveComposerCacheOnWeb(Email email) { - try { + Future saveComposerCacheOnWeb( + Email email, + { + required AccountId accountId, + required UserName userName, + required ScreenDisplayMode displayMode, + Identity? identity, + bool? readReceipentEnabled + } + ) async { + return Future.sync(() { + final composerCacheKey = TupleKey( + EmailActionType.reopenComposerBrowser.name, + accountId.asString, + userName.value).toString(); Map entries = { - EmailActionType.reopenComposerBrowser.name: email.asString() + composerCacheKey: jsonEncode( + ComposerCache( + displayMode: displayMode, + email: email, + identity: identity, + readReceipentEnabled: readReceipentEnabled, + ).toJson() + ) }; html.window.sessionStorage.addAll(entries); - } catch (e) { - throw SaveToWebSessionFailException(errorMessage: e.toString()); - } + }).catchError(_exceptionThrower.throwException); + } + + @override + Future restoreEmailInlineImages( + String htmlContent, + TransformConfiguration transformConfiguration, + Map mapUrlDownloadCID) { + return Future.sync(() async { + return await _htmlTransform.transformToHtml( + htmlContent: htmlContent, + transformConfiguration: transformConfiguration, + mapCidImageDownloadUrl: mapUrlDownloadCID); + }).catchError(_exceptionThrower.throwException); } } diff --git a/lib/features/mailbox_dashboard/data/model/composer_cache.dart b/lib/features/mailbox_dashboard/data/model/composer_cache.dart index ed58d82c4f..5eda2a6ba6 100644 --- a/lib/features/mailbox_dashboard/data/model/composer_cache.dart +++ b/lib/features/mailbox_dashboard/data/model/composer_cache.dart @@ -1,81 +1,44 @@ import 'package:equatable/equatable.dart'; -import 'package:jmap_dart_client/http/converter/email/email_body_value_converter.dart'; -import 'package:jmap_dart_client/http/converter/email/email_mailbox_ids_converter.dart'; -import 'package:jmap_dart_client/jmap/core/id.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_body_part.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email_body_value.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:model/email/email_content.dart'; -import 'package:model/extensions/media_type_nullable_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; class ComposerCache with EquatableMixin { - final EmailId? id; - final Map? mailboxIds; - final String? subject; - final Set? from; - final Set? to; - final Set? cc; - final Set? bcc; - final Set? replyTo; - final Set? textBody; - final Set? htmlBody; - final Map? bodyValues; + final Email? email; + final Identity? identity; + final bool? readReceipentEnabled; + final ScreenDisplayMode displayMode; ComposerCache({ - this.id, - this.mailboxIds, - this.subject, - this.from, - this.to, - this.cc, - this.bcc, - this.replyTo, - this.textBody, - this.htmlBody, - this.bodyValues, + required this.displayMode, + this.email, + this.identity, + this.readReceipentEnabled, }); - factory ComposerCache.fromJson(Map json) { - return ComposerCache( - id: json['id'] != null ? EmailId(Id(json['id'])) : null, - mailboxIds: (json['mailboxIds'] as Map?)?.map((key, value) => EmailMailboxIdsConverter().parseEntry(key, value)), - subject: json['subject'] as String?, - from: (json['from'] as List?)?.map((json) => EmailAddress.fromJson(json)).toSet(), - to: (json['to'] as List?)?.map((json) => EmailAddress.fromJson(json)).toSet(), - cc: (json['cc'] as List?)?.map((json) => EmailAddress.fromJson(json)).toSet(), - bcc: (json['bcc'] as List?)?.map((json) => EmailAddress.fromJson(json)).toSet(), - bodyValues: (json['bodyValues'] as Map?)?.map((key, value) => EmailBodyValueConverter().parseEntry(key, value)), - replyTo: (json['replyTo'] as List?)?.map((json) => EmailAddress.fromJson(json)).toSet(), - textBody: (json['textBody'] as List?)?.map((json) => EmailBodyPart.fromJson(json)).toSet(), - htmlBody: (json['htmlBody'] as List?)?.map((json) => EmailBodyPart.fromJson(json)).toSet(), - ); - } - @override List get props => [ - id, - subject, - from, - to, - cc, - bcc, - replyTo, + email, + identity, + readReceipentEnabled ]; - List get emailContentList { - final newHtmlBody = htmlBody - ?.where((emailBody) => emailBody.partId != null && emailBody.type != null) - .toList() ?? []; - - final mapHtmlBody = { for (var emailBody in newHtmlBody) emailBody.partId! : emailBody.type! }; - - final emailContents = bodyValues?.entries - .map((entries) => EmailContent(mapHtmlBody[entries.key].toEmailContentType(), entries.value.value)) - .toList(); + Map toJson() { + return { + 'email': email?.toJson(), + 'identity': identity?.toJson(), + 'readReceipentEnabled': readReceipentEnabled, + 'displayMode': displayMode.toJson() + }; + } - return emailContents ?? []; + factory ComposerCache.fromJson(Map map) { + return ComposerCache( + displayMode: ScreenDisplayMode.fromJson(map['displayMode'] ?? ''), + email: map['email'] != null ? Email.fromJson(map['email']) : null, + identity: map['identity'] != null ? Identity.fromJson(map['identity']) : null, + readReceipentEnabled: map['readReceipentEnabled'] as bool? + ); } -} \ No newline at end of file +} diff --git a/lib/features/mailbox_dashboard/data/repository/composer_cache_repository_impl.dart b/lib/features/mailbox_dashboard/data/repository/composer_cache_repository_impl.dart index c7ca325194..cb0e6d903a 100644 --- a/lib/features/mailbox_dashboard/data/repository/composer_cache_repository_impl.dart +++ b/lib/features/mailbox_dashboard/data/repository/composer_cache_repository_impl.dart @@ -1,4 +1,9 @@ +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/session_storage_composer_datasource.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/composer_cache.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart'; @@ -10,17 +15,46 @@ class ComposerCacheRepositoryImpl extends ComposerCacheRepository { ComposerCacheRepositoryImpl(this.composerCacheDataSource); @override - ComposerCache getComposerCacheOnWeb() { - return composerCacheDataSource.getComposerCacheOnWeb(); + Future getComposerCacheOnWeb( + AccountId accountId, + UserName userName + ) { + return composerCacheDataSource.getComposerCacheOnWeb(accountId, userName); } @override - void removeComposerCacheOnWeb() { + Future removeComposerCacheOnWeb() { return composerCacheDataSource.removeComposerCacheOnWeb(); } @override - void saveComposerCacheOnWeb(Email email) { - return composerCacheDataSource.saveComposerCacheOnWeb(email); + Future saveComposerCacheOnWeb( + Email email, + { + required AccountId accountId, + required UserName userName, + required ScreenDisplayMode displayMode, + Identity? identity, + bool? readReceipentEnabled + } + ) { + return composerCacheDataSource.saveComposerCacheOnWeb( + email, + accountId: accountId, + userName: userName, + displayMode: displayMode, + identity: identity, + readReceipentEnabled: readReceipentEnabled); + } + + @override + Future restoreEmailInlineImages( + String htmlContent, + TransformConfiguration transformConfiguration, + Map mapUrlDownloadCID) { + return composerCacheDataSource.restoreEmailInlineImages( + htmlContent, + transformConfiguration, + mapUrlDownloadCID); } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart b/lib/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart index 643e7ca7f8..07175440fd 100644 --- a/lib/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart +++ b/lib/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart @@ -1,10 +1,31 @@ +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/composer_cache.dart'; abstract class ComposerCacheRepository { - void saveComposerCacheOnWeb(Email email); + Future saveComposerCacheOnWeb( + Email email, + { + required AccountId accountId, + required UserName userName, + required ScreenDisplayMode displayMode, + Identity? identity, + bool? readReceipentEnabled + } + ); - ComposerCache getComposerCacheOnWeb(); + Future getComposerCacheOnWeb( + AccountId accountId, + UserName userName); - void removeComposerCacheOnWeb(); + Future removeComposerCacheOnWeb(); + + Future restoreEmailInlineImages( + String htmlContent, + TransformConfiguration transformConfiguration, + Map mapUrlDownloadCID); } diff --git a/lib/features/mailbox_dashboard/domain/usecases/get_composer_cache_on_web_interactor.dart b/lib/features/mailbox_dashboard/domain/usecases/get_composer_cache_on_web_interactor.dart index f5a1fc0b1e..a59257bc91 100644 --- a/lib/features/mailbox_dashboard/domain/usecases/get_composer_cache_on_web_interactor.dart +++ b/lib/features/mailbox_dashboard/domain/usecases/get_composer_cache_on_web_interactor.dart @@ -1,6 +1,8 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_composer_cache_state.dart'; @@ -9,12 +11,12 @@ class GetComposerCacheOnWebInteractor { GetComposerCacheOnWebInteractor(this.composerCacheRepository); - Either execute() { + Stream> execute(AccountId accountId, UserName userName) async* { try { - final data = composerCacheRepository.getComposerCacheOnWeb(); - return Right(GetComposerCacheSuccess(data)); + final data = await composerCacheRepository.getComposerCacheOnWeb(accountId, userName); + yield Right(GetComposerCacheSuccess(data)); } catch (exception) { - return Left(GetComposerCacheFailure(exception)); + yield Left(GetComposerCacheFailure(exception)); } } } \ No newline at end of file diff --git a/lib/features/upload/presentation/controller/upload_controller.dart b/lib/features/upload/presentation/controller/upload_controller.dart index 37569703b1..ee62b3f3b0 100644 --- a/lib/features/upload/presentation/controller/upload_controller.dart +++ b/lib/features/upload/presentation/controller/upload_controller.dart @@ -203,6 +203,17 @@ class UploadController extends BaseController { _refreshListUploadAttachmentState(); } + void initializeUploadInlineAttachments(List inlineAttachments) { + final listUploadInlineImagesState = inlineAttachments + .map((inlineAttachment) => UploadFileState( + UploadTaskId(inlineAttachment.blobId!.value), + uploadStatus: UploadFileStatus.succeed, + attachment: inlineAttachment)) + .toList(); + _uploadingStateInlineFiles.addAll(listUploadInlineImagesState); + _refreshListUploadAttachmentState(); + } + void deleteFileUploaded(UploadTaskId uploadId) { _uploadingStateFiles.deleteElementByUploadTaskId(uploadId); _refreshListUploadAttachmentState(); From 5e4f5b5c6d66bb37ed6e5e28417bd7a5d37a8f52 Mon Sep 17 00:00:00 2001 From: DatDang Date: Tue, 18 Jun 2024 15:24:20 +0700 Subject: [PATCH 3/3] TF-2901 Implement getting cache composer on log in --- .../insert_image_loading_bar_widget.dart | 40 +++++++++---------- .../bindings/mailbox_dashboard_bindings.dart | 5 ++- .../mailbox_dashboard_controller.dart | 32 +++++++++------ 3 files changed, 41 insertions(+), 36 deletions(-) diff --git a/lib/features/composer/presentation/widgets/insert_image_loading_bar_widget.dart b/lib/features/composer/presentation/widgets/insert_image_loading_bar_widget.dart index 43debb9c3f..d3fdbab56d 100644 --- a/lib/features/composer/presentation/widgets/insert_image_loading_bar_widget.dart +++ b/lib/features/composer/presentation/widgets/insert_image_loading_bar_widget.dart @@ -4,6 +4,7 @@ import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:tmail_ui_user/features/base/widget/circle_loading_widget.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/restore_email_inline_images_state.dart'; import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; class InsertImageLoadingBarWidget extends StatelessWidget { @@ -22,32 +23,27 @@ class InsertImageLoadingBarWidget extends StatelessWidget { @override Widget build(BuildContext context) { return uploadInlineViewState.fold( - (failure) { - return viewState.fold( - (failure) => const SizedBox.shrink(), - (success) { - if (success is DownloadingImageAsBase64) { - return CircleLoadingWidget(padding: padding); - } else { - return const SizedBox.shrink(); - } - } - ); - }, + (failure) => _viewStateToUI(viewState), (success) { if (success is UploadingAttachmentUploadState) { return CircleLoadingWidget(padding: padding); } else { - return viewState.fold( - (failure) => const SizedBox.shrink(), - (success) { - if (success is DownloadingImageAsBase64) { - return CircleLoadingWidget(padding: padding); - } else { - return const SizedBox.shrink(); - } - } - ); + return _viewStateToUI(viewState); + } + } + ); + } + + Widget _viewStateToUI(Either viewState) { + return viewState.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is DownloadingImageAsBase64 || + success is RestoringEmailInlineImages + ) { + return CircleLoadingWidget(padding: padding); + } else { + return const SizedBox.shrink(); } } ); diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index 375143f3ec..ace76dc1ef 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -1,5 +1,6 @@ import 'package:core/data/model/source_type/data_source_type.dart'; import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/html_transformer/html_transform.dart'; import 'package:core/utils/config/app_config_loader.dart'; import 'package:core/utils/file_utils.dart'; import 'package:core/utils/print_utils.dart'; @@ -232,7 +233,9 @@ class MailboxDashBoardBindings extends BaseBindings { Get.lazyPut(() => MailboxCacheDataSourceImpl( Get.find(), Get.find())); - Get.lazyPut(() => SessionStorageComposerDatasourceImpl()); + Get.lazyPut(() => SessionStorageComposerDatasourceImpl( + Get.find(), + Get.find())); Get.lazyPut(() => SpamReportDataSourceImpl( Get.find(), Get.find(), diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 0b766ba8a7..1c7103e456 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -291,14 +291,12 @@ class MailboxDashBoardController extends ReloadableController { } void _handleComposerCache() async { - _getEmailCacheOnWebInteractor.execute().fold( - (failure) {}, - (success) { - if (success is GetComposerCacheSuccess) { - goToComposer(ComposerArguments.fromSessionStorageBrowser(success.composerCache)); - } - }, - ); + if (accountId.value == null || sessionCurrent == null) return; + + consumeState( + _getEmailCacheOnWebInteractor.execute( + accountId.value!, + sessionCurrent!.username)); } @override @@ -373,6 +371,8 @@ class MailboxDashBoardController extends ReloadableController { _handleGetRestoredDeletedMessageSuccess(success); } else if (success is GetAllIdentitiesSuccess) { _handleGetAllIdentitiesSuccess(success); + } else if (success is GetComposerCacheSuccess) { + goToComposer(ComposerArguments.fromSessionStorageBrowser(success.composerCache)); } } @@ -466,7 +466,7 @@ class MailboxDashBoardController extends ReloadableController { final arguments = Get.arguments; log('MailboxDashBoardController::_getSessionCurrent(): arguments = $arguments'); if (arguments is Session) { - _handleSession(arguments); + _handleSessionFromArguments(arguments); } else if (arguments is MailtoArguments) { _handleMailtoURL(arguments); } else if (arguments is PreviewEmailArguments) { @@ -499,7 +499,7 @@ class MailboxDashBoardController extends ReloadableController { } } - void _handleSession(Session session) { + void _handleSessionFromArguments(Session session) { log('MailboxDashBoardController::_handleSession:'); updateAuthenticationAccount( session, @@ -509,6 +509,10 @@ class MailboxDashBoardController extends ReloadableController { _setUpComponentsFromSession(session); + if (PlatformInfo.isWeb) { + _handleComposerCache(); + } + if (PlatformInfo.isMobile && !_notificationManager.isNotificationClickedOnTerminate) { _handleClickLocalNotificationOnTerminated(); } else { @@ -539,13 +543,13 @@ class MailboxDashBoardController extends ReloadableController { void _handleMailtoURL(MailtoArguments arguments) { log('MailboxDashBoardController::_handleMailtoURL:'); routerParameters.value = arguments.toMapRouter(); - _handleSession(arguments.session); + _handleSessionFromArguments(arguments.session); } void _handleOpenEmailAction(PreviewEmailArguments arguments) { log('MailboxDashBoardController::_handleOpenEmailAction:arguments: $arguments'); dispatchRoute(DashboardRoutes.waiting); - _handleSession(arguments.session); + _handleSessionFromArguments(arguments.session); _handleNotificationMessageFromEmailId(arguments.emailId); } @@ -1398,7 +1402,9 @@ class MailboxDashBoardController extends ReloadableController { } void goToComposer(ComposerArguments arguments) async { - final argumentsWithIdentity = arguments.withIdentity(identities: List.from(_identities ?? [])); + final argumentsWithIdentity = arguments.withIdentity( + identities: List.from(_identities ?? []), + selectedIdentity: arguments.selectedIdentity); if (PlatformInfo.isWeb) { if (composerOverlayState.value == ComposerOverlayState.inActive) {