Files
workavia-mail-front/lib/features/composer/presentation/composer_controller.dart
T
Théo Poizat 6dc58e2eb6 Extract method to convert html content to text content
Editor exposes html content (so with html tags) and we do not want to send them to AI backend
2025-12-19 19:03:39 +07:00

2465 lines
94 KiB
Dart

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/ai/presentation/widgets/ai_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/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/mixins/text_selection_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:scribe/scribe.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/validate_premium_storage_extension.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/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_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
implements BeforeReconnectHandler {
final mailboxDashBoardController = Get.find<MailboxDashBoardController>();
final networkConnectionController = Get.find<NetworkConnectionController>();
final _beforeReconnectManager = Get.find<BeforeReconnectManager>();
final composerArguments = Rxn<ComposerArguments>();
final isEnableEmailSendButton = false.obs;
final isInitialRecipient = false.obs;
final subjectEmail = Rxn<String>();
final screenDisplayMode = ScreenDisplayMode.normal.obs;
final emailContentsViewState = Rxn<Either<Failure, Success>>();
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<Identity>();
final listFromIdentities = RxList<Identity>();
final isEmailChanged = Rx<bool>(false);
final isMarkAsImportant = Rx<bool>(false);
final isContentHeightExceeded = Rx<bool>(false);
final selectedText = Rxn<String>();
final hasTextSelection = false.obs;
final textSelectionCoordinates = Rxn<Offset>();
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<EmailAddress> listToEmailAddress = <EmailAddress>[];
List<EmailAddress> listCcEmailAddress = <EmailAddress>[];
List<EmailAddress> listBccEmailAddress = <EmailAddress>[];
List<EmailAddress> listReplyToEmailAddress = <EmailAddress>[];
ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.tMailContact;
final subjectEmailInputController = TextEditingController();
final toEmailAddressController = TextEditingController();
final ccEmailAddressController = TextEditingController();
final bccEmailAddressController = TextEditingController();
final replyToEmailAddressController = TextEditingController();
final searchIdentitiesInputController = TextEditingController();
final GlobalKey<TagsEditorState> keyToEmailTagEditor = GlobalKey<TagsEditorState>();
final GlobalKey<TagsEditorState> keyCcEmailTagEditor = GlobalKey<TagsEditorState>();
final GlobalKey<TagsEditorState> keyBccEmailTagEditor = GlobalKey<TagsEditorState>();
final GlobalKey<TagsEditorState> keyReplyToEmailTagEditor = GlobalKey<TagsEditorState>();
final GlobalKey headerEditorMobileWidgetKey = GlobalKey();
final GlobalKey<DropdownButton2State> identityDropdownKey = GlobalKey<DropdownButton2State>();
final GlobalKey aiScribeButtonKey = 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<html.Event>? _subscriptionOnDragEnter;
StreamSubscription<html.Event>? _subscriptionOnDragOver;
StreamSubscription<html.Event>? _subscriptionOnDragLeave;
StreamSubscription<html.Event>? _subscriptionOnDrop;
StreamSubscription<html.Event>? _subscriptionOnBlur;
StreamSubscription<String>? _composerCacheListener;
RichTextMobileTabletController? richTextMobileTabletController;
RichTextWebController? richTextWebController;
CustomPopupMenuController? menuMoreOptionController;
final ScrollController scrollController = ScrollController();
final ScrollController scrollControllerEmailAddress = ScrollController();
final ScrollController scrollControllerAttachment = ScrollController();
final ScrollController scrollControllerIdentities = ScrollController();
List<Attachment> initialAttachments = <Attachment>[];
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<RichTextWebController>(tag: composerId);
restoreEmailInlineImagesInteractor = getBinding<RestoreEmailInlineImagesInteractor>(tag: composerId);
menuMoreOptionController = CustomPopupMenuController();
} else {
richTextMobileTabletController = getBinding<RichTextMobileTabletController>(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<void> 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<void> _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) {
logError('ComposerController::_getUploadUriFromSession:Exception = $e');
return null;
}
}
Future<CreateEmailRequest?> _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<void> onLoadCompletedMobileEditorAction(HtmlEditorApi editorApi, WebUri? url) async {
_isEmailBodyLoaded = true;
await setupSelectedIdentity();
_autoFocusFieldWhenLauncher();
}
void _injectBinding() {
injectAutoCompleteBindings(
mailboxDashBoardController.sessionCurrent,
mailboxDashBoardController.accountId.value,
);
}
Future<void> 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<Attachment>? attachments,
List<Attachment>? 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<EmailAddress> 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<String> getContentInEditor() async {
try {
final htmlTextEditor = PlatformInfo.isWeb
? _textEditorWeb
: await htmlEditorApi?.getText();
return htmlTextEditor?.isNotEmpty == true
? htmlTextEditor!.removeEditorStartTag()
: '';
} catch (e) {
logError('ComposerController::getContentInEditor:Exception = $e');
return '';
}
}
String convertHtmlContentToTextContent(String htmlContent) {
String textContent = htmlContent.replaceAll(RegExp(r'<[^>]*>'), '');
textContent = textContent
.replaceAll('&nbsp;', ' ')
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&#39;', "'");
return textContent.trim();
}
Future<String> getTextOnlyContentInEditor() async {
try {
final htmlContent = await getContentInEditor();
String textContent = convertHtmlContentToTextContent(htmlContent);
return textContent;
} catch (e) {
logError('ComposerController::getTextOnlyContentInEditor:Exception = $e');
return '';
}
}
void insertTextInEditor(String text) {
final htmlContent = text.replaceAll('\n', '<br>');
if (PlatformInfo.isWeb) {
richTextWebController?.editorController.insertHtml(htmlContent);
} else {
richTextMobileTabletController?.htmlEditorApi?.insertHtml(htmlContent);
}
}
void showAIScribeMenuForFullText(BuildContext context) async {
final fullText = await getTextOnlyContentInEditor();
final RenderBox? renderBox = aiScribeButtonKey.currentContext?.findRenderObject() as RenderBox?;
Offset? buttonPosition;
if (renderBox != null) {
buttonPosition = renderBox.localToGlobal(Offset.zero);
}
if (!context.mounted) return;
showAIScribeDialog(
context: context,
imagePaths: imagePaths,
content: fullText,
onInsertText: insertTextInEditor,
buttonPosition: buttonPosition,
);
}
void showAIScribeMenuForSelectedText(BuildContext context, {Offset? buttonPosition}) {
final selection = selectedText.value;
if (selection == null || selection.isEmpty) {
return;
}
showAIScribeDialog(
context: context,
imagePaths: imagePaths,
content: selection,
onInsertText: insertTextInEditor,
buttonPosition: buttonPosition,
);
}
void handleTextSelection(TextSelectionData? textSelectionData) {
if (textSelectionData != null) {
hasTextSelection.value = textSelectionData.hasSelection;
selectedText.value = textSelectionData.selectedText;
if (textSelectionData.coordinates != null) {
textSelectionCoordinates.value = Offset(
textSelectionData.coordinates!.x,
textSelectionData.coordinates!.y,
);
} else {
textSelectionCoordinates.value = null;
}
} else {
hasTextSelection.value = false;
selectedText.value = null;
textSelectionCoordinates.value = null;
}
}
Future<void> _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 = validateAttachmentReminder(
emailSubject: subjectEmail.value ?? '',
emailContent: emailContent,
);
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<void> _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<dynamic> _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<void> _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<List<EmailAddress>> getAutoCompleteSuggestion(String queryString, {int? limit}) async {
log('ComposerController::getAutoCompleteSuggestion():queryString = $queryString | limit = $limit | $_contactSuggestionSource');
_getAllAutoCompleteInteractor = getBinding<GetAllAutoCompleteInteractor>();
_getAutoCompleteInteractor = getBinding<GetAutoCompleteInteractor>();
_getDeviceContactSuggestionsInteractor = getBinding<GetDeviceContactSuggestionsInteractor>();
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) {
logError('ComposerController::getAutoCompleteSuggestion:onFailureCallback: $failure');
consumeState(Stream.value(Left(failure)));
},
),
onError: (error) {
logError('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 <EmailAddress>[];
}
} else {
return await _getAutoCompleteInteractor
?.execute(autoCompletePattern)
.then(
(value) => handleAutoCompleteResultState(
resultState: value,
queryString: queryString,
onFailureCallback: (failure) {
logError('ComposerController::getAutoCompleteSuggestion:onFailureCallback: $failure');
consumeState(Stream.value(Left(failure)));
},
),
onError: (error) {
logError('ComposerController::getAutoCompleteSuggestion:onError: $error');
consumeState(Stream.value(Left(error)));
},
) ?? <EmailAddress>[];
}
}
void openPickAttachmentMenu(BuildContext context, List<Widget> 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<FileInfo> 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) {
logError('ComposerController::uploadAttachmentsAction: $e');
uploadController.consumeState(Stream.value(Left(UploadAttachmentFailure(e, pickedFiles[0]))));
}
} else {
logError('ComposerController::uploadAttachmentsAction: SESSION OR ACCOUNT_ID is NULL');
}
}
void deleteAttachmentUploaded(UploadTaskId uploadId) {
uploadController.deleteFileUploaded(uploadId);
}
Future<bool> _validateEmailChange() async {
final newDraftHash = await _hashComposingEmail();
return _savedEmailDraftHash != newDraftHash;
}
Future<int> _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<void> _updateSavedEmailDraftHash() async {
_savedEmailDraftHash = await _hashComposingEmail();
}
Future<void> 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<Failure, Success>(resultState)));
_updateSavedEmailDraftHash();
} else if (resultState is UpdateEmailDraftsSuccess) {
_saveToDraftButtonState = ButtonState.enabled;
emailIdEditing = resultState.emailId;
mailboxDashBoardController.consumeState(Stream.value(Right<Failure, Success>(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<void> 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, <EmailAddress>[]);
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<void> selectIdentity(Identity? newIdentity) async {
final formerIdentity = identitySelected.value;
identitySelected.value = newIdentity;
if (newIdentity == null) return;
await _applyIdentityForAllFieldComposer(formerIdentity, newIdentity);
}
Future<void> _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<EmailAddress> 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<EmailAddress> listEmailAddress) {
listBccEmailAddress = listBccEmailAddress
.where((address) => !listEmailAddress.contains(address))
.toList();
if (listBccEmailAddress.isEmpty) {
bccRecipientState.value = PrefixRecipientState.disabled;
}
updateStatusEmailSendButton();
}
Future<void> applySignature(String signature) async {
if (PlatformInfo.isWeb) {
richTextWebController?.editorController.insertSignature(
signature,
allowCollapsed: false,
);
} else {
await htmlEditorApi?.insertSignature(signature, allowCollapsed: false);
}
}
Future<void> _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) {
logError('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<void> _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<int>? 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<void> _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<bool> _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<void> 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<void> 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<void> _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<dynamic>? types) {
if (types.validateFilesTransfer) {
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active;
}
}
void handleOnDragOverHtmlEditorWeb(List<dynamic>? 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<dynamic> _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<dynamic> _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<void> _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<void> 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<FileUpload> 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<FileUpload>? listFileUpload,
String? base64,
required UploadError uploadError
}) {
logError('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();
}
}
}