From 7f09f9ceb421410e3846306bab2729a16e2707cb Mon Sep 17 00:00:00 2001 From: DatDang Date: Tue, 25 Feb 2025 16:10:20 +0700 Subject: [PATCH] TF-3515 Add save email as template feature --- .../repository/composer_repository_impl.dart | 2 + ...ethod_exception_description_extension.dart | 11 + .../repository/composer_repository.dart | 1 + ...save_composer_cache_on_web_interactor.dart | 1 + .../presentation/composer_bindings.dart | 7 + .../presentation/composer_controller.dart | 148 ++++++++++- .../composer/presentation/composer_view.dart | 14 ++ .../presentation/composer_view_web.dart | 3 + .../create_email_request_extension.dart | 18 +- .../setup_email_content_extension.dart | 10 +- ...l_request_read_receipt_flag_extension.dart | 3 +- ...receipt_flag_for_edit_draft_extension.dart | 2 +- .../setup_email_template_id_extension.dart | 13 + .../model/create_email_request.dart | 6 + ..._draft.dart => saved_composing_email.dart} | 24 +- .../widgets/saving_template_dialog_view.dart | 230 ++++++++++++++++++ .../web/bottom_bar_composer_widget.dart | 13 + ...le_responsive_app_bar_composer_widget.dart | 13 + .../data/datasource/email_datasource.dart | 18 ++ .../email_datasource_impl.dart | 40 +++ .../email_hive_cache_datasource_impl.dart | 30 +++ .../email_local_storage_datasource_impl.dart | 22 ++ ...email_session_storage_datasource_impl.dart | 22 ++ .../email/data/network/email_api.dart | 96 +++++++- .../repository/email_repository_impl.dart | 59 +++++ .../domain/repository/email_repository.dart | 18 ++ .../state/save_template_email_state.dart | 18 ++ .../state/update_template_email_state.dart | 18 ++ .../save_template_email_interactor.dart | 83 +++++++ .../email/presentation/email_view.dart | 3 +- .../model/composer_arguments.dart | 19 +- .../data/model/composer_cache.dart | 3 + .../mixin/email_action_controller.dart | 10 +- .../presentation/thread_controller.dart | 2 +- .../thread/presentation/thread_view.dart | 4 +- lib/l10n/intl_messages.arb | 52 ++++ lib/main/localizations/app_localizations.dart | 57 +++++ .../composer_repository_impl_test.dart | 3 + .../composer_controller_test.dart | 19 +- .../model/saved_email_draft_test.dart | 18 +- 40 files changed, 1079 insertions(+), 54 deletions(-) create mode 100644 lib/features/composer/domain/extensions/set_method_exception_description_extension.dart create mode 100644 lib/features/composer/presentation/extensions/setup_email_template_id_extension.dart rename lib/features/composer/presentation/model/{saved_email_draft.dart => saved_composing_email.dart} (69%) create mode 100644 lib/features/composer/presentation/widgets/saving_template_dialog_view.dart create mode 100644 lib/features/email/domain/state/save_template_email_state.dart create mode 100644 lib/features/email/domain/state/update_template_email_state.dart create mode 100644 lib/features/email/domain/usecases/save_template_email_interactor.dart diff --git a/lib/features/composer/data/repository/composer_repository_impl.dart b/lib/features/composer/data/repository/composer_repository_impl.dart index a370f24c9..027aa7798 100644 --- a/lib/features/composer/data/repository/composer_repository_impl.dart +++ b/lib/features/composer/data/repository/composer_repository_impl.dart @@ -49,6 +49,7 @@ class ComposerRepositoryImpl extends ComposerRepository { { bool withIdentityHeader = false, bool isDraft = false, + bool isTemplate = false, } ) async { String emailContent = createEmailRequest.emailContent; @@ -75,6 +76,7 @@ class ComposerRepositoryImpl extends ComposerRepository { partId: emailBodyPartId, withIdentityHeader: withIdentityHeader, isDraft: isDraft, + isTemplate: isTemplate, ); return emailObject; diff --git a/lib/features/composer/domain/extensions/set_method_exception_description_extension.dart b/lib/features/composer/domain/extensions/set_method_exception_description_extension.dart new file mode 100644 index 000000000..76e2aa4fe --- /dev/null +++ b/lib/features/composer/domain/extensions/set_method_exception_description_extension.dart @@ -0,0 +1,11 @@ +import 'package:collection/collection.dart'; +import 'package:jmap_dart_client/jmap/core/error/error_type.dart'; +import 'package:tmail_ui_user/features/composer/domain/exceptions/set_method_exception.dart'; + +extension SetMethodExceptionDescriptionExtension on SetMethodException { + String? getDescriptionFromErrorType(ErrorType errorType) { + return mapErrors.values + .firstWhereOrNull((setError) => setError.type == errorType) + ?.description; + } +} diff --git a/lib/features/composer/domain/repository/composer_repository.dart b/lib/features/composer/domain/repository/composer_repository.dart index 38fdd16ea..738282e25 100644 --- a/lib/features/composer/domain/repository/composer_repository.dart +++ b/lib/features/composer/domain/repository/composer_repository.dart @@ -10,6 +10,7 @@ abstract class ComposerRepository { { bool withIdentityHeader = false, bool isDraft = false, + bool isTemplate = false, }); Future uploadAttachment(FileInfo fileInfo, Uri uploadUri, {CancelToken? cancelToken}); 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 27ea726c6..4f5c71d33 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 @@ -42,6 +42,7 @@ class SaveComposerCacheOnWebInteractor { draftHash: createEmailRequest.savedDraftHash, actionType: createEmailRequest.savedActionType, draftEmailId: createEmailRequest.draftsEmailId, + templateEmailId: createEmailRequest.templateEmailId, )); return Right(SaveComposerCacheSuccess()); } catch (exception) { diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index 29727746a..f21024ba4 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -35,6 +35,7 @@ import 'package:tmail_ui_user/features/email/data/repository/email_repository_im import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/save_template_email_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; @@ -303,6 +304,11 @@ class ComposerBindings extends BaseBindings { IdentityInteractorsBindings(composerId: composerId).dependencies(); PreferencesInteractorsBindings(composerId: composerId).dependencies(); + + Get.lazyPut(() => SaveTemplateEmailInteractor( + Get.find(tag: composerId), + Get.find(tag: composerId), + ), tag: composerId); } @override @@ -331,6 +337,7 @@ class ComposerBindings extends BaseBindings { Get.find(tag: composerId), Get.find(tag: composerId), Get.find(tag: composerId), + Get.find(tag: composerId), composerId: composerId, composerArgs: composerArguments, ), tag: composerId); diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index e4d16de8f..549f4f843 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -16,10 +16,12 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:html_editor_enhanced/html_editor.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; @@ -32,6 +34,8 @@ import 'package:tmail_ui_user/features/base/mixin/auto_complete_result_mixin.dar import 'package:tmail_ui_user/features/base/state/base_ui_state.dart'; import 'package:tmail_ui_user/features/base/state/button_state.dart'; import 'package:tmail_ui_user/features/composer/domain/exceptions/compose_email_exception.dart'; +import 'package:tmail_ui_user/features/composer/domain/exceptions/set_method_exception.dart'; +import 'package:tmail_ui_user/features/composer/domain/extensions/set_method_exception_description_extension.dart'; import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; import 'package:tmail_ui_user/features/composer/domain/state/download_image_as_base64_state.dart'; @@ -64,27 +68,33 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_em import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_recipients_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_subject_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_template_id_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_list_identities_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_selected_identity_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/update_screen_display_mode_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/mixin/drag_drog_file_mixin.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/saved_email_draft.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/saved_composing_email.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; import 'package:tmail_ui_user/features/composer/presentation/styles/composer_style.dart'; import 'package:tmail_ui_user/features/composer/presentation/view/editor_view_mixin.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from_composer_bottom_sheet_builder.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/saving_message_dialog_view.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/saving_template_dialog_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/sending_message_dialog_view.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/save_template_email_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/update_template_email_state.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/save_template_email_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/presentation_email_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_by_id_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/open_and_close_composer_extension.dart'; @@ -157,6 +167,7 @@ class ComposerController extends BaseController final ComposerRepository _composerRepository; final String? composerId; final ComposerArguments? composerArgs; + final SaveTemplateEmailInteractor _saveTemplateEmailInteractor; GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; @@ -226,6 +237,7 @@ class ComposerController extends BaseController EmailActionType? currentEmailActionType; EmailActionType? savedActionType; int minInputLengthAutocomplete = AppConfig.defaultMinInputLengthAutocomplete; + EmailId? currentTemplateEmailId; @visibleForTesting int? get savedEmailDraftHash => _savedEmailDraftHash; @@ -257,6 +269,7 @@ class ComposerController extends BaseController this._createNewAndSaveEmailToDraftsInteractor, this.printEmailInteractor, this._composerRepository, + this._saveTemplateEmailInteractor, { this.composerId, this.composerArgs, @@ -528,6 +541,7 @@ class ComposerController extends BaseController savedDraftHash: arguments.savedDraftHash ?? _savedEmailDraftHash, savedActionType: savedActionType ?? currentEmailActionType, savedEmailDraftId: emailIdEditing, + templateEmailId: currentTemplateEmailId, ); } @@ -627,6 +641,7 @@ class ComposerController extends BaseController setupEmailAttachments(arguments); setupEmailOtherComponents(arguments); setupEmailRequestReadReceiptFlag(arguments); + setupEmailTemplateId(arguments); await setupListIdentities(arguments); await setupEmailContent(arguments); @@ -1130,12 +1145,11 @@ class ComposerController extends BaseController } Future _validateEmailChange() async { - final newDraftHash = await _hashDraftEmail(); - log('ComposerController::_validateEmailChange:newDraftHash = $newDraftHash | _savedEmailDraftHash = $_savedEmailDraftHash'); + final newDraftHash = await _hashComposingEmail(); return _savedEmailDraftHash != newDraftHash; } - Future _hashDraftEmail() async { + Future _hashComposingEmail() async { String emailContent = await getContentInEditor(); emailContent = await _composerRepository.removeCollapsedExpandedSignatureEffect( @@ -1149,7 +1163,7 @@ class ComposerController extends BaseController ); } - final savedEmailDraft = SavedEmailDraft( + final savedEmailDraft = SavedComposingEmail( subject: subjectEmail.value ?? '', content: emailContent, toRecipients: listToEmailAddress.toSet(), @@ -1168,11 +1182,11 @@ class ComposerController extends BaseController } Future _updateSavedEmailDraftHash() async { - _savedEmailDraftHash = await _hashDraftEmail(); + _savedEmailDraftHash = await _hashComposingEmail(); } Future initEmailDraftHash() async { - final currentDraftHash = await _hashDraftEmail(); + final currentDraftHash = await _hashComposingEmail(); final oldSavedDraftHash = composerArguments.value?.savedDraftHash; @@ -1264,6 +1278,76 @@ class ComposerController extends BaseController } } + Future handleClickSaveAsTemplateButton(BuildContext context) async { + if (composerArguments.value == null || + mailboxDashBoardController.sessionCurrent == null || + mailboxDashBoardController.accountId.value == null + ) { + log('ComposerController::handleClickSaveAsTemplateButton: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); + return; + } + + MailboxId? templateMailboxId = mailboxDashBoardController + .getMailboxIdByRole(PresentationMailbox.roleTemplates); + templateMailboxId ??= mailboxDashBoardController.mapMailboxById + .where((_, mailbox) => mailbox.name?.name.toLowerCase() == + PresentationMailbox.roleTemplates.value.toLowerCase()) + .keys + .firstOrNull; + + final emailContent = await getContentInEditor(); + final cancelToken = CancelToken(); + final resultState = await _showSavingMessageToTemplateDialog( + emailContent: emailContent, + templateMailboxId: templateMailboxId, + templateEmailId: currentTemplateEmailId, + createNewMailboxRequest: templateMailboxId != null + ? null + : CreateNewMailboxRequest( + MailboxName(PresentationMailbox.roleTemplates.value.toUpperCase()), + ), + cancelToken: cancelToken, + ); + + if (resultState is SaveTemplateEmailSuccess && context.mounted == true) { + currentTemplateEmailId = resultState.emailId; + appToast.showToastSuccessMessage( + context, + AppLocalizations.of(context).saveMessageToTemplateSuccess, + ); + } else if (resultState is UpdateTemplateEmailSuccess && context.mounted == true) { + currentTemplateEmailId = resultState.emailId; + appToast.showToastSuccessMessage( + context, + AppLocalizations.of(context).updateMessageToTemplateSuccess, + ); + } else if (resultState is SaveTemplateEmailFailure || + resultState is UpdateTemplateEmailFailure || + resultState is GenerateEmailFailure + ) { + if (resultState.exception is BadCredentialsException) { + handleBadCredentialsException(); + } else if (context.mounted) { + String message = ''; + final exception = resultState.exception; + if (exception is SetMethodException) { + final exceptionDescription = exception.getDescriptionFromErrorType( + ErrorMethodResponse.invalidArguments, + ); + message = exceptionDescription != null + ? AppLocalizations.of(context).invalidArguments(exceptionDescription) + : AppLocalizations.of(context).saveMessageToTemplateFailed; + } else if (cancelToken.isCancelled) { + message = AppLocalizations.of(context).saveMessageToTemplateCancelled; + } else { + message = AppLocalizations.of(context).saveMessageToTemplateFailed; + } + + appToast.showToastErrorMessage(context, message); + } + } + } + void clearFocus(BuildContext context) { log('ComposerController::clearFocus:'); if (PlatformInfo.isMobile) { @@ -2032,6 +2116,56 @@ class ComposerController extends BaseController ); } + Future _showSavingMessageToTemplateDialog({ + required String emailContent, + required MailboxId? templateMailboxId, + required EmailId? templateEmailId, + required CreateNewMailboxRequest? createNewMailboxRequest, + CancelToken? cancelToken, + }) { + final childWidget = PointerInterceptor( + child: SavingTemplateDialogView( + createEmailRequest: CreateEmailRequest( + session: mailboxDashBoardController.sessionCurrent!, + accountId: mailboxDashBoardController.accountId.value!, + emailActionType: composerArguments.value!.emailActionType, + subject: subjectEmail.value ?? '', + emailContent: emailContent, + fromSender: composerArguments.value!.presentationEmail?.from ?? {}, + toRecipients: listToEmailAddress.toSet(), + ccRecipients: listCcEmailAddress.toSet(), + bccRecipients: listBccEmailAddress.toSet(), + replyToRecipients: listReplyToEmailAddress.toSet(), + hasRequestReadReceipt: hasRequestReadReceipt.value, + isMarkAsImportant: isMarkAsImportant.value, + identity: identitySelected.value, + attachments: uploadController.attachmentsUploaded, + inlineAttachments: uploadController.mapInlineAttachments, + sentMailboxId: getSentMailboxIdForComposer(), + templateMailboxId: templateMailboxId, + templateEmailId: templateEmailId, + answerForwardEmailId: composerArguments.value!.presentationEmail?.id, + unsubscribeEmailId: composerArguments.value!.previousEmailId, + messageId: composerArguments.value!.messageId, + references: composerArguments.value!.references, + emailSendingQueue: composerArguments.value!.sendingEmail, + displayMode: screenDisplayMode.value + ), + saveTemplateEmailInteractor: _saveTemplateEmailInteractor, + createNewMailboxRequest: createNewMailboxRequest, + onCancel: (cancelToken) => cancelToken?.cancel(), + cancelToken: cancelToken, + ), + ); + return Get.dialog( + PlatformInfo.isMobile + ? PopScope(canPop: false, child: childWidget) + : childWidget, + barrierDismissible: false, + barrierColor: AppColor.colorDefaultCupertinoActionSheet, + ); + } + void _handleCancelSavingMessageToDrafts({CancelToken? cancelToken}) { cancelToken?.cancel([SavingEmailToDraftsCanceledException()]); } diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index c8f8637df..da43bc714 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -563,6 +563,20 @@ class ComposerView extends GetWidget { } ) ), + PopupMenuItem( + padding: EdgeInsets.zero, + child: PopupItemWidget( + iconAction: controller.imagePaths.icSaveToDraft, + nameAction: AppLocalizations.of(context).saveAsTemplate, + colorIcon: ComposerStyle.popupItemIconColor, + styleName: ComposerStyle.popupItemTextStyle, + padding: ComposerStyle.popupItemPadding, + onCallbackAction: () { + popBack(); + controller.handleClickSaveAsTemplateButton(context); + } + ) + ), PopupMenuItem( padding: EdgeInsets.zero, child: PopupItemWidget( diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index 277f37da2..74e47a7f7 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -66,6 +66,7 @@ class ComposerView extends GetWidget { toggleRequestReadReceiptAction: () => controller.toggleRequestReadReceipt(context), toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context), saveToDraftsAction: () => controller.handleClickSaveAsDraftsButton(context), + saveToTemplateAction: () => controller.handleClickSaveAsTemplateButton(context), deleteComposerAction: () => controller.handleClickDeleteComposer(context), )), ConstrainedBox( @@ -576,6 +577,7 @@ class ComposerView extends GetWidget { printDraftAction: () => controller.printDraft(context), toggleRequestReadReceiptAction: () => controller.toggleRequestReadReceipt(context), toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context), + saveAsTemplateAction: () => controller.handleClickSaveAsTemplateButton(context), )), ], ), @@ -885,6 +887,7 @@ class ComposerView extends GetWidget { printDraftAction: () => controller.printDraft(context), toggleRequestReadReceiptAction: () => controller.toggleRequestReadReceipt(context), toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context), + saveAsTemplateAction: () => controller.handleClickSaveAsTemplateButton(context), )), ], ), diff --git a/lib/features/composer/presentation/extensions/create_email_request_extension.dart b/lib/features/composer/presentation/extensions/create_email_request_extension.dart index cc5adc261..970427c97 100644 --- a/lib/features/composer/presentation/extensions/create_email_request_extension.dart +++ b/lib/features/composer/presentation/extensions/create_email_request_extension.dart @@ -39,12 +39,12 @@ extension CreateEmailRequestExtension on CreateEmailRequest { } } - Set? createReplyToRecipients({bool isDraft = false}) { + Set? createReplyToRecipients({bool isNotReplyTo = false}) { if (replyToRecipients?.isNotEmpty == true) { return replyToRecipients; } - if (isDraft) return null; + if (isNotReplyTo) return null; return identity?.replyTo?.isNotEmpty == true ? identity!.replyTo! @@ -60,18 +60,27 @@ extension CreateEmailRequestExtension on CreateEmailRequest { KeyWordIdentifier.emailDraft: true, KeyWordIdentifier.emailSeen: true, }; + } else if (templateMailboxId != null) { + return { + KeyWordIdentifier.emailSeen: true, + }; } else { return null; } } Map? createMailboxIds() { - if (draftsMailboxId != null || outboxMailboxId != null) { + if (draftsMailboxId != null + || outboxMailboxId != null + || templateMailboxId != null + ) { return { if (draftsMailboxId != null) draftsMailboxId!: true, if (outboxMailboxId != null) outboxMailboxId!: true, + if (templateMailboxId != null) + templateMailboxId!: true, }; } else { return null; @@ -115,6 +124,7 @@ extension CreateEmailRequestExtension on CreateEmailRequest { required PartId partId, bool withIdentityHeader = false, bool isDraft = false, + bool isTemplate = false, }) { return Email( mailboxIds: createMailboxIds(), @@ -122,7 +132,7 @@ extension CreateEmailRequestExtension on CreateEmailRequest { to: toRecipients, cc: ccRecipients, bcc: bccRecipients, - replyTo: createReplyToRecipients(isDraft: isDraft), + replyTo: createReplyToRecipients(isNotReplyTo: isDraft || isTemplate), inReplyTo: createInReplyTo(), references: createReferences(), keywords: createKeywords(), diff --git a/lib/features/composer/presentation/extensions/setup_email_content_extension.dart b/lib/features/composer/presentation/extensions/setup_email_content_extension.dart index 43ebbea4f..b72ee2563 100644 --- a/lib/features/composer/presentation/extensions/setup_email_content_extension.dart +++ b/lib/features/composer/presentation/extensions/setup_email_content_extension.dart @@ -65,8 +65,9 @@ extension SetupEmailContentExtension on ComposerController { inlineImages: uiState.inlineImages, ); - if (currentEmailActionType == EmailActionType.editDraft) { - setupEmailRequestReadReceiptFlagForEditDraft( + if (currentEmailActionType == EmailActionType.editDraft || + currentEmailActionType == EmailActionType.editAsNewEmail) { + setupEmailRequestReadReceiptFlag( uiState.emailCurrent!.hasRequestReadReceipt, ); setupSelectedIdentityForEditDraft( @@ -81,8 +82,9 @@ extension SetupEmailContentExtension on ComposerController { inlineImages: uiState.inlineImages, ); - if (currentEmailActionType == EmailActionType.editDraft) { - setupEmailRequestReadReceiptFlagForEditDraft( + if (currentEmailActionType == EmailActionType.editDraft || + currentEmailActionType == EmailActionType.editAsNewEmail) { + setupEmailRequestReadReceiptFlag( uiState.emailCurrent.hasRequestReadReceipt, ); setupSelectedIdentityForEditDraft( diff --git a/lib/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_extension.dart b/lib/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_extension.dart index 44a62ea11..e62f510ee 100644 --- a/lib/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_extension.dart +++ b/lib/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_extension.dart @@ -10,7 +10,8 @@ extension SetupEmailRequestReadReceiptFlagExtension on ComposerController { void setupEmailRequestReadReceiptFlag(ComposerArguments arguments) { if (currentEmailActionType == EmailActionType.reopenComposerBrowser) { hasRequestReadReceipt.value = arguments.hasRequestReadReceipt ?? false; - } else if (currentEmailActionType != EmailActionType.editDraft) { + } else if (currentEmailActionType != EmailActionType.editDraft && + currentEmailActionType != EmailActionType.editAsNewEmail) { getServerSetting(); } } diff --git a/lib/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_for_edit_draft_extension.dart b/lib/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_for_edit_draft_extension.dart index ae0eeb169..a88501774 100644 --- a/lib/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_for_edit_draft_extension.dart +++ b/lib/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_for_edit_draft_extension.dart @@ -3,7 +3,7 @@ import 'package:tmail_ui_user/features/composer/presentation/composer_controller extension SetupEmailRequestReadReceiptFlagExtension on ComposerController { - void setupEmailRequestReadReceiptFlagForEditDraft(bool isRequestReadReceipt) { + void setupEmailRequestReadReceiptFlag(bool isRequestReadReceipt) { hasRequestReadReceipt.value = isRequestReadReceipt; } } \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/setup_email_template_id_extension.dart b/lib/features/composer/presentation/extensions/setup_email_template_id_extension.dart new file mode 100644 index 000000000..03736ead9 --- /dev/null +++ b/lib/features/composer/presentation/extensions/setup_email_template_id_extension.dart @@ -0,0 +1,13 @@ +import 'package:model/email/email_action_type.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; + +extension SetupEmailTemplateIdExtension on ComposerController { + void setupEmailTemplateId(ComposerArguments arguments) { + if (currentEmailActionType == EmailActionType.editAsNewEmail || + currentEmailActionType == EmailActionType.reopenComposerBrowser + ) { + currentTemplateEmailId = arguments.savedEmailTemplateId; + } + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/model/create_email_request.dart b/lib/features/composer/presentation/model/create_email_request.dart index 2ffb786d5..f5d98e93d 100644 --- a/lib/features/composer/presentation/model/create_email_request.dart +++ b/lib/features/composer/presentation/model/create_email_request.dart @@ -31,7 +31,9 @@ class CreateEmailRequest with EquatableMixin { final MailboxId? outboxMailboxId; final MailboxId? sentMailboxId; final MailboxId? draftsMailboxId; + final MailboxId? templateMailboxId; final EmailId? draftsEmailId; + final EmailId? templateEmailId; final EmailId? answerForwardEmailId; final EmailId? unsubscribeEmailId; final MessageIdsHeaderValue? messageId; @@ -64,7 +66,9 @@ class CreateEmailRequest with EquatableMixin { this.outboxMailboxId, this.sentMailboxId, this.draftsMailboxId, + this.templateMailboxId, this.draftsEmailId, + this.templateEmailId, this.answerForwardEmailId, this.unsubscribeEmailId, this.messageId, @@ -99,7 +103,9 @@ class CreateEmailRequest with EquatableMixin { outboxMailboxId, sentMailboxId, draftsMailboxId, + templateMailboxId, draftsEmailId, + templateEmailId, answerForwardEmailId, unsubscribeEmailId, references, diff --git a/lib/features/composer/presentation/model/saved_email_draft.dart b/lib/features/composer/presentation/model/saved_composing_email.dart similarity index 69% rename from lib/features/composer/presentation/model/saved_email_draft.dart rename to lib/features/composer/presentation/model/saved_composing_email.dart index ec7b7845d..02b837a95 100644 --- a/lib/features/composer/presentation/model/saved_email_draft.dart +++ b/lib/features/composer/presentation/model/saved_composing_email.dart @@ -6,10 +6,10 @@ import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:model/email/attachment.dart'; -part 'saved_email_draft.g.dart'; +part 'saved_composing_email.g.dart'; @JsonSerializable(includeIfNull: false, explicitToJson: true) -class SavedEmailDraft with EquatableMixin { +class SavedComposingEmail with EquatableMixin { final String content; final String subject; final Set toRecipients; @@ -21,7 +21,7 @@ class SavedEmailDraft with EquatableMixin { final bool hasReadReceipt; final bool isMarkAsImportant; - SavedEmailDraft({ + SavedComposingEmail({ required this.content, required this.subject, required this.toRecipients, @@ -34,12 +34,26 @@ class SavedEmailDraft with EquatableMixin { this.isMarkAsImportant = false, }); - factory SavedEmailDraft.fromJson(Map json) => _$SavedEmailDraftFromJson(json); + factory SavedComposingEmail.fromJson(Map json) => _$SavedComposingEmailFromJson(json); - Map toJson() => _$SavedEmailDraftToJson(this); + Map toJson() => _$SavedComposingEmailToJson(this); String asString() => jsonEncode(toJson()); + factory SavedComposingEmail.empty() { + return SavedComposingEmail( + subject: '', + content: '', + toRecipients: {}, + ccRecipients: {}, + bccRecipients: {}, + replyToRecipients: {}, + attachments: [], + identity: null, + hasReadReceipt: false, + ); + } + @override List get props => [ content, diff --git a/lib/features/composer/presentation/widgets/saving_template_dialog_view.dart b/lib/features/composer/presentation/widgets/saving_template_dialog_view.dart new file mode 100644 index 000000000..15df31899 --- /dev/null +++ b/lib/features/composer/presentation/widgets/saving_template_dialog_view.dart @@ -0,0 +1,230 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:core/presentation/extensions/capitalize_extension.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart' hide State; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/features/email/domain/state/save_template_email_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/update_template_email_state.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/save_template_email_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +class SavingTemplateDialogView extends StatefulWidget { + + final CreateEmailRequest createEmailRequest; + final SaveTemplateEmailInteractor saveTemplateEmailInteractor; + final CreateNewMailboxRequest? createNewMailboxRequest; + final CancelToken? cancelToken; + final void Function(CancelToken? cancelToken)? onCancel; + + const SavingTemplateDialogView({ + super.key, + required this.createEmailRequest, + required this.saveTemplateEmailInteractor, + required this.createNewMailboxRequest, + this.cancelToken, + this.onCancel, + }); + + @override + State createState() => _SavingTemplateDialogViewState(); +} + +class _SavingTemplateDialogViewState extends State { + StreamSubscription? _streamSubscription; + final ValueNotifier?> _viewStateNotifier = ValueNotifier(null); + + @override + void initState() { + super.initState(); + _streamSubscription = widget.saveTemplateEmailInteractor + .execute( + createEmailRequest: widget.createEmailRequest, + createNewMailboxRequest: widget.createNewMailboxRequest, + cancelToken: widget.cancelToken + ) + .listen(_handleDataStream, onError: _handleErrorStream); + } + + void _handleDataStream(Either newState) { + _viewStateNotifier.value = newState; + + newState.fold( + (failure) { + if (failure is SaveTemplateEmailFailure || + failure is UpdateTemplateEmailFailure || + failure is GenerateEmailFailure) { + popBack(result: failure); + } + }, + (success) { + if (success is SaveTemplateEmailSuccess || success is UpdateTemplateEmailSuccess) { + popBack(result: success); + } + } + ); + } + + void _handleErrorStream(Object error, StackTrace stackTrace) { + logError('SavingTemplateDialogView::_handleErrorStream: Exception = $error'); + popBack(result: SaveTemplateEmailFailure(exception: error)); + } + + @override + Widget build(BuildContext context) { + return Dialog( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + insetPadding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), + alignment: Alignment.center, + child: Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: Colors.white, + ), + width: min(context.width, 400), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + clipBehavior: Clip.antiAlias, + padding: const EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 12), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: AppColor.colorItemSelected, + ), + alignment: Alignment.center, + child: Text( + AppLocalizations.of(context).savingTemplate.capitalizeFirstEach, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 17 + ), + ), + ), + const Divider(), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 12, bottom: 4), + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).status}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + ), + const SizedBox(width: 8), + Expanded( + child: ValueListenableBuilder( + valueListenable: _viewStateNotifier, + builder: (context, value, child) { + if (value == null) { + return child!; + } + + return value.fold( + (failure) => child!, + (success) { + return Text( + '${_getStatusMessage(success)}...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ); + } + ); + }, + child: Text( + '...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ), + ), + ) + ], + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 4, bottom: 16), + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).progress}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + ), + const SizedBox(width: 8), + Expanded( + child: LinearProgressIndicator( + color: Colors.white.withOpacity(0.6), + backgroundColor: AppColor.primaryColor, + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + ) + ], + ), + ), + if (widget.onCancel != null) + Align( + alignment: AlignmentDirectional.centerEnd, + child: TMailButtonWidget.fromText( + text: AppLocalizations.of(context).cancel, + textStyle: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.black87, + fontSize: 15 + ), + padding: const EdgeInsetsDirectional.symmetric(horizontal: 20, vertical: 8), + margin: const EdgeInsetsDirectional.only(start: 12, end: 12, bottom: 16), + onTapActionCallback: () { + _viewStateNotifier.value = Left(SaveTemplateEmailFailure()); + widget.onCancel!(widget.cancelToken); + }, + ), + ) + ], + ) + ], + ), + ), + ); + } + + String _getStatusMessage(Success success) { + if (success is GenerateEmailLoading) { + return AppLocalizations.of(context).creatingMessage; + } else { + return AppLocalizations.of(context).savingMessageToTemplateFolder; + } + } + + @override + void dispose() { + _streamSubscription?.cancel(); + _viewStateNotifier.dispose(); + super.dispose(); + } +} diff --git a/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart index a8559970c..98e093dff 100644 --- a/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart @@ -28,6 +28,7 @@ class BottomBarComposerWidget extends StatelessWidget { final VoidCallback toggleRequestReadReceiptAction; final VoidCallback printDraftAction; final VoidCallback toggleMarkAsImportantAction; + final VoidCallback saveAsTemplateAction; const BottomBarComposerWidget({ super.key, @@ -48,6 +49,7 @@ class BottomBarComposerWidget extends StatelessWidget { required this.toggleRequestReadReceiptAction, required this.printDraftAction, required this.toggleMarkAsImportantAction, + required this.saveAsTemplateAction, }); @override @@ -158,6 +160,17 @@ class BottomBarComposerWidget extends StatelessWidget { printDraftAction(); }, ), + PopupItemWidget( + iconAction: imagePaths.icSaveToDraft, + nameAction: AppLocalizations.of(context).saveAsTemplate, + colorIcon: BottomBarComposerWidgetStyle.iconColor, + styleName: BottomBarComposerWidgetStyle.popupItemTextStyle, + padding: BottomBarComposerWidgetStyle.popupItemPadding, + onCallbackAction: () { + menuMoreOptionController.hideMenu(); + saveAsTemplateAction(); + }, + ), ], arrangeAsList: true, position: null, diff --git a/lib/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart index d2c22550f..98f4a9730 100644 --- a/lib/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart @@ -28,6 +28,7 @@ class MobileResponsiveAppBarComposerWidget extends StatelessWidget { final VoidCallback toggleRequestReadReceiptAction; final VoidCallback printDraftAction; final VoidCallback saveToDraftsAction; + final VoidCallback saveToTemplateAction; final VoidCallback deleteComposerAction; final VoidCallback toggleMarkAsImportantAction; @@ -50,6 +51,7 @@ class MobileResponsiveAppBarComposerWidget extends StatelessWidget { required this.toggleRequestReadReceiptAction, required this.printDraftAction, required this.saveToDraftsAction, + required this.saveToTemplateAction, required this.deleteComposerAction, required this.toggleMarkAsImportantAction, }); @@ -186,6 +188,17 @@ class MobileResponsiveAppBarComposerWidget extends StatelessWidget { saveToDraftsAction(); }, ), + PopupItemWidget( + iconAction: imagePaths.icSaveToDraft, + nameAction: AppLocalizations.of(context).saveAsTemplate, + colorIcon: MobileAppBarComposerWidgetStyle.popupItemIconColor, + styleName: MobileAppBarComposerWidgetStyle.popupItemTextStyle, + padding: MobileAppBarComposerWidgetStyle.popupItemPadding, + onCallbackAction: () { + menuMoreOptionController.hideMenu(); + saveToTemplateAction(); + }, + ), PopupItemWidget( iconAction: imagePaths.icDeleteMailbox, nameAction: AppLocalizations.of(context).delete, diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index 652405b32..8a9fce660 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -123,6 +123,24 @@ abstract class EmailDataSource { {CancelToken? cancelToken} ); + Future saveEmailAsTemplate( + Session session, + AccountId accountId, + Email email, + { + CreateNewMailboxRequest? createNewMailboxRequest, + CancelToken? cancelToken + } + ); + + Future updateEmailTemplate( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, + {CancelToken? cancelToken} + ); + Future<({ List emailIdsSuccess, Map mapErrors, diff --git a/lib/features/email/data/datasource_impl/email_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_datasource_impl.dart index 32b149a57..ce5e8ebed 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -221,6 +221,46 @@ class EmailDataSourceImpl extends EmailDataSource { }).catchError(_exceptionThrower.throwException); } + @override + Future saveEmailAsTemplate( + Session session, + AccountId accountId, + Email email, + { + CreateNewMailboxRequest? createNewMailboxRequest, + CancelToken? cancelToken + } + ) { + return Future.sync(() async { + return await emailAPI.saveEmailAsTemplate( + session, + accountId, + email, + createNewMailboxRequest: createNewMailboxRequest, + cancelToken: cancelToken + ); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future updateEmailTemplate( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, + {CancelToken? cancelToken} + ) { + return Future.sync(() async { + return await emailAPI.updateEmailTemplate( + session, + accountId, + newEmail, + oldEmailId, + cancelToken: cancelToken + ); + }).catchError(_exceptionThrower.throwException); + } + @override Future downloadAttachmentForWeb( DownloadTaskId taskId, diff --git a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart index c860d09b1..c83c92c3f 100644 --- a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart @@ -275,6 +275,36 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { return email; } + @override + Future saveEmailAsTemplate( + Session session, + AccountId accountId, + Email email, + { + CreateNewMailboxRequest? createNewMailboxRequest, + CancelToken? cancelToken + } + ) async { + await _emailCacheManager.update(accountId, session.username, created: [email]); + return email; + } + + @override + Future updateEmailTemplate( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, + {CancelToken? cancelToken} + ) async { + await _emailCacheManager.update( + accountId, + session.username, + updated: [newEmail], + ); + return newEmail; + } + @override Future sendEmail( Session session, diff --git a/lib/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart index 86cc04855..4f01fce44 100644 --- a/lib/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart @@ -355,4 +355,26 @@ class EmailLocalStorageDataSourceImpl extends EmailDataSource { return htmlDocument; }).catchError(_exceptionThrower.throwException); } + + @override + Future saveEmailAsTemplate( + Session session, + AccountId accountId, + Email email, { + CreateNewMailboxRequest? createNewMailboxRequest, + CancelToken? cancelToken, + }) { + throw UnimplementedError(); + } + + @override + Future updateEmailTemplate( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, { + CancelToken? cancelToken, + }) { + throw UnimplementedError(); + } } \ No newline at end of file diff --git a/lib/features/email/data/datasource_impl/email_session_storage_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_session_storage_datasource_impl.dart index 4f0feceab..5c3b0a0fd 100644 --- a/lib/features/email/data/datasource_impl/email_session_storage_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_session_storage_datasource_impl.dart @@ -271,4 +271,26 @@ class EmailSessionStorageDatasourceImpl extends EmailDataSource { Future generateEntireMessageAsDocument(ViewEntireMessageRequest entireMessageRequest) { throw UnimplementedError(); } + + @override + Future saveEmailAsTemplate( + Session session, + AccountId accountId, + Email email, { + CreateNewMailboxRequest? createNewMailboxRequest, + CancelToken? cancelToken, + }) { + throw UnimplementedError(); + } + + @override + Future updateEmailTemplate( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, { + CancelToken? cancelToken, + }) { + throw UnimplementedError(); + } } \ No newline at end of file diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 75e7cc0d1..17ef3544f 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -656,18 +656,45 @@ class EmailAPI with HandleSetErrorMixin { return (emailIdsSuccess: updatedEmailIds, mapErrors: mapErrors); } - Future saveEmailAsDrafts( + Future _emailSetCreateMethod( Session session, AccountId accountId, Email email, - {CancelToken? cancelToken} + { + CreateNewMailboxRequest? createNewMailboxRequest, + CancelToken? cancelToken + } ) async { + final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); + + MailboxId? mailboxId; + if (createNewMailboxRequest != null) { + final generateCreateId = Id(_uuid.v1()); + final setMailboxMethod = SetMailboxMethod(accountId) + ..addCreate( + generateCreateId, + Mailbox( + name: createNewMailboxRequest.newName, + parentId: createNewMailboxRequest.parentId, + isSubscribed: IsSubscribed(createNewMailboxRequest.isSubscribed) + ) + ); + + requestBuilder.invocation(setMailboxMethod); + + mailboxId = MailboxId(ReferenceId( + ReferencePrefix.defaultPrefix, + generateCreateId)); + } else { + mailboxId = email.mailboxIds?.keys.first; + } + if (mailboxId != null) { + email.mailboxIds?.addAll({mailboxId: true}); + } final idCreateMethod = Id(_uuid.v1()); final setEmailMethod = SetEmailMethod(accountId) ..addCreate(idCreateMethod, email); - final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - final setEmailInvocation = requestBuilder.invocation(setEmailMethod); final capabilities = setEmailMethod.requiredCapabilities @@ -693,7 +720,7 @@ class EmailAPI with HandleSetErrorMixin { } } - Future removeEmailDrafts( + Future _emailSetDestroyMethod( Session session, AccountId accountId, EmailId emailId, @@ -728,6 +755,20 @@ class EmailAPI with HandleSetErrorMixin { } } + Future saveEmailAsDrafts( + Session session, + AccountId accountId, + Email email, + {CancelToken? cancelToken} + ) => _emailSetCreateMethod(session, accountId, email, cancelToken: cancelToken); + + Future removeEmailDrafts( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) => _emailSetDestroyMethod(session, accountId, emailId, cancelToken: cancelToken); + Future updateEmailDrafts( Session session, AccountId accountId, @@ -756,6 +797,51 @@ class EmailAPI with HandleSetErrorMixin { return emailCreated; } + Future saveEmailAsTemplate( + Session session, + AccountId accountId, + Email email, + { + CreateNewMailboxRequest? createNewMailboxRequest, + CancelToken? cancelToken + } + ) => _emailSetCreateMethod(session, accountId, email, createNewMailboxRequest: createNewMailboxRequest, cancelToken: cancelToken); + + Future removeEmailTemplate( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) => _emailSetDestroyMethod(session, accountId, emailId, cancelToken: cancelToken); + + Future updateEmailTemplate( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, + {CancelToken? cancelToken} + ) async { + final emailCreated = await saveEmailAsTemplate( + session, + accountId, + newEmail, + cancelToken: cancelToken + ); + + try { + await removeEmailTemplate( + session, + accountId, + oldEmailId, + cancelToken: cancelToken + ); + } catch (e) { + logError('EmailAPI::updateEmailTemplate: Exception = $e'); + } + + return emailCreated; + } + Future<({ List emailIdsSuccess, Map mapErrors, diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index d8598196e..5857a1ad5 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -336,6 +336,65 @@ class EmailRepositoryImpl extends EmailRepository { return result; } + @override + Future saveEmailAsTemplate( + Session session, + AccountId accountId, + Email email, + { + CreateNewMailboxRequest? createNewMailboxRequest, + CancelToken? cancelToken, + } + ) async { + final result = await emailDataSource[DataSourceType.network]!.saveEmailAsTemplate( + session, + accountId, + email, + createNewMailboxRequest: createNewMailboxRequest, + cancelToken: cancelToken + ); + try { + await emailDataSource[DataSourceType.hiveCache]!.saveEmailAsTemplate( + session, + accountId, + result, + createNewMailboxRequest: createNewMailboxRequest, + cancelToken: cancelToken + ); + } catch (e) { + logError('EmailRepositoryImpl::saveEmailAsTemplate:exception $e'); + } + return result; + } + + @override + Future updateEmailTemplate( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, + {CancelToken? cancelToken} + ) async { + final result = await emailDataSource[DataSourceType.network]!.updateEmailTemplate( + session, + accountId, + newEmail, + oldEmailId, + cancelToken: cancelToken + ); + try { + await emailDataSource[DataSourceType.hiveCache]!.updateEmailTemplate( + session, + accountId, + result, + oldEmailId, + ); + } catch (e) { + logError('EmailRepositoryImpl::updateEmailTemplate:exception $e'); + } + return result; + } + @override Future downloadAttachmentForWeb( DownloadTaskId taskId, diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart index 67d55fc90..58e2d2f58 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -131,6 +131,24 @@ abstract class EmailRepository { {CancelToken? cancelToken} ); + Future saveEmailAsTemplate( + Session session, + AccountId accountId, + Email email, + { + CreateNewMailboxRequest? createNewMailboxRequest, + CancelToken? cancelToken + } + ); + + Future updateEmailTemplate( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, + {CancelToken? cancelToken} + ); + Future<({ List emailIdsSuccess, Map mapErrors, diff --git a/lib/features/email/domain/state/save_template_email_state.dart b/lib/features/email/domain/state/save_template_email_state.dart new file mode 100644 index 000000000..0a84854b8 --- /dev/null +++ b/lib/features/email/domain/state/save_template_email_state.dart @@ -0,0 +1,18 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; + +class SavingTemplateEmail extends LoadingState {} + +class SaveTemplateEmailSuccess extends UIState { + SaveTemplateEmailSuccess(this.emailId); + + final EmailId emailId; + + @override + List get props => [emailId]; +} + +class SaveTemplateEmailFailure extends FeatureFailure { + SaveTemplateEmailFailure({super.exception}); +} \ No newline at end of file diff --git a/lib/features/email/domain/state/update_template_email_state.dart b/lib/features/email/domain/state/update_template_email_state.dart new file mode 100644 index 000000000..ae9138c6d --- /dev/null +++ b/lib/features/email/domain/state/update_template_email_state.dart @@ -0,0 +1,18 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; + +class UpdatingTemplateEmail extends LoadingState {} + +class UpdateTemplateEmailSuccess extends UIState { + UpdateTemplateEmailSuccess(this.emailId); + + final EmailId emailId; + + @override + List get props => [emailId]; +} + +class UpdateTemplateEmailFailure extends FeatureFailure { + UpdateTemplateEmailFailure({super.exception}); +} \ No newline at end of file diff --git a/lib/features/email/domain/usecases/save_template_email_interactor.dart b/lib/features/email/domain/usecases/save_template_email_interactor.dart new file mode 100644 index 000000000..42d26bf02 --- /dev/null +++ b/lib/features/email/domain/usecases/save_template_email_interactor.dart @@ -0,0 +1,83 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/email/domain/state/save_template_email_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/update_template_email_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; + +class SaveTemplateEmailInteractor { + const SaveTemplateEmailInteractor( + this._composerRepository, + this._emailRepository, + ); + + final ComposerRepository _composerRepository; + final EmailRepository _emailRepository; + + Stream> execute({ + required CreateEmailRequest createEmailRequest, + required CreateNewMailboxRequest? createNewMailboxRequest, + CancelToken? cancelToken, + }) async* { + yield Right(GenerateEmailLoading()); + + try { + final emailCreated = await _createEmailObject(createEmailRequest); + + if (emailCreated == null) { + yield Left(GenerateEmailFailure(CannotCreateEmailObjectException())); + return; + } + + if (createEmailRequest.templateEmailId != null) { + yield Right(UpdatingTemplateEmail()); + final emailTemplateSaved = await _emailRepository.updateEmailTemplate( + createEmailRequest.session, + createEmailRequest.accountId, + emailCreated, + createEmailRequest.templateEmailId!, + cancelToken: cancelToken, + ); + yield Right(UpdateTemplateEmailSuccess(emailTemplateSaved.id!)); + } else { + yield Right(SavingTemplateEmail()); + final emailTemplateSaved = await _emailRepository.saveEmailAsTemplate( + createEmailRequest.session, + createEmailRequest.accountId, + emailCreated, + createNewMailboxRequest: createNewMailboxRequest, + cancelToken: cancelToken, + ); + yield Right(SaveTemplateEmailSuccess(emailTemplateSaved.id!)); + } + } catch (e) { + logError('SaveTemplateEmailInteractor::execute(): $e'); + if (createEmailRequest.templateEmailId != null) { + yield Left(UpdateTemplateEmailFailure(exception: e)); + } else { + yield Left(SaveTemplateEmailFailure(exception: e)); + } + } + } + + Future _createEmailObject(CreateEmailRequest createEmailRequest) async { + try { + final emailCreated = await _composerRepository.generateEmail( + createEmailRequest, + withIdentityHeader: true, + isTemplate: true); + return emailCreated; + } catch (e) { + logError('CreateNewAndSaveEmailToDraftsInteractor::_createEmailObject: Exception: $e'); + return null; + } + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 957c185e3..d2c9e7679 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -548,7 +548,8 @@ class EmailView extends GetWidget { EmailActionType.archiveMessage, if (PlatformInfo.isWeb && PlatformInfo.isCanvasKit) EmailActionType.downloadMessageAsEML, - EmailActionType.editAsNewEmail, + if (mailboxContain?.isTemplates == false) + EmailActionType.editAsNewEmail, ]; if (position == null) { diff --git a/lib/features/email/presentation/model/composer_arguments.dart b/lib/features/email/presentation/model/composer_arguments.dart index de16c586d..805d1df3d 100644 --- a/lib/features/email/presentation/model/composer_arguments.dart +++ b/lib/features/email/presentation/model/composer_arguments.dart @@ -38,6 +38,7 @@ class ComposerArguments extends RouterArguments { final int? savedDraftHash; final EmailActionType? savedActionType; final EmailId? savedEmailDraftId; + final EmailId? savedEmailTemplateId; ComposerArguments({ this.emailActionType = EmailActionType.compose, @@ -66,6 +67,7 @@ class ComposerArguments extends RouterArguments { this.savedDraftHash, this.savedActionType, this.savedEmailDraftId, + this.savedEmailTemplateId, }); factory ComposerArguments.fromSendingEmail(SendingEmail sendingEmail) => @@ -113,11 +115,14 @@ class ComposerArguments extends RouterArguments { presentationEmail: presentationEmail, ); - factory ComposerArguments.editAsNewEmail(PresentationEmail presentationEmail) => - ComposerArguments( - emailActionType: EmailActionType.editAsNewEmail, - presentationEmail: presentationEmail, - ); + factory ComposerArguments.editAsNewEmail( + PresentationEmail presentationEmail, { + EmailId? savedEmailTemplateId + }) => ComposerArguments( + emailActionType: EmailActionType.editAsNewEmail, + presentationEmail: presentationEmail, + savedEmailTemplateId: savedEmailTemplateId, + ); factory ComposerArguments.fromSessionStorageBrowser(ComposerCache composerCache) => ComposerArguments( @@ -134,6 +139,7 @@ class ComposerArguments extends RouterArguments { savedDraftHash: composerCache.draftHash, savedActionType: composerCache.actionType, savedEmailDraftId: composerCache.draftEmailId, + savedEmailTemplateId: composerCache.templateEmailId, ); factory ComposerArguments.replyEmail({ @@ -258,6 +264,7 @@ class ComposerArguments extends RouterArguments { savedDraftHash, savedActionType, savedEmailDraftId, + sendingEmailActionType, ]; ComposerArguments copyWith({ @@ -287,6 +294,7 @@ class ComposerArguments extends RouterArguments { int? savedDraftHash, EmailActionType? savedActionType, EmailId? savedEmailDraftId, + EmailId? savedEmailTemplateId, }) { return ComposerArguments( emailActionType: emailActionType ?? this.emailActionType, @@ -315,6 +323,7 @@ class ComposerArguments extends RouterArguments { savedDraftHash: savedDraftHash ?? this.savedDraftHash, savedActionType: savedActionType ?? this.savedActionType, savedEmailDraftId: savedEmailDraftId ?? this.savedEmailDraftId, + savedEmailTemplateId: savedEmailTemplateId ?? this.savedEmailTemplateId, ); } } diff --git a/lib/features/mailbox_dashboard/data/model/composer_cache.dart b/lib/features/mailbox_dashboard/data/model/composer_cache.dart index 30f6b9ed2..428a4ee8f 100644 --- a/lib/features/mailbox_dashboard/data/model/composer_cache.dart +++ b/lib/features/mailbox_dashboard/data/model/composer_cache.dart @@ -25,6 +25,7 @@ class ComposerCache with EquatableMixin { final int? draftHash; final EmailActionType? actionType; final EmailId? draftEmailId; + final EmailId? templateEmailId; ComposerCache({ this.email, @@ -36,6 +37,7 @@ class ComposerCache with EquatableMixin { this.draftHash, this.actionType, this.draftEmailId, + this.templateEmailId, }); factory ComposerCache.fromJson(Map json) => _$ComposerCacheFromJson(json); @@ -53,5 +55,6 @@ class ComposerCache with EquatableMixin { draftHash, actionType, draftEmailId, + templateEmailId, ]; } diff --git a/lib/features/thread/presentation/mixin/email_action_controller.dart b/lib/features/thread/presentation/mixin/email_action_controller.dart index 3fa7996ea..40ab8e1a0 100644 --- a/lib/features/thread/presentation/mixin/email_action_controller.dart +++ b/lib/features/thread/presentation/mixin/email_action_controller.dart @@ -43,9 +43,15 @@ mixin EmailActionController { mailboxDashBoardController.openComposer(ComposerArguments.editDraftEmail(presentationEmail)); } - void editAsNewEmail(PresentationEmail presentationEmail) { + void editAsNewEmail( + PresentationEmail presentationEmail, { + EmailId? savedEmailTemplateId, + }) { mailboxDashBoardController.openComposer( - ComposerArguments.editAsNewEmail(presentationEmail), + ComposerArguments.editAsNewEmail( + presentationEmail, + savedEmailTemplateId: savedEmailTemplateId, + ), ); } diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 74e401e02..ec4d8b092 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -1176,7 +1176,7 @@ class ThreadController extends BaseController with EmailActionController { if (mailboxContain?.isDrafts == true) { editDraftEmail(selectedEmail); } else if (mailboxContain?.isTemplates == true) { - editAsNewEmail(selectedEmail); + editAsNewEmail(selectedEmail, savedEmailTemplateId: selectedEmail.id); } else { previewEmail(selectedEmail); } diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index 3a3241ff2..ef0777709 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -724,7 +724,7 @@ class ThreadView extends GetWidget _markAsEmailSpamOrUnSpamContextMenuItemAction(context, email, mailboxContain), if (mailboxContain?.isArchive == false) _archiveMessageContextMenuItemAction(context, email), - if (mailboxContain?.isDrafts == false) + if (mailboxContain?.isDrafts == false && mailboxContain?.isTemplates == false) _editAsNewEmailContextMenuItemAction(context, email), ]; } @@ -846,7 +846,7 @@ class ThreadView extends GetWidget _buildMarkAsSpamPopupMenuItem(context, email, mailboxContain), if (mailboxContain?.isArchive == false) _buildArchiveMessagePopupMenuItem(context, email), - if (mailboxContain?.isDrafts == false) + if (mailboxContain?.isDrafts == false && mailboxContain?.isTemplates == false) _buildEditAsNewEmailPopupMenuItem(AppLocalizations.of(context), email), ]; } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 7c996afc6..b055b3e01 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -4417,5 +4417,57 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "saveAsTemplate": "Save as template", + "@saveAsTemplate": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "savingTemplate": "Saving template", + "@savingTemplate": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "savingMessageToTemplateFolder": "Saving message to template folder", + "@savingMessageToTemplateFolder": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveMessageToTemplateSuccess": "Save message to template folder successfully", + "@saveMessageToTemplateSuccess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "updateMessageToTemplateSuccess": "Update message to template folder successfully", + "@updateMessageToTemplateSuccess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveMessageToTemplateFailed": "Save message to template folder failed", + "@saveMessageToTemplateFailed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveMessageToTemplateCancelled": "Save message to template folder has been cancelled", + "@saveMessageToTemplateCancelled": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "invalidArguments": "Invalid arguments: \"{description}\"", + "@invalidArguments": { + "type": "text", + "placeholders_order": [ + "description" + ], + "placeholders": { + "description": {} + } } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index e0a57f205..55e437da5 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4645,4 +4645,61 @@ class AppLocalizations { name: 'pullHarderFor', ); } + + String get saveAsTemplate { + return Intl.message( + 'Save as template', + name: 'saveAsTemplate', + ); + } + + String get savingTemplate { + return Intl.message( + 'Saving template', + name: 'savingTemplate', + ); + } + + String get savingMessageToTemplateFolder { + return Intl.message( + 'Saving message to template folder', + name: 'savingMessageToTemplateFolder', + ); + } + + String get saveMessageToTemplateSuccess { + return Intl.message( + 'Save message to template folder successfully', + name: 'saveMessageToTemplateSuccess', + ); + } + + String get updateMessageToTemplateSuccess { + return Intl.message( + 'Update message to template folder successfully', + name: 'updateMessageToTemplateSuccess', + ); + } + + String get saveMessageToTemplateFailed { + return Intl.message( + 'Save message to template folder failed', + name: 'saveMessageToTemplateFailed', + ); + } + + String get saveMessageToTemplateCancelled { + return Intl.message( + 'Save message to template folder has been cancelled', + name: 'saveMessageToTemplateCancelled', + ); + } + + String invalidArguments(String description) { + return Intl.message( + 'Invalid arguments: "$description"', + name: 'invalidArguments', + args: [description], + ); + } } diff --git a/test/features/composer/data/repository/composer_repository_impl_test.dart b/test/features/composer/data/repository/composer_repository_impl_test.dart index a2c677652..9beebca85 100644 --- a/test/features/composer/data/repository/composer_repository_impl_test.dart +++ b/test/features/composer/data/repository/composer_repository_impl_test.dart @@ -103,6 +103,7 @@ void main() { when(mockCreateEmailRequest.emailActionType).thenReturn(EmailActionType.compose); when(mockCreateEmailRequest.hasRequestReadReceipt).thenReturn(false); when(mockCreateEmailRequest.isMarkAsImportant).thenReturn(false); + when(mockCreateEmailRequest.templateMailboxId).thenReturn(null); when(mockHtmlDataSource.replaceImageBase64ToImageCID( emailContent: emailContentWithBase64, @@ -160,6 +161,7 @@ void main() { when(mockCreateEmailRequest.emailActionType).thenReturn(EmailActionType.compose); when(mockCreateEmailRequest.hasRequestReadReceipt).thenReturn(false); when(mockCreateEmailRequest.isMarkAsImportant).thenReturn(false); + when(mockCreateEmailRequest.templateMailboxId).thenReturn(null); when(mockHtmlDataSource.replaceImageBase64ToImageCID( emailContent: emailContentWithBase64, @@ -214,6 +216,7 @@ void main() { when(mockCreateEmailRequest.emailActionType).thenReturn(EmailActionType.compose); when(mockCreateEmailRequest.hasRequestReadReceipt).thenReturn(false); when(mockCreateEmailRequest.isMarkAsImportant).thenReturn(false); + when(mockCreateEmailRequest.templateMailboxId).thenReturn(null); when(mockHtmlDataSource.replaceImageBase64ToImageCID( emailContent: plainEmailContent, diff --git a/test/features/composer/presentation/composer_controller_test.dart b/test/features/composer/presentation/composer_controller_test.dart index bd1b7db0a..e4360540b 100644 --- a/test/features/composer/presentation/composer_controller_test.dart +++ b/test/features/composer/presentation/composer_controller_test.dart @@ -39,10 +39,11 @@ import 'package:tmail_ui_user/features/composer/presentation/controller/rich_tex import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_selected_identity_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/formatting_options_state.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/saved_email_draft.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/saved_composing_email.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/save_template_email_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart'; @@ -160,6 +161,7 @@ class MockMailboxDashBoardController extends Mock implements MailboxDashBoardCon MockSpec(), MockSpec(), MockSpec(), + MockSpec(), // Additional Getx dependencies mock specs MockSpec(fallbackGenerators: fallbackGenerators), @@ -204,6 +206,7 @@ void main() { late MockCreateNewAndSaveEmailToDraftsInteractor mockCreateNewAndSaveEmailToDraftsInteractor; late MockPrintEmailInteractor mockPrintEmailInteractor; late MockComposerRepository mockComposerRepository; + late MockSaveTemplateEmailInteractor mockSaveTemplateEmailInteractor; // Declaration Getx dependencies final mockMailboxDashBoardController = MockMailboxDashBoardController(); @@ -273,6 +276,7 @@ void main() { mockCreateNewAndSaveEmailToDraftsInteractor = MockCreateNewAndSaveEmailToDraftsInteractor(); mockPrintEmailInteractor = MockPrintEmailInteractor(); mockComposerRepository = MockComposerRepository(); + mockSaveTemplateEmailInteractor = MockSaveTemplateEmailInteractor(); composerController = ComposerController( mockLocalFilePickerInteractor, @@ -289,6 +293,7 @@ void main() { mockCreateNewAndSaveEmailToDraftsInteractor, mockPrintEmailInteractor, mockComposerRepository, + mockSaveTemplateEmailInteractor, ); mockHtmlEditorApi = MockHtmlEditorApi(); @@ -344,7 +349,7 @@ void main() { emailContent: anyNamed('emailContent'), )).thenAnswer((_) async => emailContent); - final savedEmailDraft = SavedEmailDraft( + final savedEmailDraft = SavedComposingEmail( content: emailContent, subject: emailSubject, toRecipients: {toRecipient}, @@ -404,7 +409,7 @@ void main() { emailContent: anyNamed('emailContent'), )).thenAnswer((_) async => emailContent); - final savedEmailDraft = SavedEmailDraft( + final savedEmailDraft = SavedComposingEmail( content: emailContent, subject: emailSubject, toRecipients: {toRecipient}, @@ -471,7 +476,7 @@ void main() { .thenAnswer((_) => Stream.value( Right(SaveEmailAsDraftsSuccess(EmailId(Id('123')), null)))); - final savedEmailDraft = SavedEmailDraft( + final savedEmailDraft = SavedComposingEmail( content: emailContent, subject: emailSubject, toRecipients: {toRecipient}, @@ -551,7 +556,7 @@ void main() { .thenAnswer((_) => Stream.value( Right(UpdateEmailDraftsSuccess(EmailId(Id('123')))))); - final savedEmailDraft = SavedEmailDraft( + final savedEmailDraft = SavedComposingEmail( content: emailContent, subject: emailSubject, toRecipients: {toRecipient}, @@ -626,7 +631,7 @@ void main() { emailContent: anyNamed('emailContent'), )).thenAnswer((_) async => emailContent); - final savedEmailDraft = SavedEmailDraft( + final savedEmailDraft = SavedComposingEmail( content: emailContent, subject: emailSubject, toRecipients: {toRecipient}, @@ -686,7 +691,7 @@ void main() { emailContent: anyNamed('emailContent'), )).thenAnswer((_) async => emailContent); - final savedEmailDraft = SavedEmailDraft( + final savedEmailDraft = SavedComposingEmail( content: emailContent, subject: emailSubject, toRecipients: {toRecipient}, diff --git a/test/features/composer/presentation/model/saved_email_draft_test.dart b/test/features/composer/presentation/model/saved_email_draft_test.dart index 2ed46f3b7..bec6b9bb0 100644 --- a/test/features/composer/presentation/model/saved_email_draft_test.dart +++ b/test/features/composer/presentation/model/saved_email_draft_test.dart @@ -2,7 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/email/attachment.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/saved_email_draft.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/saved_composing_email.dart'; void main() { group('saved email draft test', () { @@ -10,7 +10,7 @@ void main() { 'should tag toRecipients, ccRecipients and bccRecipients in props', () { // arrange - final savedEmailDraft = SavedEmailDraft( + final savedEmailDraft = SavedComposingEmail( subject: 'subject', content: 'content', toRecipients: {EmailAddress('to name', 'to email')}, @@ -43,7 +43,7 @@ void main() { final attachments = []; const hasReadReceipt = false; - final toSavedEmailDraft = SavedEmailDraft( + final toSavedEmailDraft = SavedComposingEmail( subject: subject, content: content, toRecipients: {recipient}, @@ -55,7 +55,7 @@ void main() { hasReadReceipt: hasReadReceipt ); - final ccSavedEmailDraft = SavedEmailDraft( + final ccSavedEmailDraft = SavedComposingEmail( subject: subject, content: content, toRecipients: {}, @@ -67,7 +67,7 @@ void main() { hasReadReceipt: hasReadReceipt ); - final bccSavedEmailDraft = SavedEmailDraft( + final bccSavedEmailDraft = SavedComposingEmail( subject: subject, content: content, toRecipients: {}, @@ -79,7 +79,7 @@ void main() { hasReadReceipt: hasReadReceipt ); - final replyToSavedEmailDraft = SavedEmailDraft( + final replyToSavedEmailDraft = SavedComposingEmail( subject: subject, content: content, toRecipients: {}, @@ -112,7 +112,7 @@ void main() { final listToRecipients = { EmailAddress('to name', 'to email') }; - final savedEmailDraft = SavedEmailDraft( + final savedEmailDraft = SavedComposingEmail( subject: 'subject', content: 'content', toRecipients: listToRecipients, @@ -138,7 +138,7 @@ void main() { 'when all properties are the same', () { // arrange - final savedEmailDraft = SavedEmailDraft( + final savedEmailDraft = SavedComposingEmail( subject: 'subject', content: 'content', toRecipients: {EmailAddress('to name', 'to email')}, @@ -150,7 +150,7 @@ void main() { hasReadReceipt: false ); - final savedEmailDraft2 = SavedEmailDraft( + final savedEmailDraft2 = SavedComposingEmail( subject: 'subject', content: 'content', toRecipients: {EmailAddress('to name', 'to email')},