diff --git a/core/lib/domain/exceptions/web_session_exception.dart b/core/lib/domain/exceptions/web_session_exception.dart index 62a2535a3..8d118d831 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 8cfcce26b..2c1dfbf5c 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 6c8ad4520..17fdea07e 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 000000000..221cd0fc2 --- /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 000000000..0d10be819 --- /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 cbb6c924d..086709ab7 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 b600963b6..83a655661 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 4bbf61c6c..711c052ee 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 074587b32..f480c1697 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 625576a76..3840d6ece 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 0b6291fd1..a7cc563d6 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 622506aa0..1c36acf85 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 1b01fd42d..9ff3d3c88 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 ed58d82c4..5eda2a6ba 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(); - - return emailContents ?? []; + Map toJson() { + return { + 'email': email?.toJson(), + 'identity': identity?.toJson(), + 'readReceipentEnabled': readReceipentEnabled, + 'displayMode': displayMode.toJson() + }; } -} \ No newline at end of file + + 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? + ); + } +} 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 c7ca32519..cb0e6d903 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 643e7ca7f..07175440f 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 f5a1fc0b1..a59257bc9 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 37569703b..ee62b3f3b 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();