import 'dart:async'; import 'dart:math'; import 'package:core/core.dart'; import 'package:custom_pop_up_menu/custom_pop_up_menu.dart'; import 'package:dartz/dartz.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:dio/dio.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:file_picker/file_picker.dart'; import 'package:filesize/filesize.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; 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/error/set_error.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'; import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:scribe/scribe.dart'; import 'package:super_tag_editor/tag_editor.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/before_reconnect_handler.dart'; import 'package:tmail_ui_user/features/base/before_reconnect_manager.dart'; import 'package:tmail_ui_user/features/base/mixin/ai_scribe_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/auto_complete_result_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_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'; 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'; import 'package:tmail_ui_user/features/composer/domain/state/generate_email_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'; import 'package:tmail_ui_user/features/composer/domain/state/upload_attachment_state.dart'; 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/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'; import 'package:tmail_ui_user/features/composer/presentation/extensions/attachment_detection_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/auto_create_tag_for_recipients_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/get_draft_mailbox_id_for_composer_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/get_outbox_mailbox_id_for_composer_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/get_sent_mailbox_id_for_composer_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_keyboard_shortcut_actions_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_message_failure_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_recipients_collapsed_extensions.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/sanitize_signature_in_email_content_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_attachments_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_content_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_important_flag_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_other_components_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_recipients_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_subject_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_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/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/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'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_text_formatting_menu_state_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/validate_premium_storage_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/identity_extension.dart'; import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart' if (dart.library.html) 'package:tmail_ui_user/features/network_connection/presentation/web_network_connection_controller.dart'; import 'package:tmail_ui_user/features/server_settings/domain/usecases/get_server_setting_interactor.dart'; import 'package:tmail_ui_user/features/upload/domain/exceptions/pick_file_exception.dart'; import 'package:tmail_ui_user/features/upload/domain/extensions/file_info_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_upload_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; import 'package:tmail_ui_user/features/upload/domain/state/local_file_picker_state.dart'; import 'package:tmail_ui_user/features/upload/domain/state/local_image_picker_state.dart'; import 'package:tmail_ui_user/features/upload/domain/usecases/local_file_picker_interactor.dart'; import 'package:tmail_ui_user/features/upload/domain/usecases/local_image_picker_interactor.dart'; import 'package:tmail_ui_user/features/upload/presentation/controller/upload_controller.dart'; import 'package:tmail_ui_user/main/exceptions/remote/authentication_exception.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/universal_import/html_stub.dart' as html; import 'package:tmail_ui_user/main/utils/app_config.dart'; class ComposerController extends BaseController with DragDropFileMixin, AutoCompleteResultMixin, EditorViewMixin, AiScribeMixin implements BeforeReconnectHandler { final mailboxDashBoardController = Get.find(); final networkConnectionController = Get.find(); final _beforeReconnectManager = Get.find(); final composerArguments = Rxn(); final isEnableEmailSendButton = false.obs; final isInitialRecipient = false.obs; final subjectEmail = Rxn(); final screenDisplayMode = ScreenDisplayMode.normal.obs; final emailContentsViewState = Rxn>(); final hasRequestReadReceipt = false.obs; final fromRecipientState = PrefixRecipientState.disabled.obs; final toRecipientState = PrefixRecipientState.enabled.obs; final ccRecipientState = PrefixRecipientState.disabled.obs; final bccRecipientState = PrefixRecipientState.disabled.obs; final replyToRecipientState = PrefixRecipientState.disabled.obs; final recipientsCollapsedState = PrefixRecipientState.disabled.obs; final prefixRootState = PrefixEmailAddress.to.obs; final identitySelected = Rxn(); final listFromIdentities = RxList(); final isEmailChanged = Rx(false); final isMarkAsImportant = Rx(false); final isContentHeightExceeded = Rx(false); final editorTextSelection = Rxn(); final LocalFilePickerInteractor _localFilePickerInteractor; final LocalImagePickerInteractor _localImagePickerInteractor; final GetEmailContentInteractor _getEmailContentInteractor; final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; final UploadController uploadController; final RemoveComposerCacheByIdOnWebInteractor _removeComposerCacheByIdOnWebInteractor; final SaveComposerCacheOnWebInteractor _saveComposerCacheOnWebInteractor; final DownloadImageAsBase64Interactor _downloadImageAsBase64Interactor; final TransformHtmlEmailContentInteractor _transformHtmlEmailContentInteractor; final GetServerSettingInteractor _getServerSettingInteractor; final CreateNewAndSendEmailInteractor _createNewAndSendEmailInteractor; final CreateNewAndSaveEmailToDraftsInteractor _createNewAndSaveEmailToDraftsInteractor; final PrintEmailInteractor printEmailInteractor; final ComposerRepository _composerRepository; final String? composerId; final ComposerArguments? composerArgs; final SaveTemplateEmailInteractor _saveTemplateEmailInteractor; GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; GetDeviceContactSuggestionsInteractor? _getDeviceContactSuggestionsInteractor; RestoreEmailInlineImagesInteractor? restoreEmailInlineImagesInteractor; List listToEmailAddress = []; List listCcEmailAddress = []; List listBccEmailAddress = []; List listReplyToEmailAddress = []; ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.tMailContact; final subjectEmailInputController = TextEditingController(); final toEmailAddressController = TextEditingController(); final ccEmailAddressController = TextEditingController(); final bccEmailAddressController = TextEditingController(); final replyToEmailAddressController = TextEditingController(); final searchIdentitiesInputController = TextEditingController(); final GlobalKey keyToEmailTagEditor = GlobalKey(); final GlobalKey keyCcEmailTagEditor = GlobalKey(); final GlobalKey keyBccEmailTagEditor = GlobalKey(); final GlobalKey keyReplyToEmailTagEditor = GlobalKey(); final GlobalKey headerEditorMobileWidgetKey = GlobalKey(); final GlobalKey identityDropdownKey = GlobalKey(); final double defaultPaddingCoordinateYCursorEditor = 8; FocusNode? subjectEmailInputFocusNode; FocusNode? toAddressFocusNode; FocusNode? ccAddressFocusNode; FocusNode? bccAddressFocusNode; FocusNode? replyToAddressFocusNode; FocusNode? searchIdentitiesFocusNode; FocusNode? toAddressFocusNodeKeyboard; FocusNode? ccAddressFocusNodeKeyboard; FocusNode? bccAddressFocusNodeKeyboard; FocusNode? replyToAddressFocusNodeKeyboard; FocusNode? keyboardShortcutFocusNode; StreamSubscription? _subscriptionOnDragEnter; StreamSubscription? _subscriptionOnDragOver; StreamSubscription? _subscriptionOnDragLeave; StreamSubscription? _subscriptionOnDrop; StreamSubscription? _subscriptionOnBlur; StreamSubscription? _composerCacheListener; RichTextMobileTabletController? richTextMobileTabletController; RichTextWebController? richTextWebController; CustomPopupMenuController? menuMoreOptionController; final ScrollController scrollController = ScrollController(); final ScrollController scrollControllerEmailAddress = ScrollController(); final ScrollController scrollControllerAttachment = ScrollController(); final ScrollController scrollControllerIdentities = ScrollController(); List initialAttachments = []; String? _textEditorWeb; double? maxWithEditor; EmailId? emailIdEditing; bool isAttachmentCollapsed = false; ButtonState _closeComposerButtonState = ButtonState.enabled; ButtonState _saveToDraftButtonState = ButtonState.enabled; ButtonState _sendButtonState = ButtonState.enabled; ButtonState printDraftButtonState = ButtonState.enabled; int? _savedEmailDraftHash; bool restoringSignatureButton = false; bool synchronizeInitDraftHash = false; GlobalKey? responsiveContainerKey; EmailActionType? currentEmailActionType; EmailActionType? savedActionType; int minInputLengthAutocomplete = AppConfig.defaultMinInputLengthAutocomplete; EmailId? currentTemplateEmailId; @visibleForTesting int? get savedEmailDraftHash => _savedEmailDraftHash; GetEmailContentInteractor get getEmailContentInteractor => _getEmailContentInteractor; GetServerSettingInteractor get getServerSettingInteractor => _getServerSettingInteractor; GetAllIdentitiesInteractor get getAllIdentitiesInteractor => _getAllIdentitiesInteractor; TransformHtmlEmailContentInteractor get transformHtmlEmailContentInteractor => _transformHtmlEmailContentInteractor; String get ownEmailAddress => mailboxDashBoardController.ownEmailAddress.value; late Worker uploadInlineImageWorker; late bool _isEmailBodyLoaded; ComposerController( this._localFilePickerInteractor, this._localImagePickerInteractor, this._getEmailContentInteractor, this._getAllIdentitiesInteractor, this.uploadController, this._removeComposerCacheByIdOnWebInteractor, this._saveComposerCacheOnWebInteractor, this._downloadImageAsBase64Interactor, this._transformHtmlEmailContentInteractor, this._getServerSettingInteractor, this._createNewAndSendEmailInteractor, this._createNewAndSaveEmailToDraftsInteractor, this.printEmailInteractor, this._composerRepository, this._saveTemplateEmailInteractor, { this.composerId, this.composerArgs, } ); @override void onInit() { super.onInit(); if (PlatformInfo.isWeb) { responsiveContainerKey = GlobalKey(); richTextWebController = getBinding(tag: composerId); restoreEmailInlineImagesInteractor = getBinding(tag: composerId); menuMoreOptionController = CustomPopupMenuController(); } else { richTextMobileTabletController = getBinding(tag: composerId); } createFocusNodeInput(); scrollControllerEmailAddress.addListener(_scrollControllerEmailAddressListener); _listenStreamEvent(); _beforeReconnectManager.addListener(onBeforeReconnect); _injectBinding(); onKeyboardShortcutInit(); } @override void onReady() { if (PlatformInfo.isWeb) { _triggerBrowserEventListener(); } setupComposer(); if (PlatformInfo.isMobile) { Future.delayed(const Duration(milliseconds: 500), _checkContactPermission); } super.onReady(); } @override void onClose() { _textEditorWeb = null; savedActionType = null; _savedEmailDraftHash = null; currentEmailActionType = null; emailIdEditing = null; maxWithEditor = null; initialAttachments.clear(); dispatchState(Right(UIClosedState())); composerArguments.value = null; emailContentsViewState.value = Right(UIClosedState()); identitySelected.value = null; listFromIdentities.clear(); _subscriptionOnDragEnter?.cancel(); _subscriptionOnDragOver?.cancel(); _subscriptionOnDragLeave?.cancel(); _subscriptionOnDrop?.cancel(); _subscriptionOnBlur?.cancel(); subjectEmailInputFocusNode?.removeListener(_subjectEmailInputFocusListener); _composerCacheListener?.cancel(); _beforeReconnectManager.removeListener(onBeforeReconnect); if (PlatformInfo.isWeb) { richTextWebController = null; responsiveContainerKey = null; menuMoreOptionController?.dispose(); menuMoreOptionController = null; } else { richTextMobileTabletController = null; } onKeyboardShortcutDispose(); super.onClose(); } @override void dispose() { subjectEmailInputFocusNode?.dispose(); subjectEmailInputFocusNode = null; toAddressFocusNode?.dispose(); toAddressFocusNode = null; ccAddressFocusNode?.dispose(); ccAddressFocusNode = null; bccAddressFocusNode?.dispose(); bccAddressFocusNode = null; replyToAddressFocusNode?.dispose(); replyToAddressFocusNode = null; toAddressFocusNodeKeyboard?.dispose(); toAddressFocusNodeKeyboard = null; ccAddressFocusNodeKeyboard?.dispose(); ccAddressFocusNodeKeyboard = null; bccAddressFocusNodeKeyboard?.dispose(); bccAddressFocusNodeKeyboard = null; replyToAddressFocusNodeKeyboard?.dispose(); replyToAddressFocusNodeKeyboard = null; searchIdentitiesFocusNode?.dispose(); searchIdentitiesFocusNode = null; subjectEmailInputController.dispose(); toEmailAddressController.dispose(); ccEmailAddressController.dispose(); bccEmailAddressController.dispose(); replyToEmailAddressController.dispose(); uploadInlineImageWorker.dispose(); scrollController.dispose(); scrollControllerEmailAddress.removeListener(_scrollControllerEmailAddressListener); scrollControllerEmailAddress.dispose(); scrollControllerAttachment.dispose(); scrollControllerIdentities.dispose(); super.dispose(); } @override void handleSuccessViewState(Success success) { if (success is LocalFilePickerSuccess) { _handlePickFileSuccess(success); } else if (success is LocalImagePickerSuccess) { _handlePickImageSuccess(success); } else if (success is GetAllIdentitiesSuccess) { _handleGetAllIdentitiesSuccess(success); } else if (success is DownloadImageAsBase64Success) { final inlineImage = InlineImage(fileInfo: success.fileInfo, base64Uri: success.base64Uri); if (PlatformInfo.isWeb) { richTextWebController?.insertImage(inlineImage); } else { richTextMobileTabletController?.insertImage(inlineImage); } maxWithEditor = null; } else { super.handleSuccessViewState(success); } } @override void handleFailureViewState(Failure failure) { if (failure is LocalFilePickerFailure) { _handlePickFileFailure(failure); } else if (failure is LocalImagePickerFailure) { _handlePickImageFailure(failure); } else { super.handleFailureViewState(failure); } } @override Future onUnloadBrowserListener(html.Event event) async { final username = mailboxDashBoardController.sessionCurrent?.username; final accountId = mailboxDashBoardController.accountId.value; if (composerId != null && username != null && accountId != null) { await _removeComposerCacheByIdOnWebInteractor.execute( accountId, username, composerId!, ); } await _saveComposerCacheOnWebAction(); } void _listenStreamEvent() { uploadInlineImageWorker = ever(uploadController.uploadInlineViewState, (state) { log('ComposerController::_listenStreamEvent()::uploadInlineImageWorker: $state'); state.fold((failure) => null, (success) { if (success is SuccessAttachmentUploadState) { _handleUploadInlineSuccess(success); } }); }); } void _triggerBrowserEventListener() { _subscriptionOnDragEnter = html.window.onDragEnter.listen((event) { event.preventDefault(); if (event.dataTransfer.types.validateFilesTransfer) { mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active; } }); _subscriptionOnDragOver = html.window.onDragOver.listen((event) { event.preventDefault(); if (event.dataTransfer.types.validateFilesTransfer) { mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active; } }); _subscriptionOnDragLeave = html.window.onDragLeave.listen((event) { event.preventDefault(); if (event.dataTransfer.types.validateFilesTransfer) { mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive; } }); _subscriptionOnDrop = html.window.onDrop.listen((event) { event.preventDefault(); if (event.dataTransfer.types.validateFilesTransfer) { mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive; } }); // https://github.com/flutter/flutter/issues/155265#issuecomment-2417101524 _subscriptionOnBlur = html.window.onBlur.listen((event) { html.document.activeElement?.blur(); }); } Future _saveComposerCacheOnWebAction() async { autoCreateEmailTag(); final createEmailRequest = await _generateCreateEmailRequestToSaveAsCache(); if (createEmailRequest == null) return; await _saveComposerCacheOnWebInteractor.execute( createEmailRequest, mailboxDashBoardController.accountId.value!, mailboxDashBoardController.sessionCurrent!.username); } Uri? _getUploadUriFromSession(Session session, AccountId accountId) { try { return session.getUploadUri(accountId, jmapUrl: dynamicUrlInterceptors.jmapUrl); } catch (e) { logWarning('ComposerController::_getUploadUriFromSession:Exception = $e'); return null; } } Future _generateCreateEmailRequestToSaveAsCache() async { final arguments = composerArguments.value; final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; if (arguments == null || session == null || accountId == null) { log('ComposerController::_generateCreateEmailRequest: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); return null; } String emailContent = await getContentInEditor(); if (currentEmailActionType == EmailActionType.compose) { emailContent = await _composerRepository.removeCollapsedExpandedSignatureEffect( emailContent: emailContent, ); } final uploadUri = _getUploadUriFromSession(session, accountId); final composerIndex = composerId != null ? mailboxDashBoardController.composerManager.getComposerIndex(composerId!) : null; return CreateEmailRequest( session: session, accountId: accountId, emailActionType: arguments.emailActionType, ownEmailAddress: ownEmailAddress, subject: subjectEmail.value ?? '', emailContent: emailContent, fromSender: arguments.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, outboxMailboxId: getOutboxMailboxIdForComposer(), sentMailboxId: getSentMailboxIdForComposer(), draftsMailboxId: getDraftMailboxIdForComposer(), draftsEmailId: getDraftEmailId(), answerForwardEmailId: arguments.presentationEmail?.id, unsubscribeEmailId: arguments.previousEmailId, messageId: arguments.messageId, references: arguments.references, emailSendingQueue: arguments.sendingEmail, displayMode: screenDisplayMode.value, uploadUri: uploadUri, composerIndex: composerIndex, composerId: composerId, savedDraftHash: arguments.savedDraftHash ?? _savedEmailDraftHash, savedActionType: savedActionType ?? currentEmailActionType, savedEmailDraftId: emailIdEditing, templateEmailId: currentTemplateEmailId, ); } void _scrollControllerEmailAddressListener() { _closeSuggestionBox(); } void createFocusNodeInput() { toAddressFocusNode = FocusNode(); ccAddressFocusNode = FocusNode(); bccAddressFocusNode = FocusNode(); replyToAddressFocusNode = FocusNode(); searchIdentitiesFocusNode = FocusNode(); toAddressFocusNodeKeyboard = FocusNode(); ccAddressFocusNodeKeyboard = FocusNode(); bccAddressFocusNodeKeyboard = FocusNode(); replyToAddressFocusNodeKeyboard = FocusNode(); subjectEmailInputFocusNode = FocusNode( onKeyEvent: PlatformInfo.isWeb ? _subjectEmailInputOnKeyListener : null, ); subjectEmailInputFocusNode?.addListener(_subjectEmailInputFocusListener); } void _subjectEmailInputFocusListener() { if (subjectEmailInputFocusNode?.hasFocus == true) { if (PlatformInfo.isMobile && currentContext != null && !responsiveUtils.isScreenWithShortestSide(currentContext!)) { richTextMobileTabletController?.richTextController.hideRichTextView(); } autoCreateEmailTag(); triggerHideRecipientsFieldsWhenUnfocus(); } } KeyEventResult _subjectEmailInputOnKeyListener(FocusNode node, KeyEvent event) { if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) { subjectEmailInputFocusNode?.unfocus(); richTextWebController?.editorController.setFocus(); return KeyEventResult.handled; } return KeyEventResult.ignored; } void onCreatedMobileEditorAction(BuildContext context, HtmlEditorApi editorApi, String? content) { richTextMobileTabletController?.htmlEditorApi = editorApi; richTextMobileTabletController?.richTextController.onCreateHTMLEditor( editorApi, onEnterKeyDown: _onEnterKeyDown, onFocus: _onEditorFocusOnMobile, onChangeCursor: (coordinates) { _onChangeCursorOnMobile(coordinates, context); }, ); } Future onLoadCompletedMobileEditorAction(HtmlEditorApi editorApi, WebUri? url) async { _isEmailBodyLoaded = true; await setupSelectedIdentity(); _autoFocusFieldWhenLauncher(); } void _injectBinding() { injectAutoCompleteBindings( mailboxDashBoardController.sessionCurrent, mailboxDashBoardController.accountId.value, ); } Future setupComposer() async { _isEmailBodyLoaded = false; final arguments = PlatformInfo.isWeb ? composerArgs : Get.arguments; if (arguments is! ComposerArguments) return; composerArguments.value = arguments; currentEmailActionType = arguments.emailActionType; savedActionType = arguments.savedActionType; emailIdEditing = arguments.savedEmailDraftId; emailContentsViewState.value = Right(GetEmailContentLoading()); setupEmailSubject(arguments); setupEmailRecipients(arguments); setupEmailImportantFlag(arguments); setupEmailAttachments(arguments); setupEmailOtherComponents(arguments); setupEmailRequestReadReceiptFlag(arguments); setupEmailTemplateId(arguments); await setupListIdentities(arguments); await setupEmailContent(arguments); if (screenDisplayMode.value.isNotContentVisible() && currentContext != null && responsiveUtils.isWebDesktop(currentContext!)) { await setupSelectedIdentityWithoutApplySignature(); } if (PlatformInfo.isWeb) { richTextWebController?.updateFormattingOptions( mailboxDashBoardController.isTextFormattingMenuOpened.value, ); } } void initAttachmentsAndInlineImages({ List? attachments, List? inlineImages }) { if (attachments?.isNotEmpty == true) { initialAttachments = attachments!; uploadController.initializeUploadAttachments(attachments); } if (inlineImages?.isNotEmpty == true) { uploadController.initializeUploadInlineAttachments(inlineImages!); } } void _getAllIdentities() { log('ComposerController::_getAllIdentities: Fetch again identity !'); final accountId = mailboxDashBoardController.accountId.value; final session = mailboxDashBoardController.sessionCurrent; if (accountId != null && session != null) { consumeState(_getAllIdentitiesInteractor.execute(session, accountId)); } } void _handleGetAllIdentitiesSuccess(GetAllIdentitiesSuccess success) { final listIdentitiesMayDeleted = success.identities?.toListMayDeleted() ?? []; if (listIdentitiesMayDeleted.isNotEmpty) { listFromIdentities.value = listIdentitiesMayDeleted; } } void initEmailAddress({ required PresentationEmail presentationEmail, required EmailActionType actionType, String? listPost, }) { final senderEmailAddress = ownEmailAddress; final isSender = presentationEmail.from .asList() .any((element) => element.emailAddress.isNotEmpty && element.emailAddress == senderEmailAddress); final recipients = presentationEmail.generateRecipientsEmailAddressForComposer( emailActionType: actionType, isSender: isSender, userName: senderEmailAddress, listPost: listPost, ); listToEmailAddress = List.from(recipients.to); listCcEmailAddress = List.from(recipients.cc); listBccEmailAddress = List.from(recipients.bcc); listReplyToEmailAddress = List.from(recipients.replyTo); if (isRecipientsWithoutReplyToNotEmpty) { hideAllRecipientsFields(); isInitialRecipient.value = true; } else { toRecipientState.value = PrefixRecipientState.enabled; } updateStatusEmailSendButton(); } void updateListEmailAddress( PrefixEmailAddress prefixEmailAddress, List newListEmailAddress ) { switch(prefixEmailAddress) { case PrefixEmailAddress.to: listToEmailAddress = List.from(newListEmailAddress); break; case PrefixEmailAddress.cc: listCcEmailAddress = List.from(newListEmailAddress); break; case PrefixEmailAddress.bcc: listBccEmailAddress = List.from(newListEmailAddress); break; case PrefixEmailAddress.replyTo: listReplyToEmailAddress = List.from(newListEmailAddress); break; default: break; } updateStatusEmailSendButton(); } void updateStatusEmailSendButton() { if (listToEmailAddress.isNotEmpty || listCcEmailAddress.isNotEmpty || listBccEmailAddress.isNotEmpty) { isEnableEmailSendButton.value = true; } else { isEnableEmailSendButton.value = false; } } void handleClickSendButton(BuildContext context) async { if (_sendButtonState == ButtonState.disabled) { log('ComposerController::handleClickSendButton: SENDING EMAIL'); return; } _sendButtonState = ButtonState.disabled; clearFocus(); if (toEmailAddressController.text.isNotEmpty || ccEmailAddressController.text.isNotEmpty || bccEmailAddressController.text.isNotEmpty || replyToEmailAddressController.text.isNotEmpty) { autoCreateEmailTag(); } final appLocalizations = AppLocalizations.of(context); if (!isEnableEmailSendButton.value) { MessageDialogActionManager().showConfirmDialogAction(context, appLocalizations.message_dialog_send_email_without_recipient, appLocalizations.add_recipients, title: appLocalizations.sending_failed, hasCancelButton: false, showAsBottomSheet: true, dialogMargin: MediaQuery.paddingOf(context).add(const EdgeInsets.only(bottom: 12)), ).whenComplete(() => _sendButtonState = ButtonState.enabled); return; } if (existEmailAddressInvalid) { MessageDialogActionManager().showConfirmDialogAction(context, appLocalizations.message_dialog_send_email_with_email_address_invalid, appLocalizations.fix_email_addresses, onConfirmAction: showFullRecipients, showAsBottomSheet: true, title: appLocalizations.sending_failed, hasCancelButton: false, dialogMargin: MediaQuery.paddingOf(context).add(const EdgeInsets.only(bottom: 12)), ).whenComplete(() => _sendButtonState = ButtonState.enabled); return; } if (subjectEmail.value == null || subjectEmail.isEmpty == true) { MessageDialogActionManager().showConfirmDialogAction(context, appLocalizations.message_dialog_send_email_without_a_subject, appLocalizations.send_anyway, cancelTitle: appLocalizations.cancel, onConfirmAction: () => _prepareToSendMessages(context), onCancelAction: popBack, autoPerformPopBack: false, title: appLocalizations.empty_subject, showAsBottomSheet: true, dialogMargin: MediaQuery.paddingOf(context).add(const EdgeInsets.only(bottom: 12)), ).whenComplete(() => _sendButtonState = ButtonState.enabled); return; } if (!uploadController.allUploadAttachmentsCompleted) { MessageDialogActionManager().showConfirmDialogAction( context, appLocalizations.messageDialogSendEmailUploadingAttachment, appLocalizations.got_it, title: appLocalizations.sending_failed, showAsBottomSheet: true, hasCancelButton: false, dialogMargin: MediaQuery.paddingOf(context).add(const EdgeInsets.only(bottom: 12)), ).whenComplete(() => _sendButtonState = ButtonState.enabled); return; } if (uploadController.isExceededMaxSizeAttachmentsPerEmail()) { MessageDialogActionManager().showConfirmDialogAction( context, appLocalizations.message_dialog_send_email_exceeds_maximum_size( filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0), ), appLocalizations.got_it, title: appLocalizations.sending_failed, hasCancelButton: false ).whenComplete(() => _sendButtonState = ButtonState.enabled); return; } _prepareToSendMessages(context); } Future getContentInEditor() async { try { final htmlTextEditor = PlatformInfo.isWeb ? _textEditorWeb : await htmlEditorApi?.getText(); return htmlTextEditor?.isNotEmpty == true ? htmlTextEditor!.removeEditorStartTag() : ''; } catch (e) { logWarning('ComposerController::getContentInEditor:Exception = $e'); return ''; } } Future _prepareToSendMessages(BuildContext context) async { final arguments = composerArguments.value; final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; if (arguments == null || session == null || accountId == null) { log('ComposerController::_handleSendMessages: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); _sendButtonState = ButtonState.enabled; _closeComposerAction(closeOverlays: true); return; } if (Get.isDialogOpen == true || Get.isBottomSheetOpen == true) { popBack(); } final emailContent = await getContentInEditor(); if (!context.mounted) { _sendButtonState = ButtonState.enabled; return; } final attachmentKeywords = await validateAttachmentReminder( emailSubject: subjectEmail.value ?? '', emailContent: emailContent, ); if (!context.mounted) { logWarning('ComposerController::_prepareToSendMessages: CONTEXT IS NOT MOUNTED'); return; } if (attachmentKeywords.isNotEmpty && uploadController.attachmentsUploaded.isEmpty) { showAttachmentReminderModal( context: context, keywords: attachmentKeywords, onConfirmAction: () { _sendMessageToServer( context: context, session: session, accountId: accountId, arguments: arguments, emailContent: emailContent, ); }, onCancelAction: () { _sendButtonState = ButtonState.enabled; } ).whenComplete(() => _sendButtonState = ButtonState.enabled); return; } _sendMessageToServer( context: context, session: session, accountId: accountId, arguments: arguments, emailContent: emailContent, ); } Future _sendMessageToServer({ required BuildContext context, required Session session, required AccountId accountId, required ComposerArguments arguments, required String emailContent, }) async { final uploadUri = _getUploadUriFromSession(session, accountId); final cancelToken = CancelToken(); final resultState = await _showSendingMessageDialog( session: session, accountId: accountId, arguments: arguments, emailContent: emailContent, uploadUri: uploadUri, cancelToken: cancelToken, ); if (resultState is SendEmailSuccess || mailboxDashBoardController .validateSendingEmailFailedWhenNetworkIsLostOnMobile(resultState)) { _sendButtonState = ButtonState.enabled; _closeComposerAction(result: resultState); } else if (resultState is SendEmailFailure && resultState.exception is SendingEmailCanceledException) { _sendButtonState = ButtonState.enabled; } else if (resultState is SendEmailFailure || resultState is GenerateEmailFailure) { if (resultState.exception is BadCredentialsException) { _sendButtonState = ButtonState.enabled; handleBadCredentialsException(); } else if (context.mounted) { await _showConfirmDialogWhenSendMessageFailure( context: context, failure: resultState, ); } else { _sendButtonState = ButtonState.enabled; } } else { _sendButtonState = ButtonState.enabled; } } Future _showSendingMessageDialog({ required Session session, required AccountId accountId, required ComposerArguments arguments, required String emailContent, required Uri? uploadUri, CancelToken? cancelToken, }) { final childWidget = PointerInterceptor( child: SendingMessageDialogView( createEmailRequest: CreateEmailRequest( session: session, accountId: accountId, emailActionType: arguments.emailActionType, ownEmailAddress: ownEmailAddress, subject: subjectEmail.value ?? '', emailContent: emailContent, fromSender: arguments.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, outboxMailboxId: getOutboxMailboxIdForComposer(), sentMailboxId: getSentMailboxIdForComposer(), draftsEmailId: getDraftEmailId(), answerForwardEmailId: arguments.presentationEmail?.id, unsubscribeEmailId: arguments.previousEmailId, messageId: arguments.messageId, references: arguments.references, emailSendingQueue: arguments.sendingEmail, displayMode: screenDisplayMode.value, uploadUri: uploadUri, ), createNewAndSendEmailInteractor: _createNewAndSendEmailInteractor, onCancelSendingEmailAction: _handleCancelSendingMessage, cancelToken: cancelToken, ), ); return Get.dialog( PlatformInfo.isMobile ? PopScope(canPop: false, child: childWidget) : childWidget, barrierDismissible: false, barrierColor: AppColor.colorDefaultCupertinoActionSheet, ); } void _handleCancelSendingMessage({CancelToken? cancelToken}) { cancelToken?.cancel([SendingEmailCanceledException()]); } Future _showConfirmDialogWhenSendMessageFailure({ required BuildContext context, required FeatureFailure failure }) async { final messageRecord = getMessageFailure( appLocalizations: AppLocalizations.of(context), exception: failure.exception, ); final isIncreaseMySpaceIsDisabled = !mailboxDashBoardController.validatePremiumIsAvailable() || mailboxDashBoardController.validateUserHasIsAlreadyHighestSubscription(); final needIncreaseMySpace = !isIncreaseMySpaceIsDisabled && messageRecord.errorType == SetError.overQuota; await MessageDialogActionManager().showConfirmDialogAction( context, title: '', messageRecord.message, needIncreaseMySpace ? AppLocalizations.of(context).increaseYourSpace : AppLocalizations.of(context).edit, cancelTitle: needIncreaseMySpace ? AppLocalizations.of(context).edit : AppLocalizations.of(context).closeAnyway, alignCenter: true, outsideDismissible: false, autoPerformPopBack: false, onConfirmAction: () { _sendButtonState = ButtonState.enabled; popBack(); if (needIncreaseMySpace) { mailboxDashBoardController.paywallController?.navigateToPaywall(); } else { _autoFocusFieldWhenLauncher(); } }, onCancelAction: () { _sendButtonState = ButtonState.enabled; if (needIncreaseMySpace) { popBack(); _autoFocusFieldWhenLauncher(); } else { _closeComposerAction(closeOverlays: true); } }, ).whenComplete(() => _sendButtonState = ButtonState.enabled); } void _checkContactPermission() async { final permissionStatus = await Permission.contacts.status; if (permissionStatus.isGranted) { _contactSuggestionSource = ContactSuggestionSource.all; } else if (!permissionStatus.isPermanentlyDenied) { final requestedPermission = await Permission.contacts.request(); _contactSuggestionSource = requestedPermission == PermissionStatus.granted ? ContactSuggestionSource.all : _contactSuggestionSource; } } Future> getAutoCompleteSuggestion(String queryString, {int? limit}) async { log('ComposerController::getAutoCompleteSuggestion():queryString = $queryString | limit = $limit | $_contactSuggestionSource'); _getAllAutoCompleteInteractor = getBinding(); _getAutoCompleteInteractor = getBinding(); _getDeviceContactSuggestionsInteractor = getBinding(); final autoCompletePattern = AutoCompletePattern( word: queryString, limit: limit, accountId: mailboxDashBoardController.accountId.value); if (_contactSuggestionSource == ContactSuggestionSource.all) { if (_getAllAutoCompleteInteractor != null) { return await _getAllAutoCompleteInteractor! .execute(autoCompletePattern) .then( (value) => handleAutoCompleteResultState( resultState: value, queryString: queryString, onFailureCallback: (failure) { logWarning('ComposerController::getAutoCompleteSuggestion:onFailureCallback: $failure'); consumeState(Stream.value(Left(failure))); }, ), onError: (error) { logWarning('ComposerController::getAutoCompleteSuggestion:onError: $error'); consumeState(Stream.value(Left(error))); }, ); } else if (_getDeviceContactSuggestionsInteractor != null) { return await _getDeviceContactSuggestionsInteractor! .execute(autoCompletePattern) .then((value) => handleAutoCompleteResultState( resultState: value, queryString: queryString, ) ); } else { return []; } } else { return await _getAutoCompleteInteractor ?.execute(autoCompletePattern) .then( (value) => handleAutoCompleteResultState( resultState: value, queryString: queryString, onFailureCallback: (failure) { logWarning('ComposerController::getAutoCompleteSuggestion:onFailureCallback: $failure'); consumeState(Stream.value(Left(failure))); }, ), onError: (error) { logWarning('ComposerController::getAutoCompleteSuggestion:onError: $error'); consumeState(Stream.value(Left(error))); }, ) ?? []; } } void openPickAttachmentMenu(BuildContext context, List actionTiles) { clearFocus(); (ContextMenuBuilder(context) ..addHeader((ContextMenuHeaderBuilder(const Key('attachment_picker_context_menu_header_builder')) ..addLabel(AppLocalizations.of(context).pick_attachments)) .build()) ..addTiles(actionTiles) ..addOnCloseContextMenuAction(() => popBack())) .build(); } void openFilePickerByType(BuildContext context, FileType fileType) async { if (!kIsWeb) { popBack(); } consumeState(_localFilePickerInteractor.execute(fileType: fileType)); } void _handlePickFileFailure(LocalFilePickerFailure failure) { if (currentOverlayContext != null && currentContext != null && failure.exception is! PickFileCanceledException) { appToast.showToastErrorMessage( currentOverlayContext!, AppLocalizations.of(currentContext!).thisFileCannotBePicked); } } void _handlePickImageFailure(LocalImagePickerFailure failure) { if (currentOverlayContext != null && currentContext != null && failure.exception is! PickFileCanceledException) { appToast.showToastErrorMessage( currentOverlayContext!, AppLocalizations.of(currentContext!).cannotSelectThisImage); } } void _handlePickFileSuccess(LocalFilePickerSuccess success) { uploadController.validateTotalSizeAttachmentsBeforeUpload( totalSizePreparedFiles: success.pickedFiles.totalSize, onValidationSuccess: () => uploadAttachmentsAction(pickedFiles: success.pickedFiles) ); } void _handlePickImageSuccess(LocalImagePickerSuccess success) { uploadController.validateTotalSizeInlineAttachmentsBeforeUpload( totalSizePreparedFiles: success.fileInfo.fileSize, onValidationSuccess: () => uploadAttachmentsAction(pickedFiles: [success.fileInfo.withInline()]) ); } void uploadAttachmentsAction({required List pickedFiles}) { final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; if (session != null && accountId != null) { try { final uploadUri = session.getUploadUri(accountId, jmapUrl: dynamicUrlInterceptors.jmapUrl); uploadController.justUploadAttachmentsAction( uploadFiles: pickedFiles, uploadUri: uploadUri, ); } catch (e) { logWarning('ComposerController::uploadAttachmentsAction: $e'); uploadController.consumeState(Stream.value(Left(UploadAttachmentFailure(e, pickedFiles[0])))); } } else { logWarning('ComposerController::uploadAttachmentsAction: SESSION OR ACCOUNT_ID is NULL'); } } void deleteAttachmentUploaded(UploadTaskId uploadId) { uploadController.deleteFileUploaded(uploadId); } Future _validateEmailChange() async { final newDraftHash = await _hashComposingEmail(); return _savedEmailDraftHash != newDraftHash; } Future _hashComposingEmail() async { String emailContent = await getContentInEditor(); emailContent = await _composerRepository.removeCollapsedExpandedSignatureEffect( emailContent: emailContent, ); if (emailIdEditing != null && savedActionType == EmailActionType.compose && currentEmailActionType == EmailActionType.reopenComposerBrowser) { emailContent = await _composerRepository.removeStyleLazyLoadDisplayInlineImages( emailContent: emailContent, ); } final savedEmailDraft = SavedComposingEmail( subject: subjectEmail.value ?? '', content: emailContent, toRecipients: listToEmailAddress.toSet(), ccRecipients: listCcEmailAddress.toSet(), bccRecipients: listBccEmailAddress.toSet(), replyToRecipients: listReplyToEmailAddress.toSet(), identity: identitySelected.value, attachments: uploadController.attachmentsUploaded, hasReadReceipt: hasRequestReadReceipt.value, isMarkAsImportant: isMarkAsImportant.value, ); final draftAsString = savedEmailDraft.asString(); final draftAsHasCode = draftAsString.hashCode; log('ComposerController::_hashDraftEmail:draftAsString = $draftAsString | draftAsHasCode = $draftAsHasCode'); return draftAsHasCode; } Future _updateSavedEmailDraftHash() async { _savedEmailDraftHash = await _hashComposingEmail(); } Future initEmailDraftHash() async { final currentDraftHash = await _hashComposingEmail(); final oldSavedDraftHash = composerArguments.value?.savedDraftHash; if (currentEmailActionType == EmailActionType.compose || currentEmailActionType == EmailActionType.editDraft) { _savedEmailDraftHash = currentDraftHash; } else if (currentEmailActionType == EmailActionType.reopenComposerBrowser) { _savedEmailDraftHash = oldSavedDraftHash; } log('ComposerController::initEmailDraftHash:oldSavedDraftHash = $oldSavedDraftHash | currentDraftHash = $currentDraftHash | _savedEmailDraftHash = $_savedEmailDraftHash'); isEmailChanged.value = currentDraftHash != _savedEmailDraftHash; } void handleClickSaveAsDraftsButton(BuildContext context) async { if (_saveToDraftButtonState == ButtonState.disabled) { log('ComposerController::handleClickSaveAsDraftsButton: Saving to draft'); return; } _saveToDraftButtonState = ButtonState.disabled; final arguments = composerArguments.value; final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; if (arguments == null || session == null || accountId == null || getDraftMailboxIdForComposer() == null ) { log('ComposerController::handleClickSaveAsDraftsButton: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); _saveToDraftButtonState = ButtonState.enabled; return; } final emailContent = await getContentInEditor(); final uploadUri = _getUploadUriFromSession(session, accountId); final cancelToken = CancelToken(); final resultState = await _showSavingMessageToDraftsDialog( session: session, accountId: accountId, arguments: arguments, emailContent: emailContent, uploadUri: uploadUri, draftEmailId: emailIdEditing, cancelToken: cancelToken ); if (resultState is SaveEmailAsDraftsSuccess) { _saveToDraftButtonState = ButtonState.enabled; emailIdEditing = resultState.emailId; mailboxDashBoardController.consumeState(Stream.value(Right(resultState))); _updateSavedEmailDraftHash(); } else if (resultState is UpdateEmailDraftsSuccess) { _saveToDraftButtonState = ButtonState.enabled; emailIdEditing = resultState.emailId; mailboxDashBoardController.consumeState(Stream.value(Right(resultState))); _updateSavedEmailDraftHash(); } else if ((resultState is SaveEmailAsDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException) || (resultState is UpdateEmailDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException)) { _saveToDraftButtonState = ButtonState.enabled; } else if (resultState is SaveEmailAsDraftsFailure || resultState is UpdateEmailDraftsFailure || resultState is GenerateEmailFailure ) { if (resultState.exception is BadCredentialsException) { _saveToDraftButtonState = ButtonState.enabled; handleBadCredentialsException(); } else if (context.mounted) { await _showConfirmDialogWhenSaveMessageToDraftsFailure( context: context, failure: resultState, onConfirmAction: (needIncreaseMySpace) { _saveToDraftButtonState = ButtonState.enabled; popBack(); if (needIncreaseMySpace) { mailboxDashBoardController.paywallController?.navigateToPaywall(); } else { _autoFocusFieldWhenLauncher(); } }, onCancelAction: (needIncreaseMySpace) { _saveToDraftButtonState = ButtonState.enabled; if (needIncreaseMySpace) { popBack(); _autoFocusFieldWhenLauncher(); } else { _closeComposerAction(closeOverlays: true); } }, ); } else { _saveToDraftButtonState = ButtonState.enabled; } } else { _saveToDraftButtonState = ButtonState.enabled; } } 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() { if (PlatformInfo.isMobile) { htmlEditorApi?.unfocus(); FocusManager.instance.primaryFocus?.unfocus(); } else { toAddressFocusNode?.unfocus(); ccAddressFocusNode?.unfocus(); bccAddressFocusNode?.unfocus(); replyToAddressFocusNode?.unfocus(); } } void clickOutsideComposer() { clearFocus(); triggerHideRecipientsFieldsWhenUnfocus(); if (PlatformInfo.isWeb) { refocusKeyboardShortcutFocus(); } } void _closeComposerAction({dynamic result, bool closeOverlays = false}) { if (PlatformInfo.isWeb && richTextWebController != null) { mailboxDashBoardController.updateTextFormattingMenuState( richTextWebController!.isFormattingOptionsEnabled, ); } mailboxDashBoardController.closeComposer( result: result, closeOverlays: closeOverlays, composerId: composerId, ); } void displayScreenTypeComposerAction(ScreenDisplayMode displayMode) async { if (screenDisplayMode.value.isNotContentVisible()) { _isEmailBodyLoaded = false; } if (richTextWebController != null && !screenDisplayMode.value.isNotContentVisible()) { final textCurrent = await richTextWebController!.editorController.getText(); richTextWebController!.editorController.setText(textCurrent); } screenDisplayMode.value = displayMode; updateDisplayModeForComposerQueue(displayMode); await Future.delayed( const Duration(milliseconds: 300), _autoFocusFieldWhenLauncher); } void addEmailAddressType(PrefixEmailAddress prefixEmailAddress) { switch(prefixEmailAddress) { case PrefixEmailAddress.from: fromRecipientState.value = PrefixRecipientState.enabled; break; case PrefixEmailAddress.to: toRecipientState.value = PrefixRecipientState.enabled; break; case PrefixEmailAddress.cc: ccRecipientState.value = PrefixRecipientState.enabled; break; case PrefixEmailAddress.bcc: bccRecipientState.value = PrefixRecipientState.enabled; break; case PrefixEmailAddress.replyTo: replyToRecipientState.value = PrefixRecipientState.enabled; break; } updatePrefixRootState(); } void deleteEmailAddressType(PrefixEmailAddress prefixEmailAddress) { updateListEmailAddress(prefixEmailAddress, []); switch(prefixEmailAddress) { case PrefixEmailAddress.to: toRecipientState.value = PrefixRecipientState.disabled; toAddressFocusNode = FocusNode(); toEmailAddressController.clear(); break; case PrefixEmailAddress.cc: ccRecipientState.value = PrefixRecipientState.disabled; ccAddressFocusNode = FocusNode(); ccEmailAddressController.clear(); break; case PrefixEmailAddress.bcc: bccRecipientState.value = PrefixRecipientState.disabled; bccAddressFocusNode = FocusNode(); bccEmailAddressController.clear(); break; case PrefixEmailAddress.replyTo: replyToRecipientState.value = PrefixRecipientState.disabled; replyToAddressFocusNode = FocusNode(); replyToEmailAddressController.clear(); break; default: break; } updatePrefixRootState(); } void clearFocusRecipients() { toAddressFocusNode?.unfocus(); ccAddressFocusNode?.unfocus(); bccAddressFocusNode?.unfocus(); replyToAddressFocusNode?.unfocus(); } void clearFocusSubject() { subjectEmailInputFocusNode?.unfocus(); } void _closeSuggestionBox() { if (toEmailAddressController.text.isEmpty) { keyToEmailTagEditor.currentState?.closeSuggestionBox(); } if (ccEmailAddressController.text.isEmpty) { keyCcEmailTagEditor.currentState?.closeSuggestionBox(); } if (bccEmailAddressController.text.isEmpty) { keyBccEmailTagEditor.currentState?.closeSuggestionBox(); } if (replyToEmailAddressController.text.isEmpty) { keyReplyToEmailTagEditor.currentState?.closeSuggestionBox(); } } void onEmailAddressFocusChange(PrefixEmailAddress prefixEmailAddress, bool isFocus) { if (isFocus) { _closeSuggestionBox(); if (PlatformInfo.isMobile && currentContext != null && !responsiveUtils.isScreenWithShortestSide(currentContext!)) { richTextMobileTabletController?.richTextController.hideRichTextView(); } } else { switch(prefixEmailAddress) { case PrefixEmailAddress.to: final inputToEmail = toEmailAddressController.text; if (inputToEmail.trim().isNotEmpty) { autoCreateEmailTagForType(PrefixEmailAddress.to, inputToEmail); } break; case PrefixEmailAddress.cc: final inputCcEmail = ccEmailAddressController.text; if (inputCcEmail.trim().isNotEmpty) { autoCreateEmailTagForType(PrefixEmailAddress.cc, inputCcEmail); } break; case PrefixEmailAddress.bcc: final inputBccEmail = bccEmailAddressController.text; if (inputBccEmail.trim().isNotEmpty) { autoCreateEmailTagForType(PrefixEmailAddress.bcc, inputBccEmail); } break; case PrefixEmailAddress.replyTo: final inputReplyToEmail = replyToEmailAddressController.text; if (inputReplyToEmail.trim().isNotEmpty) { autoCreateEmailTagForType(PrefixEmailAddress.replyTo, inputReplyToEmail); } break; default: break; } } } Future selectIdentity(Identity? newIdentity) async { final formerIdentity = identitySelected.value; identitySelected.value = newIdentity; if (newIdentity == null) return; await _applyIdentityForAllFieldComposer(formerIdentity, newIdentity); } Future _applyIdentityForAllFieldComposer( Identity? formerIdentity, Identity newIdentity ) async { if (formerIdentity != null) { if (formerIdentity.bcc?.isNotEmpty == true) { _removeBccEmailAddressFromFormerIdentity(formerIdentity.bcc!); } await _removeSignature(); } if (newIdentity.bcc?.isNotEmpty == true) { _applyBccEmailAddressFromIdentity(newIdentity.bcc!); } if (newIdentity.signatureAsString.isNotEmpty == true) { await applySignature(newIdentity.signatureAsString.asSignatureHtml()); } if (PlatformInfo.isMobile) { await htmlEditorApi?.onDocumentChanged(); } } void _applyBccEmailAddressFromIdentity(Set listEmailAddress) { if (bccRecipientState.value == PrefixRecipientState.disabled) { bccRecipientState.value = PrefixRecipientState.enabled; } if (composerArguments.value?.emailActionType == EmailActionType.composeFromMailtoUri) { listBccEmailAddress = {...listEmailAddress, ...?composerArguments.value?.bcc}.toList(); } else { listBccEmailAddress = listEmailAddress.toList(); } updateStatusEmailSendButton(); } void _removeBccEmailAddressFromFormerIdentity(Set listEmailAddress) { listBccEmailAddress = listBccEmailAddress .where((address) => !listEmailAddress.contains(address)) .toList(); if (listBccEmailAddress.isEmpty) { bccRecipientState.value = PrefixRecipientState.disabled; } updateStatusEmailSendButton(); } Future applySignature(String signature) async { if (PlatformInfo.isWeb) { richTextWebController?.editorController.insertSignature( signature, allowCollapsed: false, ); } else { await htmlEditorApi?.insertSignature(signature, allowCollapsed: false); } } Future _removeSignature() async { if (PlatformInfo.isWeb) { richTextWebController?.editorController.removeSignature(); } else { await htmlEditorApi?.removeSignature(); } } void insertImage(BuildContext context, double maxWith) { clearFocus(); if (responsiveUtils.isMobile(context)) { maxWithEditor = maxWith - 40; } else { maxWithEditor = maxWith - 70; } consumeState(_localImagePickerInteractor.execute()); } void _handleUploadInlineSuccess(SuccessAttachmentUploadState uploadState) { uploadController.clearUploadInlineViewState(); String? baseDownloadUrl; try { baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl); } catch (e) { logWarning('ComposerController::_handleUploadInlineSuccess(): $e'); } final accountId = mailboxDashBoardController.accountId.value; if (baseDownloadUrl != null && accountId != null) { final imageUrl = uploadState.attachment.getDownloadUrl(baseDownloadUrl, accountId); log('ComposerController::_handleUploadInlineSuccess(): imageUrl: $imageUrl'); consumeState(_downloadImageAsBase64Interactor.execute( imageUrl, uploadState.attachment.cid!, uploadState.fileInfo, maxWidth: maxWithEditor, )); } else { log('ComposerController::_handleUploadInlineFailure(): baseDownloadUrl: $baseDownloadUrl, accountId: $accountId'); consumeState(Stream.value(Left(DownloadImageAsBase64Failure(e)))); } } void handleClickDeleteComposer() { clearFocus(); _closeComposerAction(); } Future _onEditorFocusOnMobile() async { if (PlatformInfo.isAndroid) { if (FocusManager.instance.primaryFocus?.hasFocus == true) { FocusManager.instance.primaryFocus?.unfocus(); } await Future.delayed( const Duration(milliseconds: 300), richTextMobileTabletController?.richTextController.showDeviceKeyboard); } autoCreateEmailTag(); triggerHideRecipientsFieldsWhenUnfocus(); } void _onChangeCursorOnMobile(List? coordinates, BuildContext context) { final headerEditorMobileWidgetRenderObject = headerEditorMobileWidgetKey.currentContext?.findRenderObject(); if (headerEditorMobileWidgetRenderObject is RenderBox?) { final headerEditorMobileSize = headerEditorMobileWidgetRenderObject?.size; if (coordinates?[1] != null && coordinates?[1] != 0) { final coordinateY = max((coordinates?[1] ?? 0) - defaultPaddingCoordinateYCursorEditor, 0); final realCoordinateY = coordinateY + (headerEditorMobileSize?.height ?? 0); final outsideHeight = Get.height - MediaQuery.viewInsetsOf(context).bottom - ComposerStyle.keyboardToolBarHeight; final webViewEditorClientY = max(outsideHeight, 0) + scrollController.position.pixels; if (scrollController.position.pixels >= realCoordinateY) { _scrollToCursorEditor( realCoordinateY.toDouble(), headerEditorMobileSize?.height ?? 0, context, ); } else if ((realCoordinateY) >= webViewEditorClientY) { _scrollToCursorEditor( realCoordinateY.toDouble(), headerEditorMobileSize?.height ?? 0, context, ); } } } } void _scrollToCursorEditor( double realCoordinateY, double headerEditorMobileHeight, BuildContext context, ) { final scrollTarget = realCoordinateY - (responsiveUtils.isLandscapeMobile(context) ? 0 : headerEditorMobileHeight / 2); final maxScrollExtend = scrollController.position.maxScrollExtent; scrollController.jumpTo(min(scrollTarget, maxScrollExtend)); } void _onEnterKeyDown() { if(scrollController.position.pixels < scrollController.position.maxScrollExtent) { scrollController.animateTo( scrollController.position.pixels + 20, duration: const Duration(milliseconds: 1), curve: Curves.linear, ); } } void toggleRequestReadReceipt(BuildContext context) { hasRequestReadReceipt.toggle(); appToast.showToastSuccessMessage( context, hasRequestReadReceipt.isTrue ? AppLocalizations.of(context).requestReadReceiptHasBeenEnabled : AppLocalizations.of(context).requestReadReceiptHasBeenDisabled); } Future _autoFocusFieldWhenLauncher() async { if (await _hasInputFieldFocused()) { log('ComposerController::_autoFocusFieldWhenLauncher: INPUT_FIELD_FOCUS = true'); return; } if (FocusManager.instance.primaryFocus?.hasFocus == true) { FocusManager.instance.primaryFocus?.unfocus(); } if (listToEmailAddress.isEmpty) { toAddressFocusNode?.requestFocus(); } else if (subjectEmailInputController.text.isEmpty) { subjectEmailInputFocusNode?.requestFocus(); } else if (PlatformInfo.isWeb) { richTextWebController?.editorController.setFocus(); } else if (PlatformInfo.isIOS) { await richTextMobileTabletController?.htmlEditorApi?.requestFocus(); } } Future _hasInputFieldFocused() async { if (PlatformInfo.isWeb) { return toAddressFocusNode?.hasFocus == true || ccAddressFocusNode?.hasFocus == true || bccAddressFocusNode?.hasFocus == true || replyToAddressFocusNode?.hasFocus == true || subjectEmailInputFocusNode?.hasFocus == true; } else if (PlatformInfo.isMobile) { final isEditorFocused = (await richTextMobileTabletController?.isEditorFocused) ?? false; return toAddressFocusNode?.hasFocus == true || ccAddressFocusNode?.hasFocus == true || bccAddressFocusNode?.hasFocus == true || replyToAddressFocusNode?.hasFocus == true || subjectEmailInputFocusNode?.hasFocus == true || isEditorFocused; } return false; } void handleInitHtmlEditorWeb(String initContent) { if (_isEmailBodyLoaded) return; _isEmailBodyLoaded = true; richTextWebController?.editorController.setFullScreen(); richTextWebController?.editorController.setOnDragDropEvent(); richTextWebController?.setEnableCodeView(); setTextEditorWeb(initContent); } Future onInitialContentLoadCompleteWeb(String? initContent) async { await restoreCollapsibleSignatureButton(initContent); await setupSelectedIdentity(); _autoFocusFieldWhenLauncher(); } void handleOnFocusHtmlEditorWeb() { clearFocusRecipients(); clearFocusSubject(); FocusManager.instance.primaryFocus?.unfocus(); if (richTextWebController?.codeViewEnabled != true) { richTextWebController?.editorController.setFocus(); } richTextWebController?.closeAllMenuPopup(); if (menuMoreOptionController?.menuIsShowing == true) { menuMoreOptionController?.hideMenu(); } if (mailboxDashBoardController.isPopupMenuOpened.isTrue) { popBack(); } autoCreateEmailTag(); triggerHideRecipientsFieldsWhenUnfocus(); } FocusNode? getNextFocusOfToEmailAddress() { if (ccRecipientState.value == PrefixRecipientState.enabled) { return ccAddressFocusNode; } else if (bccRecipientState.value == PrefixRecipientState.enabled) { return bccAddressFocusNode; } else if (replyToRecipientState.value == PrefixRecipientState.enabled) { return replyToAddressFocusNode; } else { return subjectEmailInputFocusNode; } } FocusNode? getNextFocusOfCcEmailAddress() { if (bccRecipientState.value == PrefixRecipientState.enabled) { return bccAddressFocusNode; } else if (replyToRecipientState.value == PrefixRecipientState.enabled) { return replyToAddressFocusNode; } else { return subjectEmailInputFocusNode; } } FocusNode? getNextFocusOfBccEmailAddress() { if (replyToRecipientState.value == PrefixRecipientState.enabled) { return replyToAddressFocusNode; } else { return subjectEmailInputFocusNode; } } void handleFocusNextAddressAction() { autoCreateEmailTag(); } bool get isNetworkConnectionAvailable => networkConnectionController.isNetworkConnectionAvailable(); String? get textEditorWeb => _textEditorWeb; void setTextEditorWeb(String value) => _textEditorWeb = value; HtmlEditorApi? get htmlEditorApi => richTextMobileTabletController?.htmlEditorApi; void onChangeTextEditorWeb(String? text) { _textEditorWeb = text; if (restoringSignatureButton || (currentEmailActionType == EmailActionType.compose && !synchronizeInitDraftHash)) { synchronizeInitEmailDraftHash(text); } } void setSubjectEmail(String subject) => subjectEmail.value = subject; void onAttachmentDropZoneListener(Attachment attachment) { log('ComposerController::onAttachmentDropZoneListener: attachment = $attachment'); uploadController.validateTotalSizeAttachmentsBeforeUpload( totalSizePreparedFiles: attachment.size?.value ?? 0, onValidationSuccess: () => uploadController.initializeUploadAttachments([attachment]) ); } Future onChangeIdentity(Identity? newIdentity) async { await selectIdentity(newIdentity); } void _searchIdentities(String searchText) { if (searchText.isEmpty) { _getAllIdentities(); } else { listFromIdentities.value = listFromIdentities .where((identity) => identity.name?.toLowerCase().contains(searchText.toLowerCase()) == true) .toList(); } } void openSelectIdentityBottomSheet(BuildContext context) { ( FromComposerBottomSheetBuilder( context, imagePaths, listFromIdentities, scrollControllerIdentities, searchIdentitiesInputController ) ..onCloseAction(() => popBack()) ..onChangeIdentityAction((identity) { onChangeIdentity(identity); popBack(); }) ..onTextSearchChangedAction((searchText) => _searchIdentities(searchText)) ).build(); } void handleClickCloseComposer(BuildContext context) async { log('ComposerController::handleClickCloseComposer:'); if (_closeComposerButtonState == ButtonState.disabled) { log('ComposerController::handleClickCloseComposer: _closeComposerButtonState = disabled'); return; } _closeComposerButtonState = ButtonState.disabled; autoCreateEmailTag(); if (_validateCloseComposerWithoutSave()) { log('ComposerController::handleClickCloseComposer: ARGUMENTS is NULL or EMAIL NOT LOADED'); _closeComposerButtonState = ButtonState.enabled; clearFocus(); _closeComposerAction(); return; } final isChanged = await _validateEmailChange(); if (isChanged && context.mounted) { clearFocus(); await _showConfirmDialogSaveMessage(context); return; } _closeComposerButtonState = ButtonState.enabled; clearFocus(); _closeComposerAction(); } bool _validateCloseComposerWithoutSave() { if (composerArguments.value == null) return true; if (PlatformInfo.isWeb && !_isEmailBodyLoaded && !screenDisplayMode.value.isNotContentVisible()) { return true; } if (PlatformInfo.isMobile && !_isEmailBodyLoaded) return true; return false; } Future _showConfirmDialogSaveMessage(BuildContext context) async { await MessageDialogActionManager().showConfirmDialogAction( context, title: AppLocalizations.of(context).saveMessage.capitalizeFirstEach, AppLocalizations.of(context).warningMessageWhenClickCloseComposer, AppLocalizations.of(context).save, cancelTitle: AppLocalizations.of(context).discardChanges, alignCenter: true, autoPerformPopBack: false, isArrangeActionButtonsVertical: true, isScrollContentEnabled: responsiveUtils.isLandscapeMobile(context), usePopScope: true, onConfirmAction: () => _handleSaveMessageToDraft(context), onCancelAction: () { _closeComposerButtonState = ButtonState.enabled; _closeComposerAction(closeOverlays: true); }, onCloseButtonAction: () { _closeComposerButtonState = ButtonState.enabled; popBack(); _autoFocusFieldWhenLauncher(); }, onPopInvoked: (didPop, _) { log('ComposerController::_showConfirmDialogSaveMessage: didPop = $didPop'); if (!didPop) { _closeComposerButtonState = ButtonState.enabled; popBack(); _autoFocusFieldWhenLauncher(); } }, ); _closeComposerButtonState = ButtonState.enabled; } void handleOnDragEnterHtmlEditorWeb(List? types) { if (types.validateFilesTransfer) { mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active; } } void handleOnDragOverHtmlEditorWeb(List? types) { if (types.validateFilesTransfer) { mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active; } } void onLocalFileDropZoneListener({ required BuildContext context, required DropDoneDetails details, required double maxWidth }) async { _setUpMaxWidthInlineImage(context: context, maxWidth: maxWidth); final listFileInfo = await onDragDone(context: context, details: details); if (listFileInfo.isEmpty && context.mounted) { appToast.showToastErrorMessage( context, AppLocalizations.of(context).can_not_upload_this_file_as_attachments ); return; } uploadController.validateTotalSizeAttachmentsBeforeUpload( totalSizePreparedFiles: listFileInfo.totalSize, totalSizePreparedFilesWithDispositionAttachment: listFileInfo.listAttachmentFiles.totalSize, onValidationSuccess: () => uploadAttachmentsAction(pickedFiles: listFileInfo) ); } void _handleSaveMessageToDraft(BuildContext context) async { final arguments = composerArguments.value; final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; if (arguments == null || session == null || accountId == null || getDraftMailboxIdForComposer() == null ) { log('ComposerController::_handleSaveMessageToDraft: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); _closeComposerButtonState = ButtonState.enabled; _closeComposerAction(closeOverlays: true); return; } popBack(); final emailContent = await getContentInEditor(); final uploadUri = _getUploadUriFromSession(session, accountId); final draftEmailId = getDraftEmailId(); log('ComposerController::_handleSaveMessageToDraft: draftEmailId = $draftEmailId'); final cancelToken = CancelToken(); final resultState = await _showSavingMessageToDraftsDialog( session: session, accountId: accountId, arguments: arguments, emailContent: emailContent, uploadUri: uploadUri, draftEmailId: draftEmailId, cancelToken: cancelToken ); if (resultState is SaveEmailAsDraftsSuccess || resultState is UpdateEmailDraftsSuccess) { _closeComposerButtonState = ButtonState.enabled; _closeComposerAction(result: resultState); } else if ((resultState is SaveEmailAsDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException) || (resultState is UpdateEmailDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException)) { _closeComposerButtonState = ButtonState.enabled; } else if (resultState is SaveEmailAsDraftsFailure || resultState is UpdateEmailDraftsFailure || resultState is GenerateEmailFailure ) { if (resultState.exception is BadCredentialsException) { _closeComposerButtonState = ButtonState.enabled; handleBadCredentialsException(); } else if (context.mounted) { await _showConfirmDialogWhenSaveMessageToDraftsFailure( context: context, failure: resultState, ); } else { _closeComposerButtonState = ButtonState.enabled; } } else { _closeComposerButtonState = ButtonState.enabled; } } EmailId? getDraftEmailId() { if (emailIdEditing != null && emailIdEditing != composerArguments.value!.presentationEmail?.id) { return emailIdEditing; } else if (currentEmailActionType == EmailActionType.editDraft) { return composerArguments.value!.presentationEmail?.id; } else { return null; } } Future _showSavingMessageToDraftsDialog({ required Session session, required AccountId accountId, required ComposerArguments arguments, required String emailContent, required Uri? uploadUri, EmailId? draftEmailId, CancelToken? cancelToken, }) { final childWidget = PointerInterceptor( child: SavingMessageDialogView( createEmailRequest: CreateEmailRequest( session: session, accountId: accountId, emailActionType: arguments.emailActionType, ownEmailAddress: ownEmailAddress, subject: subjectEmail.value ?? '', emailContent: emailContent, fromSender: arguments.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(), draftsMailboxId: getDraftMailboxIdForComposer(), draftsEmailId: draftEmailId, answerForwardEmailId: arguments.presentationEmail?.id, unsubscribeEmailId: arguments.previousEmailId, messageId: arguments.messageId, references: arguments.references, emailSendingQueue: arguments.sendingEmail, displayMode: screenDisplayMode.value, uploadUri: uploadUri, ), createNewAndSaveEmailToDraftsInteractor: _createNewAndSaveEmailToDraftsInteractor, onCancelSavingEmailToDraftsAction: _handleCancelSavingMessageToDrafts, cancelToken: cancelToken, ), ); return Get.dialog( PlatformInfo.isMobile ? PopScope(canPop: false, child: childWidget) : childWidget, barrierDismissible: false, barrierColor: AppColor.colorDefaultCupertinoActionSheet, ); } 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, ownEmailAddress: ownEmailAddress, 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()]); } Future _showConfirmDialogWhenSaveMessageToDraftsFailure({ required BuildContext context, required FeatureFailure failure, Function(bool)? onConfirmAction, Function(bool)? onCancelAction, }) async { final messageRecord = getMessageFailure( appLocalizations: AppLocalizations.of(context), exception: failure.exception, isDraft: true, ); final isIncreaseMySpaceIsDisabled = !mailboxDashBoardController.validatePremiumIsAvailable() || mailboxDashBoardController.validateUserHasIsAlreadyHighestSubscription(); final needIncreaseMySpace = !isIncreaseMySpaceIsDisabled && messageRecord.errorType == SetError.overQuota; await MessageDialogActionManager().showConfirmDialogAction( context, title: '', messageRecord.message, needIncreaseMySpace ? AppLocalizations.of(context).increaseYourSpace : AppLocalizations.of(context).edit, cancelTitle: needIncreaseMySpace ? AppLocalizations.of(context).edit : AppLocalizations.of(context).closeAnyway, alignCenter: true, outsideDismissible: false, autoPerformPopBack: false, onConfirmAction: () { if (onConfirmAction != null) { onConfirmAction(needIncreaseMySpace); } else { _closeComposerButtonState = ButtonState.enabled; popBack(); if (needIncreaseMySpace) { mailboxDashBoardController.paywallController?.navigateToPaywall(); } else { _autoFocusFieldWhenLauncher(); } } }, onCancelAction: () { if (onCancelAction != null) { onCancelAction(needIncreaseMySpace); } else { _closeComposerButtonState = ButtonState.enabled; if (needIncreaseMySpace) { popBack(); _autoFocusFieldWhenLauncher(); } else { _closeComposerAction(closeOverlays: true); } } }, ); } @override Future onBeforeReconnect() async { if (mailboxDashBoardController.accountId.value != null && mailboxDashBoardController.sessionCurrent?.username != null ) { await _saveComposerCacheOnWebAction(); } } void _setUpMaxWidthInlineImage({ required BuildContext context, required double maxWidth }) { if (context.mounted && responsiveUtils.isMobile(context)) { maxWithEditor = maxWidth - 40; } else { maxWithEditor = maxWidth - 70; } } void handleOnPasteImageSuccessAction({ required BuildContext context, required double maxWidth, required List listFileUpload }) { log('ComposerController::handleOnPasteImageSuccessAction: listFileUpload = ${listFileUpload.length}'); _setUpMaxWidthInlineImage(context: context, maxWidth: maxWidth); final listFileInfo = listFileUpload.toListFileInfo(); uploadController.validateTotalSizeAttachmentsBeforeUpload( totalSizePreparedFiles: listFileInfo.totalSize, onValidationSuccess: () => uploadAttachmentsAction(pickedFiles: listFileInfo) ); } void handleOnPasteImageFailureAction({ required BuildContext context, List? listFileUpload, String? base64, required UploadError uploadError }) { logWarning('ComposerController::handleOnPasteImageFailureAction: $uploadError'); if (!context.mounted) return; appToast.showToastErrorMessage( context, AppLocalizations.of(context).thisImageCannotBePastedIntoTheEditor); } void onCompleteSetupComposer() { initEmailDraftHash(); } void onPopupMenuChanged(bool isShowing) { if (isShowing) { clearFocusRecipients(); clearFocusSubject(); } } }