From dc855a28c726de2a298bc93510cd9020452b5551 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 1 Mar 2024 00:36:46 +0700 Subject: [PATCH 01/80] TF-2644 Display warning dialog when pick file exceeded maximum size in composer --- .../presentation/composer_controller.dart | 39 +++---- .../widgets/email_attachments_widget.dart | 2 +- .../extensions/list_file_info_extension.dart | 7 ++ .../controller/upload_controller.dart | 106 +++++++++++++++--- lib/l10n/intl_en.arb | 12 ++ lib/l10n/intl_fr.arb | 12 ++ lib/l10n/intl_messages.arb | 14 ++- lib/l10n/intl_vi.arb | 12 ++ lib/main/localizations/app_localizations.dart | 14 +++ lib/main/utils/app_config.dart | 1 + .../extensions/list_attachment_extension.dart | 2 +- 11 files changed, 177 insertions(+), 44 deletions(-) create mode 100644 lib/features/upload/domain/extensions/list_file_info_extension.dart diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 53770fffd2..0f3feb2c7d 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -79,6 +79,7 @@ import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email. import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; import 'package:tmail_ui_user/features/server_settings/domain/state/get_always_read_receipt_setting_state.dart'; import 'package:tmail_ui_user/features/server_settings/domain/usecases/get_always_read_receipt_setting_interactor.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_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'; @@ -264,7 +265,7 @@ class ComposerController extends BaseController { success is TransformHtmlEmailContentSuccess) { emailContentsViewState.value = Right(success); } else if (success is LocalFilePickerSuccess) { - _pickFileSuccess(success); + _handlePickFileSuccess(success); } else if (success is GetEmailContentSuccess) { _getEmailContentSuccess(success); } else if (success is GetEmailContentFromCacheSuccess) { @@ -877,7 +878,7 @@ class ComposerController extends BaseController { return; } - if (!uploadController.hasEnoughMaxAttachmentSize()) { + if (uploadController.isExceededMaxSizeAttachmentsPerEmail()) { showConfirmDialogAction( context, AppLocalizations.of(context).message_dialog_send_email_exceeds_maximum_size( @@ -1072,29 +1073,21 @@ class ComposerController extends BaseController { } } - void _pickFileSuccess(LocalFilePickerSuccess success) { - if (uploadController.hasEnoughMaxAttachmentSize(fileInfoTotalSize: uploadController.getTotalSizeFromListFileInfo(success.pickedFiles))) { - _uploadAttachmentsAction(success.pickedFiles); - } else { - if (currentContext != null) { - showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( - filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, - onConfirmAction: () => {isSendEmailLoading.value = false}, - title: AppLocalizations.of(currentContext!).maximum_files_size, - hasCancelButton: false); - } - } + void _handlePickFileSuccess(LocalFilePickerSuccess success) { + uploadController.validateTotalSizeAttachmentsBeforeUpload( + listFileInfo: success.pickedFiles, + callbackAction: () => _uploadAttachmentsAction(success.pickedFiles) + ); } - void _uploadAttachmentsAction(List pickedFiles) async { + void _uploadAttachmentsAction(List pickedFiles) { final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; if (session != null && accountId != null) { final uploadUri = session.getUploadUri(accountId, jmapUrl: _dynamicUrlInterceptors.jmapUrl); uploadController.justUploadAttachmentsAction(pickedFiles, uploadUri); + } else { + log('ComposerController::_uploadAttachmentsAction: SESSION OR ACCOUNT_ID is NULL'); } } @@ -1303,7 +1296,7 @@ class ComposerController extends BaseController { } if (listFileAttachmentSharedMediaFile.isNotEmpty) { final listFile = covertListSharedMediaFileToFileInfo(listSharedMediaFile); - if (uploadController.hasEnoughMaxAttachmentSize(fileInfoTotalSize: uploadController.getTotalSizeFromListFileInfo(listFile))) { + if (!uploadController.isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: listFile.totalSize)) { _uploadAttachmentsAction(listFile); } else { if (currentContext != null) { @@ -1778,7 +1771,7 @@ class ComposerController extends BaseController { } void _uploadInlineAttachmentsAction(FileInfo pickedFile, {bool fromFileShared = false}) async { - if (uploadController.hasEnoughMaxAttachmentSize(fileInfoTotalSize: uploadController.getTotalSizeFromListFileInfo([pickedFile]))) { + if (!uploadController.isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: pickedFile.fileSize)) { final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; if (session != null && accountId != null) { @@ -1985,7 +1978,7 @@ class ComposerController extends BaseController { } void _addAttachmentFromDragAndDrop({required FileInfo fileInfo}) { - if (uploadController.hasEnoughMaxAttachmentSize(fileInfoTotalSize: uploadController.getTotalSizeFromListFileInfo([fileInfo]))) { + if (!uploadController.isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: fileInfo.fileSize)) { _uploadAttachmentsAction([fileInfo]); } else { if (currentContext != null) { @@ -2094,7 +2087,7 @@ class ComposerController extends BaseController { void addAttachmentFromDropZone(Attachment attachment) { log('ComposerController::addAttachmentFromDropZone: $attachment'); - if (uploadController.hasEnoughMaxAttachmentSize(fileInfoTotalSize: attachment.size?.value)) { + if (!uploadController.isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: attachment.size?.value ?? 0)) { uploadController.initializeUploadAttachments([attachment]); } else { if (currentContext != null) { @@ -2173,7 +2166,7 @@ class ComposerController extends BaseController { _closeComposerAction(result: draftArgs); _closeComposerButtonState = ButtonState.enabled; } - + void _getAlwaysReadReceiptSetting() { final accountId = mailboxDashBoardController.accountId.value; if (accountId != null) { diff --git a/lib/features/email/presentation/widgets/email_attachments_widget.dart b/lib/features/email/presentation/widgets/email_attachments_widget.dart index ba896651c9..06e6566ae0 100644 --- a/lib/features/email/presentation/widgets/email_attachments_widget.dart +++ b/lib/features/email/presentation/widgets/email_attachments_widget.dart @@ -64,7 +64,7 @@ class EmailAttachmentsWidget extends StatelessWidget { child: Text( AppLocalizations.of(context).titleHeaderAttachment( attachments.length, - filesize(attachments.totalSize(), 1) + filesize(attachments.totalSize, 1) ), style: const TextStyle( fontSize: EmailAttachmentsStyles.headerTextSize, diff --git a/lib/features/upload/domain/extensions/list_file_info_extension.dart b/lib/features/upload/domain/extensions/list_file_info_extension.dart new file mode 100644 index 0000000000..438a30a6af --- /dev/null +++ b/lib/features/upload/domain/extensions/list_file_info_extension.dart @@ -0,0 +1,7 @@ +import 'package:model/upload/file_info.dart'; + +extension ListFileInfoExtension on List { + List get listSize => map((file) => file.fileSize).toList(); + + num get totalSize => listSize.reduce((sum, size) => sum + size); +} \ No newline at end of file diff --git a/lib/features/upload/presentation/controller/upload_controller.dart b/lib/features/upload/presentation/controller/upload_controller.dart index 501643474b..eb591053b4 100644 --- a/lib/features/upload/presentation/controller/upload_controller.dart +++ b/lib/features/upload/presentation/controller/upload_controller.dart @@ -1,12 +1,15 @@ import 'package:async/async.dart'; import 'package:collection/collection.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; +import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; import 'package:model/email/attachment.dart'; @@ -18,6 +21,7 @@ import 'package:tmail_ui_user/features/base/state/base_ui_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/upload_attachment_state.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachment_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_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/presentation/extensions/upload_attachment_extension.dart'; @@ -26,6 +30,7 @@ import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_sta import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_status.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/utils/app_config.dart'; class UploadController extends BaseController { @@ -270,31 +275,96 @@ class UploadController extends BaseController { } } - bool hasEnoughMaxAttachmentSize({num? fileInfoTotalSize}) { - final currentTotalAttachmentsSize = attachmentsUploaded.totalSize(); - final totalInlineAttachmentsSize = inlineAttachmentsUploaded.totalSize(); - log('UploadController::_validateAttachmentsSize(): $currentTotalAttachmentsSize'); - log('UploadController::_validateAttachmentsSize(): totalInlineAttachmentsSize: $totalInlineAttachmentsSize'); - num uploadedTotalSize = fileInfoTotalSize ?? 0; - - final totalSizeReadyToUpload = currentTotalAttachmentsSize + - totalInlineAttachmentsSize + - uploadedTotalSize; - log('UploadController::_validateAttachmentsSize(): totalSizeReadyToUpload: $totalSizeReadyToUpload'); - + bool isExceededMaxSizeAttachmentsPerEmail({num totalSizePreparedFiles = 0}) { + final currentTotalSize = attachmentsUploaded.totalSize + inlineAttachmentsUploaded.totalSize + totalSizePreparedFiles; final maxSizeAttachmentsPerEmail = _mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value; + log('UploadController::isExceededMaxSizeAttachmentsPerEmail(): currentTotalSize = $currentTotalSize | maxSizeAttachmentsPerEmail = $maxSizeAttachmentsPerEmail'); if (maxSizeAttachmentsPerEmail != null) { - return totalSizeReadyToUpload <= maxSizeAttachmentsPerEmail; + return currentTotalSize > maxSizeAttachmentsPerEmail; } else { return false; } } - num getTotalSizeFromListFileInfo(List listFiles) { - final uploadedListSize = listFiles.map((file) => file.fileSize).toList(); - num totalSize = uploadedListSize.reduce((sum, size) => sum + size); - log('UploadController::_getTotalSizeFromListFileInfo():totalSize: $totalSize'); - return totalSize; + bool isExceededMaxSizeFilesAttachedInComposer({num totalSizePreparedFiles = 0}) { + final currentTotalSizeAttachments = attachmentsUploaded.totalSize + totalSizePreparedFiles; + const maximumBytesSizeFileAttachedInComposer = AppConfig.maximumMegabytesSizeFileAttachedInComposer * 1024 * 1024; + log('UploadController::isExceededMaxSizeFilesAttachedInComposer(): currentTotalSizeAttachments = $currentTotalSizeAttachments | maximumBytesSizeFileAttachedInComposer = $maximumBytesSizeFileAttachedInComposer'); + return currentTotalSizeAttachments > maximumBytesSizeFileAttachedInComposer; + } + + void validateTotalSizeAttachmentsBeforeUpload({ + required List listFileInfo, + VoidCallback? callbackAction + }) { + final totalSizeListFiles = listFileInfo.totalSize; + log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: totalSizeListFiles = $totalSizeListFiles'); + if (isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: totalSizeListFiles)) { + if (currentContext == null) { + log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: CONTEXT IS NULL'); + return; + } + + _showConfirmDialogWhenExceededMaxSizeAttachmentsPerEmail(context: currentContext!); + return; + } + + if (isExceededMaxSizeFilesAttachedInComposer(totalSizePreparedFiles: totalSizeListFiles)) { + if (currentContext == null) { + log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: CONTEXT IS NULL'); + return; + } + + _showWarningDialogWhenExceededMaxSizeFilesAttachedInComposer( + context: currentContext!, + confirmAction: callbackAction + ); + return; + } + + callbackAction?.call(); + } + + void _showConfirmDialogWhenExceededMaxSizeAttachmentsPerEmail({required BuildContext context}) { + final maxSizeAttachmentsPerEmail = filesize(_mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0); + showConfirmDialogAction( + context, + AppLocalizations.of(context).message_dialog_upload_attachments_exceeds_maximum_size(maxSizeAttachmentsPerEmail), + AppLocalizations.of(context).got_it, + title: AppLocalizations.of(context).maximum_files_size, + hasCancelButton: false); + } + + void _showWarningDialogWhenExceededMaxSizeFilesAttachedInComposer({ + required BuildContext context, + VoidCallback? confirmAction, + }) { + showConfirmDialogAction( + context, + title: '', + AppLocalizations.of(context).messageWarningDialogWhenExceedMaximumFileSizeComposer, + AppLocalizations.of(context).continueAction, + cancelTitle: AppLocalizations.of(context).cancel, + alignCenter: true, + onConfirmAction: confirmAction, + icon: SvgPicture.asset( + imagePaths.icQuotasWarning, + width: 40, + height: 40, + colorFilter: AppColor.colorBackgroundQuotasWarning.asFilter(), + ), + messageStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 14, + color: Colors.black + ), + actionStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.white + ), + cancelStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.black + )); } bool get allUploadAttachmentsCompleted { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index ccd04c7928..fe33404bee 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -3238,5 +3238,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "messageWarningDialogWhenExceedMaximumFileSizeComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.", + "@messageWarningDialogWhenExceedMaximumFileSizeComposer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "continueAction": "Continue", + "@continueAction": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 357766de31..6d98e60e12 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -3935,5 +3935,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "messageWarningDialogWhenExceedMaximumFileSizeComposer": "Votre message dépasse la taille généralement acceptée pour un email. \nSi vous confirmez l'envoi il y a un risque que le mail soit rejeté par le système de votre interlocuteur.", + "@messageWarningDialogWhenExceedMaximumFileSizeComposer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "continueAction": "Continuer", + "@continueAction": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 6076c18093..0779e91730 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-04-05T12:00:06.899366", + "@@last_modified": "2024-02-29T16:08:18.479839", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3749,5 +3749,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "messageWarningDialogWhenExceedMaximumFileSizeComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.", + "@messageWarningDialogWhenExceedMaximumFileSizeComposer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "continueAction": "Continue", + "@continueAction": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_vi.arb b/lib/l10n/intl_vi.arb index d2b6308f8a..07f4cf948b 100644 --- a/lib/l10n/intl_vi.arb +++ b/lib/l10n/intl_vi.arb @@ -3941,5 +3941,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "messageWarningDialogWhenExceedMaximumFileSizeComposer": "Thư của bạn lớn hơn kích thước thường được hệ thống email của bên thứ ba chấp nhận. Nếu bạn xác nhận đã gửi thư này, có nguy cơ nó sẽ bị hệ thống người nhận từ chối.", + "@messageWarningDialogWhenExceedMaximumFileSizeComposer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "continueAction": "Tiếp tục", + "@continueAction": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 5ceea1c55d..5c446ce037 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3916,4 +3916,18 @@ class AppLocalizations { name: 'zoomOut', ); } + + String get messageWarningDialogWhenExceedMaximumFileSizeComposer { + return Intl.message( + 'Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.', + name: 'messageWarningDialogWhenExceedMaximumFileSizeComposer', + ); + } + + String get continueAction { + return Intl.message( + 'Continue', + name: 'continueAction', + ); + } } \ No newline at end of file diff --git a/lib/main/utils/app_config.dart b/lib/main/utils/app_config.dart index a4634f5a16..f4f65fa6bb 100644 --- a/lib/main/utils/app_config.dart +++ b/lib/main/utils/app_config.dart @@ -8,6 +8,7 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class AppConfig { static const int limitCharToStartSearch = 3; + static const int maximumMegabytesSizeFileAttachedInComposer = 10; static const String appDashboardConfigurationPath = "configurations/app_dashboard.json"; static const String appFCMConfigurationPath = "configurations/env.fcm"; diff --git a/model/lib/extensions/list_attachment_extension.dart b/model/lib/extensions/list_attachment_extension.dart index e882fa117f..e8f788170a 100644 --- a/model/lib/extensions/list_attachment_extension.dart +++ b/model/lib/extensions/list_attachment_extension.dart @@ -6,7 +6,7 @@ import 'package:model/extensions/attachment_extension.dart'; extension ListAttachmentExtension on List { - num totalSize() { + num get totalSize { if (isNotEmpty) { final currentListSize = map((attachment) => attachment.size?.value ?? 0).toList(); final totalSize = currentListSize.reduce((sum, size) => sum + size); From b5442a18f9107d08707003e33dc343f9b8524deb Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 1 Mar 2024 00:41:09 +0700 Subject: [PATCH 02/80] TF-2644 Display warning dialog when drag & drop file exceeded maximum size in composer Signed-off-by: dab246 --- .../presentation/composer_controller.dart | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 0f3feb2c7d..1b33f4ca96 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1978,20 +1978,10 @@ class ComposerController extends BaseController { } void _addAttachmentFromDragAndDrop({required FileInfo fileInfo}) { - if (!uploadController.isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: fileInfo.fileSize)) { - _uploadAttachmentsAction([fileInfo]); - } else { - if (currentContext != null) { - showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( - filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, - title: AppLocalizations.of(currentContext!).maximum_files_size, - hasCancelButton: false, - ); - } - } + uploadController.validateTotalSizeAttachmentsBeforeUpload( + listFileInfo: [fileInfo], + callbackAction: () => _uploadAttachmentsAction([fileInfo]) + ); } FocusNode? getNextFocusOfToEmailAddress() { From e126b98bdd681aa726517af147b470323fb4b7ee Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 1 Mar 2024 00:42:37 +0700 Subject: [PATCH 03/80] TF-2644 Display warning dialog when share file exceeded maximum size in composer Signed-off-by: dab246 --- .../presentation/composer_controller.dart | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 1b33f4ca96..162a879584 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1295,21 +1295,11 @@ class ComposerController extends BaseController { } } if (listFileAttachmentSharedMediaFile.isNotEmpty) { - final listFile = covertListSharedMediaFileToFileInfo(listSharedMediaFile); - if (!uploadController.isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: listFile.totalSize)) { - _uploadAttachmentsAction(listFile); - } else { - if (currentContext != null) { - showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( - filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, - title: AppLocalizations.of(currentContext!).maximum_files_size, - hasCancelButton: false, - ); - } - } + final listFileInfo = covertListSharedMediaFileToFileInfo(listSharedMediaFile); + uploadController.validateTotalSizeAttachmentsBeforeUpload( + listFileInfo: listFileInfo, + callbackAction: () => _uploadAttachmentsAction(listFileInfo) + ); } } From ed8b47e5755270d1495f1812a3f4452dac9b01fa Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 1 Mar 2024 00:49:32 +0700 Subject: [PATCH 04/80] TF-2644 Display warning dialog when drag from other email exceeded maximum size in composer Signed-off-by: dab246 --- .../presentation/composer_controller.dart | 29 +++++++------------ .../presentation/composer_view_web.dart | 2 +- .../controller/upload_controller.dart | 10 +++---- 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 162a879584..8b8a295d4a 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -80,6 +80,7 @@ import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_ import 'package:tmail_ui_user/features/server_settings/domain/state/get_always_read_receipt_setting_state.dart'; import 'package:tmail_ui_user/features/server_settings/domain/usecases/get_always_read_receipt_setting_interactor.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_info_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'; @@ -1075,7 +1076,7 @@ class ComposerController extends BaseController { void _handlePickFileSuccess(LocalFilePickerSuccess success) { uploadController.validateTotalSizeAttachmentsBeforeUpload( - listFileInfo: success.pickedFiles, + totalSizePreparedFiles: success.pickedFiles.totalSize, callbackAction: () => _uploadAttachmentsAction(success.pickedFiles) ); } @@ -1297,7 +1298,7 @@ class ComposerController extends BaseController { if (listFileAttachmentSharedMediaFile.isNotEmpty) { final listFileInfo = covertListSharedMediaFileToFileInfo(listSharedMediaFile); uploadController.validateTotalSizeAttachmentsBeforeUpload( - listFileInfo: listFileInfo, + totalSizePreparedFiles: listFileInfo.totalSize, callbackAction: () => _uploadAttachmentsAction(listFileInfo) ); } @@ -1969,7 +1970,7 @@ class ComposerController extends BaseController { void _addAttachmentFromDragAndDrop({required FileInfo fileInfo}) { uploadController.validateTotalSizeAttachmentsBeforeUpload( - listFileInfo: [fileInfo], + totalSizePreparedFiles: fileInfo.fileSize, callbackAction: () => _uploadAttachmentsAction([fileInfo]) ); } @@ -2065,22 +2066,12 @@ class ComposerController extends BaseController { _updateStatusEmailSendButton(); } - void addAttachmentFromDropZone(Attachment attachment) { - log('ComposerController::addAttachmentFromDropZone: $attachment'); - if (!uploadController.isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: attachment.size?.value ?? 0)) { - uploadController.initializeUploadAttachments([attachment]); - } else { - if (currentContext != null) { - showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( - filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, - title: AppLocalizations.of(currentContext!).maximum_files_size, - hasCancelButton: false, - ); - } - } + void addAttachmentWhenDragFromOtherEmail(Attachment attachment) { + log('ComposerController::addAttachmentWhenDragFromOtherEmail: attachment = $attachment'); + uploadController.validateTotalSizeAttachmentsBeforeUpload( + totalSizePreparedFiles: attachment.size?.value ?? 0, + callbackAction: () => uploadController.initializeUploadAttachments([attachment]) + ); } void _handleGetAllIdentitiesFailure(GetAllIdentitiesFailure failure) async { diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index 409921903b..3cf8788164 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -385,7 +385,7 @@ class ComposerView extends GetWidget { child: DropZoneWidget( width: constraints.maxWidth, height: constraints.maxHeight, - addAttachmentFromDropZone: controller.addAttachmentFromDropZone, + addAttachmentFromDropZone: controller.addAttachmentWhenDragFromOtherEmail, ) ) ], diff --git a/lib/features/upload/presentation/controller/upload_controller.dart b/lib/features/upload/presentation/controller/upload_controller.dart index eb591053b4..38342ba401 100644 --- a/lib/features/upload/presentation/controller/upload_controller.dart +++ b/lib/features/upload/presentation/controller/upload_controller.dart @@ -21,7 +21,6 @@ import 'package:tmail_ui_user/features/base/state/base_ui_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/upload_attachment_state.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachment_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; -import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_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/presentation/extensions/upload_attachment_extension.dart'; @@ -294,12 +293,11 @@ class UploadController extends BaseController { } void validateTotalSizeAttachmentsBeforeUpload({ - required List listFileInfo, + required num totalSizePreparedFiles, VoidCallback? callbackAction }) { - final totalSizeListFiles = listFileInfo.totalSize; - log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: totalSizeListFiles = $totalSizeListFiles'); - if (isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: totalSizeListFiles)) { + log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: totalSizePreparedFiles = $totalSizePreparedFiles'); + if (isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: totalSizePreparedFiles)) { if (currentContext == null) { log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: CONTEXT IS NULL'); return; @@ -309,7 +307,7 @@ class UploadController extends BaseController { return; } - if (isExceededMaxSizeFilesAttachedInComposer(totalSizePreparedFiles: totalSizeListFiles)) { + if (isExceededMaxSizeFilesAttachedInComposer(totalSizePreparedFiles: totalSizePreparedFiles)) { if (currentContext == null) { log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: CONTEXT IS NULL'); return; From 9d003ec24c5fdb1995df4a411aa92724be1a9f01 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 1 Mar 2024 00:57:33 +0700 Subject: [PATCH 05/80] TF-2644 Display tooltip message when attachments exceeded maximum size in composer Signed-off-by: dab246 --- .../attachment_header_composer_widget.dart | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart b/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart index ffc1070c66..e8515786e0 100644 --- a/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart @@ -1,3 +1,4 @@ +import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:filesize/filesize.dart'; @@ -7,6 +8,7 @@ import 'package:tmail_ui_user/features/composer/presentation/styles/attachment_h import 'package:tmail_ui_user/features/upload/presentation/extensions/list_upload_file_state_extension.dart'; import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_config.dart'; typedef OnToggleExpandAttachmentViewAction = Function(bool isCollapsed); @@ -66,13 +68,26 @@ class AttachmentHeaderComposerWidget extends StatelessWidget { ), padding: AttachmentHeaderComposerWidgetStyle.sizeLabelPadding, child: Text( - filesize(listFileUploaded.totalSize, 0), + filesize(listFileUploaded.totalSize), style: AttachmentHeaderComposerWidgetStyle.sizeLabelTextSize, ), - ) + ), + if (_isExceedMaximumSizeFileAttachedInComposer) + TMailButtonWidget.fromIcon( + icon: _imagePaths.icQuotasWarning, + iconSize: 20, + margin: const EdgeInsetsDirectional.only(start: 4), + padding: const EdgeInsets.all(3), + iconColor: AppColor.colorBackgroundQuotasWarning, + tooltipMessage: AppLocalizations.of(context).messageWarningDialogWhenExceedMaximumFileSizeComposer, + backgroundColor: Colors.transparent, + ) ], ), ), ); } + + bool get _isExceedMaximumSizeFileAttachedInComposer => + listFileUploaded.totalSize > AppConfig.maximumMegabytesSizeFileAttachedInComposer * 1024 * 1024; } From 2f7dd89176218245c202d2347b83e3c5db32afc0 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 1 Mar 2024 17:09:09 +0700 Subject: [PATCH 06/80] TF-2644 Handle total size exceeded maximum size when drag & drop multiple file to composer --- core/lib/core.dart | 1 + core/lib/utils/list_utils.dart | 16 + .../state/download_image_as_base64_state.dart | 7 +- .../domain/state/upload_attachment_state.dart | 35 +- .../download_image_as_base64_interactor.dart | 2 - .../upload_attachment_interactor.dart | 14 +- .../presentation/composer_bindings.dart | 5 +- .../presentation/composer_controller.dart | 356 +++++------ .../presentation/composer_view_web.dart | 577 ++++++++++-------- .../rich_text_mobile_tablet_controller.dart | 20 +- .../controller/rich_text_web_controller.dart | 10 +- .../extensions/file_extension.dart | 19 + .../extensions/file_upload_extension.dart | 3 +- .../list_shared_media_file_extension.dart | 8 + .../shared_media_file_extension.dart | 22 + .../mixin/drag_drog_file_mixin.dart | 52 ++ .../presentation/model/image_source.dart | 5 - .../presentation/model/inline_image.dart | 26 +- .../web/bottom_bar_composer_widget_style.dart | 3 +- .../styles/web/drop_zone_widget_style.dart | 10 +- .../view/web/web_editor_view.dart | 24 +- .../attachment_header_composer_widget.dart | 4 +- ....dart => attachment_drop_zone_widget.dart} | 57 +- .../web/bottom_bar_composer_widget.dart | 1 + .../web/local_file_drop_zone_widget.dart | 69 +++ .../widgets/web/web_editor_widget.dart | 19 +- .../email/presentation/email_view.dart | 15 +- .../extensions/attachment_extension.dart | 35 +- .../authorization_interceptors.dart | 18 +- .../mailbox_dashboard_controller.dart | 15 +- .../upload/data/network/file_uploader.dart | 17 +- .../exceptions/pick_file_exception.dart | 1 + .../extensions/file_info_extension.dart | 12 +- .../extensions/list_file_info_extension.dart | 2 +- .../extensions/media_type_extension.dart | 28 + .../extensions/platform_file_extension.dart | 13 + .../domain/state/attachment_upload_state.dart | 7 +- .../domain/state/local_file_picker_state.dart | 6 +- .../state/local_image_picker_state.dart | 19 + .../local_file_picker_interactor.dart | 26 +- .../local_image_picker_interactor.dart | 35 ++ .../controller/upload_controller.dart | 159 +++-- .../upload_attachment_extension.dart | 3 +- .../presentation/model/upload_file_state.dart | 21 +- lib/l10n/intl_en.arb | 4 +- lib/l10n/intl_fr.arb | 4 +- lib/l10n/intl_messages.arb | 18 +- lib/l10n/intl_vi.arb | 2 + lib/main/localizations/app_localizations.dart | 18 +- lib/main/utils/app_config.dart | 2 +- model/lib/upload/file_info.dart | 55 +- pubspec.lock | 16 + pubspec.yaml | 4 + .../model/attachment_extension_test.dart | 14 +- 54 files changed, 1137 insertions(+), 797 deletions(-) create mode 100644 core/lib/utils/list_utils.dart create mode 100644 lib/features/composer/presentation/extensions/file_extension.dart create mode 100644 lib/features/composer/presentation/extensions/list_shared_media_file_extension.dart create mode 100644 lib/features/composer/presentation/extensions/shared_media_file_extension.dart create mode 100644 lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart delete mode 100644 lib/features/composer/presentation/model/image_source.dart rename lib/features/composer/presentation/widgets/web/{drop_zone_widget.dart => attachment_drop_zone_widget.dart} (59%) create mode 100644 lib/features/composer/presentation/widgets/web/local_file_drop_zone_widget.dart create mode 100644 lib/features/upload/domain/exceptions/pick_file_exception.dart create mode 100644 lib/features/upload/domain/extensions/media_type_extension.dart create mode 100644 lib/features/upload/domain/extensions/platform_file_extension.dart create mode 100644 lib/features/upload/domain/state/local_image_picker_state.dart create mode 100644 lib/features/upload/domain/usecases/local_image_picker_interactor.dart diff --git a/core/lib/core.dart b/core/lib/core.dart index 596285f9c5..b25e073b86 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -45,6 +45,7 @@ export 'utils/file_utils.dart'; export 'utils/option_param_mixin.dart'; export 'utils/print_utils.dart'; export 'utils/broadcast_channel/broadcast_channel.dart'; +export 'utils/list_utils.dart'; // Views export 'presentation/views/text/slogan_builder.dart'; diff --git a/core/lib/utils/list_utils.dart b/core/lib/utils/list_utils.dart new file mode 100644 index 0000000000..9449a9e237 --- /dev/null +++ b/core/lib/utils/list_utils.dart @@ -0,0 +1,16 @@ +import 'package:dartz/dartz.dart'; + +Tuple2, List> partition(List list, bool Function(T) predicate) { + List trueList = []; + List falseList = []; + + for (var element in list) { + if (predicate(element)) { + trueList.add(element); + } else { + falseList.add(element); + } + } + + return Tuple2(trueList, falseList); +} \ No newline at end of file diff --git a/lib/features/composer/domain/state/download_image_as_base64_state.dart b/lib/features/composer/domain/state/download_image_as_base64_state.dart index af801f1a28..9370e13a62 100644 --- a/lib/features/composer/domain/state/download_image_as_base64_state.dart +++ b/lib/features/composer/domain/state/download_image_as_base64_state.dart @@ -10,15 +10,11 @@ class DownloadImageAsBase64Success extends UIState { final String base64Uri; final String cid; final FileInfo fileInfo; - final bool fromFileShared; DownloadImageAsBase64Success( this.base64Uri, this.cid, - this.fileInfo, - { - this.fromFileShared = false - } + this.fileInfo ); @override @@ -26,7 +22,6 @@ class DownloadImageAsBase64Success extends UIState { base64Uri, cid, fileInfo, - fromFileShared, ]; } diff --git a/lib/features/composer/domain/state/upload_attachment_state.dart b/lib/features/composer/domain/state/upload_attachment_state.dart index c58d04d11e..08f4e9f9a1 100644 --- a/lib/features/composer/domain/state/upload_attachment_state.dart +++ b/lib/features/composer/domain/state/upload_attachment_state.dart @@ -1,46 +1,25 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:model/upload/file_info.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_attachment.dart'; class UploadAttachmentSuccess extends UIState { final UploadAttachment uploadAttachment; - final bool isInline; - final bool fromFileShared; - UploadAttachmentSuccess( - this.uploadAttachment, - { - this.isInline = false, - this.fromFileShared = false, - } - ); + UploadAttachmentSuccess(this.uploadAttachment); @override - List get props => [ - uploadAttachment, - isInline, - fromFileShared, - ]; + List get props => [uploadAttachment]; } class UploadAttachmentFailure extends FeatureFailure { - final bool isInline; - final bool fromFileShared; - UploadAttachmentFailure( - dynamic exception, - { - this.isInline = false, - this.fromFileShared = false, - } - ) : super(exception: exception); + final FileInfo fileInfo; + + UploadAttachmentFailure(dynamic exception, this.fileInfo) : super(exception: exception); @override - List get props => [ - isInline, - fromFileShared, - ...super.props - ]; + List get props => [...super.props, fileInfo]; } \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/download_image_as_base64_interactor.dart b/lib/features/composer/domain/usecases/download_image_as_base64_interactor.dart index b3fb5d0ac5..b3417daa17 100644 --- a/lib/features/composer/domain/usecases/download_image_as_base64_interactor.dart +++ b/lib/features/composer/domain/usecases/download_image_as_base64_interactor.dart @@ -16,7 +16,6 @@ class DownloadImageAsBase64Interactor { { double? maxWidth, bool? compress, - bool fromFileShared = false, } ) async* { try { @@ -33,7 +32,6 @@ class DownloadImageAsBase64Interactor { result!, cid, fileInfo, - fromFileShared: fromFileShared )); } else { yield Left(DownloadImageAsBase64Failure(null)); diff --git a/lib/features/composer/domain/usecases/upload_attachment_interactor.dart b/lib/features/composer/domain/usecases/upload_attachment_interactor.dart index 6abdcf8018..ad1b69da53 100644 --- a/lib/features/composer/domain/usecases/upload_attachment_interactor.dart +++ b/lib/features/composer/domain/usecases/upload_attachment_interactor.dart @@ -15,8 +15,6 @@ class UploadAttachmentInteractor { FileInfo fileInfo, Uri uploadUri, { CancelToken? cancelToken, - bool isInline = false, - bool fromFileShared = false, }) async* { try { final uploadAttachment = await _composerRepository.uploadAttachment( @@ -24,17 +22,9 @@ class UploadAttachmentInteractor { uploadUri, cancelToken: cancelToken ); - yield Right(UploadAttachmentSuccess( - uploadAttachment, - isInline: isInline, - fromFileShared: fromFileShared - )); + yield Right(UploadAttachmentSuccess(uploadAttachment)); } catch (e) { - yield Left(UploadAttachmentFailure( - e, - isInline: isInline, - fromFileShared: fromFileShared - )); + yield Left(UploadAttachmentFailure(e, fileInfo)); } } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index dda6062d2f..6f9bc469aa 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -62,6 +62,7 @@ import 'package:tmail_ui_user/features/upload/data/datasource/attachment_upload_ import 'package:tmail_ui_user/features/upload/data/datasource_impl/attachment_upload_datasource_impl.dart'; import 'package:tmail_ui_user/features/upload/data/network/file_uploader.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/bindings/network/binding_tag.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; @@ -125,7 +126,7 @@ class ComposerBindings extends BaseBindings { Get.find(), Get.find())); Get.lazyPut(() => RemoteServerSettingsDataSourceImpl( - Get.find(), + Get.find(), Get.find())); } @@ -182,6 +183,7 @@ class ComposerBindings extends BaseBindings { @override void bindingsInteractor() { Get.lazyPut(() => LocalFilePickerInteractor()); + Get.lazyPut(() => LocalImagePickerInteractor()); Get.lazyPut(() => UploadAttachmentInteractor(Get.find())); Get.lazyPut(() => SaveEmailAsDraftsInteractor( Get.find(), @@ -207,6 +209,7 @@ class ComposerBindings extends BaseBindings { Get.lazyPut(() => ComposerController( Get.find(), Get.find(), + Get.find(), Get.find(), Get.find(), Get.find(), diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 8b8a295d4a..2e878affb9 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'dart:io'; import 'dart:math'; import 'package:collection/collection.dart'; import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; +import 'package:desktop_drop/desktop_drop.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:file_picker/file_picker.dart'; @@ -51,8 +51,9 @@ import 'package:tmail_ui_user/features/composer/presentation/controller/rich_tex import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/file_upload_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/list_shared_media_file_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/draggable_email_address.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/image_source.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/save_to_draft_arguments.dart'; @@ -69,6 +70,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_r import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_composer_cache_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/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'; @@ -79,18 +81,21 @@ import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email. import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; import 'package:tmail_ui_user/features/server_settings/domain/state/get_always_read_receipt_setting_state.dart'; import 'package:tmail_ui_user/features/server_settings/domain/usecases/get_always_read_receipt_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/list_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/file_info_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/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:universal_html/html.dart' as html; -class ComposerController extends BaseController { +class ComposerController extends BaseController with DragDropFileMixin { final mailboxDashBoardController = Get.find(); final richTextMobileTabletController = Get.find(); @@ -116,6 +121,7 @@ class ComposerController extends BaseController { final listFromIdentities = RxList(); final LocalFilePickerInteractor _localFilePickerInteractor; + final LocalImagePickerInteractor _localImagePickerInteractor; final DeviceInfoPlugin _deviceInfoPlugin; final GetEmailContentInteractor _getEmailContentInteractor; final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; @@ -155,6 +161,12 @@ class ComposerController extends BaseController { FocusNode? bccAddressFocusNode; FocusNode? searchIdentitiesFocusNode; + StreamSubscription? _subscriptionOnBeforeUnload; + StreamSubscription? _subscriptionOnDragEnter; + StreamSubscription? _subscriptionOnDragOver; + StreamSubscription? _subscriptionOnDragLeave; + StreamSubscription? _subscriptionOnDrop; + final RichTextController keyboardRichTextController = RichTextController(); final ScrollController scrollController = ScrollController(); @@ -178,6 +190,7 @@ class ComposerController extends BaseController { ComposerController( this._deviceInfoPlugin, this._localFilePickerInteractor, + this._localImagePickerInteractor, this._getEmailContentInteractor, this._getAllIdentitiesInteractor, this.uploadController, @@ -201,7 +214,7 @@ class ComposerController extends BaseController { }); } else { WidgetsBinding.instance.addPostFrameCallback((_) { - _listenBrowserTabRefresh(); + _listenBrowserEventAction(); }); } _getAlwaysReadReceiptSetting(); @@ -225,6 +238,11 @@ class ComposerController extends BaseController { emailContentsViewState.value = Right(UIClosedState()); identitySelected.value = null; listFromIdentities.clear(); + _subscriptionOnBeforeUnload?.cancel(); + _subscriptionOnDragEnter?.cancel(); + _subscriptionOnDragOver?.cancel(); + _subscriptionOnDragLeave?.cancel(); + _subscriptionOnDrop?.cancel(); if (PlatformInfo.isMobile) { FkUserAgent.release(); } @@ -267,6 +285,8 @@ class ComposerController extends BaseController { emailContentsViewState.value = Right(success); } else if (success is LocalFilePickerSuccess) { _handlePickFileSuccess(success); + } else if (success is LocalImagePickerSuccess) { + _handlePickImageSuccess(success); } else if (success is GetEmailContentSuccess) { _getEmailContentSuccess(success); } else if (success is GetEmailContentFromCacheSuccess) { @@ -274,23 +294,11 @@ class ComposerController extends BaseController { } 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( - ImageSource.local, - fileInfo: success.fileInfo, - cid: success.cid, - base64Uri: success.base64Uri)); + richTextWebController.insertImage(inlineImage); } else { - richTextMobileTabletController.insertImage( - InlineImage( - ImageSource.local, - fileInfo: success.fileInfo, - cid: success.cid, - base64Uri: success.base64Uri - ), - fromFileShare: success.fromFileShared - ); + richTextMobileTabletController.insertImage(inlineImage); } maxWithEditor = null; } else if (success is GetAlwaysReadReceiptSettingSuccess) { @@ -301,8 +309,10 @@ class ComposerController extends BaseController { @override void handleFailureViewState(Failure failure) { super.handleFailureViewState(failure); - if (failure is LocalFilePickerFailure || failure is LocalFilePickerCancel) { - _pickFileFailure(failure); + if (failure is LocalFilePickerFailure) { + _handlePickFileFailure(failure); + } else if (failure is LocalImagePickerFailure) { + _handlePickImageFailure(failure); } else if (failure is GetEmailContentFailure || failure is TransformHtmlEmailContentFailure) { emailContentsViewState.value = Left(failure); @@ -358,8 +368,9 @@ class ComposerController extends BaseController { }); } - void _listenBrowserTabRefresh() { - html.window.onBeforeUnload.listen((event) async { + void _listenBrowserEventAction() { + log('ComposerController::_listenBrowserEventAction:'); + _subscriptionOnBeforeUnload = html.window.onBeforeUnload.listen((event) async { final userProfile = mailboxDashBoardController.userProfile.value; _removeComposerCacheOnWebInteractor.execute(); if (userProfile != null) { @@ -367,6 +378,22 @@ class ComposerController extends BaseController { _saveComposerCacheOnWebInteractor.execute(draftEmail); } }); + + _subscriptionOnDragEnter = html.window.onDragEnter.listen((event) { + mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active; + }); + + _subscriptionOnDragOver = html.window.onDragOver.listen((event) { + mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active; + }); + + _subscriptionOnDragLeave = html.window.onDragLeave.listen((event) { + mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive; + }); + + _subscriptionOnDrop = html.window.onDrop.listen((event) { + mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive; + }); } void _scrollControllerEmailAddressListener() { @@ -1064,29 +1091,45 @@ class ComposerController extends BaseController { consumeState(_localFilePickerInteractor.execute(fileType: fileType)); } - void _pickFileFailure(Failure failure) { - if (failure is LocalFilePickerFailure) { - if (currentOverlayContext != null && currentContext != null) { - appToast.showToastErrorMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments); - } + 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, - callbackAction: () => _uploadAttachmentsAction(success.pickedFiles) + onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: success.pickedFiles) + ); + } + + void _handlePickImageSuccess(LocalImagePickerSuccess success) { + uploadController.validateTotalSizeInlineAttachmentsBeforeUpload( + totalSizePreparedFiles: success.fileInfo.fileSize, + onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: [success.fileInfo.withInline()]) ); } - void _uploadAttachmentsAction(List pickedFiles) { + void _uploadAttachmentsAction({required List pickedFiles}) { final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; if (session != null && accountId != null) { final uploadUri = session.getUploadUri(accountId, jmapUrl: _dynamicUrlInterceptors.jmapUrl); - uploadController.justUploadAttachmentsAction(pickedFiles, uploadUri); + uploadController.justUploadAttachmentsAction( + uploadFiles: pickedFiles, + uploadUri: uploadUri, + ); } else { log('ComposerController::_uploadAttachmentsAction: SESSION OR ACCOUNT_ID is NULL'); } @@ -1237,71 +1280,17 @@ class ComposerController extends BaseController { ); } - File _covertSharedMediaFileToFile(SharedMediaFile sharedMediaFile) { - return File( - Platform.isIOS - ? sharedMediaFile.type == SharedMediaType.FILE - ? sharedMediaFile.path.toString().replaceAll('file:/', '').replaceAll('%20', ' ') - : sharedMediaFile.path.toString().replaceAll('%20', ' ') - : sharedMediaFile.path, - ); - } - - FileInfo _covertFileToFileInfo(File file) { - return FileInfo( - file.path.split('/').last, - file.path, - file.existsSync() ? file.lengthSync() : 0, - ); - } - - List covertListSharedMediaFileToInlineImage(List value) { - List newFiles = List.empty(growable: true); - if (value.isNotEmpty) { - for (var element in value) { - newFiles.add(_covertSharedMediaFileToFile(element)); - } - } - - final List listInlineImage = newFiles.map( - (e) => InlineImage( - ImageSource.local, - fileInfo: _covertFileToFileInfo(e), - ) - ).toList(); - return listInlineImage; - } - - List covertListSharedMediaFileToFileInfo(List value) { - List newFiles = List.empty(growable: true); - if (value.isNotEmpty) { - for (var element in value) { - newFiles.add(_covertSharedMediaFileToFile(element)); - } - } + void _addAttachmentFromFileShare(List listSharedMediaFile) { + final listFileInfo = listSharedMediaFile.toListFileInfo(isShared: true); - final List listFileInfo = newFiles.map( - (e) => _covertFileToFileInfo(e), - ).toList(); - return listFileInfo; - } + final tupleListFileInfo = partition(listFileInfo, (fileInfo) => fileInfo.isInline == true); + final listAttachments = tupleListFileInfo.value2; - void _addAttachmentFromFileShare(List listSharedMediaFile) { - final listImageSharedMediaFile = listSharedMediaFile.where((element) => element.type == SharedMediaType.IMAGE); - final listFileAttachmentSharedMediaFile = listSharedMediaFile.where((element) => element.type != SharedMediaType.IMAGE); - if (listImageSharedMediaFile.isNotEmpty) { - final listInlineImage = covertListSharedMediaFileToInlineImage(listSharedMediaFile); - for (var e in listInlineImage) { - _uploadInlineAttachmentsAction(e.fileInfo!, fromFileShared: true); - } - } - if (listFileAttachmentSharedMediaFile.isNotEmpty) { - final listFileInfo = covertListSharedMediaFileToFileInfo(listSharedMediaFile); - uploadController.validateTotalSizeAttachmentsBeforeUpload( - totalSizePreparedFiles: listFileInfo.totalSize, - callbackAction: () => _uploadAttachmentsAction(listFileInfo) - ); - } + uploadController.validateTotalSizeAttachmentsBeforeUpload( + totalSizePreparedFiles: listFileInfo.totalSize, + totalSizePreparedFilesWithDispositionAttachment: listAttachments.totalSize, + onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo) + ); } void _getEmailContentFromSendingEmail(SendingEmail sendingEmail) { @@ -1712,80 +1701,8 @@ class ComposerController extends BaseController { } else { maxWithEditor = maxWith - 120; } - final inlineImage = await _selectFromFile(); - if (inlineImage != null) { - if (PlatformInfo.isWeb) { - _insertImageOnWeb(inlineImage); - } else { - _insertImageOnMobileAndTablet(inlineImage); - } - } else { - if (context.mounted) { - appToast.showToastErrorMessage(context, AppLocalizations.of(context).cannotSelectThisImage); - } - } - } - Future _selectFromFile() async { - final filePickerResult = await FilePicker.platform.pickFiles( - type: FileType.image, - withData: PlatformInfo.isWeb - ); - if (filePickerResult?.files.isNotEmpty == true) { - PlatformFile platformFile = filePickerResult!.files.first; - final fileSelected = FileInfo( - platformFile.name, - PlatformInfo.isWeb ? '' : platformFile.path ?? '', - platformFile.size, - bytes: PlatformInfo.isWeb ? platformFile.bytes : null, - ); - return InlineImage(ImageSource.local, fileInfo: fileSelected); - } - - return null; - } - - void _insertImageOnWeb(InlineImage inlineImage) { - if (inlineImage.source == ImageSource.local) { - _uploadInlineAttachmentsAction(inlineImage.fileInfo!); - } else { - richTextWebController.insertImage(inlineImage); - } - } - - void _insertImageOnMobileAndTablet(InlineImage inlineImage) { - if (inlineImage.source == ImageSource.local) { - _uploadInlineAttachmentsAction(inlineImage.fileInfo!); - } else { - richTextMobileTabletController.insertImage(inlineImage); - } - } - - void _uploadInlineAttachmentsAction(FileInfo pickedFile, {bool fromFileShared = false}) async { - if (!uploadController.isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: pickedFile.fileSize)) { - final session = mailboxDashBoardController.sessionCurrent; - final accountId = mailboxDashBoardController.accountId.value; - if (session != null && accountId != null) { - final uploadUri = session.getUploadUri(accountId, jmapUrl: _dynamicUrlInterceptors.jmapUrl); - uploadController.uploadFileAction( - pickedFile, - uploadUri, - isInline: true, - fromFileShared: fromFileShared - ); - } - } else { - if (currentContext != null) { - showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( - filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, - onConfirmAction: () => {isSendEmailLoading.value = false}, - title: AppLocalizations.of(currentContext!).maximum_files_size, - hasCancelButton: false); - } - } + consumeState(_localImagePickerInteractor.execute()); } void _handleUploadInlineSuccess(SuccessAttachmentUploadState uploadState) { @@ -1802,7 +1719,6 @@ class ComposerController extends BaseController { uploadState.attachment.cid!, uploadState.fileInfo, maxWidth: maxWithEditor, - fromFileShared: uploadState.fromFileShared, )); } } @@ -1896,6 +1812,7 @@ class ComposerController extends BaseController { log('ComposerController::handleInitHtmlEditorWeb:'); _isEmailBodyLoaded = true; richTextWebController.editorController.setFullScreen(); + richTextWebController.editorController.setOnDragDropEvent(); onChangeTextEditorWeb(initContent); richTextWebController.setEnableCodeView(); if (identitySelected.value == null) { @@ -1921,10 +1838,25 @@ class ComposerController extends BaseController { void handleImageUploadSuccess ( BuildContext context, - web_html_editor.FileUpload fileUpload + List listFileUpload ) async { - log('ComposerController::handleImageUploadSuccess:NAME: ${fileUpload.name} | TYPE: ${fileUpload.type} | SIZE: ${fileUpload.size}'); - if (fileUpload.base64 == null) { + log('ComposerController::handleImageUploadSuccess: COUNT_FILE_UPLOADED = ${listFileUpload.length}'); + List listFileInfo = []; + + await Future.forEach(listFileUpload, (fileUpload) async { + if (fileUpload.base64?.isNotEmpty == true) { + final fileInfo = await fileUpload.toFileInfo(); + if (fileInfo != null) { + if (fileInfo.mimeType.startsWith(MediaTypeExtension.imageType) == true) { + listFileInfo.add(fileInfo.withInline()); + } else { + listFileInfo.add(fileInfo); + } + } + } + }); + + if (listFileInfo.isEmpty && context.mounted) { appToast.showToastErrorMessage( context, AppLocalizations.of(context).can_not_upload_this_file_as_attachments @@ -1932,49 +1864,30 @@ class ComposerController extends BaseController { return; } - if (fileUpload.type?.startsWith(Constant.imageType) == true) { - final fileInfo = await fileUpload.toFileInfo(); - if (fileInfo != null) { - _uploadInlineAttachmentsAction(fileInfo); - } else if (context.mounted) { - appToast.showToastErrorMessage( - context, - AppLocalizations.of(context).can_not_upload_this_file_as_attachments - ); - } - } else { - final fileInfo = await fileUpload.toFileInfo(); - if (fileInfo != null) { - _addAttachmentFromDragAndDrop(fileInfo: fileInfo); - } else if (context.mounted) { - appToast.showToastErrorMessage( - context, - AppLocalizations.of(context).can_not_upload_this_file_as_attachments - ); - } - } + final listAttachments = listFileInfo + .where((fileInfo) => fileInfo.isInline != true) + .toList(); + + uploadController.validateTotalSizeAttachmentsBeforeUpload( + totalSizePreparedFiles: listFileInfo.totalSize, + totalSizePreparedFilesWithDispositionAttachment: listAttachments.totalSize, + onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo) + ); } void handleImageUploadFailure({ required BuildContext context, required web_html_editor.UploadError uploadError, - web_html_editor.FileUpload? fileUpload, + List? listFileUpload, String? base64Str, }) { - logError('ComposerController::handleImageUploadFailure:fileUpload: $fileUpload | uploadError: $uploadError'); + logError('ComposerController::handleImageUploadFailure: COUNT_FILE_FAILED = ${listFileUpload?.length} | ERROR = $uploadError'); appToast.showToastErrorMessage( context, '${AppLocalizations.of(context).can_not_upload_this_file_as_attachments}. (${uploadError.name})' ); } - void _addAttachmentFromDragAndDrop({required FileInfo fileInfo}) { - uploadController.validateTotalSizeAttachmentsBeforeUpload( - totalSizePreparedFiles: fileInfo.fileSize, - callbackAction: () => _uploadAttachmentsAction([fileInfo]) - ); - } - FocusNode? getNextFocusOfToEmailAddress() { if (ccRecipientState.value == PrefixRecipientState.enabled) { return ccAddressFocusNode; @@ -2066,11 +1979,11 @@ class ComposerController extends BaseController { _updateStatusEmailSendButton(); } - void addAttachmentWhenDragFromOtherEmail(Attachment attachment) { - log('ComposerController::addAttachmentWhenDragFromOtherEmail: attachment = $attachment'); + void onAttachmentDropZoneListener(Attachment attachment) { + log('ComposerController::onAttachmentDropZoneListener: attachment = $attachment'); uploadController.validateTotalSizeAttachmentsBeforeUpload( totalSizePreparedFiles: attachment.size?.value ?? 0, - callbackAction: () => uploadController.initializeUploadAttachments([attachment]) + onValidationSuccess: () => uploadController.initializeUploadAttachments([attachment]) ); } @@ -2144,4 +2057,33 @@ class ComposerController extends BaseController { consumeState(_getAlwaysReadReceiptSettingInteractor.execute(accountId)); } } + + void handleOnDragEnterHtmlEditorWeb() { + mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active; + } + + void onLocalFileDropZoneListener({ + required BuildContext context, + required DropDoneDetails details + }) async { + 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; + } + + final listAttachments = listFileInfo + .where((fileInfo) => fileInfo.isInline != true) + .toList(); + + uploadController.validateTotalSizeAttachmentsBeforeUpload( + totalSizePreparedFiles: listFileInfo.totalSize, + totalSizePreparedFilesWithDispositionAttachment: listAttachments.totalSize, + onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo) + ); + } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index 3cf8788164..ccae484332 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -17,10 +17,11 @@ import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/subject_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/attachment_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/attachment_drop_zone_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/desktop_app_bar_composer_widget.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/web/drop_zone_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/from_composer_drop_down_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/local_file_drop_zone_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -157,72 +158,110 @@ class ComposerView extends GetWidget { margin: ComposerStyle.mobileSubjectMargin, ), Expanded( - child: Stack( - children: [ - Padding( - padding: ComposerStyle.mobileEditorPadding, - child: Obx(() => WebEditorView( - editorController: controller.richTextWebController.editorController, - arguments: controller.composerArguments.value, - contentViewState: controller.emailContentsViewState.value, - currentWebContent: controller.textEditorWeb, - onInitial: controller.handleInitHtmlEditorWeb, - onChangeContent: controller.onChangeTextEditorWeb, - onFocus: controller.handleOnFocusHtmlEditorWeb, - onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, - onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, - onEditorSettings: controller.richTextWebController.onEditorSettingsChange, - onImageUploadSuccessAction: (fileUpload) => controller.handleImageUploadSuccess(context, fileUpload), - onImageUploadFailureAction: (fileUpload, base64Str, uploadError) { - return controller.handleImageUploadFailure( - context: context, - uploadError: uploadError, - fileUpload: fileUpload, - base64Str: base64Str, - ); - }, - onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, - width: constraints.maxWidth, - height: constraints.maxHeight, - )), - ), - Align( - alignment: AlignmentDirectional.topCenter, - child: Obx(() => InsertImageLoadingBarWidget( - uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, - viewState: controller.viewState.value, - padding: ComposerStyle.insertImageLoadingBarPadding, - )), - ), - ], + child: LayoutBuilder( + builder: (context, constraintsEditor) { + return Stack( + children: [ + Column( + children: [ + Expanded( + child: Padding( + padding: ComposerStyle.mobileEditorPadding, + child: Obx(() => WebEditorView( + editorController: controller.richTextWebController.editorController, + arguments: controller.composerArguments.value, + contentViewState: controller.emailContentsViewState.value, + currentWebContent: controller.textEditorWeb, + onInitial: controller.handleInitHtmlEditorWeb, + onChangeContent: controller.onChangeTextEditorWeb, + onFocus: controller.handleOnFocusHtmlEditorWeb, + onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, + onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, + onEditorSettings: controller.richTextWebController.onEditorSettingsChange, + onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, + width: constraints.maxWidth, + height: constraints.maxHeight, + onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + )), + ), + ), + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return AttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + isCollapsed: controller.isAttachmentCollapsed, + onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), + onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.richTextWebController.isFormattingOptionsEnabled) { + return ToolbarRichTextWebBuilder( + richTextWebController: controller.richTextWebController, + padding: ComposerStyle.richToolbarPadding, + decoration: const BoxDecoration( + color: ComposerStyle.richToolbarColor, + boxShadow: ComposerStyle.richToolbarShadow + ), + ); + } else { + return const SizedBox.shrink(); + } + }) + ], + ), + Align( + alignment: AlignmentDirectional.topCenter, + child: Obx(() => InsertImageLoadingBarWidget( + uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, + viewState: controller.viewState.value, + padding: ComposerStyle.insertImageLoadingBarPadding, + )), + ), + Obx(() { + if (controller.mailboxDashBoardController.isAttachmentDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: AttachmentDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onAttachmentDropZoneListener: controller.onAttachmentDropZoneListener, + ) + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.mailboxDashBoardController.isLocalFileDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: LocalFileDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onLocalFileDropZoneListener: (details) => + controller.onLocalFileDropZoneListener( + context: context, + details: details + ), + ) + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + ], + ); + } ), ), - Obx(() { - if (controller.uploadController.listUploadAttachments.isNotEmpty) { - return AttachmentComposerWidget( - listFileUploaded: controller.uploadController.listUploadAttachments, - isCollapsed: controller.isAttachmentCollapsed, - onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), - onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, - ); - } else { - return const SizedBox.shrink(); - } - }), - Obx(() { - if (controller.richTextWebController.isFormattingOptionsEnabled) { - return ToolbarRichTextWebBuilder( - richTextWebController: controller.richTextWebController, - padding: ComposerStyle.richToolbarPadding, - decoration: const BoxDecoration( - color: ComposerStyle.richToolbarColor, - boxShadow: ComposerStyle.richToolbarShadow - ), - ); - } else { - return const SizedBox.shrink(); - } - }) ] ), ); @@ -336,123 +375,149 @@ class ComposerView extends GetWidget { margin: ComposerStyle.desktopSubjectMargin, ), Expanded( - child: Container( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: ComposerStyle.borderColor, - width: 1 - ) - ), - color: ComposerStyle.backgroundEditorColor - ), - child: Stack( - children: [ - Column( - children: [ - Expanded( - child: Padding( - padding: ComposerStyle.desktopEditorPadding, - child: Obx(() { - return Stack( + child: LayoutBuilder( + builder: (context, constraintsEditor) { + return Stack( + children: [ + Column( + children: [ + Expanded( + child: Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ComposerStyle.borderColor, + width: 1 + ) + ), + color: ComposerStyle.backgroundEditorColor + ), + child: Column( children: [ - WebEditorView( - editorController: controller.richTextWebController.editorController, - arguments: controller.composerArguments.value, - contentViewState: controller.emailContentsViewState.value, - currentWebContent: controller.textEditorWeb, - onInitial: controller.handleInitHtmlEditorWeb, - onChangeContent: controller.onChangeTextEditorWeb, - onFocus: controller.handleOnFocusHtmlEditorWeb, - onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, - onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, - onEditorSettings: controller.richTextWebController.onEditorSettingsChange, - onImageUploadSuccessAction: (fileUpload) => controller.handleImageUploadSuccess(context, fileUpload), - onImageUploadFailureAction: (fileUpload, base64Str, uploadError) { - return controller.handleImageUploadFailure( - context: context, - uploadError: uploadError, - fileUpload: fileUpload, - base64Str: base64Str, - ); - }, - onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, - width: constraints.maxWidth, - height: constraints.maxHeight, + Expanded( + child: Padding( + padding: ComposerStyle.desktopEditorPadding, + child: Obx(() { + return WebEditorView( + editorController: controller.richTextWebController.editorController, + arguments: controller.composerArguments.value, + contentViewState: controller.emailContentsViewState.value, + currentWebContent: controller.textEditorWeb, + onInitial: controller.handleInitHtmlEditorWeb, + onChangeContent: controller.onChangeTextEditorWeb, + onFocus: controller.handleOnFocusHtmlEditorWeb, + onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, + onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, + onEditorSettings: controller.richTextWebController.onEditorSettingsChange, + onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, + width: constraints.maxWidth, + height: constraints.maxHeight, + onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + ); + }), + ), ), - if (controller.mailboxDashBoardController.isDraggableAppActive) - PointerInterceptor( - child: DropZoneWidget( - width: constraints.maxWidth, - height: constraints.maxHeight, - addAttachmentFromDropZone: controller.addAttachmentWhenDragFromOtherEmail, - ) - ) + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return AttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + isCollapsed: controller.isAttachmentCollapsed, + onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), + onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.richTextWebController.isFormattingOptionsEnabled) { + return ToolbarRichTextWebBuilder( + richTextWebController: controller.richTextWebController, + padding: ComposerStyle.richToolbarPadding, + decoration: const BoxDecoration( + color: ComposerStyle.richToolbarColor, + boxShadow: ComposerStyle.richToolbarShadow + ), + ); + } else { + return const SizedBox.shrink(); + } + }) ], - ); - }), - ), - ), - Obx(() { - if (controller.uploadController.listUploadAttachments.isNotEmpty) { - return AttachmentComposerWidget( - listFileUploaded: controller.uploadController.listUploadAttachments, - isCollapsed: controller.isAttachmentCollapsed, - onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), - onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, - ); - } else { - return const SizedBox.shrink(); - } - }), - Obx(() { - if (controller.richTextWebController.isFormattingOptionsEnabled) { - return ToolbarRichTextWebBuilder( - richTextWebController: controller.richTextWebController, - padding: ComposerStyle.richToolbarPadding, - decoration: const BoxDecoration( - color: ComposerStyle.richToolbarColor, - boxShadow: ComposerStyle.richToolbarShadow ), - ); - } else { - return const SizedBox.shrink(); - } - }) - ], - ), - Align( - alignment: AlignmentDirectional.topCenter, - child: Obx(() => InsertImageLoadingBarWidget( - uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, - viewState: controller.viewState.value, - padding: ComposerStyle.insertImageLoadingBarPadding, - )), - ), - ], - ), + ), + ), + Obx(() => BottomBarComposerWidget( + isCodeViewEnabled: controller.richTextWebController.codeViewEnabled, + isFormattingOptionsEnabled: controller.richTextWebController.isFormattingOptionsEnabled, + openRichToolbarAction: controller.richTextWebController.toggleFormattingOptions, + attachFileAction: () => controller.openFilePickerByType(context, FileType.any), + insertImageAction: () => controller.insertImage(context, constraints.maxWidth), + showCodeViewAction: controller.richTextWebController.toggleCodeView, + deleteComposerAction: () => controller.handleClickDeleteComposer(context), + saveToDraftAction: () => controller.saveToDraftAction(context), + sendMessageAction: () => controller.validateInformationBeforeSending(context), + requestReadReceiptAction: (position) { + controller.openPopupMenuAction( + context, + position, + _createReadReceiptPopupItems(context), + radius: ComposerStyle.popupMenuRadius + ); + }, + isSending: controller.isSendEmailLoading.value, + )), + ], + ), + Align( + alignment: AlignmentDirectional.topCenter, + child: Obx(() => InsertImageLoadingBarWidget( + uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, + viewState: controller.viewState.value, + padding: ComposerStyle.insertImageLoadingBarPadding, + )), + ), + Obx(() { + if (controller.mailboxDashBoardController.isAttachmentDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: AttachmentDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onAttachmentDropZoneListener: controller.onAttachmentDropZoneListener, + ) + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.mailboxDashBoardController.isLocalFileDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: LocalFileDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onLocalFileDropZoneListener: (details) => + controller.onLocalFileDropZoneListener( + context: context, + details: details + ), + ) + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + ], + ); + } ), ), - Obx(() => BottomBarComposerWidget( - isCodeViewEnabled: controller.richTextWebController.codeViewEnabled, - isFormattingOptionsEnabled: controller.richTextWebController.isFormattingOptionsEnabled, - openRichToolbarAction: controller.richTextWebController.toggleFormattingOptions, - attachFileAction: () => controller.openFilePickerByType(context, FileType.any), - insertImageAction: () => controller.insertImage(context, constraints.maxWidth), - showCodeViewAction: controller.richTextWebController.toggleCodeView, - deleteComposerAction: () => controller.handleClickDeleteComposer(context), - saveToDraftAction: () => controller.saveToDraftAction(context), - sendMessageAction: () => controller.validateInformationBeforeSending(context), - requestReadReceiptAction: (position) { - controller.openPopupMenuAction( - context, - position, - _createReadReceiptPopupItems(context), - radius: ComposerStyle.popupMenuRadius - ); - }, - isSending: controller.isSendEmailLoading.value, - )), ]), ); }, @@ -567,87 +632,119 @@ class ComposerView extends GetWidget { margin: ComposerStyle.tabletSubjectMargin, ), Expanded( - child: Container( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: ComposerStyle.borderColor, - width: 1 - ) - ), - color: ComposerStyle.backgroundEditorColor - ), - child: Stack( - children: [ - Column( + child: LayoutBuilder( + builder: (context, constraintsEditor) { + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ComposerStyle.borderColor, + width: 1 + ) + ), + color: ComposerStyle.backgroundEditorColor + ), + child: Stack( children: [ - Expanded( - child: Padding( - padding: ComposerStyle.tabletEditorPadding, - child: Obx(() => WebEditorView( - editorController: controller.richTextWebController.editorController, - arguments: controller.composerArguments.value, - contentViewState: controller.emailContentsViewState.value, - currentWebContent: controller.textEditorWeb, - onInitial: controller.handleInitHtmlEditorWeb, - onChangeContent: controller.onChangeTextEditorWeb, - onFocus: controller.handleOnFocusHtmlEditorWeb, - onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, - onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, - onEditorSettings: controller.richTextWebController.onEditorSettingsChange, - onImageUploadSuccessAction: (fileUpload) => controller.handleImageUploadSuccess(context, fileUpload), - onImageUploadFailureAction: (fileUpload, base64Str, uploadError) { - return controller.handleImageUploadFailure( - context: context, - uploadError: uploadError, - fileUpload: fileUpload, - base64Str: base64Str, + Column( + children: [ + Expanded( + child: Padding( + padding: ComposerStyle.tabletEditorPadding, + child: Obx(() => WebEditorView( + editorController: controller.richTextWebController.editorController, + arguments: controller.composerArguments.value, + contentViewState: controller.emailContentsViewState.value, + currentWebContent: controller.textEditorWeb, + onInitial: controller.handleInitHtmlEditorWeb, + onChangeContent: controller.onChangeTextEditorWeb, + onFocus: controller.handleOnFocusHtmlEditorWeb, + onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, + onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, + onEditorSettings: controller.richTextWebController.onEditorSettingsChange, + onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, + width: constraints.maxWidth, + height: constraints.maxHeight, + onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + )), + ), + ), + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return AttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + isCollapsed: controller.isAttachmentCollapsed, + onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), + onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, ); - }, - onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, - width: constraints.maxWidth, - height: constraints.maxHeight, - )), - ), + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.richTextWebController.isFormattingOptionsEnabled) { + return ToolbarRichTextWebBuilder( + richTextWebController: controller.richTextWebController, + padding: ComposerStyle.richToolbarPadding, + decoration: const BoxDecoration( + color: ComposerStyle.richToolbarColor, + boxShadow: ComposerStyle.richToolbarShadow + ), + ); + } else { + return const SizedBox.shrink(); + } + }) + ], + ), + Align( + alignment: AlignmentDirectional.topCenter, + child: Obx(() => InsertImageLoadingBarWidget( + uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, + viewState: controller.viewState.value, + padding: ComposerStyle.insertImageLoadingBarPadding, + )), ), Obx(() { - if (controller.uploadController.listUploadAttachments.isNotEmpty) { - return AttachmentComposerWidget( - listFileUploaded: controller.uploadController.listUploadAttachments, - isCollapsed: controller.isAttachmentCollapsed, - onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), - onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, + if (controller.mailboxDashBoardController.isAttachmentDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: AttachmentDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onAttachmentDropZoneListener: controller.onAttachmentDropZoneListener, + ) + ), ); } else { return const SizedBox.shrink(); } }), Obx(() { - if (controller.richTextWebController.isFormattingOptionsEnabled) { - return ToolbarRichTextWebBuilder( - richTextWebController: controller.richTextWebController, - padding: ComposerStyle.richToolbarPadding, - decoration: const BoxDecoration( - color: ComposerStyle.richToolbarColor, - boxShadow: ComposerStyle.richToolbarShadow + if (controller.mailboxDashBoardController.isLocalFileDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: LocalFileDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onLocalFileDropZoneListener: (details) => + controller.onLocalFileDropZoneListener( + context: context, + details: details + ), + ) ), ); } else { return const SizedBox.shrink(); } - }) + }), ], ), - Align( - alignment: AlignmentDirectional.topCenter, - child: Obx(() => InsertImageLoadingBarWidget( - uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, - viewState: controller.viewState.value, - padding: ComposerStyle.insertImageLoadingBarPadding, - )), - ), - ], - ), + ); + } ), ), Obx(() => BottomBarComposerWidget( diff --git a/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart b/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart index 84b6d98a41..269c5503eb 100644 --- a/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart +++ b/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart @@ -6,27 +6,17 @@ import 'package:file_picker/file_picker.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/base_rich_text_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/header_style_type.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; class RichTextMobileTabletController extends BaseRichTextController { HtmlEditorApi? htmlEditorApi; - void insertImage( - InlineImage image, - { - double? maxWithEditor, - bool fromFileShare = false + void insertImage(InlineImage inlineImage) async { + if (inlineImage.fileInfo.isShared == true) { + await htmlEditorApi?.moveCursorAtLastNode(); } - ) async { - log('RichTextMobileTabletController::insertImage(): $image | maxWithEditor: $maxWithEditor | $fromFileShare'); - if (image.source == ImageSource.network) { - htmlEditorApi?.insertImageLink(image.link!); - } else { - if (fromFileShare) { - await htmlEditorApi?.moveCursorAtLastNode(); - } - await htmlEditorApi?.insertHtml(image.base64Uri ?? ''); + if (inlineImage.base64Uri?.isNotEmpty == true) { + await htmlEditorApi?.insertHtml(inlineImage.base64Uri!); } } diff --git a/lib/features/composer/presentation/controller/rich_text_web_controller.dart b/lib/features/composer/presentation/controller/rich_text_web_controller.dart index 782aca1e4c..bb8bd07fb7 100644 --- a/lib/features/composer/presentation/controller/rich_text_web_controller.dart +++ b/lib/features/composer/presentation/controller/rich_text_web_controller.dart @@ -15,7 +15,6 @@ import 'package:tmail_ui_user/features/composer/presentation/model/dropdown_menu import 'package:tmail_ui_user/features/composer/presentation/model/font_name_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/formatting_options_state.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/header_style_type.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/order_list_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/paragraph_type.dart'; @@ -194,13 +193,8 @@ class RichTextWebController extends BaseRichTextController { bool isTextStyleTypeSelected(RichTextStyleType richTextStyleType) => listTextStyleApply.contains(richTextStyleType); - void insertImage(InlineImage image) async { - log('RichTextWebController::insertImage(): $image'); - if (image.source == ImageSource.network) { - editorController.insertNetworkImage(image.link!); - } else { - editorController.insertHtml("
${image.base64Uri ?? ''}

"); - } + void insertImage(InlineImage inlineImage) { + editorController.insertHtml("
${inlineImage.base64Uri ?? ''}

"); } void applyNewFontStyle(FontNameType? newFont) { diff --git a/lib/features/composer/presentation/extensions/file_extension.dart b/lib/features/composer/presentation/extensions/file_extension.dart new file mode 100644 index 0000000000..a5d4470511 --- /dev/null +++ b/lib/features/composer/presentation/extensions/file_extension.dart @@ -0,0 +1,19 @@ + +import 'dart:io'; + +import 'package:model/upload/file_info.dart'; + +extension FileExtension on File { + FileInfo toFileInfo({ + bool? isInline, + bool? isShared + }) { + return FileInfo( + fileName: path.split('/').last, + fileSize: existsSync() ? lengthSync() : 0, + filePath: path, + isInline: isInline, + isShared: isShared + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/file_upload_extension.dart b/lib/features/composer/presentation/extensions/file_upload_extension.dart index 3f6e26c254..ec8707c7c9 100644 --- a/lib/features/composer/presentation/extensions/file_upload_extension.dart +++ b/lib/features/composer/presentation/extensions/file_upload_extension.dart @@ -29,7 +29,8 @@ extension FileUploadExtension on FileUpload { return FileInfo.fromBytes( bytes: bytes, name: name, - size: size + size: size, + type: type, ); } else { return null; diff --git a/lib/features/composer/presentation/extensions/list_shared_media_file_extension.dart b/lib/features/composer/presentation/extensions/list_shared_media_file_extension.dart new file mode 100644 index 0000000000..482196e1da --- /dev/null +++ b/lib/features/composer/presentation/extensions/list_shared_media_file_extension.dart @@ -0,0 +1,8 @@ + +import 'package:model/upload/file_info.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/shared_media_file_extension.dart'; + +extension ListSharedMediaFileExtension on List { + List toListFileInfo({bool? isShared}) => map((sharedMediaFile) => sharedMediaFile.toFileInfo(isShared: isShared)).toList(); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/shared_media_file_extension.dart b/lib/features/composer/presentation/extensions/shared_media_file_extension.dart new file mode 100644 index 0000000000..93f6caa37f --- /dev/null +++ b/lib/features/composer/presentation/extensions/shared_media_file_extension.dart @@ -0,0 +1,22 @@ + +import 'dart:io'; + +import 'package:core/utils/platform_info.dart'; +import 'package:model/upload/file_info.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/file_extension.dart'; + +extension SharedMediaFileExtension on SharedMediaFile { + File toFile() { + if (PlatformInfo.isIOS) { + final pathFile = type == SharedMediaType.FILE + ? path.toString().replaceAll('file:/', '').replaceAll('%20', ' ') + : path.toString().replaceAll('%20', ' '); + return File(pathFile); + } else { + return File(path); + } + } + + FileInfo toFileInfo({bool? isShared}) => toFile().toFileInfo(isInline: type == SharedMediaType.IMAGE, isShared: isShared); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart b/lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart new file mode 100644 index 0000000000..44b763109c --- /dev/null +++ b/lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart @@ -0,0 +1,52 @@ +import 'dart:async' as async; +import 'package:async/async.dart'; +import 'package:core/domain/extensions/media_type_extension.dart'; +import 'package:desktop_drop/desktop_drop.dart'; +import 'package:flutter/material.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:model/upload/file_info.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +mixin DragDropFileMixin { + async.Future> showFutureLoadingDialogFullScreen({ + required BuildContext context, + required async.Future Function() future, + }) async { + return await showFutureLoadingDialog( + context: context, + title: AppLocalizations.of(context).loadingPleaseWait, + backLabel: AppLocalizations.of(context).close, + future: future, + ); + } + + async.Future> onDragDone({ + required BuildContext context, + required DropDoneDetails details + }) async { + final bytesList = await showFutureLoadingDialogFullScreen( + context: context, + future: () => async.Future.wait( + details.files.map( + (xFile) => xFile.readAsBytes(), + ), + ), + ); + + if (bytesList.error != null) return []; + + final listFileInfo = []; + for (var i = 0; i < bytesList.result!.length; i++) { + listFileInfo.add( + FileInfo( + bytes: bytesList.result![i], + fileName: details.files[i].name, + type: details.files[i].mimeType, + fileSize: bytesList.result![i].length, + isInline: details.files[i].mimeType?.startsWith(MediaTypeExtension.imageType) == true + ), + ); + } + return listFileInfo; + } +} diff --git a/lib/features/composer/presentation/model/image_source.dart b/lib/features/composer/presentation/model/image_source.dart deleted file mode 100644 index 6c2cbf3d2c..0000000000 --- a/lib/features/composer/presentation/model/image_source.dart +++ /dev/null @@ -1,5 +0,0 @@ - -enum ImageSource { - local, - network -} \ No newline at end of file diff --git a/lib/features/composer/presentation/model/inline_image.dart b/lib/features/composer/presentation/model/inline_image.dart index 2dc8e33f02..db16bd6acc 100644 --- a/lib/features/composer/presentation/model/inline_image.dart +++ b/lib/features/composer/presentation/model/inline_image.dart @@ -1,25 +1,19 @@ import 'package:equatable/equatable.dart'; -import 'package:model/model.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; +import 'package:model/upload/file_info.dart'; class InlineImage with EquatableMixin { - final ImageSource source; - final String? link; - final FileInfo? fileInfo; - final String? cid; + final FileInfo fileInfo; final String? base64Uri; - InlineImage( - this.source, - { - this.link, - this.fileInfo, - this.cid, - this.base64Uri, - } - ); + InlineImage({ + required this.fileInfo, + this.base64Uri, + }); @override - List get props => [source, link, fileInfo, cid, base64Uri]; + List get props => [ + fileInfo, + base64Uri + ]; } \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart b/lib/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart index 3389db408a..ef82023e20 100644 --- a/lib/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart +++ b/lib/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart @@ -10,6 +10,7 @@ class BottomBarComposerWidgetStyle { static const double richTextIconSize = 24; static const double sendButtonRadius = 8; static const double sendButtonIconSpace = 5; + static const double height = 60; static const Color backgroundColor = Colors.white; static const Color iconColor = AppColor.colorRichButtonComposer; @@ -17,7 +18,7 @@ class BottomBarComposerWidgetStyle { static const Color selectedBackgroundColor = AppColor.colorSelected; static const Color selectedIconColor = AppColor.primaryColor; - static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(horizontal: 32, vertical: 12); + static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(horizontal: 32); static const EdgeInsetsGeometry iconPadding = EdgeInsetsDirectional.all(5); static const EdgeInsetsGeometry sendButtonPadding = EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 24); static const EdgeInsetsGeometry richTextIconPadding = EdgeInsetsDirectional.all(2); diff --git a/lib/features/composer/presentation/styles/web/drop_zone_widget_style.dart b/lib/features/composer/presentation/styles/web/drop_zone_widget_style.dart index 033c224f68..35fe785430 100644 --- a/lib/features/composer/presentation/styles/web/drop_zone_widget_style.dart +++ b/lib/features/composer/presentation/styles/web/drop_zone_widget_style.dart @@ -1,5 +1,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart'; class DropZoneWidgetStyle { static const double space = 20; @@ -8,11 +9,16 @@ class DropZoneWidgetStyle { static const List dashSize = [6, 3]; - static const Color backgroundColor = AppColor.colorDropZoneBackground; + static Color backgroundColor = AppColor.colorDropZoneBackground.withOpacity(0.7); static const Color borderColor = AppColor.colorDropZoneBorder; static const EdgeInsetsGeometry padding = EdgeInsets.all(20); - static const EdgeInsetsGeometry margin = EdgeInsetsDirectional.symmetric(vertical: 8); + static const EdgeInsetsGeometry margin = EdgeInsetsDirectional.only( + bottom: BottomBarComposerWidgetStyle.height, + start: 8, + end: 8, + top: 8 + ); static const TextStyle labelTextStyle = TextStyle( color: Colors.black, diff --git a/lib/features/composer/presentation/view/web/web_editor_view.dart b/lib/features/composer/presentation/view/web/web_editor_view.dart index e9d2826cc1..5da136ef90 100644 --- a/lib/features/composer/presentation/view/web/web_editor_view.dart +++ b/lib/features/composer/presentation/view/web/web_editor_view.dart @@ -26,11 +26,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { final VoidCallback? onUnFocus; final OnMouseDownEditorAction? onMouseDown; final OnEditorSettingsChange? onEditorSettings; - final OnImageUploadSuccessAction? onImageUploadSuccessAction; - final OnImageUploadFailureAction? onImageUploadFailureAction; final OnEditorTextSizeChanged? onEditorTextSizeChanged; final double? width; final double? height; + final VoidCallback? onDragEnter; const WebEditorView({ super.key, @@ -44,11 +43,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { this.onUnFocus, this.onMouseDown, this.onEditorSettings, - this.onImageUploadSuccessAction, - this.onImageUploadFailureAction, this.onEditorTextSizeChanged, this.width, this.height, + this.onDragEnter, }); @override @@ -71,11 +69,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ); case EmailActionType.editDraft: case EmailActionType.editSendingEmail: @@ -97,11 +94,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ), (success) { if (success is GetEmailContentLoading) { @@ -123,11 +119,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ); } } @@ -156,11 +151,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ); }, (success) { @@ -185,11 +179,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ); } } @@ -205,11 +198,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ); } } diff --git a/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart b/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart index e8515786e0..174621b47e 100644 --- a/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart @@ -79,7 +79,7 @@ class AttachmentHeaderComposerWidget extends StatelessWidget { margin: const EdgeInsetsDirectional.only(start: 4), padding: const EdgeInsets.all(3), iconColor: AppColor.colorBackgroundQuotasWarning, - tooltipMessage: AppLocalizations.of(context).messageWarningDialogWhenExceedMaximumFileSizeComposer, + tooltipMessage: AppLocalizations.of(context).warningMessageWhenExceedGenerallySizeInComposer, backgroundColor: Colors.transparent, ) ], @@ -89,5 +89,5 @@ class AttachmentHeaderComposerWidget extends StatelessWidget { } bool get _isExceedMaximumSizeFileAttachedInComposer => - listFileUploaded.totalSize > AppConfig.maximumMegabytesSizeFileAttachedInComposer * 1024 * 1024; + listFileUploaded.totalSize > AppConfig.warningAttachmentFileSizeInMegabytes * 1024 * 1024; } diff --git a/lib/features/composer/presentation/widgets/web/drop_zone_widget.dart b/lib/features/composer/presentation/widgets/web/attachment_drop_zone_widget.dart similarity index 59% rename from lib/features/composer/presentation/widgets/web/drop_zone_widget.dart rename to lib/features/composer/presentation/widgets/web/attachment_drop_zone_widget.dart index 6391feb457..2faae585a8 100644 --- a/lib/features/composer/presentation/widgets/web/drop_zone_widget.dart +++ b/lib/features/composer/presentation/widgets/web/attachment_drop_zone_widget.dart @@ -3,42 +3,35 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; import 'package:model/email/attachment.dart'; import 'package:tmail_ui_user/features/composer/presentation/styles/web/drop_zone_widget_style.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -typedef OnAddAttachmentFromDropZone = Function(Attachment attachment); +typedef OnAttachmentDropZoneListener = Function(Attachment attachment); -class DropZoneWidget extends StatefulWidget { +class AttachmentDropZoneWidget extends StatelessWidget { + final ImagePaths imagePaths; final double? width; final double? height; - final OnAddAttachmentFromDropZone? addAttachmentFromDropZone; + final OnAttachmentDropZoneListener? onAttachmentDropZoneListener; - const DropZoneWidget({ + const AttachmentDropZoneWidget({ super.key, + required this.imagePaths, this.width, this.height, - this.addAttachmentFromDropZone + this.onAttachmentDropZoneListener }); - @override - State createState() => _DropZoneWidgetState(); -} - -class _DropZoneWidgetState extends State { - - final _imagePaths = Get.find(); - - bool _isDragging = false; - @override Widget build(BuildContext context) { return DragTarget( - builder: (context, candidateData, rejectedData) { - if (_isDragging) { - return Padding( + builder: (context, _, __) { + return SizedBox( + width: width, + height: height, + child: Padding( padding: DropZoneWidgetStyle.margin, child: DottedBorder( borderType: BorderType.RRect, @@ -48,20 +41,18 @@ class _DropZoneWidgetState extends State { dashPattern: DropZoneWidgetStyle.dashSize, child: Container( clipBehavior: Clip.antiAlias, - decoration: const ShapeDecoration( + decoration: ShapeDecoration( color: DropZoneWidgetStyle.backgroundColor, - shape: RoundedRectangleBorder( + shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(DropZoneWidgetStyle.radius)), ), ), - width: widget.width, - height: widget.height, padding: DropZoneWidgetStyle.padding, alignment: AlignmentDirectional.center, child: Column( mainAxisSize: MainAxisSize.min, children: [ - SvgPicture.asset(_imagePaths.icDropZoneIcon), + SvgPicture.asset(imagePaths.icDropZoneIcon), const SizedBox(height: DropZoneWidgetStyle.space), Text( AppLocalizations.of(context).dropFileHereToAttachThem, @@ -71,22 +62,10 @@ class _DropZoneWidgetState extends State { ), ), ), - ); - } else { - return SizedBox(width: widget.width, height: widget.height); - } - }, - onAccept: widget.addAttachmentFromDropZone, - onLeave: (attachment) { - if (_isDragging) { - setState(() => _isDragging = false); - } - }, - onMove: (details) { - if (!_isDragging) { - setState(() => _isDragging = true); - } + ), + ); }, + onAccept: onAttachmentDropZoneListener ); } } diff --git a/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart index 90655a40e2..648ac86302 100644 --- a/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart @@ -42,6 +42,7 @@ class BottomBarComposerWidget extends StatelessWidget { Widget build(BuildContext context) { return Container( padding: BottomBarComposerWidgetStyle.padding, + height: BottomBarComposerWidgetStyle.height, color: BottomBarComposerWidgetStyle.backgroundColor, child: Row( children: [ diff --git a/lib/features/composer/presentation/widgets/web/local_file_drop_zone_widget.dart b/lib/features/composer/presentation/widgets/web/local_file_drop_zone_widget.dart new file mode 100644 index 0000000000..058e4a7a94 --- /dev/null +++ b/lib/features/composer/presentation/widgets/web/local_file_drop_zone_widget.dart @@ -0,0 +1,69 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/web/drop_zone_widget_style.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:desktop_drop/desktop_drop.dart'; + +typedef OnLocalFileDropZoneListener = Function(DropDoneDetails details); + +class LocalFileDropZoneWidget extends StatelessWidget { + + final ImagePaths imagePaths; + final double? width; + final double? height; + final OnLocalFileDropZoneListener? onLocalFileDropZoneListener; + + const LocalFileDropZoneWidget({ + super.key, + required this.imagePaths, + this.width, + this.height, + this.onLocalFileDropZoneListener + }); + + @override + Widget build(BuildContext context) { + return DropTarget( + onDragDone: onLocalFileDropZoneListener, + child: SizedBox( + width: width, + height: height, + child: Padding( + padding: DropZoneWidgetStyle.margin, + child: DottedBorder( + borderType: BorderType.RRect, + radius: const Radius.circular(DropZoneWidgetStyle.radius), + color: DropZoneWidgetStyle.borderColor, + strokeWidth: DropZoneWidgetStyle.borderWidth, + dashPattern: DropZoneWidgetStyle.dashSize, + child: Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: DropZoneWidgetStyle.backgroundColor, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(DropZoneWidgetStyle.radius)), + ), + ), + padding: DropZoneWidgetStyle.padding, + alignment: AlignmentDirectional.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset(imagePaths.icDropZoneIcon), + const SizedBox(height: DropZoneWidgetStyle.space), + Text( + AppLocalizations.of(context).dropFileHereToAttachThem, + style: DropZoneWidgetStyle.labelTextStyle, + ) + ] + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart index c3078af3cf..a2ac28b523 100644 --- a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart +++ b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart @@ -11,8 +11,6 @@ typedef OnChangeContentEditorAction = Function(String? text); typedef OnInitialContentEditorAction = Function(String text); typedef OnMouseDownEditorAction = Function(BuildContext context); typedef OnEditorSettingsChange = Function(EditorSettings settings); -typedef OnImageUploadSuccessAction = Function(FileUpload fileUpload); -typedef OnImageUploadFailureAction = Function(FileUpload? fileUpload, String? base64Str, UploadError error); typedef OnEditorTextSizeChanged = Function(int? size); class WebEditorWidget extends StatefulWidget { @@ -26,11 +24,10 @@ class WebEditorWidget extends StatefulWidget { final VoidCallback? onUnFocus; final OnMouseDownEditorAction? onMouseDown; final OnEditorSettingsChange? onEditorSettings; - final OnImageUploadSuccessAction? onImageUploadSuccessAction; - final OnImageUploadFailureAction? onImageUploadFailureAction; final OnEditorTextSizeChanged? onEditorTextSizeChanged; final double? width; final double? height; + final VoidCallback? onDragEnter; const WebEditorWidget({ super.key, @@ -43,11 +40,10 @@ class WebEditorWidget extends StatefulWidget { this.onUnFocus, this.onMouseDown, this.onEditorSettings, - this.onImageUploadSuccessAction, - this.onImageUploadFailureAction, this.onEditorTextSizeChanged, this.width, this.height, + this.onDragEnter, }); @override @@ -133,6 +129,7 @@ class _WebEditorState extends State { initialText: widget.content, customBodyCssStyle: HtmlUtils.customCssStyleHtmlEditor(direction: widget.direction), spellCheck: true, + disableDragAndDrop: true, webInitialScripts: UnmodifiableListView([ WebScript( name: HtmlUtils.lineHeight100Percent.name, @@ -145,7 +142,7 @@ class _WebEditorState extends State { WebScript( name: HtmlUtils.unregisterDropListener.name, script: HtmlUtils.unregisterDropListener.script, - ), + ) ]) ), htmlToolbarOptions: const HtmlToolbarOptions( @@ -154,8 +151,8 @@ class _WebEditorState extends State { ), otherOptions: OtherOptions( height: height, - dropZoneWidth: dropZoneWidth, - dropZoneHeight: dropZoneHeight, + // dropZoneWidth: dropZoneWidth, + // dropZoneHeight: dropZoneHeight, ), callbacks: Callbacks( onBeforeCommand: widget.onChangeContent, @@ -173,12 +170,12 @@ class _WebEditorState extends State { onMouseDown: () => widget.onMouseDown?.call(context), onChangeSelection: widget.onEditorSettings, onChangeCodeview: widget.onChangeContent, - onImageUpload: widget.onImageUploadSuccessAction, - onImageUploadError: widget.onImageUploadFailureAction, onTextFontSizeChanged: widget.onEditorTextSizeChanged, onPaste: () => _editorController.evaluateJavascriptWeb( HtmlUtils.lineHeight100Percent.name ), + onDragEnter: widget.onDragEnter, + onDragLeave: () {}, ), ); } diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 3848123135..94c9d11df6 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -339,9 +339,9 @@ class EmailView extends GetWidget { responsiveUtils: controller.responsiveUtils, attachments: controller.attachments, imagePaths: controller.imagePaths, - onDragStarted: controller.mailboxDashBoardController.enableDraggableApp, + onDragStarted: controller.mailboxDashBoardController.enableAttachmentDraggableApp, onDragEnd: (details) { - controller.mailboxDashBoardController.disableDraggableApp(); + controller.mailboxDashBoardController.disableAttachmentDraggableApp(); }, downloadAttachmentAction: (attachment) => controller.handleDownloadAttachmentAction(context, attachment), viewAttachmentAction: (attachment) => controller.handleViewAttachmentAction(context, attachment), @@ -376,7 +376,7 @@ class EmailView extends GetWidget { Obx(() => CalendarEventDetailWidget( calendarEvent: calendarEvent, emailContent: controller.currentEmailLoaded?.htmlContent ?? '', - isDraggableAppActive: controller.mailboxDashBoardController.isDraggableAppActive, + isDraggableAppActive: controller.mailboxDashBoardController.isAttachmentDraggableAppActive, onOpenComposerAction: controller.openNewComposerAction, onOpenNewTabAction: controller.openNewTabAction, onMailtoDelegateAction: controller.openMailToLink, @@ -407,7 +407,14 @@ class EmailView extends GetWidget { mailtoDelegate: controller.openMailToLink, direction: AppUtils.getCurrentDirection(context), ), - if (controller.mailboxDashBoardController.isDraggableAppActive) + if (controller.mailboxDashBoardController.isAttachmentDraggableAppActive) + PointerInterceptor( + child: SizedBox( + width: constraints.maxWidth, + height: constraints.maxHeight, + ) + ), + if (controller.mailboxDashBoardController.isLocalFileDraggableAppActive) PointerInterceptor( child: SizedBox( width: constraints.maxWidth, diff --git a/lib/features/email/presentation/extensions/attachment_extension.dart b/lib/features/email/presentation/extensions/attachment_extension.dart index c1d1db8ae0..27087a60da 100644 --- a/lib/features/email/presentation/extensions/attachment_extension.dart +++ b/lib/features/email/presentation/extensions/attachment_extension.dart @@ -1,38 +1,7 @@ -import 'package:core/data/constants/constant.dart'; -import 'package:core/domain/extensions/media_type_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; -import 'package:http_parser/http_parser.dart'; import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart'; extension AttachmentExtension on Attachment { - - String getIcon(ImagePaths imagePaths, {MediaType? fileMediaType}) { - final mediaType = type ?? fileMediaType; - - if (isDisplayedPDFIcon) { - return imagePaths.icFilePdf; - } - - if (mediaType == null) { - return imagePaths.icFileEPup; - } - if (mediaType.isDocFile()) { - return imagePaths.icFileDocx; - } else if (mediaType.isExcelFile()) { - return imagePaths.icFileXlsx; - } else if (mediaType.isPowerPointFile()) { - return imagePaths.icFilePptx; - } else if (mediaType.isPdfFile()) { - return imagePaths.icFilePdf; - } else if (mediaType.isZipFile()) { - return imagePaths.icFileZip; - } else if (mediaType.isImageFile()) { - return imagePaths.icFilePng; - } - return imagePaths.icFileEPup; - } - - bool get isDisplayedPDFIcon => type?.mimeType == Constant.pdfMimeType - || (type?.mimeType == Constant.octetStreamMimeType - && name?.endsWith(Constant.pdfExtension) == true); + String getIcon(ImagePaths imagePaths) => type?.getIcon(imagePaths, fileName: name) ?? imagePaths.icFileEPup; } \ No newline at end of file diff --git a/lib/features/login/data/network/interceptors/authorization_interceptors.dart b/lib/features/login/data/network/interceptors/authorization_interceptors.dart index 1e75a46e33..34d21b5db6 100644 --- a/lib/features/login/data/network/interceptors/authorization_interceptors.dart +++ b/lib/features/login/data/network/interceptors/authorization_interceptors.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:dio/dio.dart'; -import 'package:get/get_connect/http/src/request/request.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/authentication_type.dart'; import 'package:model/account/password.dart'; @@ -122,8 +121,6 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { final uploadExtra = extraInRequest[FileUploader.uploadAttachmentExtraKey]; requestOptions.headers[HttpHeaders.authorizationHeader] = _getTokenAsBearerHeader(_token!.token); - requestOptions.headers[HttpHeaders.contentTypeHeader] = uploadExtra[FileUploader.typeExtraKey]; - requestOptions.headers[HttpHeaders.contentLengthHeader] = uploadExtra[FileUploader.sizeExtraKey]; final newOptions = Options( method: requestOptions.method, @@ -155,11 +152,16 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { } Stream>? _getDataUploadRequest(dynamic mapUploadExtra) { - final currentPlatform = mapUploadExtra[FileUploader.platformExtraKey]; - if (currentPlatform == 'web') { - return BodyBytesStream.fromBytes(mapUploadExtra[FileUploader.bytesExtraKey]); - } else { - return File(mapUploadExtra[FileUploader.filePathExtraKey]).openRead(); + try { + String? filePath = mapUploadExtra[FileUploader.filePathExtraKey]; + if (filePath?.isNotEmpty == true) { + return File(filePath!).openRead(); + } else { + return mapUploadExtra[FileUploader.streamDataExtraKey]; + } + } catch(e) { + log('AuthorizationInterceptors::_getDataUploadRequest: Exception = $e'); + return null; } } diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 7b259b794d..4314ec2e25 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -216,8 +216,9 @@ class MailboxDashBoardController extends ReloadableController { final searchMailboxActivated = RxBool(false); final listSendingEmails = RxList(); final refreshingMailboxState = Rx>(Right(UIState.idle)); - final draggableAppState = Rxn(); + final attachmentDraggableAppState = Rxn(); final isRecoveringDeletedMessage = RxBool(false); + final localFileDraggableAppState = Rxn(); Session? sessionCurrent; Map mapDefaultMailboxIdByRole = {}; @@ -2148,14 +2149,16 @@ class MailboxDashBoardController extends ReloadableController { openMailboxAction(inboxPresentation); } - bool get isDraggableAppActive => draggableAppState.value == DraggableAppState.active; + bool get isAttachmentDraggableAppActive => attachmentDraggableAppState.value == DraggableAppState.active; - void enableDraggableApp() { - draggableAppState.value = DraggableAppState.active; + bool get isLocalFileDraggableAppActive => localFileDraggableAppState.value == DraggableAppState.active; + + void enableAttachmentDraggableApp() { + attachmentDraggableAppState.value = DraggableAppState.active; } - void disableDraggableApp() { - draggableAppState.value = DraggableAppState.inActive; + void disableAttachmentDraggableApp() { + attachmentDraggableAppState.value = DraggableAppState.inActive; } void saveEmailToDraft({required SaveToDraftArguments arguments}) { diff --git a/lib/features/upload/data/network/file_uploader.dart b/lib/features/upload/data/network/file_uploader.dart index 46fc1f8579..c6715357ff 100644 --- a/lib/features/upload/data/network/file_uploader.dart +++ b/lib/features/upload/data/network/file_uploader.dart @@ -27,10 +27,7 @@ import 'package:worker_manager/worker_manager.dart' as worker; class FileUploader { static const String uploadAttachmentExtraKey = 'upload-attachment'; - static const String platformExtraKey = 'platform'; - static const String bytesExtraKey = 'bytes'; - static const String typeExtraKey = 'type'; - static const String sizeExtraKey = 'size'; + static const String streamDataExtraKey = 'streamData'; static const String filePathExtraKey = 'path'; final DioClient _dioClient; @@ -94,10 +91,7 @@ class FileUploader { final mapExtra = { uploadAttachmentExtraKey: { - platformExtraKey: 'mobile', filePathExtraKey: argsUpload.mobileFileUpload.filePath, - typeExtraKey: argsUpload.mobileFileUpload.mimeType, - sizeExtraKey: argsUpload.mobileFileUpload.fileSize, } }; @@ -109,7 +103,7 @@ class FileUploader { ), data: File(argsUpload.mobileFileUpload.filePath).openRead(), onSendProgress: (count, total) { - log('FileUploader::_handleUploadAttachmentAction():onSendProgress: [${argsUpload.uploadId.id}] = $count'); + log('FileUploader::_handleUploadAttachmentAction():onSendProgress: FILE[${argsUpload.uploadId.id}] : { PROGRESS = $count | TOTAL = $total}'); sendPort.send( UploadingAttachmentUploadState( argsUpload.uploadId, @@ -139,10 +133,7 @@ class FileUploader { final mapExtra = { uploadAttachmentExtraKey: { - platformExtraKey: 'web', - bytesExtraKey: fileInfo.bytes, - typeExtraKey: fileInfo.mimeType, - sizeExtraKey: fileInfo.fileSize, + streamDataExtraKey: BodyBytesStream.fromBytes(fileInfo.bytes!), } }; @@ -155,7 +146,7 @@ class FileUploader { data: BodyBytesStream.fromBytes(fileInfo.bytes!), cancelToken: cancelToken, onSendProgress: (count, total) { - log('FileUploader::_handleUploadAttachmentActionOnWeb():onSendProgress: [${uploadId.id}] = $count'); + log('FileUploader::_handleUploadAttachmentActionOnWeb():onSendProgress: FILE[${uploadId.id}] : { PROGRESS = $count | TOTAL = $total}'); onSendController.add( Right(UploadingAttachmentUploadState( uploadId, diff --git a/lib/features/upload/domain/exceptions/pick_file_exception.dart b/lib/features/upload/domain/exceptions/pick_file_exception.dart new file mode 100644 index 0000000000..0620cf3a1e --- /dev/null +++ b/lib/features/upload/domain/exceptions/pick_file_exception.dart @@ -0,0 +1 @@ +class PickFileCanceledException implements Exception {} \ No newline at end of file diff --git a/lib/features/upload/domain/extensions/file_info_extension.dart b/lib/features/upload/domain/extensions/file_info_extension.dart index 58866921c4..ce079bacdf 100644 --- a/lib/features/upload/domain/extensions/file_info_extension.dart +++ b/lib/features/upload/domain/extensions/file_info_extension.dart @@ -3,5 +3,15 @@ import 'package:model/upload/file_info.dart'; import 'package:tmail_ui_user/features/upload/domain/model/mobile_file_upload.dart'; extension FileInfoExtension on FileInfo { - MobileFileUpload toMobileFileUpload() => MobileFileUpload(fileName, filePath, fileSize, mimeType); + MobileFileUpload toMobileFileUpload() => MobileFileUpload(fileName, filePath!, fileSize, mimeType); + + FileInfo withInline() => FileInfo( + fileName: fileName, + fileSize: fileSize, + filePath: filePath, + bytes: bytes, + readStream: readStream, + type: type, + isInline: true, + isShared: isShared); } \ No newline at end of file diff --git a/lib/features/upload/domain/extensions/list_file_info_extension.dart b/lib/features/upload/domain/extensions/list_file_info_extension.dart index 438a30a6af..3934f631d6 100644 --- a/lib/features/upload/domain/extensions/list_file_info_extension.dart +++ b/lib/features/upload/domain/extensions/list_file_info_extension.dart @@ -3,5 +3,5 @@ import 'package:model/upload/file_info.dart'; extension ListFileInfoExtension on List { List get listSize => map((file) => file.fileSize).toList(); - num get totalSize => listSize.reduce((sum, size) => sum + size); + num get totalSize => listSize.isEmpty ? 0 : listSize.reduce((sum, size) => sum + size); } \ No newline at end of file diff --git a/lib/features/upload/domain/extensions/media_type_extension.dart b/lib/features/upload/domain/extensions/media_type_extension.dart new file mode 100644 index 0000000000..e3bfcc8d80 --- /dev/null +++ b/lib/features/upload/domain/extensions/media_type_extension.dart @@ -0,0 +1,28 @@ +import 'package:core/domain/extensions/media_type_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:http_parser/http_parser.dart'; + +extension MediaTypeExtension on MediaType { + String getIcon(ImagePaths imagePaths, {String? fileName}) { + if (validatePDFIcon(fileName: fileName) == true) { + return imagePaths.icFilePdf; + } else if (isDocFile()) { + return imagePaths.icFileDocx; + } else if (isExcelFile()) { + return imagePaths.icFileXlsx; + } else if (isPowerPointFile()) { + return imagePaths.icFilePptx; + } else if (isPdfFile()) { + return imagePaths.icFilePdf; + } else if (isZipFile()) { + return imagePaths.icFileZip; + } else if (isImageFile()) { + return imagePaths.icFilePng; + } else { + return imagePaths.icFileEPup; + } + } + + bool validatePDFIcon({required String? fileName}) => mimeType == 'application/pdf' || + (mimeType == 'application/octet-stream' && fileName?.endsWith('.pdf') == true); +} diff --git a/lib/features/upload/domain/extensions/platform_file_extension.dart b/lib/features/upload/domain/extensions/platform_file_extension.dart new file mode 100644 index 0000000000..f24da3baa3 --- /dev/null +++ b/lib/features/upload/domain/extensions/platform_file_extension.dart @@ -0,0 +1,13 @@ +import 'package:core/utils/platform_info.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:model/upload/file_info.dart'; + +extension PlatformFileExtension on PlatformFile { + FileInfo toFileInfo() => FileInfo( + fileName: name, + fileSize: size, + filePath: PlatformInfo.isWeb ? '' : path ?? '', + bytes: bytes, + readStream: readStream + ); +} \ No newline at end of file diff --git a/lib/features/upload/domain/state/attachment_upload_state.dart b/lib/features/upload/domain/state/attachment_upload_state.dart index b5f0ee2f57..5b5b20cdfe 100644 --- a/lib/features/upload/domain/state/attachment_upload_state.dart +++ b/lib/features/upload/domain/state/attachment_upload_state.dart @@ -31,15 +31,11 @@ class SuccessAttachmentUploadState extends Success { final UploadTaskId uploadId; final Attachment attachment; final FileInfo fileInfo; - final bool fromFileShared; SuccessAttachmentUploadState( this.uploadId, this.attachment, - this.fileInfo, - { - this.fromFileShared = false - } + this.fileInfo ); @override @@ -47,7 +43,6 @@ class SuccessAttachmentUploadState extends Success { uploadId, attachment, fileInfo, - fromFileShared, ]; } diff --git a/lib/features/upload/domain/state/local_file_picker_state.dart b/lib/features/upload/domain/state/local_file_picker_state.dart index a2d948fe58..37745e9897 100644 --- a/lib/features/upload/domain/state/local_file_picker_state.dart +++ b/lib/features/upload/domain/state/local_file_picker_state.dart @@ -2,6 +2,8 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:model/upload/file_info.dart'; +class LocalFilePickerLoading extends LoadingState {} + class LocalFilePickerSuccess extends UIState { final List pickedFiles; @@ -14,6 +16,4 @@ class LocalFilePickerSuccess extends UIState { class LocalFilePickerFailure extends FeatureFailure { LocalFilePickerFailure(dynamic exception) : super(exception: exception); -} - -class LocalFilePickerCancel extends FeatureFailure {} \ No newline at end of file +} \ No newline at end of file diff --git a/lib/features/upload/domain/state/local_image_picker_state.dart b/lib/features/upload/domain/state/local_image_picker_state.dart new file mode 100644 index 0000000000..d48ac02453 --- /dev/null +++ b/lib/features/upload/domain/state/local_image_picker_state.dart @@ -0,0 +1,19 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/upload/file_info.dart'; + +class LocalImagePickerLoading extends LoadingState {} + +class LocalImagePickerSuccess extends UIState { + final FileInfo fileInfo; + + LocalImagePickerSuccess(this.fileInfo); + + @override + List get props => [fileInfo]; +} + +class LocalImagePickerFailure extends FeatureFailure { + + LocalImagePickerFailure(dynamic exception) : super(exception: exception); +} diff --git a/lib/features/upload/domain/usecases/local_file_picker_interactor.dart b/lib/features/upload/domain/usecases/local_file_picker_interactor.dart index c617892676..dd7cf46fab 100644 --- a/lib/features/upload/domain/usecases/local_file_picker_interactor.dart +++ b/lib/features/upload/domain/usecases/local_file_picker_interactor.dart @@ -4,7 +4,8 @@ import 'package:core/presentation/state/success.dart'; import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:model/upload/file_info.dart'; +import 'package:tmail_ui_user/features/upload/domain/exceptions/pick_file_exception.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/platform_file_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/state/local_file_picker_state.dart'; class LocalFilePickerInteractor { @@ -13,23 +14,22 @@ class LocalFilePickerInteractor { Stream> execute({FileType fileType = FileType.any}) async* { try { + yield Right(LocalFilePickerLoading()); + final filesResult = await FilePicker.platform.pickFiles( type: fileType, allowMultiple: true, - withData: PlatformInfo.isWeb + withData: PlatformInfo.isWeb, + withReadStream: PlatformInfo.isMobile ); - if (filesResult != null && filesResult.files.isNotEmpty) { - final fileInfoResults = filesResult.files - .map((platformFile) => FileInfo( - platformFile.name, - PlatformInfo.isWeb ? '' : platformFile.path ?? '', - platformFile.size, - bytes: PlatformInfo.isWeb ? platformFile.bytes : null - )) - .toList(); - yield Right(LocalFilePickerSuccess(fileInfoResults)); + + if (filesResult?.files.isNotEmpty == true) { + final listFileInfo = filesResult!.files + .map((platformFile) => platformFile.toFileInfo()) + .toList(); + yield Right(LocalFilePickerSuccess(listFileInfo)); } else { - yield Left(LocalFilePickerCancel()); + yield Left(LocalFilePickerFailure(PickFileCanceledException())); } } catch (exception) { yield Left(LocalFilePickerFailure(exception)); diff --git a/lib/features/upload/domain/usecases/local_image_picker_interactor.dart b/lib/features/upload/domain/usecases/local_image_picker_interactor.dart new file mode 100644 index 0000000000..ca91a31be7 --- /dev/null +++ b/lib/features/upload/domain/usecases/local_image_picker_interactor.dart @@ -0,0 +1,35 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:dartz/dartz.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:tmail_ui_user/features/upload/domain/exceptions/pick_file_exception.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/platform_file_extension.dart'; +import 'package:tmail_ui_user/features/upload/domain/state/local_image_picker_state.dart'; + +class LocalImagePickerInteractor { + + LocalImagePickerInteractor(); + + Stream> execute() async* { + try { + yield Right(LocalImagePickerLoading()); + + final filePickerResult = await FilePicker.platform.pickFiles( + type: FileType.image, + allowMultiple: false, + withReadStream: PlatformInfo.isMobile, + withData: PlatformInfo.isWeb + ); + + if (filePickerResult?.files.isNotEmpty == true) { + final fileInfo = filePickerResult!.files.first.toFileInfo(); + yield Right(LocalImagePickerSuccess(fileInfo)); + } else { + yield Left(LocalImagePickerFailure(PickFileCanceledException())); + } + } catch (exception) { + yield Left(LocalImagePickerFailure(exception)); + } + } +} \ No newline at end of file diff --git a/lib/features/upload/presentation/controller/upload_controller.dart b/lib/features/upload/presentation/controller/upload_controller.dart index 38342ba401..f59ef21887 100644 --- a/lib/features/upload/presentation/controller/upload_controller.dart +++ b/lib/features/upload/presentation/controller/upload_controller.dart @@ -14,13 +14,13 @@ import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; import 'package:model/email/attachment.dart'; import 'package:model/extensions/attachment_extension.dart'; -import 'package:model/extensions/list_attachment_extension.dart'; import 'package:model/upload/file_info.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/state/base_ui_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/upload_attachment_state.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachment_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_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/presentation/extensions/upload_attachment_extension.dart'; @@ -80,7 +80,7 @@ class UploadController extends BaseController { failure.uploadId, (currentState) => currentState?.copyWith(uploadStatus: UploadFileStatus.uploadFailed)); deleteFileUploaded(failure.uploadId); - _handleUploadAttachmentsFailure(failure); + _showToastMessageWhenUploadAttachmentsFailure(failure); } }, (success) { @@ -115,7 +115,7 @@ class UploadController extends BaseController { ); _refreshListUploadAttachmentState(); - _handleUploadAttachmentsSuccess(success); + _showToastMessageWhenUploadAttachmentsSuccess(success); } } ); @@ -162,7 +162,7 @@ class UploadController extends BaseController { ); final uploadFileState = _uploadingStateInlineFiles.getUploadFileStateById(success.uploadId); - log('UploadController::_handleProgressUploadInlineImageStateStream:uploadId: ${uploadFileState?.uploadTaskId} | fromFileShared: ${uploadFileState?.fromFileShared}'); + log('UploadController::_handleProgressUploadInlineImageStateStream:uploadId: ${uploadFileState?.uploadTaskId} | fromFileShared: ${uploadFileState?.file?.isShared}'); _uploadingStateInlineFiles.updateElementByUploadTaskId( success.uploadId, @@ -179,7 +179,6 @@ class UploadController extends BaseController { success.uploadId, inlineAttachment, success.fileInfo, - fromFileShared: uploadFileState?.fromFileShared ?? false ); _handleUploadInlineAttachmentsSuccess(newUploadSuccess); } @@ -203,27 +202,24 @@ class UploadController extends BaseController { _refreshListUploadAttachmentState(); } - Future justUploadAttachmentsAction(List uploadFiles, Uri uploadUri) { + Future justUploadAttachmentsAction({ + required List uploadFiles, + required Uri uploadUri, + }) { return Future.forEach(uploadFiles, (uploadFile) async { - await uploadFileAction(uploadFile, uploadUri); + await uploadFileAction(uploadFile: uploadFile, uploadUri: uploadUri); }); } - Future uploadFileAction( - FileInfo uploadFile, - Uri uploadUri, - { - bool isInline = false, - bool fromFileShared = false, - } - ) { - log('UploadController::_uploadFile():fileName: ${uploadFile.fileName} | isInline: $isInline | fromFileShared: $fromFileShared'); + Future uploadFileAction({ + required FileInfo uploadFile, + required Uri uploadUri, + }) { + log('UploadController::_uploadFile():fileName: ${uploadFile.fileName} | mimeType: ${uploadFile.mimeType} | isInline: ${uploadFile.isInline} | fromFileShared: ${uploadFile.isShared}'); consumeState(_uploadAttachmentInteractor.execute( uploadFile, uploadUri, cancelToken: CancelToken(), - isInline: isInline, - fromFileShared: fromFileShared )); return Future.value(); } @@ -244,6 +240,16 @@ class UploadController extends BaseController { .toList(); } + List get attachmentsPicked { + if (listUploadAttachments.isEmpty) { + return List.empty(); + } + return listUploadAttachments + .map((fileState) => fileState.file) + .whereNotNull() + .toList(); + } + Set? generateAttachments() { if (attachmentsUploaded.isEmpty) { return null; @@ -254,7 +260,7 @@ class UploadController extends BaseController { .toSet(); } - void _handleUploadAttachmentsFailure(ErrorAttachmentUploadState failure) { + void _showToastMessageWhenUploadAttachmentsFailure(ErrorAttachmentUploadState failure) { if (currentContext != null && currentOverlayContext != null) { appToast.showToastErrorMessage( currentOverlayContext!, @@ -264,7 +270,7 @@ class UploadController extends BaseController { } } - void _handleUploadAttachmentsSuccess(SuccessAttachmentUploadState success) { + void _showToastMessageWhenUploadAttachmentsSuccess(SuccessAttachmentUploadState success) { if (currentContext != null && currentOverlayContext != null && _uploadingStateFiles.allSuccess) { appToast.showToastSuccessMessage( currentOverlayContext!, @@ -275,7 +281,7 @@ class UploadController extends BaseController { } bool isExceededMaxSizeAttachmentsPerEmail({num totalSizePreparedFiles = 0}) { - final currentTotalSize = attachmentsUploaded.totalSize + inlineAttachmentsUploaded.totalSize + totalSizePreparedFiles; + final currentTotalSize = attachmentsPicked.totalSize + inlineAttachmentsPicked.totalSize + totalSizePreparedFiles; final maxSizeAttachmentsPerEmail = _mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value; log('UploadController::isExceededMaxSizeAttachmentsPerEmail(): currentTotalSize = $currentTotalSize | maxSizeAttachmentsPerEmail = $maxSizeAttachmentsPerEmail'); if (maxSizeAttachmentsPerEmail != null) { @@ -285,16 +291,17 @@ class UploadController extends BaseController { } } - bool isExceededMaxSizeFilesAttachedInComposer({num totalSizePreparedFiles = 0}) { - final currentTotalSizeAttachments = attachmentsUploaded.totalSize + totalSizePreparedFiles; - const maximumBytesSizeFileAttachedInComposer = AppConfig.maximumMegabytesSizeFileAttachedInComposer * 1024 * 1024; + bool isExceededWarningAttachmentFileSizeInComposer({num totalSizePreparedFiles = 0}) { + final currentTotalSizeAttachments = attachmentsPicked.totalSize + totalSizePreparedFiles; + const maximumBytesSizeFileAttachedInComposer = AppConfig.warningAttachmentFileSizeInMegabytes * 1024 * 1024; log('UploadController::isExceededMaxSizeFilesAttachedInComposer(): currentTotalSizeAttachments = $currentTotalSizeAttachments | maximumBytesSizeFileAttachedInComposer = $maximumBytesSizeFileAttachedInComposer'); return currentTotalSizeAttachments > maximumBytesSizeFileAttachedInComposer; } void validateTotalSizeAttachmentsBeforeUpload({ required num totalSizePreparedFiles, - VoidCallback? callbackAction + num? totalSizePreparedFilesWithDispositionAttachment, + VoidCallback? onValidationSuccess }) { log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: totalSizePreparedFiles = $totalSizePreparedFiles'); if (isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: totalSizePreparedFiles)) { @@ -307,7 +314,7 @@ class UploadController extends BaseController { return; } - if (isExceededMaxSizeFilesAttachedInComposer(totalSizePreparedFiles: totalSizePreparedFiles)) { + if (isExceededWarningAttachmentFileSizeInComposer(totalSizePreparedFiles: totalSizePreparedFilesWithDispositionAttachment ?? totalSizePreparedFiles)) { if (currentContext == null) { log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: CONTEXT IS NULL'); return; @@ -315,12 +322,34 @@ class UploadController extends BaseController { _showWarningDialogWhenExceededMaxSizeFilesAttachedInComposer( context: currentContext!, - confirmAction: callbackAction + confirmAction: () async { + await Future.delayed( + const Duration(milliseconds: 100), + onValidationSuccess + ); + } ); return; } - callbackAction?.call(); + onValidationSuccess?.call(); + } + + void validateTotalSizeInlineAttachmentsBeforeUpload({ + required num totalSizePreparedFiles, + VoidCallback? onValidationSuccess + }) { + if (isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: totalSizePreparedFiles)) { + if (currentContext == null) { + log('UploadController::validateTotalSizeInlineAttachmentsBeforeUpload: CONTEXT IS NULL'); + return; + } + + _showConfirmDialogWhenExceededMaxSizeAttachmentsPerEmail(context: currentContext!); + return; + } + + onValidationSuccess?.call(); } void _showConfirmDialogWhenExceededMaxSizeAttachmentsPerEmail({required BuildContext context}) { @@ -340,7 +369,7 @@ class UploadController extends BaseController { showConfirmDialogAction( context, title: '', - AppLocalizations.of(context).messageWarningDialogWhenExceedMaximumFileSizeComposer, + AppLocalizations.of(context).warningMessageWhenExceedGenerallySizeInComposer, AppLocalizations.of(context).continueAction, cancelTitle: AppLocalizations.of(context).cancel, alignCenter: true, @@ -394,6 +423,17 @@ class UploadController extends BaseController { .toList(); } + List get inlineAttachmentsPicked { + if (_uploadingStateInlineFiles.uploadingStateFiles.isEmpty) { + return List.empty(); + } + return _uploadingStateInlineFiles.uploadingStateFiles + .whereNotNull() + .map((fileState) => fileState.file) + .whereNotNull() + .toList(); + } + Map get mapInlineAttachments { final inlineAttachments = _uploadingStateInlineFiles.uploadingStateFiles .whereNotNull() @@ -415,42 +455,47 @@ class UploadController extends BaseController { return mapInlineAttachments; } + void _handleUploadAttachmentFailure(UploadAttachmentFailure failure) { + if (currentContext != null && currentOverlayContext != null) { + appToast.showToastErrorMessage( + currentOverlayContext!, + failure.fileInfo.isInline == true + ? AppLocalizations.of(currentContext!).thisImageCannotBeAdded + : AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments, + leadingSVGIconColor: Colors.white, + leadingSVGIcon: failure.fileInfo.isInline == true + ? imagePaths.icInsertImage + : imagePaths.icAttachment + ); + } + } + + void _handleUploadAttachmentSuccess(UploadAttachmentSuccess success) async { + if (success.uploadAttachment.fileInfo.isInline == true) { + _uploadingStateInlineFiles.add(success.uploadAttachment.toUploadFileState()); + await _progressUploadInlineImageStateStreamGroup.add(success.uploadAttachment.progressState); + } else { + _uploadingStateFiles.add(success.uploadAttachment.toUploadFileState()); + await _progressUploadStateStreamGroup.add(success.uploadAttachment.progressState); + _refreshListUploadAttachmentState(); + } + } + @override - void handleFailureViewState(Failure failure) async { - super.handleFailureViewState(failure); + void handleFailureViewState(Failure failure) { if (failure is UploadAttachmentFailure) { - if (failure.isInline) { - if (currentContext != null && currentOverlayContext != null) { - appToast.showToastErrorMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).thisImageCannotBeAdded, - leadingSVGIconColor: Colors.white, - leadingSVGIcon: imagePaths.icInsertImage); - } - } else { - if (currentContext != null && currentOverlayContext != null) { - appToast.showToastErrorMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments, - leadingSVGIconColor: Colors.white, - leadingSVGIcon: imagePaths.icAttachment); - } - } + _handleUploadAttachmentFailure(failure); + } else { + super.handleFailureViewState(failure); } } @override void handleSuccessViewState(Success success) async { - super.handleSuccessViewState(success); if (success is UploadAttachmentSuccess) { - if (success.isInline) { - _uploadingStateInlineFiles.add(success.uploadAttachment.toUploadFileState(fromFileShared: success.fromFileShared)); - await _progressUploadInlineImageStateStreamGroup.add(success.uploadAttachment.progressState); - } else { - _uploadingStateFiles.add(success.uploadAttachment.toUploadFileState()); - await _progressUploadStateStreamGroup.add(success.uploadAttachment.progressState); - _refreshListUploadAttachmentState(); - } + _handleUploadAttachmentSuccess(success); + } else { + super.handleSuccessViewState(success); } } } \ No newline at end of file diff --git a/lib/features/upload/presentation/extensions/upload_attachment_extension.dart b/lib/features/upload/presentation/extensions/upload_attachment_extension.dart index 4b45c9427b..64feed8fe7 100644 --- a/lib/features/upload/presentation/extensions/upload_attachment_extension.dart +++ b/lib/features/upload/presentation/extensions/upload_attachment_extension.dart @@ -4,12 +4,11 @@ import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_sta extension UploadAttachmentExtension on UploadAttachment { - UploadFileState toUploadFileState({bool fromFileShared = false}) { + UploadFileState toUploadFileState() { return UploadFileState( uploadTaskId, file: fileInfo, cancelToken: cancelToken, - fromFileShared: fromFileShared, ); } } \ No newline at end of file diff --git a/lib/features/upload/presentation/model/upload_file_state.dart b/lib/features/upload/presentation/model/upload_file_state.dart index 20876fbe43..f0e58b78c6 100644 --- a/lib/features/upload/presentation/model/upload_file_state.dart +++ b/lib/features/upload/presentation/model/upload_file_state.dart @@ -1,11 +1,12 @@ import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:dio/dio.dart'; import 'package:equatable/equatable.dart'; import 'package:http_parser/http_parser.dart'; import 'package:model/email/attachment.dart'; import 'package:model/upload/file_info.dart'; -import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_status.dart'; @@ -17,7 +18,6 @@ class UploadFileState with EquatableMixin { final int uploadingProgress; final Attachment? attachment; final CancelToken? cancelToken; - final bool fromFileShared; UploadFileState( this.uploadTaskId, @@ -27,7 +27,6 @@ class UploadFileState with EquatableMixin { this.uploadingProgress = 0, this.attachment, this.cancelToken, - this.fromFileShared = false, } ); @@ -38,7 +37,6 @@ class UploadFileState with EquatableMixin { int? uploadingProgress, Attachment? attachment, CancelToken? cancelToken, - bool? fromFileShared, }) { return UploadFileState( uploadTaskId ?? this.uploadTaskId, @@ -47,7 +45,6 @@ class UploadFileState with EquatableMixin { uploadingProgress: uploadingProgress ?? this.uploadingProgress, attachment: attachment ?? this.attachment, cancelToken: cancelToken ?? this.cancelToken, - fromFileShared: fromFileShared ?? this.fromFileShared ); } @@ -72,11 +69,16 @@ class UploadFileState with EquatableMixin { double get percentUploading => uploadingProgress / 100; String getIcon(ImagePaths imagePaths) { - var mediaType = attachment?.type; - if (mediaType == null && file != null) { - mediaType = MediaType.parse(file!.mimeType); + try { + MediaType? mediaType = attachment?.type; + if (mediaType == null && file != null) { + mediaType = MediaType.parse(file!.mimeType); + } + return mediaType?.getIcon(imagePaths, fileName: fileName) ?? imagePaths.icFileEPup; + } catch (e) { + logError('UploadFileState::getIcon: Exception: $e'); + return imagePaths.icFileEPup; } - return attachment?.getIcon(imagePaths, fileMediaType: mediaType) ?? imagePaths.icFileEPup; } @override @@ -87,6 +89,5 @@ class UploadFileState with EquatableMixin { uploadingProgress, attachment, cancelToken, - fromFileShared, ]; } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index fe33404bee..9b9fd9e7a2 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -3239,8 +3239,8 @@ "placeholders_order": [], "placeholders": {} }, - "messageWarningDialogWhenExceedMaximumFileSizeComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.", - "@messageWarningDialogWhenExceedMaximumFileSizeComposer": { + "warningMessageWhenExceedGenerallySizeInComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.", + "@warningMessageWhenExceedGenerallySizeInComposer": { "type": "text", "placeholders_order": [], "placeholders": {} diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 6d98e60e12..a5c8dad9b2 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -3936,8 +3936,8 @@ "placeholders_order": [], "placeholders": {} }, - "messageWarningDialogWhenExceedMaximumFileSizeComposer": "Votre message dépasse la taille généralement acceptée pour un email. \nSi vous confirmez l'envoi il y a un risque que le mail soit rejeté par le système de votre interlocuteur.", - "@messageWarningDialogWhenExceedMaximumFileSizeComposer": { + "warningMessageWhenExceedGenerallySizeInComposer": "Votre message dépasse la taille généralement acceptée pour un email. \nSi vous confirmez l'envoi il y a un risque que le mail soit rejeté par le système de votre interlocuteur.", + "@warningMessageWhenExceedGenerallySizeInComposer": { "type": "text", "placeholders_order": [], "placeholders": {} diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 0779e91730..8675ed7ae7 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-02-29T16:08:18.479839", + "@@last_modified": "2024-03-15T21:34:52.177175", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3750,8 +3750,8 @@ "placeholders_order": [], "placeholders": {} }, - "messageWarningDialogWhenExceedMaximumFileSizeComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.", - "@messageWarningDialogWhenExceedMaximumFileSizeComposer": { + "warningMessageWhenExceedGenerallySizeInComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.", + "@warningMessageWhenExceedGenerallySizeInComposer": { "type": "text", "placeholders_order": [], "placeholders": {} @@ -3761,5 +3761,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "thisFileCannotBePicked": "This file cannot be picked.", + "@thisFileCannotBePicked": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "loadingPleaseWait": "Loading... Please wait!", + "@loadingPleaseWait": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_vi.arb b/lib/l10n/intl_vi.arb index 07f4cf948b..8a0cf56058 100644 --- a/lib/l10n/intl_vi.arb +++ b/lib/l10n/intl_vi.arb @@ -3944,6 +3944,8 @@ }, "messageWarningDialogWhenExceedMaximumFileSizeComposer": "Thư của bạn lớn hơn kích thước thường được hệ thống email của bên thứ ba chấp nhận. Nếu bạn xác nhận đã gửi thư này, có nguy cơ nó sẽ bị hệ thống người nhận từ chối.", "@messageWarningDialogWhenExceedMaximumFileSizeComposer": { + "warningMessageWhenExceedGenerallySizeInComposer": "Thư của bạn lớn hơn kích thước thường được hệ thống email của bên thứ ba chấp nhận. Nếu bạn xác nhận đã gửi thư này, có nguy cơ nó sẽ bị hệ thống người nhận từ chối.", + "@warningMessageWhenExceedGenerallySizeInComposer": { "type": "text", "placeholders_order": [], "placeholders": {} diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 5c446ce037..ff1b9d64f9 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3917,10 +3917,10 @@ class AppLocalizations { ); } - String get messageWarningDialogWhenExceedMaximumFileSizeComposer { + String get warningMessageWhenExceedGenerallySizeInComposer { return Intl.message( 'Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.', - name: 'messageWarningDialogWhenExceedMaximumFileSizeComposer', + name: 'warningMessageWhenExceedGenerallySizeInComposer', ); } @@ -3930,4 +3930,18 @@ class AppLocalizations { name: 'continueAction', ); } + + String get thisFileCannotBePicked { + return Intl.message( + 'This file cannot be picked.', + name: 'thisFileCannotBePicked', + ); + } + + String get loadingPleaseWait { + return Intl.message( + 'Loading... Please wait!', + name: 'loadingPleaseWait', + ); + } } \ No newline at end of file diff --git a/lib/main/utils/app_config.dart b/lib/main/utils/app_config.dart index f4f65fa6bb..bff358776c 100644 --- a/lib/main/utils/app_config.dart +++ b/lib/main/utils/app_config.dart @@ -8,7 +8,7 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class AppConfig { static const int limitCharToStartSearch = 3; - static const int maximumMegabytesSizeFileAttachedInComposer = 10; + static const int warningAttachmentFileSizeInMegabytes = 10; static const String appDashboardConfigurationPath = "configurations/app_dashboard.json"; static const String appFCMConfigurationPath = "configurations/env.fcm"; diff --git a/model/lib/upload/file_info.dart b/model/lib/upload/file_info.dart index 924f5e86e2..fa79b85fcf 100644 --- a/model/lib/upload/file_info.dart +++ b/model/lib/upload/file_info.dart @@ -1,32 +1,65 @@ - import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:mime/mime.dart'; class FileInfo with EquatableMixin { final String fileName; - final String filePath; final int fileSize; + final String? filePath; final Uint8List? bytes; + final Stream>? readStream; + final String? type; + final bool? isInline; + final bool? isShared; - FileInfo(this.fileName, this.filePath, this.fileSize, {this.bytes}); - - factory FileInfo.empty() { - return FileInfo('', '', 0); - } + FileInfo({ + required this.fileName, + required this.fileSize, + this.filePath, + this.bytes, + this.readStream, + this.type, + this.isInline, + this.isShared, + }); factory FileInfo.fromBytes({ required Uint8List bytes, String? name, - int? size + int? size, + String? type, }) { - return FileInfo(name ?? '', '', size ?? 0, bytes: bytes); + return FileInfo( + fileName: name ?? '', + fileSize: size ?? 0, + bytes: bytes, + type: type + ); } String get fileExtension => fileName.split('.').last; - String get mimeType => lookupMimeType(kIsWeb ? fileName : filePath) ?? 'application/octet-stream'; + String get mimeType { + if (type?.isNotEmpty == true) { + return type!; + } else if (filePath?.isNotEmpty == true){ + final matchedType = lookupMimeType(filePath!, headerBytes: bytes) ?? 'application/octet-stream'; + return matchedType; + } else { + final matchedType = lookupMimeType(fileName, headerBytes: bytes) ?? 'application/octet-stream'; + return matchedType; + } + } @override - List get props => [fileName, filePath, fileSize, bytes]; + List get props => [ + fileName, + filePath, + fileSize, + bytes, + readStream, + type, + isInline, + isShared, + ]; } \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 8a2ef32cdb..0f8d80bcdb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -360,6 +360,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + desktop_drop: + dependency: "direct main" + description: + name: desktop_drop + sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d + url: "https://pub.dev" + source: hosted + version: "0.4.4" device_info_plus: dependency: "direct main" description: @@ -990,6 +998,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + future_loading_dialog: + dependency: "direct main" + description: + name: future_loading_dialog + sha256: "2718b1a308db452da32ab9bca9ad496ff92b683e217add9e92cf50520f90537e" + url: "https://pub.dev" + source: hosted + version: "0.3.0" get: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7acf157001..45f423df55 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -240,6 +240,10 @@ dependencies: mime: 1.0.4 + desktop_drop: 0.4.4 + + future_loading_dialog: 0.3.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/test/features/model/attachment_extension_test.dart b/test/features/model/attachment_extension_test.dart index 33b050a38c..2b686ec274 100644 --- a/test/features/model/attachment_extension_test.dart +++ b/test/features/model/attachment_extension_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http_parser/http_parser.dart'; -import 'package:model/model.dart'; -import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart'; +import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart'; void main() { final attachmentA = Attachment( @@ -34,7 +34,7 @@ void main() { 'WHEN perform call `attachmentA.isDisplayedPDFIcon()`\n' 'SHOULD return true', () { - bool result = attachmentA.isDisplayedPDFIcon; + bool result = attachmentA.type!.validatePDFIcon(fileName: null); expect(result, isTrue); }); @@ -45,7 +45,7 @@ void main() { 'WHEN perform call `attachmentB.isDisplayedPDFIcon()`\n' 'SHOULD return true', () { - bool result = attachmentB.isDisplayedPDFIcon; + bool result = attachmentB.type!.validatePDFIcon(fileName: 'attachmentB.pdf'); expect(result, isTrue); }); @@ -56,7 +56,7 @@ void main() { 'WHEN perform call `attachmentC.isDisplayedPDFIcon()`\n' 'SHOULD return false', () { - bool result = attachmentC.isDisplayedPDFIcon; + bool result = attachmentC.type!.validatePDFIcon(fileName: 'attachmentC.docx'); expect(result, isFalse); }); @@ -67,7 +67,7 @@ void main() { 'WHEN perform call `attachmentD.isDisplayedPDFIcon()`\n' 'SHOULD return false', () { - bool result = attachmentD.isDisplayedPDFIcon; + bool result = attachmentD.type!.validatePDFIcon(fileName: 'attachmentD.png'); expect(result, isFalse); }); @@ -78,7 +78,7 @@ void main() { 'WHEN perform call `attachmentE.isDisplayedPDFIcon()`\n' 'SHOULD return false', () { - bool result = attachmentE.isDisplayedPDFIcon; + bool result = attachmentE.type!.validatePDFIcon(fileName: 'attachmentE.pdf'); expect(result, isFalse); }); From be35b16cd8d389a1ba490c949b1d0dcc01d341f3 Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Tue, 19 Mar 2024 14:20:20 +0700 Subject: [PATCH 07/80] TF-2644 Remove wrong text in intl_vi.arb --- lib/l10n/intl_vi.arb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/l10n/intl_vi.arb b/lib/l10n/intl_vi.arb index 8a0cf56058..c17bd48988 100644 --- a/lib/l10n/intl_vi.arb +++ b/lib/l10n/intl_vi.arb @@ -3942,8 +3942,6 @@ "placeholders_order": [], "placeholders": {} }, - "messageWarningDialogWhenExceedMaximumFileSizeComposer": "Thư của bạn lớn hơn kích thước thường được hệ thống email của bên thứ ba chấp nhận. Nếu bạn xác nhận đã gửi thư này, có nguy cơ nó sẽ bị hệ thống người nhận từ chối.", - "@messageWarningDialogWhenExceedMaximumFileSizeComposer": { "warningMessageWhenExceedGenerallySizeInComposer": "Thư của bạn lớn hơn kích thước thường được hệ thống email của bên thứ ba chấp nhận. Nếu bạn xác nhận đã gửi thư này, có nguy cơ nó sẽ bị hệ thống người nhận từ chối.", "@warningMessageWhenExceedGenerallySizeInComposer": { "type": "text", From 568309bc1d2d9fe7b04e988f06284e7b6a3997af Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 5 Mar 2024 10:02:08 +0700 Subject: [PATCH 08/80] TF-2667 Use `Username` replace `UserProfile` --- .../presentation/composer_controller.dart | 54 +++++++++---------- .../model/save_to_draft_view_event.dart | 4 -- .../presentation/contact_controller.dart | 15 ++---- .../contact/presentation/contact_view.dart | 7 +-- .../controller/single_email_controller.dart | 6 +-- .../identity_creator_controller.dart | 7 +-- .../model/identity_creator_arguments.dart | 6 +-- .../datasource/authentication_datasource.dart | 3 +- .../authentication_datasource_impl.dart | 14 ++--- .../authentication_repository_impl.dart | 3 +- .../repository/authentication_repository.dart | 3 +- .../state/authentication_user_state.dart | 8 +-- .../authentication_user_interactor.dart | 8 +-- .../mailbox/presentation/mailbox_view.dart | 2 +- .../presentation/mailbox_view_web.dart | 2 +- .../widgets/user_information_widget.dart | 13 +++-- .../domain/state/get_user_profile_state.dart | 17 ------ .../mailbox_dashboard_controller.dart | 8 +-- .../controller/search_controller.dart | 20 ++++--- .../mailbox_dashboard_view_web.dart | 9 ++-- .../mixin/user_setting_popup_menu_mixin.dart | 48 +++++++++-------- .../model/search/quick_search_filter.dart | 3 +- .../widgets/search_input_form_widget.dart | 4 +- .../manage_account_dashboard_controller.dart | 13 +---- .../manage_account_dashboard_view.dart | 5 +- .../settings/settings_first_level_view.dart | 2 +- .../identities/identities_controller.dart | 9 ++-- .../presentation/search_email_controller.dart | 3 -- .../credential/credential_bindings.dart | 2 +- lib/main/localizations/app_localizations.dart | 18 +++++++ .../extensions/user_profile_extension.dart | 7 --- model/lib/extensions/username_extension.dart | 7 +++ model/lib/model.dart | 7 +-- model/lib/user/avatar_id.dart | 15 ------ model/lib/user/user_profile.dart | 15 ------ model/lib/user/user_profile_response.dart | 26 --------- 36 files changed, 148 insertions(+), 245 deletions(-) delete mode 100644 lib/features/mailbox_dashboard/domain/state/get_user_profile_state.dart delete mode 100644 model/lib/extensions/user_profile_extension.dart create mode 100644 model/lib/extensions/username_extension.dart delete mode 100644 model/lib/user/avatar_id.dart delete mode 100644 model/lib/user/user_profile.dart delete mode 100644 model/lib/user/user_profile_response.dart diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 2e878affb9..8132dcbcaa 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -19,6 +19,7 @@ import 'package:get/get.dart'; import 'package:html_editor_enhanced/html_editor.dart' as web_html_editor; import 'package:http_parser/http_parser.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; @@ -368,13 +369,13 @@ class ComposerController extends BaseController with DragDropFileMixin { }); } - void _listenBrowserEventAction() { - log('ComposerController::_listenBrowserEventAction:'); - _subscriptionOnBeforeUnload = html.window.onBeforeUnload.listen((event) async { - final userProfile = mailboxDashBoardController.userProfile.value; + void _listenBrowserTabRefresh() { + _subscriptionOnBeforeUnload = html.window.onBeforeUnload.listen((event) async { _removeComposerCacheOnWebInteractor.execute(); - if (userProfile != null) { - final draftEmail = await _generateEmail(currentContext!, userProfile); + if (mailboxDashBoardController.sessionCurrent != null) { + final draftEmail = await _generateEmail( + currentContext!, + mailboxDashBoardController.sessionCurrent!.username); _saveComposerCacheOnWebInteractor.execute(draftEmail); } }); @@ -624,17 +625,17 @@ class ComposerController extends BaseController with DragDropFileMixin { emailActionType: actionType, mailboxRole: mailboxRole ); - final userProfile = mailboxDashBoardController.userProfile.value; - if (userProfile != null) { - final isSender = presentationEmail.from.asList().every((element) => element.email == userProfile.email); + final userName = mailboxDashBoardController.sessionCurrent?.username; + if (userName != null) { + final isSender = presentationEmail.from.asList().every((element) => element.email == userName.value); if (isSender) { listToEmailAddress = List.from(recipients.value1.toSet()); listCcEmailAddress = List.from(recipients.value2.toSet()); listBccEmailAddress = List.from(recipients.value3.toSet()); } else { - listToEmailAddress = List.from(recipients.value1.toSet().filterEmailAddress(userProfile.email)); - listCcEmailAddress = List.from(recipients.value2.toSet().filterEmailAddress(userProfile.email)); - listBccEmailAddress = List.from(recipients.value3.toSet().filterEmailAddress(userProfile.email)); + listToEmailAddress = List.from(recipients.value1.toSet().filterEmailAddress(userName.value)); + listCcEmailAddress = List.from(recipients.value2.toSet().filterEmailAddress(userName.value)); + listBccEmailAddress = List.from(recipients.value3.toSet().filterEmailAddress(userName.value)); } } else { listToEmailAddress = List.from(recipients.value1.toSet()); @@ -692,7 +693,7 @@ class ComposerController extends BaseController with DragDropFileMixin { Future _generateEmail( BuildContext context, - UserProfile userProfile, + UserName userName, { bool asDrafts = false, MailboxId? draftMailboxId, @@ -700,7 +701,7 @@ class ComposerController extends BaseController with DragDropFileMixin { ComposerArguments? arguments, } ) async { - Set listFromEmailAddress = {EmailAddress(null, userProfile.email)}; + Set listFromEmailAddress = {EmailAddress(null, userName.value)}; if (identitySelected.value?.email?.isNotEmpty == true) { listFromEmailAddress = { EmailAddress( @@ -709,7 +710,7 @@ class ComposerController extends BaseController with DragDropFileMixin { ) }; } - Set listReplyToEmailAddress = {EmailAddress(null, userProfile.email)}; + Set listReplyToEmailAddress = {EmailAddress(null, userName.value)}; if (identitySelected.value?.replyTo?.isNotEmpty == true) { listReplyToEmailAddress = identitySelected.value!.replyTo!; } @@ -925,7 +926,6 @@ class ComposerController extends BaseController with DragDropFileMixin { bool get _isParamUserNull { if (composerArguments.value == null || - mailboxDashBoardController.userProfile.value == null || mailboxDashBoardController.sessionCurrent == null || mailboxDashBoardController.accountId.value == null ) { @@ -942,19 +942,18 @@ class ComposerController extends BaseController with DragDropFileMixin { return; } - final sendingArgs = await _createSendingEmailArguments(context); + final sendingArgs = await createSendingEmailArguments(context); _closeComposerAction(result: sendingArgs); } - Future _createSendingEmailArguments(BuildContext context) async { + Future createSendingEmailArguments(BuildContext context) async { final session = mailboxDashBoardController.sessionCurrent!; final arguments = composerArguments.value!; final accountId = mailboxDashBoardController.accountId.value!; - final userProfile = mailboxDashBoardController.userProfile.value!; final createdEmail = await _generateEmail( context, - userProfile, + session.username, outboxMailboxId: mailboxDashBoardController.outboxMailbox?.id, arguments: arguments ); @@ -1187,14 +1186,12 @@ class ComposerController extends BaseController with DragDropFileMixin { Future _generateSaveAsDraftsArguments(BuildContext context) async { log('ComposerController::_generateSaveAsDraftsArguments:'); final arguments = composerArguments.value; - final userProfile = mailboxDashBoardController.userProfile.value; final accountId = mailboxDashBoardController.accountId.value; final session = mailboxDashBoardController.sessionCurrent; final draftMailboxId = mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts]; if (arguments == null || draftMailboxId == null || - userProfile == null || session == null || accountId == null ) { @@ -1204,7 +1201,7 @@ class ComposerController extends BaseController with DragDropFileMixin { if (_emailIdEditing != null && _emailIdEditing != arguments.presentationEmail?.id) { final newEmail = await _generateEmail( context, - userProfile, + session.username, asDrafts: true, draftMailboxId: draftMailboxId, arguments: arguments, @@ -1226,7 +1223,7 @@ class ComposerController extends BaseController with DragDropFileMixin { if (isChanged && context.mounted) { final newEmail = await _generateEmail( context, - userProfile, + session.username, asDrafts: true, draftMailboxId: draftMailboxId, arguments: arguments, @@ -1253,19 +1250,18 @@ class ComposerController extends BaseController with DragDropFileMixin { _saveToDraftButtonState = ButtonState.disabled; - final userProfile = mailboxDashBoardController.userProfile.value; final accountId = mailboxDashBoardController.accountId.value; final session = mailboxDashBoardController.sessionCurrent; final draftMailboxId = mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts]; - if (draftMailboxId == null || userProfile == null || session == null || accountId == null) { + if (draftMailboxId == null || session == null || accountId == null) { log('ComposerController::saveToDraftAction: Param is NULL'); return; } final newEmail = await _generateEmail( context, - userProfile, + session.username, asDrafts: true, draftMailboxId: draftMailboxId, arguments: mailboxDashBoardController.composerArguments); @@ -1383,7 +1379,7 @@ class ComposerController extends BaseController with DragDropFileMixin { if (arguments.emailActionType == EmailActionType.editDraft) { return arguments.presentationEmail?.firstEmailAddressInFrom ?? ''; } else { - return mailboxDashBoardController.userProfile.value?.email ?? ''; + return mailboxDashBoardController.sessionCurrent?.username.value ?? ''; } } return ''; @@ -1912,8 +1908,6 @@ class ComposerController extends BaseController with DragDropFileMixin { bool get isNetworkConnectionAvailable => networkConnectionController.isNetworkConnectionAvailable(); - UserProfile? get userProfile => mailboxDashBoardController.userProfile.value; - String? get textEditorWeb => _textEditorWeb; HtmlEditorApi? get htmlEditorApi => richTextMobileTabletController.htmlEditorApi; diff --git a/lib/features/composer/presentation/model/save_to_draft_view_event.dart b/lib/features/composer/presentation/model/save_to_draft_view_event.dart index 14db275c05..08c84a326f 100644 --- a/lib/features/composer/presentation/model/save_to_draft_view_event.dart +++ b/lib/features/composer/presentation/model/save_to_draft_view_event.dart @@ -4,14 +4,12 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:model/user/user_profile.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; class SaveToDraftViewEvent extends ViewEvent { final BuildContext context; final Session session; final AccountId accountId; - final UserProfile userProfile; final MailboxId draftMailboxId; final EmailId? emailIdEditing; final ComposerArguments? arguments; @@ -20,7 +18,6 @@ class SaveToDraftViewEvent extends ViewEvent { required this.context, required this.session, required this.accountId, - required this.userProfile, required this.draftMailboxId, this.emailIdEditing, this.arguments, @@ -31,7 +28,6 @@ class SaveToDraftViewEvent extends ViewEvent { context, session, accountId, - userProfile, draftMailboxId, emailIdEditing, arguments, diff --git a/lib/features/contact/presentation/contact_controller.dart b/lib/features/contact/presentation/contact_controller.dart index c6cca3804b..4dc1415749 100644 --- a/lib/features/contact/presentation/contact_controller.dart +++ b/lib/features/contact/presentation/contact_controller.dart @@ -2,21 +2,20 @@ import 'package:core/presentation/utils/keyboard_utils.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; +import 'package:debounce_throttle/debounce_throttle.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/autocomplete/auto_complete_pattern.dart'; -import 'package:model/user/user_profile.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:debounce_throttle/debounce_throttle.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_device_contact_suggestions_state.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_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/contact/presentation/model/contact_arguments.dart'; import 'package:tmail_ui_user/features/contact/presentation/widgets/contact_suggestion_box_item.dart'; @@ -32,7 +31,6 @@ class ContactController extends BaseController { final searchQuery = SearchQuery.initial().obs; final listContactSearched = RxList(); final scrollListViewController = ScrollController(); - final userProfile = Rxn(); GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; @@ -40,7 +38,7 @@ class ContactController extends BaseController { final Debouncer _deBouncerTime = Debouncer(const Duration(milliseconds: 300), initialValue: ''); AccountId? _accountId; - Session? _session; + Session? session; ContactArguments? arguments; EmailAddress? contactSelected; @@ -65,17 +63,14 @@ class ContactController extends BaseController { textInputSearchFocus.requestFocus(); if (arguments != null) { _accountId = arguments!.accountId; - _session = arguments!.session; - if (_session != null) { - userProfile.value = UserProfile(_session!.username.value); - } + session = arguments!.session; final listContactSelected = arguments!.listContactSelected; log('ContactController::onReady(): arguments: $arguments'); log('ContactController::onReady(): listContactSelected: $listContactSelected'); if (listContactSelected.isNotEmpty) { contactSelected = EmailAddress(listContactSelected.first, listContactSelected.first); } - injectAutoCompleteBindings(_session, _accountId); + injectAutoCompleteBindings(session, _accountId); } if (PlatformInfo.isMobile) { Future.delayed( diff --git a/lib/features/contact/presentation/contact_view.dart b/lib/features/contact/presentation/contact_view.dart index 632917dc5d..2818c5b9a8 100644 --- a/lib/features/contact/presentation/contact_view.dart +++ b/lib/features/contact/presentation/contact_view.dart @@ -80,9 +80,10 @@ class ContactView extends GetWidget { ), if (PlatformInfo.isWeb) Obx(() { - final userEmail = controller.userProfile.value?.email; - if (userEmail != null && userEmail.isNotEmpty) { - final userEmailAddress = EmailAddress(AppLocalizations.of(context).me, controller.userProfile.value?.email); + if (controller.session?.username.value.isNotEmpty == true) { + final userEmailAddress = EmailAddress( + AppLocalizations.of(context).me, + controller.session?.username.value); final fromMeSuggestionEmailAddress = SuggestionEmailAddress(userEmailAddress, state: SuggestionEmailState.valid); return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 5c65219bee..18cb841f6b 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -1193,8 +1193,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _handleSendReceiptToSenderAction(BuildContext context) { final accountId = mailboxDashBoardController.accountId.value; - final userProfile = mailboxDashBoardController.userProfile.value; - if (accountId == null || userProfile == null) { + final session = mailboxDashBoardController.sessionCurrent; + if (accountId == null || session == null) { return; } @@ -1219,7 +1219,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { return; } - final receiverEmailAddress = _getReceiverEmailAddress(currentEmail!) ?? userProfile.email; + final receiverEmailAddress = _getReceiverEmailAddress(currentEmail!) ?? session.username.value; log('SingleEmailController::_handleSendReceiptToSenderAction():receiverEmailAddress: $receiverEmailAddress'); final mdnToSender = _generateMDN(context, currentEmail!, receiverEmailAddress); final sendReceiptRequest = SendReceiptToSenderRequest( diff --git a/lib/features/identity_creator/presentation/identity_creator_controller.dart b/lib/features/identity_creator/presentation/identity_creator_controller.dart index 302ac7837d..a76fd9364f 100644 --- a/lib/features/identity_creator/presentation/identity_creator_controller.dart +++ b/lib/features/identity_creator/presentation/identity_creator_controller.dart @@ -23,7 +23,6 @@ import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:model/extensions/identity_extension.dart'; import 'package:model/extensions/session_extension.dart'; -import 'package:model/user/user_profile.dart'; import 'package:path_provider/path_provider.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; @@ -81,7 +80,6 @@ class IdentityCreatorController extends BaseController { String? _contentHtmlEditor; AccountId? accountId; Session? session; - UserProfile? userProfile; Identity? identity; IdentityCreatorArguments? arguments; @@ -123,7 +121,6 @@ class IdentityCreatorController extends BaseController { if (arguments != null) { accountId = arguments!.accountId; session = arguments!.session; - userProfile = arguments!.userProfile; identity = arguments!.identity; actionType.value = arguments!.actionType; _checkDefaultIdentityIsSupported(); @@ -214,8 +211,8 @@ class IdentityCreatorController extends BaseController { void _setDefaultEmailAddressList() { listEmailAddressOfReplyTo.add(noneEmailAddress); - if (userProfile != null && userProfile?.email.isNotEmpty == true) { - final userEmailAddress = EmailAddress(null, userProfile!.email); + if (session?.username.value.isNotEmpty == true) { + final userEmailAddress = EmailAddress(null, session?.username.value); listEmailAddressDefault.add(userEmailAddress); listEmailAddressOfReplyTo.addAll(listEmailAddressDefault); } diff --git a/lib/features/identity_creator/presentation/model/identity_creator_arguments.dart b/lib/features/identity_creator/presentation/model/identity_creator_arguments.dart index 0259f2b443..16245588f2 100644 --- a/lib/features/identity_creator/presentation/model/identity_creator_arguments.dart +++ b/lib/features/identity_creator/presentation/model/identity_creator_arguments.dart @@ -3,20 +3,17 @@ import 'package:equatable/equatable.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; -import 'package:model/model.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/identity_action_type.dart'; class IdentityCreatorArguments with EquatableMixin { final AccountId accountId; final Session session; - final UserProfile userProfile; final IdentityActionType actionType; final Identity? identity; IdentityCreatorArguments( this.accountId, this.session, - this.userProfile, { this.identity, this.actionType = IdentityActionType.create @@ -27,7 +24,6 @@ class IdentityCreatorArguments with EquatableMixin { List get props => [ accountId, session, - userProfile, - identity, + identity, actionType]; } \ No newline at end of file diff --git a/lib/features/login/data/datasource/authentication_datasource.dart b/lib/features/login/data/datasource/authentication_datasource.dart index a7f775ee71..b574c2764d 100644 --- a/lib/features/login/data/datasource/authentication_datasource.dart +++ b/lib/features/login/data/datasource/authentication_datasource.dart @@ -1,7 +1,6 @@ import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/password.dart'; -import 'package:model/user/user_profile.dart'; abstract class AuthenticationDataSource { - Future authenticationUser(Uri baseUrl, UserName userName, Password password); + Future authenticationUser(Uri baseUrl, UserName userName, Password password); } \ No newline at end of file diff --git a/lib/features/login/data/datasource_impl/authentication_datasource_impl.dart b/lib/features/login/data/datasource_impl/authentication_datasource_impl.dart index 781563a414..7691cc0916 100644 --- a/lib/features/login/data/datasource_impl/authentication_datasource_impl.dart +++ b/lib/features/login/data/datasource_impl/authentication_datasource_impl.dart @@ -1,16 +1,18 @@ import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/password.dart'; -import 'package:model/user/user_profile.dart'; import 'package:tmail_ui_user/features/login/data/datasource/authentication_datasource.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; class AuthenticationDataSourceImpl extends AuthenticationDataSource { - AuthenticationDataSourceImpl(); + final ExceptionThrower _exceptionThrower; + + AuthenticationDataSourceImpl(this._exceptionThrower); @override - Future authenticationUser(Uri baseUrl, UserName userName, Password password) { - return Future.sync(() { - return UserProfile(userName.value); - }); + Future authenticationUser(Uri baseUrl, UserName userName, Password password) async { + return Future.sync(() async { + return userName; + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/login/data/repository/authentication_repository_impl.dart b/lib/features/login/data/repository/authentication_repository_impl.dart index ea2e77e4c4..410a8d7a42 100644 --- a/lib/features/login/data/repository/authentication_repository_impl.dart +++ b/lib/features/login/data/repository/authentication_repository_impl.dart @@ -1,6 +1,5 @@ import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/password.dart'; -import 'package:model/user/user_profile.dart'; import 'package:tmail_ui_user/features/login/data/datasource/authentication_datasource.dart'; import 'package:tmail_ui_user/features/login/domain/repository/authentication_repository.dart'; @@ -10,7 +9,7 @@ class AuthenticationRepositoryImpl extends AuthenticationRepository { AuthenticationRepositoryImpl(this.loginDataSource); @override - Future authenticationUser(Uri baseUrl, UserName userName, Password password) { + Future authenticationUser(Uri baseUrl, UserName userName, Password password) { return loginDataSource.authenticationUser(baseUrl, userName, password); } } \ No newline at end of file diff --git a/lib/features/login/domain/repository/authentication_repository.dart b/lib/features/login/domain/repository/authentication_repository.dart index 03aa122dbe..189f29c3e3 100644 --- a/lib/features/login/domain/repository/authentication_repository.dart +++ b/lib/features/login/domain/repository/authentication_repository.dart @@ -1,8 +1,7 @@ import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/password.dart'; -import 'package:model/user/user_profile.dart'; abstract class AuthenticationRepository { - Future authenticationUser(Uri baseUrl, UserName userName, Password password); + Future authenticationUser(Uri baseUrl, UserName userName, Password password); } \ No newline at end of file diff --git a/lib/features/login/domain/state/authentication_user_state.dart b/lib/features/login/domain/state/authentication_user_state.dart index bf9fc83653..611e806a6c 100644 --- a/lib/features/login/domain/state/authentication_user_state.dart +++ b/lib/features/login/domain/state/authentication_user_state.dart @@ -1,16 +1,16 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:model/user/user_profile.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; class AuthenticationUserLoading extends LoadingState {} class AuthenticationUserSuccess extends UIState { - final UserProfile userProfile; + final UserName userName; - AuthenticationUserSuccess(this.userProfile); + AuthenticationUserSuccess(this.userName); @override - List get props => [userProfile]; + List get props => [userName]; } class AuthenticationUserFailure extends FeatureFailure { diff --git a/lib/features/login/domain/usecases/authentication_user_interactor.dart b/lib/features/login/domain/usecases/authentication_user_interactor.dart index ba1f4f8272..c60c802521 100644 --- a/lib/features/login/domain/usecases/authentication_user_interactor.dart +++ b/lib/features/login/domain/usecases/authentication_user_interactor.dart @@ -29,24 +29,24 @@ class AuthenticationInteractor { }) async* { try { yield Right(AuthenticationUserLoading()); - final user = await authenticationRepository.authenticationUser(baseUrl, userName, password); + final username = await authenticationRepository.authenticationUser(baseUrl, userName, password); await Future.wait([ credentialRepository.saveBaseUrl(baseUrl), credentialRepository.storeAuthenticationInfo( AuthenticationInfoCache( - userName.value, + username.value, password.value ) ), ]); await _accountRepository.setCurrentAccount( PersonalAccount( - userName.value, + username.value, AuthenticationType.basic, isSelected: true ) ); - yield Right(AuthenticationUserSuccess(user)); + yield Right(AuthenticationUserSuccess(username)); } catch (e) { yield Left(AuthenticationUserFailure(e)); } diff --git a/lib/features/mailbox/presentation/mailbox_view.dart b/lib/features/mailbox/presentation/mailbox_view.dart index 069ebf4107..d660d8d1f0 100644 --- a/lib/features/mailbox/presentation/mailbox_view.dart +++ b/lib/features/mailbox/presentation/mailbox_view.dart @@ -167,7 +167,7 @@ class MailboxView extends BaseMailboxView { return const SizedBox.shrink(); } return UserInformationWidget( - userProfile: controller.mailboxDashBoardController.userProfile.value, + userName: controller.mailboxDashBoardController.sessionCurrent?.username, subtitle: AppLocalizations.of(context).manage_account, onSubtitleClick: controller.mailboxDashBoardController.goToSettings, border: const Border( diff --git a/lib/features/mailbox/presentation/mailbox_view_web.dart b/lib/features/mailbox/presentation/mailbox_view_web.dart index 19b3f23ed0..07e4900944 100644 --- a/lib/features/mailbox/presentation/mailbox_view_web.dart +++ b/lib/features/mailbox/presentation/mailbox_view_web.dart @@ -103,7 +103,7 @@ class MailboxView extends BaseMailboxView { child: Column(children: [ if (!controller.responsiveUtils.isDesktop(context)) Obx(() => UserInformationWidget( - userProfile: controller.mailboxDashBoardController.userProfile.value, + userName: controller.mailboxDashBoardController.sessionCurrent?.username, subtitle: AppLocalizations.of(context).manage_account, onSubtitleClick: controller.mailboxDashBoardController.goToSettings, border: const Border( diff --git a/lib/features/mailbox/presentation/widgets/user_information_widget.dart b/lib/features/mailbox/presentation/widgets/user_information_widget.dart index a4a6140cdd..45d7122ecf 100644 --- a/lib/features/mailbox/presentation/widgets/user_information_widget.dart +++ b/lib/features/mailbox/presentation/widgets/user_information_widget.dart @@ -4,14 +4,17 @@ import 'package:core/presentation/views/image/avatar_builder.dart'; import 'package:core/presentation/views/text/text_overflow_builder.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; -import 'package:model/user/user_profile.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/extensions/username_extension.dart'; import 'package:tmail_ui_user/features/base/widget/material_text_button.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; typedef OnSubtitleClick = void Function(); class UserInformationWidget extends StatelessWidget { - final UserProfile? userProfile; + final UserName? userName; final String? subtitle; final EdgeInsetsGeometry? titlePadding; final OnSubtitleClick? onSubtitleClick; @@ -20,7 +23,7 @@ class UserInformationWidget extends StatelessWidget { const UserInformationWidget({ Key? key, - this.userProfile, + this.userName, this.subtitle, this.titlePadding, this.onSubtitleClick, @@ -35,7 +38,7 @@ class UserInformationWidget extends StatelessWidget { decoration: BoxDecoration(border: border), child: Row(children: [ (AvatarBuilder() - ..text(userProfile != null ? userProfile!.getAvatarText() : '') + ..text(userName?.firstCharacter ?? '') ..backgroundColor(Colors.white) ..textColor(Colors.black) ..addBoxShadows([const BoxShadow( @@ -48,7 +51,7 @@ class UserInformationWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ TextOverflowBuilder( - userProfile != null ? '${userProfile?.email}' : '', + userName?.value ?? '', style: const TextStyle( fontSize: 17, color: AppColor.colorNameEmail, diff --git a/lib/features/mailbox_dashboard/domain/state/get_user_profile_state.dart b/lib/features/mailbox_dashboard/domain/state/get_user_profile_state.dart deleted file mode 100644 index 0598dd21c1..0000000000 --- a/lib/features/mailbox_dashboard/domain/state/get_user_profile_state.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:core/presentation/state/failure.dart'; -import 'package:core/presentation/state/success.dart'; -import 'package:model/user/user_profile.dart'; - -class GetUserProfileSuccess extends UIState { - final UserProfile userProfile; - - GetUserProfileSuccess(this.userProfile); - - @override - List get props => []; -} - -class GetUserProfileFailure extends FeatureFailure { - - GetUserProfileFailure(dynamic exception) : super(exception: exception); -} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 4314ec2e25..2a56ca6481 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -199,7 +199,6 @@ class MailboxDashBoardController extends ReloadableController { final selectedMailbox = Rxn(); final selectedEmail = Rxn(); final accountId = Rxn(); - final userProfile = Rxn(); final dashBoardAction = Rxn(); final mailboxUIAction = Rxn(); final emailUIAction = Rxn(); @@ -514,7 +513,6 @@ class MailboxDashBoardController extends ReloadableController { final currentAccountId = session.personalAccount.accountId; sessionCurrent = session; accountId.value = currentAccountId; - userProfile.value = UserProfile(session.username.value); injectAutoCompleteBindings(session, currentAccountId); injectRuleFilterBindings(session, currentAccountId); @@ -1447,13 +1445,12 @@ class MailboxDashBoardController extends ReloadableController { } void selectQuickSearchFilter(QuickSearchFilter filter) { - return searchController.selectQuickSearchFilter(filter, userProfile.value!); + return searchController.selectQuickSearchFilter(filter); } void selectQuickSearchFilterFrom(EmailAddress fromEmailFilter) { return searchController.selectQuickSearchFilter( QuickSearchFilter.fromMe, - userProfile.value!, fromEmailFilter: fromEmailFilter ); } @@ -1463,11 +1460,10 @@ class MailboxDashBoardController extends ReloadableController { } Future> quickSearchEmails(String query) async { - if (sessionCurrent != null && accountId.value != null && userProfile.value != null) { + if (sessionCurrent != null && accountId.value != null) { return searchController.quickSearchEmails( session: sessionCurrent!, accountId: accountId.value!, - userProfile: userProfile.value!, query: query ); } else { diff --git a/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart index ea55dd620c..c725cda47d 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart @@ -9,6 +9,7 @@ import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/core/utc_date.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_comparator.dart'; @@ -17,7 +18,6 @@ import 'package:jmap_dart_client/jmap/mail/email/email_filter_condition.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/email_filter_condition_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; -import 'package:model/user/user_profile.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/mixin/date_range_picker_mixin.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.dart'; @@ -73,10 +73,9 @@ class SearchController extends BaseController with DateRangePickerMixin { void selectQuickSearchFilter( QuickSearchFilter quickSearchFilter, - UserProfile userProfile, {EmailAddress? fromEmailFilter} ) { - final isFilterSelected = quickSearchFilter.isSelected(searchEmailFilter.value, userProfile); + final isFilterSelected = quickSearchFilter.isSelected(searchEmailFilter.value); switch (quickSearchFilter) { case QuickSearchFilter.hasAttachment: @@ -110,7 +109,6 @@ class SearchController extends BaseController with DateRangePickerMixin { required Session session, required AccountId accountId, required String query, - required UserProfile userProfile, }) async { return await _quickSearchEmailInteractor.execute( session, @@ -119,7 +117,7 @@ class SearchController extends BaseController with DateRangePickerMixin { sort: {}..add( EmailComparator(EmailComparatorProperty.receivedAt) ..setIsAscending(false)), - filter: _mappingToFilterOnSuggestionForm(userProfile: userProfile, query: query), + filter: _mappingToFilterOnSuggestionForm(userName: session.username, query: query), properties: ThreadConstants.propertiesQuickSearch ).then((result) => result.fold( (failure) => [], @@ -129,7 +127,7 @@ class SearchController extends BaseController with DateRangePickerMixin { )); } - Filter? _mappingToFilterOnSuggestionForm({required String query, required UserProfile userProfile}) { + Filter? _mappingToFilterOnSuggestionForm({required String query, required UserName userName}) { log('SearchController::_mappingToFilterOnSuggestionForm():query: $query'); final filterCondition = EmailFilterCondition( text: query.isNotEmpty == true ? query : null, @@ -143,7 +141,7 @@ class SearchController extends BaseController with DateRangePickerMixin { ? true : null, from: listFilterOnSuggestionForm.contains(QuickSearchFilter.fromMe) - ? userProfile.email + ? userName.value : null ); @@ -152,7 +150,7 @@ class SearchController extends BaseController with DateRangePickerMixin { : null; } - void applyFilterSuggestionToSearchFilter(UserProfile? userProfile) { + void applyFilterSuggestionToSearchFilter(UserName? userName) { final receiveTime = listFilterOnSuggestionForm.contains(QuickSearchFilter.last7Days) ? EmailReceiveTimeType.last7Days : EmailReceiveTimeType.allTime; @@ -160,11 +158,11 @@ class SearchController extends BaseController with DateRangePickerMixin { final hasAttachment = listFilterOnSuggestionForm.contains(QuickSearchFilter.hasAttachment) ? true : false; var listFromAddress = searchEmailFilter.value.from; - if (userProfile != null) { + if (userName != null) { if (listFilterOnSuggestionForm.contains(QuickSearchFilter.fromMe)) { - listFromAddress.add(userProfile.email); + listFromAddress.add(userName.value); } else { - listFromAddress.remove(userProfile.email); + listFromAddress.remove(userName.value); } } diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index 31c5a86a0c..1386e7dc10 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; +import 'package:model/extensions/username_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/base/widget/popup_item_no_icon_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_view_web.dart'; @@ -339,13 +340,13 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { : const SizedBox.shrink(), const SizedBox(width: 24), Obx(() => (AvatarBuilder() - ..text(controller.userProfile.value?.getAvatarText() ?? '') + ..text(controller.sessionCurrent?.username.firstCharacter ?? '') ..backgroundColor(Colors.white) ..textColor(Colors.black) ..context(context) ..addOnTapAvatarActionWithPositionClick((position) => controller.openPopupMenuAction(context, position, popupMenuUserSettingActionTile(context, - controller.userProfile.value, + controller.sessionCurrent?.username, onLogoutAction: () { popBack(); controller.logout(controller.sessionCurrent, controller.accountId.value); @@ -598,7 +599,6 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { return Obx(() { final isFilterSelected = filter.isSelected( controller.searchController.searchEmailFilter.value, - controller.userProfile.value ); return Padding( @@ -776,7 +776,8 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { String _getQuickSearchFilterFromTitle(BuildContext context) { final searchEmailFilterFromFiled = controller.searchController.searchEmailFilter.value.from; if (searchEmailFilterFromFiled.length == 1) { - if (searchEmailFilterFromFiled.first == controller.userProfile.value?.email) { + if (searchEmailFilterFromFiled.first == controller.sessionCurrent?.username.value && + controller.sessionCurrent?.username.value.isNotEmpty == true) { return QuickSearchFilter.fromMe.getTitle(context); } else { return '${AppLocalizations.of(context).from_email_address_prefix} ${searchEmailFilterFromFiled.first}'; diff --git a/lib/features/mailbox_dashboard/presentation/mixin/user_setting_popup_menu_mixin.dart b/lib/features/mailbox_dashboard/presentation/mixin/user_setting_popup_menu_mixin.dart index f0f25871c2..363859e194 100644 --- a/lib/features/mailbox_dashboard/presentation/mixin/user_setting_popup_menu_mixin.dart +++ b/lib/features/mailbox_dashboard/presentation/mixin/user_setting_popup_menu_mixin.dart @@ -1,8 +1,10 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/popup_menu/popup_menu_item_widget.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:model/model.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; mixin UserSettingPopupMenuMixin { @@ -10,18 +12,33 @@ mixin UserSettingPopupMenuMixin { List popupMenuUserSettingActionTile( BuildContext context, - UserProfile? userProfile, + UserName? userName, { Function? onLogoutAction, Function? onSettingAction } ) { return [ - PopupMenuItem( - enabled: false, - padding: EdgeInsets.zero, - child: _userInformation(context, userProfile) - ), + if (userName != null) + PopupMenuItem( + enabled: false, + padding: EdgeInsets.zero, + child: SizedBox( + width: 300, + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 20), + title: Text( + userName.value, + maxLines: 1, + style: const TextStyle( + fontSize: 15, + color: AppColor.colorHintSearchBar, + fontWeight: FontWeight.normal + ) + ) + ), + ) + ), if (onSettingAction != null) ...[ const PopupMenuDivider(height: 0.5), @@ -42,21 +59,6 @@ mixin UserSettingPopupMenuMixin { ]; } - Widget _userInformation(BuildContext context, UserProfile? userProfile) { - if (userProfile != null) { - return SizedBox( - width: 300, - child: ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20), - title: Text(userProfile.email, maxLines: 1, style: const TextStyle( - fontSize: 15, - color: AppColor.colorHintSearchBar, - fontWeight: FontWeight.normal))), - ); - } - return const SizedBox.shrink(); - } - Widget _settingAction(BuildContext context, Function? onCallBack) { return PopupMenuItemWidget( _imagePaths.icSetting, diff --git a/lib/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart b/lib/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart index 3b1e078cad..2dc8a9e9f9 100644 --- a/lib/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart +++ b/lib/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart @@ -2,7 +2,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:flutter/cupertino.dart'; -import 'package:model/user/user_profile.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart'; @@ -89,7 +88,7 @@ enum QuickSearchFilter { bool isApplied(List listFilter) => listFilter.contains(this); - bool isSelected(SearchEmailFilter filter, UserProfile? userProfile) { + bool isSelected(SearchEmailFilter filter) { switch (this) { case QuickSearchFilter.hasAttachment: return filter.hasAttachment == true; diff --git a/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart index 2df963c8a9..d8463069d6 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart @@ -125,7 +125,7 @@ class SearchInputFormWidget extends StatelessWidget with AppLoaderMixin { } if (query.isNotEmpty || _searchController.listFilterOnSuggestionForm.isNotEmpty) { - _searchController.applyFilterSuggestionToSearchFilter(_dashBoardController.userProfile.value); + _searchController.applyFilterSuggestionToSearchFilter(_dashBoardController.sessionCurrent?.username); _dashBoardController.searchEmail(context, queryString: query); } else { _dashBoardController.clearSearchEmail(); @@ -146,7 +146,7 @@ class SearchInputFormWidget extends StatelessWidget with AppLoaderMixin { _searchController.searchFocus.unfocus(); _searchController.enableSearch(); - _searchController.applyFilterSuggestionToSearchFilter(_dashBoardController.userProfile.value); + _searchController.applyFilterSuggestionToSearchFilter(_dashBoardController.sessionCurrent?.username); _dashBoardController.searchEmail(context, queryString: recent.value); } diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart index 405b087498..2c7a77ca8f 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart @@ -16,7 +16,6 @@ import 'package:server_settings/server_settings/capability_server_settings.dart' import 'package:tmail_ui_user/features/base/action/ui_action.dart'; import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart'; import 'package:tmail_ui_user/features/base/state/banner_state.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_user_profile_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_vacation_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/update_vacation_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_vacation_interactor.dart'; @@ -46,7 +45,6 @@ class ManageAccountDashBoardController extends ReloadableController { UpdateVacationInteractor? _updateVacationInteractor; final appInformation = Rxn(); - final userProfile = Rxn(); final accountId = Rxn(); final accountMenuItemSelected = AccountMenuItem.profiles.obs; final settingsPageLevel = SettingsPageLevel.universal.obs; @@ -74,9 +72,7 @@ class ManageAccountDashBoardController extends ReloadableController { @override void handleSuccessViewState(Success success) { - if (success is GetUserProfileSuccess) { - userProfile.value = success.userProfile; - } else if (success is GetAllVacationSuccess) { + if (success is GetAllVacationSuccess) { if (success.listVacationResponse.isNotEmpty) { vacationResponse.value = success.listVacationResponse.first; } @@ -92,7 +88,6 @@ class ManageAccountDashBoardController extends ReloadableController { log('ManageAccountDashBoardController::handleReloaded:'); sessionCurrent = session; accountId.value = session.personalAccount.accountId; - _getUserProfile(); _bindingInteractorForMenuItemView(sessionCurrent, accountId.value); _getVacationResponse(); _getParametersRouter(); @@ -104,7 +99,6 @@ class ManageAccountDashBoardController extends ReloadableController { sessionCurrent = arguments.session; accountId.value = arguments.session?.personalAccount.accountId; previousUri = arguments.previousUri; - _getUserProfile(); _bindingInteractorForMenuItemView(sessionCurrent, accountId.value); _getVacationResponse(); if (arguments.menuSettingCurrent != null) { @@ -163,11 +157,6 @@ class ManageAccountDashBoardController extends ReloadableController { appInformation.value = info; } - void _getUserProfile() async { - log('ManageAccountDashBoardController::_getUserProfile(): $sessionCurrent'); - userProfile.value = sessionCurrent != null ? UserProfile(sessionCurrent!.username.value) : null; - } - void _getVacationResponse() { if (accountId.value != null && _getAllVacationInteractor != null) { consumeState(_getAllVacationInteractor!.execute(accountId.value!)); diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart index 1cd383a8a3..1190fd0971 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart @@ -7,6 +7,7 @@ import 'package:core/presentation/views/text/slogan_builder.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/state/banner_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mixin/user_setting_popup_menu_mixin.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/always_read_receipt/always_read_receipt_view.dart'; @@ -139,7 +140,7 @@ class ManageAccountDashBoardView extends GetWidget (AvatarBuilder() - ..text(controller.userProfile.value?.getAvatarText() ?? '') + ..text(controller.sessionCurrent?.username.firstCharacter ?? '') ..backgroundColor(Colors.white) ..textColor(Colors.black) ..context(context) @@ -149,7 +150,7 @@ class ManageAccountDashBoardView extends GetWidget { controller: PlatformInfo.isMobile ? null : controller.settingScrollController, child: Column(children: [ Obx(() => UserInformationWidget( - userProfile: controller.manageAccountDashboardController.userProfile.value, + userName: controller.manageAccountDashboardController.sessionCurrent?.username, padding: SettingsUtils.getPaddingInFirstLevel(context, controller.responsiveUtils), titlePadding: const EdgeInsetsDirectional.only(start: 16))), Divider( diff --git a/lib/features/manage_account/presentation/profiles/identities/identities_controller.dart b/lib/features/manage_account/presentation/profiles/identities/identities_controller.dart index 34beda441b..e7abe242d2 100644 --- a/lib/features/manage_account/presentation/profiles/identities/identities_controller.dart +++ b/lib/features/manage_account/presentation/profiles/identities/identities_controller.dart @@ -142,10 +142,9 @@ class IdentitiesController extends BaseController { void goToCreateNewIdentity(BuildContext context) async { final accountId = _accountDashBoardController.accountId.value; - final userProfile = _accountDashBoardController.userProfile.value; final session = _accountDashBoardController.sessionCurrent; - if (accountId != null && session != null && userProfile != null) { - final arguments = IdentityCreatorArguments(accountId, session, userProfile); + if (accountId != null && session != null) { + final arguments = IdentityCreatorArguments(accountId, session); final newIdentityArguments = PlatformInfo.isWeb ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.identityCreator, arguments: arguments) @@ -249,13 +248,11 @@ class IdentitiesController extends BaseController { void goToEditIdentity(BuildContext context, Identity identity) async { final accountId = _accountDashBoardController.accountId.value; - final userProfile = _accountDashBoardController.userProfile.value; final session = _accountDashBoardController.sessionCurrent; - if (accountId != null && session != null && userProfile != null) { + if (accountId != null && session != null) { final arguments = IdentityCreatorArguments( accountId, session, - userProfile, identity: identity, actionType: IdentityActionType.edit); diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index 03f58018ec..c0465e517c 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -24,7 +24,6 @@ import 'package:model/extensions/presentation_email_extension.dart'; import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:model/mailbox/select_mode.dart'; -import 'package:model/user/user_profile.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/mixin/date_range_picker_mixin.dart'; import 'package:tmail_ui_user/features/contact/presentation/model/contact_arguments.dart'; @@ -112,8 +111,6 @@ class SearchEmailController extends BaseController Session? get session => mailboxDashBoardController.sessionCurrent; - UserProfile? get userProfile => mailboxDashBoardController.userProfile.value; - SearchQuery? get searchQuery => simpleSearchFilter.value.text; RxList get listResultSearch => mailboxDashBoardController.listResultSearch; diff --git a/lib/main/bindings/credential/credential_bindings.dart b/lib/main/bindings/credential/credential_bindings.dart index f2b7699b19..82abf014d2 100644 --- a/lib/main/bindings/credential/credential_bindings.dart +++ b/lib/main/bindings/credential/credential_bindings.dart @@ -71,7 +71,7 @@ class CredentialBindings extends InteractorsBindings { Get.find(), Get.find()) ); - Get.put(AuthenticationDataSourceImpl()); + Get.put(AuthenticationDataSourceImpl(Get.find())); Get.put(AuthenticationOIDCDataSourceImpl( Get.find(), Get.find(), diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index ff1b9d64f9..fedc723135 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3944,4 +3944,22 @@ class AppLocalizations { name: 'loadingPleaseWait', ); } + + String get status { + return Intl.message( + 'Status', + name: 'status'); + } + + String get progress { + return Intl.message( + 'Progress', + name: 'progress'); + } + + String get sendingMessage { + return Intl.message( + 'Sending message', + name: 'sendingMessage'); + } } \ No newline at end of file diff --git a/model/lib/extensions/user_profile_extension.dart b/model/lib/extensions/user_profile_extension.dart deleted file mode 100644 index b9f332d1a7..0000000000 --- a/model/lib/extensions/user_profile_extension.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:model/model.dart'; - -extension UserProfileExtension on UserProfile { - UserProfileResponse toUserProfileResponse() { - return UserProfileResponse(email); - } -} \ No newline at end of file diff --git a/model/lib/extensions/username_extension.dart b/model/lib/extensions/username_extension.dart new file mode 100644 index 0000000000..66005a6b4b --- /dev/null +++ b/model/lib/extensions/username_extension.dart @@ -0,0 +1,7 @@ + +import 'package:core/presentation/extensions/string_extension.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; + +extension UsernameExtension on UserName { + String get firstCharacter => value.firstLetterToUpperCase.toUpperCase(); +} \ No newline at end of file diff --git a/model/lib/model.dart b/model/lib/model.dart index 760cccb81e..2835ec4dfa 100644 --- a/model/lib/model.dart +++ b/model/lib/model.dart @@ -57,7 +57,7 @@ export 'extensions/presentation_email_extension.dart'; export 'extensions/presentation_mailbox_extension.dart'; export 'extensions/properties_extension.dart'; export 'extensions/session_extension.dart'; -export 'extensions/user_profile_extension.dart'; +export 'extensions/username_extension.dart'; export 'extensions/utc_date_extension.dart'; // Identity export 'identity/identity_request_dto.dart'; @@ -79,7 +79,4 @@ export 'oidc/token_id.dart'; export 'oidc/token_oidc.dart'; // Upload export 'upload/file_info.dart'; -export 'upload/upload_response.dart'; -// User -export 'user/user_profile.dart'; -export 'user/user_profile_response.dart'; \ No newline at end of file +export 'upload/upload_response.dart'; \ No newline at end of file diff --git a/model/lib/user/avatar_id.dart b/model/lib/user/avatar_id.dart deleted file mode 100644 index 2710f8aa85..0000000000 --- a/model/lib/user/avatar_id.dart +++ /dev/null @@ -1,15 +0,0 @@ - -import 'package:equatable/equatable.dart'; - -class AvatarId with EquatableMixin { - final String id; - - AvatarId(this.id); - - factory AvatarId.initial() { - return AvatarId(''); - } - - @override - List get props => [id]; -} \ No newline at end of file diff --git a/model/lib/user/user_profile.dart b/model/lib/user/user_profile.dart deleted file mode 100644 index 80609cd21b..0000000000 --- a/model/lib/user/user_profile.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:equatable/equatable.dart'; - -class UserProfile with EquatableMixin { - - final String email; - - UserProfile(this.email); - - String getAvatarText() { - return email[0].toUpperCase(); - } - - @override - List get props => [email]; -} \ No newline at end of file diff --git a/model/lib/user/user_profile_response.dart b/model/lib/user/user_profile_response.dart deleted file mode 100644 index 10f4842cbc..0000000000 --- a/model/lib/user/user_profile_response.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:model/model.dart'; - -part 'user_profile_response.g.dart'; - -@JsonSerializable() -class UserProfileResponse with EquatableMixin { - - final String email; - - UserProfileResponse(this.email); - - factory UserProfileResponse.fromJson(Map json) => _$UserProfileResponseFromJson(json); - - Map toJson() => _$UserProfileResponseToJson(this); - - @override - List get props => [email]; -} - -extension UserProfileResponseExtension on UserProfileResponse { - UserProfile toUserProfile() { - return UserProfile(email); - } -} \ No newline at end of file From 190223171c0a5a304112fc97e6e2a4c30a8b4959 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 6 Mar 2024 11:14:33 +0700 Subject: [PATCH 09/80] TF-2667 Create ApplicationVersionWidget to display app version --- contact/pubspec.lock | 24 +++++++++ core/lib/utils/application_manager.dart | 49 +++++++++++++++++++ core/pubspec.lock | 24 +++++++++ core/pubspec.yaml | 4 ++ lib/features/base/base_controller.dart | 2 + .../widget/application_version_widget.dart | 44 +++++++++++++++++ .../presentation/composer_bindings.dart | 1 - .../presentation/composer_controller.dart | 29 +---------- .../mailbox/presentation/mailbox_view.dart | 44 ++++++++--------- .../presentation/mailbox_view_web.dart | 24 ++------- .../mailbox_dashboard_controller.dart | 23 ++++----- .../mailbox_dashboard_view_web.dart | 24 +++------ .../manage_account_dashboard_controller.dart | 9 ---- .../manage_account_dashboard_view.dart | 16 ++---- .../menu/manage_account_menu_view.dart | 19 ++----- lib/main/bindings/core/core_bindings.dart | 2 + model/pubspec.lock | 24 +++++++++ pubspec.lock | 4 +- pubspec.yaml | 4 -- .../single_email_controller_test.dart | 6 ++- 20 files changed, 233 insertions(+), 143 deletions(-) create mode 100644 core/lib/utils/application_manager.dart create mode 100644 lib/features/base/widget/application_version_widget.dart diff --git a/contact/pubspec.lock b/contact/pubspec.lock index cbf82dd470..afc4ca7524 100644 --- a/contact/pubspec.lock +++ b/contact/pubspec.lock @@ -304,6 +304,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + fk_user_agent: + dependency: transitive + description: + name: fk_user_agent + sha256: fd6c94e120786985a292d12f61422a581f4e851148d5940af38b819357b8ad0d + url: "https://pub.dev" + source: hosted + version: "2.1.0" flex_color_picker: dependency: transitive description: @@ -671,6 +679,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" path: dependency: transitive description: diff --git a/core/lib/utils/application_manager.dart b/core/lib/utils/application_manager.dart new file mode 100644 index 0000000000..c7b291c470 --- /dev/null +++ b/core/lib/utils/application_manager.dart @@ -0,0 +1,49 @@ + +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:fk_user_agent/fk_user_agent.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +class ApplicationManager { + + final DeviceInfoPlugin _deviceInfoPlugin; + + ApplicationManager(this._deviceInfoPlugin); + + Future getPackageInfo() async { + final packageInfo = await PackageInfo.fromPlatform(); + log('ApplicationManager::getPackageInto: $packageInfo'); + return packageInfo; + } + + Future getVersion() async { + final version = (await getPackageInfo()).version; + log('ApplicationManager::getVersion: $version'); + return version; + } + + Future getUserAgent() async { + try { + String userAgent; + if (PlatformInfo.isWeb) { + final webBrowserInfo = await _deviceInfoPlugin.webBrowserInfo; + userAgent = webBrowserInfo.userAgent ?? ''; + } else { + await FkUserAgent.init(); + userAgent = FkUserAgent.userAgent ?? ''; + FkUserAgent.release(); + } + return userAgent; + } catch(e) { + logError('ApplicationManager::getUserAgent: Exception: $e'); + return ''; + } + } + + Future generateApplicationUserAgent() async { + final userAgent = await getUserAgent(); + final version = await getVersion(); + return 'Team-Mail/$version $userAgent'; + } +} \ No newline at end of file diff --git a/core/pubspec.lock b/core/pubspec.lock index 994ccee134..1082651361 100644 --- a/core/pubspec.lock +++ b/core/pubspec.lock @@ -185,6 +185,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.4" + fk_user_agent: + dependency: "direct main" + description: + name: fk_user_agent + sha256: fd6c94e120786985a292d12f61422a581f4e851148d5940af38b819357b8ad0d + url: "https://pub.dev" + source: hosted + version: "2.1.0" flex_color_picker: dependency: "direct main" description: @@ -440,6 +448,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" path: dependency: transitive description: diff --git a/core/pubspec.yaml b/core/pubspec.yaml index 8c8531d11a..6f94e72e02 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -75,6 +75,10 @@ dependencies: printing: 5.12.0 + package_info_plus: 4.2.0 + + fk_user_agent: 2.1.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index 97b14ba252..eacf5b8cb3 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -8,6 +8,7 @@ import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/views/toast/tmail_toast.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/application_manager.dart'; import 'package:core/utils/fps_manager.dart'; import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; @@ -76,6 +77,7 @@ abstract class BaseController extends GetxController final ImagePaths imagePaths = Get.find(); final ResponsiveUtils responsiveUtils = Get.find(); final Uuid uuid = Get.find(); + final ApplicationManager applicationManager = Get.find(); bool _isFcmEnabled = false; diff --git a/lib/features/base/widget/application_version_widget.dart b/lib/features/base/widget/application_version_widget.dart new file mode 100644 index 0000000000..38eab27168 --- /dev/null +++ b/lib/features/base/widget/application_version_widget.dart @@ -0,0 +1,44 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/utils/application_manager.dart'; +import 'package:flutter/material.dart'; + +class ApplicationVersionWidget extends StatelessWidget { + + final ApplicationManager applicationManager; + final EdgeInsetsGeometry? padding; + final String? title; + final TextStyle? textStyle; + + const ApplicationVersionWidget({ + super.key, + required this.applicationManager, + this.title, + this.textStyle, + this.padding, + }); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: applicationManager.getVersion(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return Padding( + padding: padding ?? const EdgeInsets.only(top: 6), + child: Text( + '${title ?? 'v.'}${snapshot.data}', + textAlign: TextAlign.center, + style: textStyle ?? Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 13, + color: AppColor.colorContentEmail, + fontWeight: FontWeight.w500 + ), + ), + ); + } else { + return const SizedBox.shrink(); + } + } + ); + } +} diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index 6f9bc469aa..341bcd4be3 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -207,7 +207,6 @@ class ComposerBindings extends BaseBindings { Get.lazyPut(() => UploadController(Get.find())); Get.lazyPut(() => RichTextWebController()); Get.lazyPut(() => ComposerController( - Get.find(), Get.find(), Get.find(), Get.find(), diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 8132dcbcaa..f0717c2d2b 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -10,7 +10,6 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:file_picker/file_picker.dart'; import 'package:filesize/filesize.dart'; -import 'package:fk_user_agent/fk_user_agent.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -123,7 +122,6 @@ class ComposerController extends BaseController with DragDropFileMixin { final LocalFilePickerInteractor _localFilePickerInteractor; final LocalImagePickerInteractor _localImagePickerInteractor; - final DeviceInfoPlugin _deviceInfoPlugin; final GetEmailContentInteractor _getEmailContentInteractor; final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; final UploadController uploadController; @@ -189,7 +187,6 @@ class ComposerController extends BaseController with DragDropFileMixin { late bool _isEmailBodyLoaded; ComposerController( - this._deviceInfoPlugin, this._localFilePickerInteractor, this._localImagePickerInteractor, this._getEmailContentInteractor, @@ -209,11 +206,7 @@ class ComposerController extends BaseController with DragDropFileMixin { createFocusNodeInput(); scrollControllerEmailAddress.addListener(_scrollControllerEmailAddressListener); _listenStreamEvent(); - if (PlatformInfo.isMobile) { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { - await FkUserAgent.init(); - }); - } else { + if (PlatformInfo.isWeb) { WidgetsBinding.instance.addPostFrameCallback((_) { _listenBrowserEventAction(); }); @@ -244,9 +237,6 @@ class ComposerController extends BaseController with DragDropFileMixin { _subscriptionOnDragOver?.cancel(); _subscriptionOnDragLeave?.cancel(); _subscriptionOnDrop?.cancel(); - if (PlatformInfo.isMobile) { - FkUserAgent.release(); - } super.onClose(); } @@ -729,7 +719,7 @@ class ComposerController extends BaseController with DragDropFileMixin { attachments.addAll(listInlineEmailBodyPart); } - final userAgent = await userAgentPlatform; + final userAgent = await applicationManager.getUserAgent(); log('ComposerController::_generateEmail(): userAgent: $userAgent'); Map mailboxIds = {}; @@ -814,21 +804,6 @@ class ComposerController extends BaseController with DragDropFileMixin { } } - Future get userAgentPlatform async { - String userAgent; - try { - if (kIsWeb) { - final webBrowserInfo = await _deviceInfoPlugin.webBrowserInfo; - userAgent = webBrowserInfo.userAgent ?? ''; - } else { - userAgent = FkUserAgent.userAgent ?? ''; - } - } catch (e) { - userAgent = ''; - } - return 'Team-Mail/${mailboxDashBoardController.appInformation.value?.version} $userAgent'; - } - void validateInformationBeforeSending(BuildContext context) async { if (isSendEmailLoading.isTrue) { return; diff --git a/lib/features/mailbox/presentation/mailbox_view.dart b/lib/features/mailbox/presentation/mailbox_view.dart index d660d8d1f0..cd892d688e 100644 --- a/lib/features/mailbox/presentation/mailbox_view.dart +++ b/lib/features/mailbox/presentation/mailbox_view.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:model/model.dart'; -import 'package:package_info_plus/package_info_plus.dart'; +import 'package:tmail_ui_user/features/base/widget/application_version_widget.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/base_mailbox_view.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; @@ -85,13 +85,25 @@ class MailboxView extends BaseMailboxView { : const SizedBox.shrink(), ), Obx(() { - final appInformation = controller.mailboxDashBoardController.appInformation.value; - if (appInformation != null - && !controller.isSelectionEnabled()) { - if (controller.responsiveUtils.isLandscapeMobile(context)) { - return const SizedBox.shrink(); - } - return _buildVersionInformation(context, appInformation); + if (!controller.isSelectionEnabled() && controller.responsiveUtils.isPortraitMobile(context)) { + return Container( + color: AppColor.colorBgMailbox, + width: double.infinity, + padding: const EdgeInsets.all(16), + child: SafeArea( + top: false, + child: ApplicationVersionWidget( + applicationManager: controller.applicationManager, + padding: EdgeInsets.zero, + title: '${AppLocalizations.of(context).version} ', + textStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 16, + color: AppColor.colorContentEmail, + fontWeight: FontWeight.w500 + ), + ), + ), + ); } else { return const SizedBox.shrink(); } @@ -365,20 +377,4 @@ class MailboxView extends BaseMailboxView { } }).toList() ?? []; } - - Widget _buildVersionInformation(BuildContext context, PackageInfo packageInfo) { - return Container( - color: AppColor.colorBgMailbox, - width: double.infinity, - padding: const EdgeInsets.all(16), - child: SafeArea( - top: false, - child: Text( - '${AppLocalizations.of(context).version} ${packageInfo.version}', - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 16, color: AppColor.colorContentEmail, fontWeight: FontWeight.w500), - ), - ), - ); - } } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/mailbox_view_web.dart b/lib/features/mailbox/presentation/mailbox_view_web.dart index 07e4900944..4dbab42c82 100644 --- a/lib/features/mailbox/presentation/mailbox_view_web.dart +++ b/lib/features/mailbox/presentation/mailbox_view_web.dart @@ -3,7 +3,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:model/model.dart'; -import 'package:package_info_plus/package_info_plus.dart'; +import 'package:tmail_ui_user/features/base/widget/application_version_widget.dart'; import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/base_mailbox_view.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories.dart'; @@ -68,13 +68,10 @@ class MailboxView extends BaseMailboxView { textStyle: const TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold), logoSVG: controller.imagePaths.icTMailLogo ), - Obx(() { - if (controller.mailboxDashBoardController.appInformation.value != null) { - return _buildVersionInformation(context, controller.mailboxDashBoardController.appInformation.value!); - } else { - return const SizedBox.shrink(); - } - }), + ApplicationVersionWidget( + applicationManager: controller.mailboxDashBoardController.applicationManager, + padding: const EdgeInsets.only(top: 4), + ) ]) ); } @@ -331,15 +328,4 @@ class MailboxView extends BaseMailboxView { controller.mailboxDashBoardController.dragSelectedMultipleEmailToMailboxAction(listEmails, presentationMailbox); } } - - Widget _buildVersionInformation(BuildContext context, PackageInfo packageInfo) { - return Container( - padding: const EdgeInsets.only(top: 4), - child: Text( - 'v.${packageInfo.version}', - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 13, color: AppColor.colorContentEmail, fontWeight: FontWeight.w500), - ), - ); - } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 2a56ca6481..a3773f68dc 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -23,7 +23,6 @@ import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:jmap_dart_client/jmap/mail/vacation/vacation_response.dart'; import 'package:model/model.dart'; -import 'package:package_info_plus/package_info_plus.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:rxdart/transformers.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; @@ -203,7 +202,6 @@ class MailboxDashBoardController extends ReloadableController { final mailboxUIAction = Rxn(); final emailUIAction = Rxn(); final dashboardRoute = DashboardRoutes.waiting.obs; - final appInformation = Rxn(); final currentSelectMode = SelectMode.INACTIVE.obs; final filterMessageOption = FilterMessageOption.all.obs; final listEmailSelected = [].obs; @@ -281,7 +279,6 @@ class MailboxDashBoardController extends ReloadableController { _registerPendingEmailContents(); _registerPendingFileInfo(); _handleArguments(); - _getAppVersion(); super.onReady(); } @@ -541,12 +538,6 @@ class MailboxDashBoardController extends ReloadableController { _handleNotificationMessageFromEmailId(arguments.emailId); } - Future _getAppVersion() async { - final info = await PackageInfo.fromPlatform(); - log('MailboxDashBoardController::_getAppVersion(): ${info.version}'); - appInformation.value = info; - } - void _getVacationResponse() { if (accountId.value != null && _getAllVacationInteractor != null) { consumeState(_getAllVacationInteractor!.execute(accountId.value!)); @@ -1941,12 +1932,16 @@ class MailboxDashBoardController extends ReloadableController { } void _storeSendingEmailInCaseOfSendingFailureInMobile(SendEmailFailure failure) { - if (PlatformInfo.isMobile) { + if (PlatformInfo.isMobile && + failure.session != null && + failure.accountId != null && + failure.emailRequest != null + ) { _tryToStoreSendingEmail( - failure.session, - failure.accountId, - failure.emailRequest, - failure.mailboxRequest + failure.session!, + failure.accountId!, + failure.emailRequest!, + failure.mailboxRequest ); } } diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index 1386e7dc10..622fa1d07c 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -5,6 +5,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:model/extensions/username_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/base/widget/application_version_widget.dart'; import 'package:tmail_ui_user/features/base/widget/popup_item_no_icon_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_view_web.dart'; import 'package:tmail_ui_user/features/email/presentation/email_view.dart'; @@ -76,21 +77,9 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { logoSVG: controller.imagePaths.icTMailLogo, onTapCallback: controller.redirectToInboxAction, ), - Obx(() { - if (controller.appInformation.value != null) { - return Padding(padding: const EdgeInsets.only(top: 6), - child: Text( - 'v${controller.appInformation.value!.version}', - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 13, - color: AppColor.colorContentEmail, - fontWeight: FontWeight.w500), - )); - } else { - return const SizedBox.shrink(); - } - }), + ApplicationVersionWidget( + applicationManager: controller.applicationManager + ) ]) ), Expanded(child: Container( @@ -340,7 +329,10 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { : const SizedBox.shrink(), const SizedBox(width: 24), Obx(() => (AvatarBuilder() - ..text(controller.sessionCurrent?.username.firstCharacter ?? '') + ..text(controller.accountId.value != null + ? controller.sessionCurrent?.username.firstCharacter ?? '' + : '' + ) ..backgroundColor(Colors.white) ..textColor(Colors.black) ..context(context) diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart index 2c7a77ca8f..1cc6b2404b 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart @@ -10,7 +10,6 @@ import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/vacation/vacation_response.dart'; import 'package:model/model.dart'; -import 'package:package_info_plus/package_info_plus.dart'; import 'package:rule_filter/rule_filter/capability_rule_filter.dart'; import 'package:server_settings/server_settings/capability_server_settings.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; @@ -44,7 +43,6 @@ class ManageAccountDashBoardController extends ReloadableController { GetAllVacationInteractor? _getAllVacationInteractor; UpdateVacationInteractor? _updateVacationInteractor; - final appInformation = Rxn(); final accountId = Rxn(); final accountMenuItemSelected = AccountMenuItem.profiles.obs; final settingsPageLevel = SettingsPageLevel.universal.obs; @@ -66,7 +64,6 @@ class ManageAccountDashBoardController extends ReloadableController { void onReady() { _initialPageLevel(); _getArguments(); - _getAppVersion(); super.onReady(); } @@ -151,12 +148,6 @@ class ManageAccountDashBoardController extends ReloadableController { } } - Future _getAppVersion() async { - final info = await PackageInfo.fromPlatform(); - log('ManageAccountDashBoardController::_getAppVersion(): ${info.version}'); - appInformation.value = info; - } - void _getVacationResponse() { if (accountId.value != null && _getAllVacationInteractor != null) { consumeState(_getAllVacationInteractor!.execute(accountId.value!)); diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart index 1190fd0971..efb63fabf8 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/state/banner_state.dart'; +import 'package:tmail_ui_user/features/base/widget/application_version_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mixin/user_setting_popup_menu_mixin.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/always_read_receipt/always_read_receipt_view.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/email_rules/email_rules_view.dart'; @@ -55,18 +56,9 @@ class ManageAccountDashBoardView extends GetWidget controller.backToMailboxDashBoard(context: context), ), - Obx(() { - if (controller.appInformation.value != null) { - return Padding(padding: const EdgeInsets.only(top: 6), - child: Text( - 'v.${controller.appInformation.value!.version}', - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 13, color: AppColor.colorContentEmail, fontWeight: FontWeight.w500), - )); - } else { - return const SizedBox.shrink(); - } - }), + ApplicationVersionWidget( + applicationManager: controller.applicationManager + ) ]) ), Expanded(child: Padding( diff --git a/lib/features/manage_account/presentation/menu/manage_account_menu_view.dart b/lib/features/manage_account/presentation/menu/manage_account_menu_view.dart index 7de028f2b3..7154338c69 100644 --- a/lib/features/manage_account/presentation/menu/manage_account_menu_view.dart +++ b/lib/features/manage_account/presentation/menu/manage_account_menu_view.dart @@ -4,6 +4,7 @@ import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/widget/application_version_widget.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/manage_account_menu_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/widgets/account_menu_item_tile_builder.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -31,20 +32,10 @@ class ManageAccountMenuView extends GetWidget { textStyle: const TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold), logoSVG: controller.imagePaths.icTMailLogo ), - Obx(() { - if (controller.dashBoardController.appInformation.value != null) { - return Padding( - padding: const EdgeInsets.only(top: 4), - child: Text( - 'v.${controller.dashBoardController.appInformation.value!.version}', - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 13, color: AppColor.colorContentEmail, fontWeight: FontWeight.w500), - ), - ); - } else { - return const SizedBox.shrink(); - } - }), + ApplicationVersionWidget( + applicationManager: controller.dashBoardController.applicationManager, + padding: const EdgeInsets.only(top: 4), + ) ]) ), if (!controller.responsiveUtils.isWebDesktop(context)) diff --git a/lib/main/bindings/core/core_bindings.dart b/lib/main/bindings/core/core_bindings.dart index 389a91fcb9..fea26aee3c 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -3,6 +3,7 @@ import 'package:core/data/utils/device_manager.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/application_manager.dart'; import 'package:core/utils/config/app_config_loader.dart'; import 'package:core/utils/file_utils.dart'; import 'package:core/utils/platform_info.dart'; @@ -62,6 +63,7 @@ class CoreBindings extends Bindings { Get.put(AppConfigLoader()); Get.put(FileUtils()); Get.put(PrintUtils()); + Get.put(ApplicationManager(Get.find())); } void _bindingIsolate() { diff --git a/model/pubspec.lock b/model/pubspec.lock index 2361d5268e..aee232529b 100644 --- a/model/pubspec.lock +++ b/model/pubspec.lock @@ -304,6 +304,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + fk_user_agent: + dependency: transitive + description: + name: fk_user_agent + sha256: fd6c94e120786985a292d12f61422a581f4e851148d5940af38b819357b8ad0d + url: "https://pub.dev" + source: hosted + version: "2.1.0" flex_color_picker: dependency: transitive description: @@ -648,6 +656,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" path: dependency: transitive description: diff --git a/pubspec.lock b/pubspec.lock index 0f8d80bcdb..7d5ccb764f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -594,7 +594,7 @@ packages: source: hosted version: "1.1.0" fk_user_agent: - dependency: "direct main" + dependency: transitive description: name: fk_user_agent sha256: fd6c94e120786985a292d12f61422a581f4e851148d5940af38b819357b8ad0d @@ -1272,7 +1272,7 @@ packages: source: hosted version: "2.1.0" package_info_plus: - dependency: "direct main" + dependency: transitive description: name: package_info_plus sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" diff --git a/pubspec.yaml b/pubspec.yaml index 45f423df55..3b82ed9479 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -152,16 +152,12 @@ dependencies: hive: 2.2.3 - fk_user_agent: 2.1.0 - pointer_interceptor: 0.9.1 rxdart: 0.27.7 connectivity_plus: 3.0.3 - package_info_plus: 4.2.0 - dropdown_button2: 2.0.0 flutter_staggered_grid_view: 0.6.2 diff --git a/test/features/email/presentation/controller/single_email_controller_test.dart b/test/features/email/presentation/controller/single_email_controller_test.dart index a64474e935..79491b7136 100644 --- a/test/features/email/presentation/controller/single_email_controller_test.dart +++ b/test/features/email/presentation/controller/single_email_controller_test.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:core/core.dart'; +import 'package:core/utils/application_manager.dart'; import 'package:dartz/dartz.dart' hide State; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; @@ -86,6 +87,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -118,6 +120,7 @@ void main() { final printEmailInteractor = MockPrintEmailInteractor(); final storeEventAttendanceStatusInteractor = MockStoreEventAttendanceStatusInteractor(); final printUtils = MockPrintUtils(); + final applicationManager = MockApplicationManager(); late SingleEmailController singleEmailController; @@ -151,6 +154,7 @@ void main() { Get.put(responsiveUtils); Get.put(uuid); Get.put(printUtils); + Get.put(applicationManager); when(mailboxDashboardController.accountId).thenReturn(Rxn(testAccountId)); when(uuid.v4()).thenReturn(testTaskId); @@ -257,7 +261,7 @@ void main() { BlobCalendarEvent( blobId: blobId, calendarEventList: [calendarEvent])])); - + // act singleEmailController.onCalendarEventReplyAction(EventActionType.no, emailId); await untilCalled(rejectCalendarEventInteractor.execute(any, any, any, any)); From 873272e6e479db46580e89da71133333567b001d Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 5 Mar 2024 18:47:31 +0700 Subject: [PATCH 10/80] TF-2667 Move the creation of mailbox request id to the data layer Signed-off-by: dab246 --- .../domain/extensions/email_request_extension.dart | 1 - .../presentation/destination_picker_controller.dart | 3 --- lib/features/email/data/network/email_api.dart | 11 +++++------ lib/features/mailbox/data/network/mailbox_api.dart | 10 ++++++---- .../domain/model/create_new_mailbox_request.dart | 4 ---- .../mailbox/presentation/mailbox_controller.dart | 3 --- .../sending_email_hive_cache_extension.dart | 2 -- .../offline_mode/model/sending_email_hive_cache.dart | 7 +------ .../work_manager/sending_email_worker.dart | 4 +--- .../presentation/search_mailbox_controller.dart | 3 --- .../domain/extensions/sending_email_extension.dart | 4 ---- .../sending_queue/domain/model/sending_email.dart | 7 ------- .../presentation/sending_queue_controller.dart | 5 +---- 13 files changed, 14 insertions(+), 50 deletions(-) diff --git a/lib/features/composer/domain/extensions/email_request_extension.dart b/lib/features/composer/domain/extensions/email_request_extension.dart index 41591c0c61..83274d99c2 100644 --- a/lib/features/composer/domain/extensions/email_request_extension.dart +++ b/lib/features/composer/domain/extensions/email_request_extension.dart @@ -24,7 +24,6 @@ extension EmailRequestExtension on EmailRequest { emailIdAnsweredOrForwarded: emailIdAnsweredOrForwarded, identityId: identityId, mailboxNameRequest: mailboxRequest?.newName, - creationIdRequest: mailboxRequest?.creationId, sendingState: newState, previousEmailId: previousEmailId ); diff --git a/lib/features/destination_picker/presentation/destination_picker_controller.dart b/lib/features/destination_picker/presentation/destination_picker_controller.dart index 4f02e76096..9437b23eaa 100644 --- a/lib/features/destination_picker/presentation/destination_picker_controller.dart +++ b/lib/features/destination_picker/presentation/destination_picker_controller.dart @@ -7,7 +7,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.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/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; @@ -387,7 +386,6 @@ class DestinationPickerController extends BaseMailboxController { final nameMailbox = newNameMailbox.value; if (nameMailbox != null && nameMailbox.isNotEmpty) { - final generateCreateId = Id(uuid.v1()); final parentId = mailboxDestination.value == PresentationMailbox.unifiedMailbox ? null : mailboxDestination.value?.id; @@ -396,7 +394,6 @@ class DestinationPickerController extends BaseMailboxController { _session!, accountId!, CreateNewMailboxRequest( - generateCreateId, MailboxName(nameMailbox), parentId: parentId)); } diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 94c71e57e2..55dadc49a0 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -113,7 +113,7 @@ class EmailAPI with HandleSetErrorMixin { } } - Future sendEmail( + Future sendEmail( Session session, AccountId accountId, EmailRequest emailRequest, @@ -125,9 +125,10 @@ class EmailAPI with HandleSetErrorMixin { MailboxId? outboxMailboxId; if (mailboxRequest != null) { + final generateCreateId = Id(_uuid.v1()); final setMailboxMethod = SetMailboxMethod(accountId) ..addCreate( - mailboxRequest.creationId, + generateCreateId, Mailbox( name: mailboxRequest.newName, parentId: mailboxRequest.parentId, @@ -139,7 +140,7 @@ class EmailAPI with HandleSetErrorMixin { outboxMailboxId = MailboxId(ReferenceId( ReferencePrefix.defaultPrefix, - mailboxRequest.creationId)); + generateCreateId)); emailNeedsToBeCreated = emailRequest.email.updatedEmail(newMailboxIds: {outboxMailboxId: true}); } else { outboxMailboxId = emailRequest.email.mailboxIds?.keys.first; @@ -222,9 +223,7 @@ class EmailAPI with HandleSetErrorMixin { markAsAnsweredOrForwardedSetResponse ]); - if (emailCreated != null && mapErrors.isEmpty) { - return true; - } else { + if (emailCreated == null || mapErrors.isNotEmpty) { throw SetMethodException(mapErrors); } } diff --git a/lib/features/mailbox/data/network/mailbox_api.dart b/lib/features/mailbox/data/network/mailbox_api.dart index 6d7df8bfe9..901a0c4739 100644 --- a/lib/features/mailbox/data/network/mailbox_api.dart +++ b/lib/features/mailbox/data/network/mailbox_api.dart @@ -148,9 +148,11 @@ class MailboxAPI with HandleSetErrorMixin { } Future createNewMailbox(Session session, AccountId accountId, CreateNewMailboxRequest request) async { + final generateCreateId = Id(_uuid.v1()); + final setMailboxMethod = SetMailboxMethod(accountId) ..addCreate( - request.creationId, + generateCreateId, Mailbox( name: request.newName, isSubscribed: IsSubscribed(request.isSubscribed), @@ -176,14 +178,14 @@ class MailboxAPI with HandleSetErrorMixin { final mapMailboxCreated = setMailboxResponse?.created; if (mapMailboxCreated != null && - mapMailboxCreated.containsKey(request.creationId)) { - final mailboxCreated = mapMailboxCreated[request.creationId]!; + mapMailboxCreated.containsKey(generateCreateId)) { + final mailboxCreated = mapMailboxCreated[generateCreateId]!; final newMailboxCreated = mailboxCreated.toMailbox( request.newName, parentId: request.parentId); return newMailboxCreated; } else { - throw _parseErrorForSetMailboxResponse(setMailboxResponse, request.creationId); + throw _parseErrorForSetMailboxResponse(setMailboxResponse, generateCreateId); } } diff --git a/lib/features/mailbox/domain/model/create_new_mailbox_request.dart b/lib/features/mailbox/domain/model/create_new_mailbox_request.dart index 3c50807a5f..b9273b5cb6 100644 --- a/lib/features/mailbox/domain/model/create_new_mailbox_request.dart +++ b/lib/features/mailbox/domain/model/create_new_mailbox_request.dart @@ -1,17 +1,14 @@ import 'package:equatable/equatable.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; class CreateNewMailboxRequest with EquatableMixin { final MailboxName newName; - final Id creationId; final MailboxId? parentId; final bool isSubscribed; CreateNewMailboxRequest( - this.creationId, this.newName, { this.parentId, @@ -21,7 +18,6 @@ class CreateNewMailboxRequest with EquatableMixin { @override List get props => [ - creationId, newName, parentId, isSubscribed diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 8dd3ff48ec..25247f7891 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -8,7 +8,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; @@ -591,9 +590,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM : await push(AppRoutes.mailboxCreator, arguments: arguments); if (result != null && result is NewMailboxArguments) { - final generateCreateId = Id(uuid.v1()); _createNewMailboxAction(session, accountId, CreateNewMailboxRequest( - generateCreateId, result.newName, parentId: result.mailboxLocation?.id)); } diff --git a/lib/features/offline_mode/extensions/sending_email_hive_cache_extension.dart b/lib/features/offline_mode/extensions/sending_email_hive_cache_extension.dart index e9d2e7e570..3c0418b751 100644 --- a/lib/features/offline_mode/extensions/sending_email_hive_cache_extension.dart +++ b/lib/features/offline_mode/extensions/sending_email_hive_cache_extension.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:jmap_dart_client/http/converter/email_id_nullable_converter.dart'; -import 'package:jmap_dart_client/http/converter/id_nullable_converter.dart'; import 'package:jmap_dart_client/http/converter/identities/identity_id_nullable_converter.dart'; import 'package:jmap_dart_client/http/converter/mailbox_id_nullable_converter.dart'; import 'package:jmap_dart_client/http/converter/mailbox_name_converter.dart'; @@ -25,7 +24,6 @@ extension SendingEmailHiveCacheExtension on SendingEmailHiveCache { emailIdAnsweredOrForwarded: const EmailIdNullableConverter().fromJson(emailIdAnsweredOrForwarded), identityId: const IdentityIdNullableConverter().fromJson(identityId), mailboxNameRequest: const MailboxNameConverter().fromJson(mailboxNameRequest), - creationIdRequest: const IdNullableConverter().fromJson(creationIdRequest), sendingState: SendingState.values.firstWhere((value) => value.name == sendingState), previousEmailId: const EmailIdNullableConverter().fromJson(previousEmailId), ); diff --git a/lib/features/offline_mode/model/sending_email_hive_cache.dart b/lib/features/offline_mode/model/sending_email_hive_cache.dart index ee8cae8b66..34cdce08d6 100644 --- a/lib/features/offline_mode/model/sending_email_hive_cache.dart +++ b/lib/features/offline_mode/model/sending_email_hive_cache.dart @@ -36,12 +36,9 @@ class SendingEmailHiveCache extends HiveObject with EquatableMixin { final String? mailboxNameRequest; @HiveField(9) - final String? creationIdRequest; - - @HiveField(10) final String sendingState; - @HiveField(11) + @HiveField(10) final String? previousEmailId; SendingEmailHiveCache( @@ -54,7 +51,6 @@ class SendingEmailHiveCache extends HiveObject with EquatableMixin { this.emailIdAnsweredOrForwarded, this.identityId, this.mailboxNameRequest, - this.creationIdRequest, this.sendingState, this.previousEmailId, ); @@ -70,7 +66,6 @@ class SendingEmailHiveCache extends HiveObject with EquatableMixin { emailIdAnsweredOrForwarded, identityId, mailboxNameRequest, - creationIdRequest, sendingState, previousEmailId, ]; diff --git a/lib/features/offline_mode/work_manager/sending_email_worker.dart b/lib/features/offline_mode/work_manager/sending_email_worker.dart index 530dfb57f2..cc8064914c 100644 --- a/lib/features/offline_mode/work_manager/sending_email_worker.dart +++ b/lib/features/offline_mode/work_manager/sending_email_worker.dart @@ -193,10 +193,8 @@ class SendingEmailWorker extends Worker { } CreateNewMailboxRequest? _getMailboxRequest() { - if (_sendingEmail.mailboxNameRequest != null && - _sendingEmail.creationIdRequest != null) { + if (_sendingEmail.mailboxNameRequest != null) { return CreateNewMailboxRequest( - _sendingEmail.creationIdRequest!, _sendingEmail.mailboxNameRequest!); } else { return null; diff --git a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart index 275808a626..a04f107793 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart @@ -11,7 +11,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; @@ -676,9 +675,7 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa : await push(AppRoutes.mailboxCreator, arguments: arguments); if (result != null && result is NewMailboxArguments) { - final generateCreateId = Id(uuid.v1()); _createNewMailboxAction(session, accountId, CreateNewMailboxRequest( - generateCreateId, result.newName, parentId: result.mailboxLocation?.id)); } diff --git a/lib/features/sending_queue/domain/extensions/sending_email_extension.dart b/lib/features/sending_queue/domain/extensions/sending_email_extension.dart index 6b01c6dbf6..f907fe2cf8 100644 --- a/lib/features/sending_queue/domain/extensions/sending_email_extension.dart +++ b/lib/features/sending_queue/domain/extensions/sending_email_extension.dart @@ -21,7 +21,6 @@ extension SendingEmailExtension on SendingEmail { emailIdAnsweredOrForwarded?.asString, identityId?.asString, mailboxNameRequest?.name, - creationIdRequest?.value, sendingState.name, previousEmailId?.asString, ); @@ -51,7 +50,6 @@ extension SendingEmailExtension on SendingEmail { emailIdAnsweredOrForwarded: emailIdAnsweredOrForwarded, identityId: identityId, mailboxNameRequest: mailboxNameRequest, - creationIdRequest: creationIdRequest, sendingState: sendingState, selectMode: selectMode == SelectMode.INACTIVE ? SelectMode.ACTIVE : SelectMode.INACTIVE, previousEmailId: previousEmailId, @@ -69,7 +67,6 @@ extension SendingEmailExtension on SendingEmail { emailIdAnsweredOrForwarded: emailIdAnsweredOrForwarded, identityId: identityId, mailboxNameRequest: mailboxNameRequest, - creationIdRequest: creationIdRequest, sendingState: sendingState, selectMode: SelectMode.INACTIVE, previousEmailId: previousEmailId, @@ -87,7 +84,6 @@ extension SendingEmailExtension on SendingEmail { emailIdAnsweredOrForwarded: emailIdAnsweredOrForwarded, identityId: identityId, mailboxNameRequest: mailboxNameRequest, - creationIdRequest: creationIdRequest, sendingState: newState, selectMode: selectMode, previousEmailId: previousEmailId, diff --git a/lib/features/sending_queue/domain/model/sending_email.dart b/lib/features/sending_queue/domain/model/sending_email.dart index 13f96059da..1a2af61162 100644 --- a/lib/features/sending_queue/domain/model/sending_email.dart +++ b/lib/features/sending_queue/domain/model/sending_email.dart @@ -5,11 +5,9 @@ import 'package:core/utils/platform_info.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_date_range_picker/flutter_date_range_picker.dart'; import 'package:jmap_dart_client/http/converter/email_id_nullable_converter.dart'; -import 'package:jmap_dart_client/http/converter/id_nullable_converter.dart'; import 'package:jmap_dart_client/http/converter/identities/identity_id_nullable_converter.dart'; import 'package:jmap_dart_client/http/converter/mailbox_id_nullable_converter.dart'; import 'package:jmap_dart_client/http/converter/mailbox_name_converter.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; @@ -28,7 +26,6 @@ class SendingEmail with EquatableMixin { final IdentityId? identityId; final EmailActionType emailActionType; final MailboxName? mailboxNameRequest; - final Id? creationIdRequest; final DateTime createTime; final SelectMode selectMode; final SendingState sendingState; @@ -44,7 +41,6 @@ class SendingEmail with EquatableMixin { this.emailIdAnsweredOrForwarded, this.identityId, this.mailboxNameRequest, - this.creationIdRequest, this.selectMode = SelectMode.INACTIVE, this.sendingState = SendingState.waiting, this.previousEmailId, @@ -68,7 +64,6 @@ class SendingEmail with EquatableMixin { writeNotNull('emailIdAnsweredOrForwarded', const EmailIdNullableConverter().toJson(emailIdAnsweredOrForwarded)); writeNotNull('identityId', const IdentityIdNullableConverter().toJson(identityId)); writeNotNull('mailboxNameRequest', mailboxNameRequest?.name); - writeNotNull('creationIdRequest', const IdNullableConverter().toJson(creationIdRequest)); writeNotNull('previousEmailId', const EmailIdNullableConverter().toJson(previousEmailId)); return val; @@ -91,7 +86,6 @@ class SendingEmail with EquatableMixin { emailIdAnsweredOrForwarded: const EmailIdNullableConverter().fromJson(json['emailIdAnsweredOrForwarded'] as String?), identityId: const IdentityIdNullableConverter().fromJson(json['identityId'] as String?), mailboxNameRequest: const MailboxNameConverter().fromJson(json['mailboxNameRequest'] as String?), - creationIdRequest: const IdNullableConverter().fromJson(json['creationIdRequest'] as String?), previousEmailId: const EmailIdNullableConverter().fromJson(json['previousEmailId'] as String?), ); } @@ -134,7 +128,6 @@ class SendingEmail with EquatableMixin { emailIdAnsweredOrForwarded, identityId, mailboxNameRequest, - creationIdRequest, selectMode, sendingState, previousEmailId, diff --git a/lib/features/sending_queue/presentation/sending_queue_controller.dart b/lib/features/sending_queue/presentation/sending_queue_controller.dart index 9ed699ed10..9606ce1319 100644 --- a/lib/features/sending_queue/presentation/sending_queue_controller.dart +++ b/lib/features/sending_queue/presentation/sending_queue_controller.dart @@ -283,11 +283,8 @@ class SendingQueueController extends BaseController with MessageDialogActionMixi } CreateNewMailboxRequest? _getMailboxRequest(SendingEmail sendingEmail) { - if (sendingEmail.mailboxNameRequest != null && - sendingEmail.creationIdRequest != null - ) { + if (sendingEmail.mailboxNameRequest != null) { return CreateNewMailboxRequest( - sendingEmail.creationIdRequest!, sendingEmail.mailboxNameRequest! ); } else { From 8dbcbf5405ae3c37dbe87844f092d6b7dfee7056 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 5 Mar 2024 22:57:51 +0700 Subject: [PATCH 11/80] TF-2667 Show sending message dialog when click send button --- core/lib/data/constants/constant.dart | 1 + core/lib/utils/application_manager.dart | 1 + .../widget/application_version_widget.dart | 24 +- .../repository/composer_repository_impl.dart | 80 ++++++- .../repository/composer_repository.dart | 4 + .../domain/state/generate_email_state.dart | 9 + .../domain/state/send_email_state.dart | 14 +- .../create_new_and_send_email_interactor.dart | 128 +++++++++++ .../usecases/send_email_interactor.dart | 32 +-- .../presentation/composer_bindings.dart | 19 +- .../presentation/composer_controller.dart | 111 +++++----- .../create_email_request_extension.dart | 176 +++++++++++++++ .../extensions/identity_extension.dart | 6 + .../model/create_email_request.dart | 88 ++++++++ .../widgets/sending_message_dialog_view.dart | 206 ++++++++++++++++++ .../data/datasource/email_datasource.dart | 2 +- .../data/datasource/html_datasource.dart | 13 +- .../email_datasource_impl.dart | 2 +- .../datasource_impl/html_datasource_impl.dart | 25 +++ .../email/data/local/html_analyzer.dart | 58 +++++ .../repository/email_repository_impl.dart | 2 +- .../domain/exceptions/email_exceptions.dart | 4 +- .../list_attachments_extension.dart | 4 + .../domain/repository/email_repository.dart | 2 +- .../presentation/bindings/email_bindings.dart | 2 +- .../bindings/mailbox_dashboard_bindings.dart | 2 +- .../mailbox_dashboard_controller.dart | 10 +- .../manage_account_dashboard_view.dart | 5 +- .../web_network_connection_controller.dart | 2 + .../sending_email_interactor_bindings.dart | 2 +- .../bindings/fcm_interactor_bindings.dart | 2 +- .../controller/upload_controller.dart | 3 +- .../send_email_exception_thrower.dart | 3 +- .../localizations/localization_service.dart | 27 ++- model/lib/email/attachment.dart | 13 -- .../lib/extensions/attachment_extension.dart | 7 +- model/lib/extensions/username_extension.dart | 7 +- 37 files changed, 954 insertions(+), 142 deletions(-) create mode 100644 lib/features/composer/domain/state/generate_email_state.dart create mode 100644 lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart create mode 100644 lib/features/composer/presentation/extensions/create_email_request_extension.dart create mode 100644 lib/features/composer/presentation/extensions/identity_extension.dart create mode 100644 lib/features/composer/presentation/model/create_email_request.dart create mode 100644 lib/features/composer/presentation/widgets/sending_message_dialog_view.dart diff --git a/core/lib/data/constants/constant.dart b/core/lib/data/constants/constant.dart index adf3581cbe..5540b0ac1c 100644 --- a/core/lib/data/constants/constant.dart +++ b/core/lib/data/constants/constant.dart @@ -2,6 +2,7 @@ class Constant { static const acceptHeaderDefault = 'application/json'; static const contentTypeHeaderDefault = 'application/json'; static const pdfMimeType = 'application/pdf'; + static const base64Charset = 'base64'; static const textHtmlMimeType = 'text/html'; static const octetStreamMimeType = 'application/octet-stream'; static const pdfExtension = '.pdf'; diff --git a/core/lib/utils/application_manager.dart b/core/lib/utils/application_manager.dart index c7b291c470..b452f6670f 100644 --- a/core/lib/utils/application_manager.dart +++ b/core/lib/utils/application_manager.dart @@ -34,6 +34,7 @@ class ApplicationManager { userAgent = FkUserAgent.userAgent ?? ''; FkUserAgent.release(); } + log('ApplicationManager::getUserAgent: $userAgent'); return userAgent; } catch(e) { logError('ApplicationManager::getUserAgent: Exception: $e'); diff --git a/lib/features/base/widget/application_version_widget.dart b/lib/features/base/widget/application_version_widget.dart index 38eab27168..8a05bd0407 100644 --- a/lib/features/base/widget/application_version_widget.dart +++ b/lib/features/base/widget/application_version_widget.dart @@ -2,7 +2,7 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/utils/application_manager.dart'; import 'package:flutter/material.dart'; -class ApplicationVersionWidget extends StatelessWidget { +class ApplicationVersionWidget extends StatefulWidget { final ApplicationManager applicationManager; final EdgeInsetsGeometry? padding; @@ -17,18 +17,32 @@ class ApplicationVersionWidget extends StatelessWidget { this.padding, }); + @override + State createState() => _ApplicationVersionWidgetState(); +} + +class _ApplicationVersionWidgetState extends State { + + Future? _versionStream; + + @override + void initState() { + super.initState(); + _versionStream = widget.applicationManager.getVersion(); + } + @override Widget build(BuildContext context) { return FutureBuilder( - future: applicationManager.getVersion(), + future: _versionStream, builder: (context, snapshot) { if (snapshot.hasData) { return Padding( - padding: padding ?? const EdgeInsets.only(top: 6), + padding: widget.padding ?? const EdgeInsets.only(top: 6), child: Text( - '${title ?? 'v.'}${snapshot.data}', + '${widget.title ?? 'v.'}${snapshot.data}', textAlign: TextAlign.center, - style: textStyle ?? Theme.of(context).textTheme.labelMedium?.copyWith( + style: widget.textStyle ?? Theme.of(context).textTheme.labelMedium?.copyWith( fontSize: 13, color: AppColor.colorContentEmail, fontWeight: FontWeight.w500 diff --git a/lib/features/composer/data/repository/composer_repository_impl.dart b/lib/features/composer/data/repository/composer_repository_impl.dart index 196e4de47d..1ee0c4b918 100644 --- a/lib/features/composer/data/repository/composer_repository_impl.dart +++ b/lib/features/composer/data/repository/composer_repository_impl.dart @@ -1,19 +1,37 @@ - +import 'package:core/data/constants/constant.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/application_manager.dart'; +import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:model/email/attachment.dart'; import 'package:model/upload/file_info.dart'; import 'package:tmail_ui_user/features/composer/data/datasource/composer_datasource.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/create_email_request_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/list_attachments_extension.dart'; import 'package:tmail_ui_user/features/upload/data/datasource/attachment_upload_datasource.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_attachment.dart'; +import 'package:uuid/uuid.dart'; class ComposerRepositoryImpl extends ComposerRepository { final AttachmentUploadDataSource _attachmentUploadDataSource; final ComposerDataSource _composerDataSource; + final HtmlDataSource _htmlDataSource; + final ApplicationManager _applicationManager; + final Uuid _uuid; ComposerRepositoryImpl( this._attachmentUploadDataSource, - this._composerDataSource); + this._composerDataSource, + this._htmlDataSource, + this._applicationManager, + this._uuid, + ); @override Future uploadAttachment(FileInfo fileInfo, Uri uploadUri, {CancelToken? cancelToken}) { @@ -24,4 +42,62 @@ class ComposerRepositoryImpl extends ComposerRepository { Future downloadImageAsBase64(String url, String cid, FileInfo fileInfo, {double? maxWidth, bool? compress}) { return _composerDataSource.downloadImageAsBase64(url, cid, fileInfo, maxWidth: maxWidth, compress: compress); } + + @override + Future generateEmail(CreateEmailRequest createEmailRequest) async { + String emailContent = createEmailRequest.emailContent; + Set emailAttachments = Set.from(createEmailRequest.createAttachments()); + + if (createEmailRequest.inlineAttachments?.isNotEmpty == true) { + final tupleContentInlineAttachments = await _replaceImageBase64ToImageCID( + emailContent: emailContent, + inlineAttachments: createEmailRequest.inlineAttachments! + ); + + emailContent = tupleContentInlineAttachments.value1; + emailAttachments.addAll(tupleContentInlineAttachments.value2); + } + + emailContent = await _removeCollapsedExpandedSignatureEffect(emailContent: emailContent); + + final userAgent = await _applicationManager.generateApplicationUserAgent(); + final emailBodyPartId = PartId(_uuid.v1()); + + final emailObject = createEmailRequest.generateEmail( + newEmailContent: emailContent, + newEmailAttachments: emailAttachments, + userAgent: userAgent, + partId: emailBodyPartId + ); + + return emailObject; + } + + Future>> _replaceImageBase64ToImageCID({ + required String emailContent, + required Map inlineAttachments + }) { + try { + return _htmlDataSource.replaceImageBase64ToImageCID( + emailContent: emailContent, + inlineAttachments: inlineAttachments); + } catch (e) { + logError('ComposerRepositoryImpl::_replaceImageBase64ToImageCID: Exception: $e'); + return Future.value( + Tuple2( + emailContent, + inlineAttachments.values.toList().toEmailBodyPart(charset: Constant.base64Charset) + ) + ); + } + } + + Future _removeCollapsedExpandedSignatureEffect({required String emailContent}) { + try { + return _htmlDataSource.removeCollapsedExpandedSignatureEffect(emailContent: emailContent); + } catch (e) { + logError('ComposerRepositoryImpl::_removeCollapsedExpandedSignatureEffect: Exception: $e'); + return Future.value(emailContent); + } + } } \ No newline at end of file diff --git a/lib/features/composer/domain/repository/composer_repository.dart b/lib/features/composer/domain/repository/composer_repository.dart index 33c9cf4fd7..8ba787980c 100644 --- a/lib/features/composer/domain/repository/composer_repository.dart +++ b/lib/features/composer/domain/repository/composer_repository.dart @@ -1,8 +1,12 @@ import 'package:dio/dio.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/upload/file_info.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_attachment.dart'; abstract class ComposerRepository { + Future generateEmail(CreateEmailRequest createEmailRequest); + Future uploadAttachment(FileInfo fileInfo, Uri uploadUri, {CancelToken? cancelToken}); Future downloadImageAsBase64(String url, String cid, FileInfo fileInfo, {double? maxWidth, bool? compress}); diff --git a/lib/features/composer/domain/state/generate_email_state.dart b/lib/features/composer/domain/state/generate_email_state.dart new file mode 100644 index 0000000000..f41c7e300d --- /dev/null +++ b/lib/features/composer/domain/state/generate_email_state.dart @@ -0,0 +1,9 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class GenerateEmailLoading extends LoadingState {} + +class GenerateEmailFailure extends FeatureFailure { + + GenerateEmailFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/composer/domain/state/send_email_state.dart b/lib/features/composer/domain/state/send_email_state.dart index 3aa23b7f00..01312e80ff 100644 --- a/lib/features/composer/domain/state/send_email_state.dart +++ b/lib/features/composer/domain/state/send_email_state.dart @@ -8,7 +8,7 @@ import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart' import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_action_type.dart'; -class SendEmailLoading extends UIState {} +class SendEmailLoading extends LoadingState {} class SendEmailSuccess extends UIActionState { @@ -29,17 +29,17 @@ class SendEmailSuccess extends UIActionState { class SendEmailFailure extends FeatureFailure { - final Session session; - final AccountId accountId; - final EmailRequest emailRequest; + final Session? session; + final AccountId? accountId; + final EmailRequest? emailRequest; final CreateNewMailboxRequest? mailboxRequest; final SendingEmailActionType? sendingEmailActionType; SendEmailFailure({ dynamic exception, - required this.session, - required this.accountId, - required this.emailRequest, + this.session, + this.accountId, + this.emailRequest, this.mailboxRequest, this.sendingEmailActionType }) : super(exception: exception); diff --git a/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart b/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart new file mode 100644 index 0000000000..2c21547d97 --- /dev/null +++ b/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart @@ -0,0 +1,128 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart' as dartz; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/create_email_request_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; + +class CreateNewAndSendEmailInteractor { + final EmailRepository _emailRepository; + final MailboxRepository _mailboxRepository; + final ComposerRepository _composerRepository; + + CreateNewAndSendEmailInteractor( + this._emailRepository, + this._mailboxRepository, + this._composerRepository, + ); + + Stream> execute(CreateEmailRequest createEmailRequest) async* { + SendingEmailArguments? sendingEmailArguments; + try { + yield dartz.Right(GenerateEmailLoading()); + + final listCurrentState = await _getStoredCurrentState( + session: createEmailRequest.session, + accountId: createEmailRequest.accountId + ); + + sendingEmailArguments = await _createEmailObject(createEmailRequest); + + if (sendingEmailArguments != null) { + yield dartz.Right(SendEmailLoading()); + + await _emailRepository.sendEmail( + sendingEmailArguments.session, + sendingEmailArguments.accountId, + sendingEmailArguments.emailRequest, + mailboxRequest: sendingEmailArguments.mailboxRequest + ); + + if (sendingEmailArguments.emailRequest.emailIdDestroyed != null) { + await _deleteOldDraftsEmail( + session: sendingEmailArguments.session, + accountId: sendingEmailArguments.accountId, + draftEmailId: sendingEmailArguments.emailRequest.emailIdDestroyed! + ); + } + + yield dartz.Right( + SendEmailSuccess( + currentMailboxState: listCurrentState?.value1, + currentEmailState: listCurrentState?.value2, + emailRequest: sendingEmailArguments.emailRequest + ) + ); + } else { + yield dartz.Left(GenerateEmailFailure(CannotCreateEmailObjectException())); + } + } catch (e) { + logError('CreateNewAndSendEmailInteractor::execute: Exception: $e'); + yield dartz.Left(SendEmailFailure( + exception: e, + session: sendingEmailArguments?.session, + accountId: sendingEmailArguments?.accountId, + emailRequest: sendingEmailArguments?.emailRequest, + mailboxRequest: sendingEmailArguments?.mailboxRequest, + )); + } + } + + Future _createEmailObject(CreateEmailRequest createEmailRequest) async { + try { + final emailCreated = await _composerRepository.generateEmail(createEmailRequest); + final sendingEmailArgument = createEmailRequest.toSendingEmailArguments(emailObject: emailCreated); + return sendingEmailArgument; + } catch (e) { + logError('CreateNewAndSendEmailInteractor::_createEmailObject: Exception: $e'); + return null; + } + } + + Future?> _getStoredCurrentState({ + required Session session, + required AccountId accountId + }) async { + try { + final listState = await Future.wait([ + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), + ]); + + final mailboxState = listState.first; + final emailState = listState.last; + + return dartz.Tuple2(mailboxState, emailState); + } catch (e) { + logError('CreateNewAndSendEmailInteractor::_getStoredCurrentState: Exception: $e'); + return null; + } + } + + Future _deleteOldDraftsEmail({ + required Session session, + required AccountId accountId, + required EmailId draftEmailId + }) async { + try { + await _emailRepository.deleteEmailPermanently( + session, + accountId, + draftEmailId + ); + } catch (e) { + logError('CreateNewAndSendEmailInteractor::_deleteOldDraftsEmail: Exception: $e'); + } + } +} \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/send_email_interactor.dart b/lib/features/composer/domain/usecases/send_email_interactor.dart index 218d9b71a9..83ade78aca 100644 --- a/lib/features/composer/domain/usecases/send_email_interactor.dart +++ b/lib/features/composer/domain/usecases/send_email_interactor.dart @@ -38,34 +38,24 @@ class SendEmailInteractor { final currentMailboxState = listState.first; final currentEmailState = listState.last; - final result = await _emailRepository.sendEmail( + await _emailRepository.sendEmail( session, accountId, emailRequest, mailboxRequest: mailboxRequest ); - if (result) { - if (emailRequest.emailIdDestroyed != null) { - await _emailRepository.deleteEmailPermanently(session, accountId, emailRequest.emailIdDestroyed!); - } - - yield Right( - SendEmailSuccess( - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState, - emailRequest: emailRequest - ) - ); - } else { - yield Left(SendEmailFailure( - session: session, - accountId: accountId, - emailRequest: emailRequest, - mailboxRequest: mailboxRequest, - sendingEmailActionType: sendingEmailActionType, - )); + if (emailRequest.emailIdDestroyed != null) { + await _emailRepository.deleteEmailPermanently(session, accountId, emailRequest.emailIdDestroyed!); } + + yield Right( + SendEmailSuccess( + currentEmailState: currentEmailState, + currentMailboxState: currentMailboxState, + emailRequest: emailRequest + ) + ); } catch (e) { yield Left(SendEmailFailure( exception: e, diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index 341bcd4be3..7f282169a2 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -1,5 +1,6 @@ import 'package:core/core.dart'; -import 'package:device_info_plus/device_info_plus.dart'; +import 'package:core/utils/application_manager.dart'; +import 'package:core/utils/file_utils.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; import 'package:tmail_ui_user/features/composer/data/datasource/composer_datasource.dart'; @@ -10,6 +11,7 @@ import 'package:tmail_ui_user/features/composer/data/repository/composer_reposit import 'package:tmail_ui_user/features/composer/data/repository/contact_repository_impl.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/contact_repository.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/save_email_as_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/update_email_drafts_interactor.dart'; @@ -104,7 +106,7 @@ class ComposerBindings extends BaseBindings { Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find())); + Get.find())); Get.lazyPut(() => StateDataSourceImpl( Get.find(), Get.find(), @@ -147,8 +149,12 @@ class ComposerBindings extends BaseBindings { @override void bindingsRepositoryImpl() { Get.lazyPut(() => ComposerRepositoryImpl( - Get.find(), - Get.find())); + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + )); Get.lazyPut(() => ContactRepositoryImpl(Get.find())); Get.lazyPut(() => MailboxRepositoryImpl( { @@ -197,6 +203,11 @@ class ComposerBindings extends BaseBindings { Get.lazyPut(() => DownloadImageAsBase64Interactor(Get.find())); Get.lazyPut(() => TransformHtmlEmailContentInteractor(Get.find())); Get.lazyPut(() => GetAlwaysReadReceiptSettingInteractor(Get.find())); + Get.lazyPut(() => CreateNewAndSendEmailInteractor( + Get.find(), + Get.find(), + Get.find(), + )); IdentityInteractorsBindings().dependencies(); } diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index f0717c2d2b..dfc8b0b0e0 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -17,7 +17,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:html_editor_enhanced/html_editor.dart' as web_html_editor; import 'package:http_parser/http_parser.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; @@ -29,6 +28,7 @@ import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.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:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:super_tag_editor/tag_editor.dart'; @@ -36,12 +36,12 @@ import 'package:tmail_ui_user/features/base/base_controller.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/model/contact_suggestion_source.dart'; -import 'package:tmail_ui_user/features/composer/domain/model/email_request.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/get_autocomplete_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_device_contact_suggestions_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.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'; @@ -53,6 +53,7 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/file_upl import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/list_shared_media_file_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/draggable_email_address.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'; @@ -60,13 +61,13 @@ import 'package:tmail_ui_user/features/composer/presentation/model/save_to_draft 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/widgets/mobile/from_composer_bottom_sheet_builder.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/sending_message_dialog_view.dart'; import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_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/transform_html_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; @@ -76,7 +77,6 @@ import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_id 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/sending_queue/domain/extensions/sending_email_extension.dart'; import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; import 'package:tmail_ui_user/features/server_settings/domain/state/get_always_read_receipt_setting_state.dart'; @@ -131,6 +131,7 @@ class ComposerController extends BaseController with DragDropFileMixin { final DownloadImageAsBase64Interactor _downloadImageAsBase64Interactor; final TransformHtmlEmailContentInteractor _transformHtmlEmailContentInteractor; final GetAlwaysReadReceiptSettingInteractor _getAlwaysReadReceiptSettingInteractor; + final CreateNewAndSendEmailInteractor _createNewAndSendEmailInteractor; GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; @@ -198,6 +199,7 @@ class ComposerController extends BaseController with DragDropFileMixin { this._downloadImageAsBase64Interactor, this._transformHtmlEmailContentInteractor, this._getAlwaysReadReceiptSettingInteractor, + this._createNewAndSendEmailInteractor, ); @override @@ -760,7 +762,6 @@ class ComposerController extends BaseController with DragDropFileMixin { bodyValues: { generatePartId: EmailBodyValue(emailBodyText, false, false) }, - headerUserAgent: {IndividualHeaderIdentifier.headerUserAgent : userAgent}, attachments: attachments.isNotEmpty ? attachments : null, headerMdn: hasRequestReadReceipt.value ? { IndividualHeaderIdentifier.headerMdn: getEmailAddressSender() } : {}, ); @@ -899,67 +900,64 @@ class ComposerController extends BaseController with DragDropFileMixin { _handleSendMessages(context); } - bool get _isParamUserNull { - if (composerArguments.value == null || - mailboxDashBoardController.sessionCurrent == null || - mailboxDashBoardController.accountId.value == null - ) { - logError('ComposerController::isParamUserNotNull: Param is NULL'); - return true; + Future _getContentInEditor() async { + final htmlTextEditor = PlatformInfo.isWeb + ? _textEditorWeb + : await htmlEditorApi?.getText(); + if (htmlTextEditor?.isNotEmpty == true) { + return htmlTextEditor!.removeEditorStartTag(); + } else { + return ''; } - return false; } void _handleSendMessages(BuildContext context) async { - if (_isParamUserNull) { - logError('ComposerController::_handleSendMessages: Param is NULL'); + if (composerArguments.value == null || + mailboxDashBoardController.sessionCurrent == null || + mailboxDashBoardController.accountId.value == null + ) { + log('ComposerController::_handleSendMessages: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); _closeComposerAction(); return; } - final sendingArgs = await createSendingEmailArguments(context); - _closeComposerAction(result: sendingArgs); - } - - Future createSendingEmailArguments(BuildContext context) async { - final session = mailboxDashBoardController.sessionCurrent!; - final arguments = composerArguments.value!; - final accountId = mailboxDashBoardController.accountId.value!; - - final createdEmail = await _generateEmail( - context, - session.username, - outboxMailboxId: mailboxDashBoardController.outboxMailbox?.id, - arguments: arguments + final emailContent = await _getContentInEditor(); + + final resultState = await Get.dialog( + PointerInterceptor( + child: SendingMessageDialogView( + createEmailRequest: CreateEmailRequest( + session: mailboxDashBoardController.sessionCurrent!, + accountId: mailboxDashBoardController.accountId.value!, + emailActionType: composerArguments.value!.emailActionType, + subject: subjectEmail.value ?? '', + emailContent: emailContent, + fromSender: composerArguments.value!.presentationEmail?.from ?? {}, + toRecipients: listToEmailAddress.toSet(), + ccRecipients: listCcEmailAddress.toSet(), + bccRecipients: listBccEmailAddress.toSet(), + isRequestReadReceipt: hasRequestReadReceipt.value, + identity: identitySelected.value, + attachments: uploadController.attachmentsUploaded, + inlineAttachments: uploadController.mapInlineAttachments, + outboxMailboxId: mailboxDashBoardController.outboxMailbox?.mailboxId, + sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], + draftsEmailId: composerArguments.value!.emailActionType == EmailActionType.editDraft + ? composerArguments.value!.presentationEmail?.id + : null, + answerForwardEmailId: composerArguments.value!.presentationEmail?.id, + unsubscribeEmailId: composerArguments.value!.previousEmailId, + messageId: composerArguments.value!.messageId, + references: composerArguments.value!.references, + emailSendingQueue: composerArguments.value!.sendingEmail + ), + createNewAndSendEmailInteractor: _createNewAndSendEmailInteractor + ), + ), + barrierColor: AppColor.colorDefaultCupertinoActionSheet, ); - final emailRequest = arguments.emailActionType == EmailActionType.editSendingEmail - ? arguments.sendingEmail!.toEmailRequest(newEmail: createdEmail) - : EmailRequest( - email: createdEmail, - sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], - identityId: identitySelected.value?.id, - emailIdDestroyed: arguments.emailActionType == EmailActionType.editDraft - ? arguments.presentationEmail?.id - : null, - emailIdAnsweredOrForwarded: arguments.presentationEmail?.id, - emailActionType: arguments.emailActionType, - previousEmailId: arguments.previousEmailId, - ); - - final mailboxRequest = mailboxDashBoardController.outboxMailbox?.id == null - ? CreateNewMailboxRequest( - Id(uuid.v1()), - MailboxName(PresentationMailbox.outboxRole.inCaps) - ) - : null; - - return SendingEmailArguments( - session, - accountId, - emailRequest, - mailboxRequest, - ); + _closeComposerAction(result: resultState); } void _checkContactPermission() async { @@ -1370,7 +1368,6 @@ class ComposerController extends BaseController with DragDropFileMixin { } void _closeComposerAction({dynamic result}) { - log('ComposerController::_closeComposerAction:'); isSendEmailLoading.value = false; if (PlatformInfo.isWeb) { diff --git a/lib/features/composer/presentation/extensions/create_email_request_extension.dart b/lib/features/composer/presentation/extensions/create_email_request_extension.dart new file mode 100644 index 0000000000..cc3b18b342 --- /dev/null +++ b/lib/features/composer/presentation/extensions/create_email_request_extension.dart @@ -0,0 +1,176 @@ +import 'package:core/core.dart'; +import 'package:http_parser/http_parser.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_value.dart'; +import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:model/extensions/email_address_extension.dart'; +import 'package:model/extensions/username_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/identity_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/list_attachments_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/extensions/sending_email_extension.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; + +extension CreateEmailRequestExtension on CreateEmailRequest { + + Set createSenders() { + if (identity?.email?.isNotEmpty == true) { + return { identity!.toEmailAddress() }; + } else { + return { session.username.toEmailAddress() }; + } + } + + String createMdnEmailAddress() { + if (emailActionType == EmailActionType.editDraft && fromSender.isNotEmpty) { + return fromSender.first.emailAddress; + } else { + return session.username.value; + } + } + + Set createReplyToRecipients() { + if (identity?.replyTo?.isNotEmpty == true) { + return identity!.replyTo!.toSet(); + } else { + return { session.username.toEmailAddress() }; + } + } + + Set createAttachments() => attachments?.toEmailBodyPart() ?? {}; + + Map? createKeywords() { + if (draftsMailboxId != null) { + return { + KeyWordIdentifier.emailDraft: true, + KeyWordIdentifier.emailSeen: true, + }; + } else { + return null; + } + } + + Map? createMailboxIds() { + if (draftsMailboxId != null || outboxMailboxId != null) { + return { + if (draftsMailboxId != null) + draftsMailboxId!: true, + if (outboxMailboxId != null) + outboxMailboxId!: true, + }; + } else { + return null; + } + } + + MessageIdsHeaderValue? createInReplyTo() { + if (emailActionType == EmailActionType.reply || + emailActionType == EmailActionType.replyAll + ) { + return messageId; + } + return null; + } + + MessageIdsHeaderValue? createReferences() { + if (emailActionType == EmailActionType.reply || + emailActionType == EmailActionType.replyAll || + emailActionType == EmailActionType.forward + ) { + Set ids = {}; + if (messageId?.ids.isNotEmpty == true) { + ids.addAll(messageId!.ids); + } + if (references?.ids.isNotEmpty == true) { + ids.addAll(references!.ids); + } + if (ids.isNotEmpty) { + return MessageIdsHeaderValue(ids); + } + } + return null; + } + + Email generateEmail({ + required String newEmailContent, + required Set newEmailAttachments, + required String userAgent, + required PartId partId, + }) { + return Email( + mailboxIds: createMailboxIds(), + from: createSenders(), + to: toRecipients, + cc: ccRecipients, + bcc: bccRecipients, + replyTo: createReplyToRecipients(), + inReplyTo: createInReplyTo(), + references: createReferences(), + keywords: createKeywords(), + subject: subject, + htmlBody: { + EmailBodyPart( + partId: partId, + type: MediaType.parse(Constant.textHtmlMimeType) + ) + }, + bodyValues: { + partId: EmailBodyValue( + newEmailContent, + false, + false + ) + }, + headerUserAgent: { + IndividualHeaderIdentifier.headerUserAgent : userAgent + }, + attachments: newEmailAttachments.isNotEmpty + ? newEmailAttachments + : null, + headerMdn: isRequestReadReceipt + ? { IndividualHeaderIdentifier.headerMdn: createMdnEmailAddress() } + : null, + ); + } + + EmailRequest createEmailRequest({required Email emailObject}) { + if (emailActionType == EmailActionType.editSendingEmail) { + return emailSendingQueue!.toEmailRequest(newEmail: emailObject); + } else { + return EmailRequest( + email: emailObject, + sentMailboxId: sentMailboxId, + identityId: identity?.id, + emailIdDestroyed: draftsEmailId, + emailIdAnsweredOrForwarded: answerForwardEmailId, + emailActionType: emailActionType, + previousEmailId: unsubscribeEmailId, + ); + } + } + + CreateNewMailboxRequest? createMailboxRequest() { + if (outboxMailboxId == null) { + return CreateNewMailboxRequest(MailboxName(PresentationMailbox.outboxRole.inCaps)); + } else { + return null; + } + } + + SendingEmailArguments toSendingEmailArguments({required Email emailObject}) { + return SendingEmailArguments( + session, + accountId, + createEmailRequest(emailObject: emailObject), + createMailboxRequest() + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/identity_extension.dart b/lib/features/composer/presentation/extensions/identity_extension.dart new file mode 100644 index 0000000000..b5f15ba162 --- /dev/null +++ b/lib/features/composer/presentation/extensions/identity_extension.dart @@ -0,0 +1,6 @@ +import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; + +extension IdentityExtension on Identity { + EmailAddress toEmailAddress() => EmailAddress(name, email); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/model/create_email_request.dart b/lib/features/composer/presentation/model/create_email_request.dart new file mode 100644 index 0000000000..1cc956a167 --- /dev/null +++ b/lib/features/composer/presentation/model/create_email_request.dart @@ -0,0 +1,88 @@ + +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/jmap/account_id.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/email/attachment.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; + +class CreateEmailRequest with EquatableMixin { + + final Session session; + final AccountId accountId; + final EmailActionType emailActionType; + final String subject; + final String emailContent; + final bool isRequestReadReceipt; + final Set fromSender; + final Set toRecipients; + final Set ccRecipients; + final Set bccRecipients; + final Identity? identity; + final List? attachments; + final Map? inlineAttachments; + final MailboxId? outboxMailboxId; + final MailboxId? sentMailboxId; + final MailboxId? draftsMailboxId; + final EmailId? draftsEmailId; + final EmailId? answerForwardEmailId; + final EmailId? unsubscribeEmailId; + final MessageIdsHeaderValue? messageId; + final MessageIdsHeaderValue? references; + final SendingEmail? emailSendingQueue; + + CreateEmailRequest({ + required this.session, + required this.accountId, + required this.emailActionType, + required this.subject, + required this.emailContent, + required this.fromSender, + required this.toRecipients, + required this.ccRecipients, + required this.bccRecipients, + this.isRequestReadReceipt = true, + this.identity, + this.attachments, + this.inlineAttachments, + this.outboxMailboxId, + this.sentMailboxId, + this.draftsMailboxId, + this.draftsEmailId, + this.answerForwardEmailId, + this.unsubscribeEmailId, + this.messageId, + this.references, + this.emailSendingQueue + }); + + @override + List get props => [ + session, + accountId, + emailActionType, + subject, + emailContent, + fromSender, + toRecipients, + ccRecipients, + bccRecipients, + identity, + isRequestReadReceipt, + attachments, + inlineAttachments, + outboxMailboxId, + sentMailboxId, + draftsMailboxId, + draftsEmailId, + answerForwardEmailId, + unsubscribeEmailId, + references, + references, + emailSendingQueue + ]; +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart b/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart new file mode 100644 index 0000000000..a1aa4d173b --- /dev/null +++ b/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart @@ -0,0 +1,206 @@ +import 'dart:async'; + +import 'package:core/presentation/extensions/capitalize_extension.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart' as dartz; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +class SendingMessageDialogView extends StatefulWidget { + + final CreateEmailRequest createEmailRequest; + final CreateNewAndSendEmailInteractor createNewAndSendEmailInteractor; + + const SendingMessageDialogView({ + super.key, + required this.createEmailRequest, + required this.createNewAndSendEmailInteractor, + }); + + @override + State createState() => _SendingMessageDialogViewState(); +} + +class _SendingMessageDialogViewState extends State { + + StreamSubscription? _sendingStreamSubscription; + final ValueNotifier?> _sendingNotifier = ValueNotifier(null); + + @override + void initState() { + super.initState(); + _sendingStreamSubscription = widget.createNewAndSendEmailInteractor + .execute(widget.createEmailRequest) + .listen( + _handleDataStream, + onError: _handleErrorStream + ); + } + + void _handleDataStream(dartz.Either newState) { + _sendingNotifier.value = newState; + + newState.fold( + (failure) { + if (failure is SendEmailFailure || failure is GenerateEmailFailure) { + popBack(result: failure); + } + }, + (success) { + if (success is SendEmailSuccess) { + popBack(result: success); + } + } + ); + } + + void _handleErrorStream(Object error, StackTrace stackTrace) { + logError('_SendingMessageDialogViewState::_handleErrorStream: Exception = $error'); + popBack(result: SendEmailFailure(exception: error)); + } + + @override + Widget build(BuildContext context) { + return Dialog( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12))), + insetPadding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 16.0 + ), + alignment: Alignment.center, + child: Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: Colors.white, + ), + width: 400, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + padding: const EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 12), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: AppColor.colorItemSelected, + ), + alignment: Alignment.center, + child: Text( + AppLocalizations.of(context).sendingMessage.capitalizeFirstEach, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 17 + ), + ), + ), + const Divider(), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 12, bottom: 4), + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).status}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + ), + const SizedBox(width: 8), + Expanded( + child: ValueListenableBuilder( + valueListenable: _sendingNotifier, + builder: (context, value, child) { + if (value == null) { + return child!; + } + + return value.fold( + (failure) => child!, + (success) { + if (success is GenerateEmailLoading) { + return Text( + 'Creating email...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ); + } else if (success is SendEmailLoading) { + return Text( + '${AppLocalizations.of(context).sendingMessage}...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ); + } else { + return child!; + } + } + ); + }, + child: Text( + '${AppLocalizations.of(context).sendingMessage}...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ), + ), + ) + ], + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 4, bottom: 24), + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).progress}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + ), + const SizedBox(width: 8), + Expanded( + child: LinearProgressIndicator( + color: Colors.white.withOpacity(0.6), + backgroundColor: AppColor.primaryColor, + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + ) + ], + ), + ) + ], + ) + ], + ), + ), + ); + } + + @override + void dispose() { + _sendingStreamSubscription?.cancel(); + _sendingNotifier.dispose(); + super.dispose(); + } +} diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index 05c236aff5..9255b5fe2d 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -28,7 +28,7 @@ import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email. abstract class EmailDataSource { Future getEmailContent(Session session, AccountId accountId, EmailId emailId); - Future sendEmail( + Future sendEmail( Session session, AccountId accountId, EmailRequest emailRequest, diff --git a/lib/features/email/data/datasource/html_datasource.dart b/lib/features/email/data/datasource/html_datasource.dart index 36f7d8975b..3545774623 100644 --- a/lib/features/email/data/datasource/html_datasource.dart +++ b/lib/features/email/data/datasource/html_datasource.dart @@ -1,6 +1,8 @@ - import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; -import 'package:model/model.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:model/email/attachment.dart'; +import 'package:model/email/email_content.dart'; abstract class HtmlDataSource { Future transformEmailContent( @@ -13,4 +15,11 @@ abstract class HtmlDataSource { String htmlContent, TransformConfiguration configuration ); + + Future>> replaceImageBase64ToImageCID({ + required String emailContent, + required Map inlineAttachments + }); + + Future removeCollapsedExpandedSignatureEffect({required String emailContent}); } \ No newline at end of file diff --git a/lib/features/email/data/datasource_impl/email_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_datasource_impl.dart index 3983a16dfe..36fa298aa0 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -42,7 +42,7 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future sendEmail( + Future sendEmail( Session session, AccountId accountId, EmailRequest emailRequest, diff --git a/lib/features/email/data/datasource_impl/html_datasource_impl.dart b/lib/features/email/data/datasource_impl/html_datasource_impl.dart index 7ce83177e6..5d695d739d 100644 --- a/lib/features/email/data/datasource_impl/html_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/html_datasource_impl.dart @@ -1,4 +1,7 @@ import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:model/email/attachment.dart'; import 'package:model/email/email_content.dart'; import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; @@ -35,4 +38,26 @@ class HtmlDataSourceImpl extends HtmlDataSource { ); }).catchError(_exceptionThrower.throwException); } + + @override + Future>> replaceImageBase64ToImageCID({ + required String emailContent, + required Map inlineAttachments + }) { + return Future.sync(() async { + return await _htmlAnalyzer.replaceImageBase64ToImageCID( + emailContent: emailContent, + inlineAttachments: inlineAttachments + ); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future removeCollapsedExpandedSignatureEffect({required String emailContent}) { + return Future.sync(() async { + return await _htmlAnalyzer.removeCollapsedExpandedSignatureEffect( + emailContent: emailContent + ); + }).catchError(_exceptionThrower.throwException); + } } \ No newline at end of file diff --git a/lib/features/email/data/local/html_analyzer.dart b/lib/features/email/data/local/html_analyzer.dart index ca7550dc0e..904c320a05 100644 --- a/lib/features/email/data/local/html_analyzer.dart +++ b/lib/features/email/data/local/html_analyzer.dart @@ -1,10 +1,15 @@ import 'package:collection/collection.dart'; +import 'package:core/data/constants/constant.dart'; import 'package:core/presentation/utils/html_transformer/html_transform.dart'; import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; import 'package:html/parser.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:model/email/attachment.dart'; import 'package:model/email/email_content.dart'; import 'package:model/email/email_content_type.dart'; +import 'package:model/extensions/attachment_extension.dart'; import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; class HtmlAnalyzer { @@ -99,4 +104,57 @@ class HtmlAnalyzer { ); return htmlContentTransformed; } + + Future>> replaceImageBase64ToImageCID({ + required String emailContent, + required Map inlineAttachments + }) async { + final document = parse(emailContent); + final listImgTag = document.querySelectorAll('img[src^="data:image/"][id^="cid:"]'); + + final listInlineAttachment = await Future.wait(listImgTag.map((imgTag) async { + final idImg = imgTag.attributes['id']; + final cid = idImg!.replaceFirst('cid:', '').trim(); + imgTag.attributes['src'] = 'cid:$cid'; + imgTag.attributes.remove('id'); + return cid; + })).then((listCid) { + final listInlineAttachment = listCid + .map((cid) { + if (inlineAttachments.containsKey(cid)) { + return inlineAttachments[cid]!.toEmailBodyPart(charset: Constant.base64Charset); + } else { + return null; + } + }) + .whereNotNull() + .toSet(); + + return listInlineAttachment; + }); + + final newContent = document.body?.innerHtml ?? emailContent; + + return Tuple2(newContent, listInlineAttachment); + } + + Future removeCollapsedExpandedSignatureEffect({required String emailContent}) async { + log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect: BEFORE = $emailContent'); + final document = parse(emailContent); + final signatureElements = document.querySelectorAll('div.tmail-signature'); + await Future.wait(signatureElements.map((signatureTag) async { + final signatureChildren = signatureTag.children; + for (var child in signatureChildren) { + log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect: CHILD = ${child.outerHtml}'); + if (child.attributes['class']?.contains('tmail-signature-button') == true) { + child.remove(); + } else if (child.attributes['class']?.contains('tmail-signature-content') == true) { + signatureTag.innerHtml = child.innerHtml; + } + } + })); + final newContent = document.body?.innerHtml ?? emailContent; + log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect: AFTER = $newContent'); + return newContent; + } } \ No newline at end of file diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index 91f9f1e750..422682ca20 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -54,7 +54,7 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future sendEmail( + Future sendEmail( Session session, AccountId accountId, EmailRequest emailRequest, diff --git a/lib/features/email/domain/exceptions/email_exceptions.dart b/lib/features/email/domain/exceptions/email_exceptions.dart index 8057441639..8199c94c2f 100644 --- a/lib/features/email/domain/exceptions/email_exceptions.dart +++ b/lib/features/email/domain/exceptions/email_exceptions.dart @@ -6,4 +6,6 @@ class EmptyEmailContentException implements Exception {} class NotFoundEmailRecoveryActionException implements Exception {} -class NotFoundEmailBlobIdException implements Exception {} \ No newline at end of file +class NotFoundEmailBlobIdException implements Exception {} + +class CannotCreateEmailObjectException implements Exception {} \ No newline at end of file diff --git a/lib/features/email/domain/extensions/list_attachments_extension.dart b/lib/features/email/domain/extensions/list_attachments_extension.dart index 4bfa32c976..dfa9fb1048 100644 --- a/lib/features/email/domain/extensions/list_attachments_extension.dart +++ b/lib/features/email/domain/extensions/list_attachments_extension.dart @@ -1,7 +1,9 @@ import 'package:collection/collection.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; import 'package:model/email/attachment.dart'; +import 'package:model/extensions/attachment_extension.dart'; import 'package:tmail_ui_user/features/email/domain/extensions/attachment_extension.dart'; import 'package:tmail_ui_user/features/offline_mode/model/attachment_hive_cache.dart'; @@ -19,4 +21,6 @@ extension ListAttachmentsExtension on List { .map((attachment) => attachment.blobId) .whereNotNull() .toSet(); + + Set toEmailBodyPart({String? charset}) => map((attachment) => attachment.toEmailBodyPart(charset: charset)).toSet(); } \ No newline at end of file diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart index acedd10061..a2906e0afa 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -30,7 +30,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_r abstract class EmailRepository { Future getEmailContent(Session session, AccountId accountId, EmailId emailId); - Future sendEmail( + Future sendEmail( Session session, AccountId accountId, EmailRequest emailRequest, diff --git a/lib/features/email/presentation/bindings/email_bindings.dart b/lib/features/email/presentation/bindings/email_bindings.dart index 849339bd01..a589ded969 100644 --- a/lib/features/email/presentation/bindings/email_bindings.dart +++ b/lib/features/email/presentation/bindings/email_bindings.dart @@ -96,7 +96,7 @@ class EmailBindings extends BaseBindings { Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find())); + Get.find())); Get.lazyPut(() => StateDataSourceImpl( Get.find(), Get.find(), diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index c2004adb93..0c33fba6ed 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -204,7 +204,7 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find())); + Get.find())); Get.lazyPut(() => SearchDataSourceImpl( Get.find(), Get.find())); diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index a3773f68dc..784e1d67bd 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -30,6 +30,7 @@ import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dar import 'package:tmail_ui_user/features/composer/domain/exceptions/set_method_exception.dart'; import 'package:tmail_ui_user/features/composer/domain/extensions/email_request_extension.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; @@ -1256,11 +1257,14 @@ class MailboxDashBoardController extends ReloadableController { composerArguments = null; ComposerBindings().dispose(); composerOverlayState.value = ComposerOverlayState.inActive; - if (result is SendingEmailArguments) { handleSendEmailAction(result); } else if (result is SaveToDraftArguments) { saveEmailToDraft(arguments: result); + } else if (result is SendEmailSuccess) { + consumeState(Stream.value(Right(result))); + } else if (result is SendEmailFailure || result is GenerateEmailFailure) { + consumeState(Stream.value(Left(result))); } } @@ -1394,6 +1398,10 @@ class MailboxDashBoardController extends ReloadableController { handleSendEmailAction(result); } else if (result is SaveToDraftArguments) { saveEmailToDraft(arguments: result); + } else if (result is SendEmailSuccess) { + consumeState(Stream.value(Right(result))); + } else if (result is SendEmailFailure || result is GenerateEmailFailure) { + consumeState(Stream.value(Left(result))); } } } diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart index efb63fabf8..8d3a0096db 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart @@ -132,7 +132,10 @@ class ManageAccountDashBoardView extends GetWidget (AvatarBuilder() - ..text(controller.sessionCurrent?.username.firstCharacter ?? '') + ..text(controller.accountId.value != null + ? controller.sessionCurrent?.username.firstCharacter ?? '' + : '' + ) ..backgroundColor(Colors.white) ..textColor(Colors.black) ..context(context) diff --git a/lib/features/network_connection/presentation/web_network_connection_controller.dart b/lib/features/network_connection/presentation/web_network_connection_controller.dart index 4afdb5be73..c4bbde67e1 100644 --- a/lib/features/network_connection/presentation/web_network_connection_controller.dart +++ b/lib/features/network_connection/presentation/web_network_connection_controller.dart @@ -94,4 +94,6 @@ class NetworkConnectionController extends GetxController { ); } } + + Future hasInternetConnection() async => isNetworkConnectionAvailable(); } \ No newline at end of file diff --git a/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart b/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart index 807536be99..1829ad0248 100644 --- a/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart +++ b/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart @@ -68,7 +68,7 @@ class SendEmailInteractorBindings extends InteractorsBindings { Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find())); + Get.find())); Get.lazyPut(() => StateDataSourceImpl( Get.find(), Get.find(), diff --git a/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart b/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart index 45f6c34907..9b31a99069 100644 --- a/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart +++ b/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart @@ -105,7 +105,7 @@ class FcmInteractorBindings extends InteractorsBindings { Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find())); + Get.find())); Get.lazyPut(() => StateDataSourceImpl( Get.find(), Get.find(), diff --git a/lib/features/upload/presentation/controller/upload_controller.dart b/lib/features/upload/presentation/controller/upload_controller.dart index f59ef21887..626d7ced07 100644 --- a/lib/features/upload/presentation/controller/upload_controller.dart +++ b/lib/features/upload/presentation/controller/upload_controller.dart @@ -255,8 +255,7 @@ class UploadController extends BaseController { return null; } return attachmentsUploaded - .map((attachment) => attachment.toEmailBodyPart( - disposition: ContentDisposition.attachment.value)) + .map((attachment) => attachment.toEmailBodyPart()) .toSet(); } diff --git a/lib/main/exceptions/send_email_exception_thrower.dart b/lib/main/exceptions/send_email_exception_thrower.dart index d5d908cb4b..2e1bf96e36 100644 --- a/lib/main/exceptions/send_email_exception_thrower.dart +++ b/lib/main/exceptions/send_email_exception_thrower.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'package:core/utils/app_logger.dart'; -import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.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/main/exceptions/remote_exception.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; diff --git a/lib/main/localizations/localization_service.dart b/lib/main/localizations/localization_service.dart index eb787db761..f58a3773ba 100644 --- a/lib/main/localizations/localization_service.dart +++ b/lib/main/localizations/localization_service.dart @@ -38,17 +38,22 @@ class LocalizationService extends Translations { } static Locale getLocaleFromLanguage({String? langCode}) { - final languageCacheManager = getBinding(); - log('LocalizationService::_getLocaleFromLanguage:languageCacheManager: $languageCacheManager'); - final localeStored = languageCacheManager?.getStoredLanguage(); - log('LocalizationService::_getLocaleFromLanguage():localeStored: $localeStored'); - if (localeStored != null) { - return localeStored; - } else { - final languageCodeCurrent = langCode ?? Get.deviceLocale?.languageCode; - log('LocalizationService::_getLocaleFromLanguage():languageCodeCurrent: $languageCodeCurrent'); - final localeSelected = supportedLocales.firstWhereOrNull((locale) => locale.languageCode == languageCodeCurrent); - return localeSelected ?? Get.deviceLocale ?? defaultLocale; + try { + final languageCacheManager = getBinding(); + log('LocalizationService::_getLocaleFromLanguage:languageCacheManager: $languageCacheManager'); + final localeStored = languageCacheManager?.getStoredLanguage(); + log('LocalizationService::_getLocaleFromLanguage():localeStored: $localeStored'); + if (localeStored != null) { + return localeStored; + } else { + final languageCodeCurrent = langCode ?? Get.deviceLocale?.languageCode; + log('LocalizationService::_getLocaleFromLanguage():languageCodeCurrent: $languageCodeCurrent'); + final localeSelected = supportedLocales.firstWhereOrNull((locale) => locale.languageCode == languageCodeCurrent); + return localeSelected ?? Get.deviceLocale ?? defaultLocale; + } + } catch (e) { + logError('LocalizationService::getLocaleFromLanguage: Exception: $e'); + return Get.deviceLocale ?? defaultLocale; } } diff --git a/model/lib/email/attachment.dart b/model/lib/email/attachment.dart index 9819bdf81b..4347b829f3 100644 --- a/model/lib/email/attachment.dart +++ b/model/lib/email/attachment.dart @@ -68,19 +68,6 @@ enum ContentDisposition { other } -extension ContentDispositionExtension on ContentDisposition { - String get value { - switch(this) { - case ContentDisposition.inline: - return 'inline'; - case ContentDisposition.attachment: - return 'attachment'; - case ContentDisposition.other: - return toString(); - } - } -} - extension DispositionStringExtension on String? { ContentDisposition? toContentDisposition() { if (this != null) { diff --git a/model/lib/extensions/attachment_extension.dart b/model/lib/extensions/attachment_extension.dart index dcc25b7b4b..8b5d631622 100644 --- a/model/lib/extensions/attachment_extension.dart +++ b/model/lib/extensions/attachment_extension.dart @@ -1,9 +1,10 @@ import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; -import 'package:model/model.dart'; +import 'package:model/email/attachment.dart'; +import 'package:model/extensions/list_attachment_extension.dart'; extension AttachmentExtension on Attachment { - EmailBodyPart toEmailBodyPart({String? disposition, String? charset}) => EmailBodyPart( + EmailBodyPart toEmailBodyPart({String? charset}) => EmailBodyPart( partId: partId, blobId: blobId, size: size, @@ -11,7 +12,7 @@ extension AttachmentExtension on Attachment { type: type, cid: cid, charset: charset, - disposition: disposition ?? this.disposition?.value); + disposition: disposition?.name ?? ContentDisposition.attachment.name); Attachment toAttachmentWithDisposition({ ContentDisposition? disposition, diff --git a/model/lib/extensions/username_extension.dart b/model/lib/extensions/username_extension.dart index 66005a6b4b..ffcd436bab 100644 --- a/model/lib/extensions/username_extension.dart +++ b/model/lib/extensions/username_extension.dart @@ -1,7 +1,8 @@ - -import 'package:core/presentation/extensions/string_extension.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; extension UsernameExtension on UserName { - String get firstCharacter => value.firstLetterToUpperCase.toUpperCase(); + String get firstCharacter => value.isNotEmpty ? value[0].toUpperCase() : ''; + + EmailAddress toEmailAddress() => EmailAddress(null, value); } \ No newline at end of file From ff677ea5c87a5aae304b91872bfc63b515efc436 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 5 Mar 2024 23:13:39 +0700 Subject: [PATCH 12/80] TF-2667 Replace `isSendEmailLoading` to `ButtonState` --- .../presentation/composer_controller.dart | 38 ++++++++----------- .../composer/presentation/composer_view.dart | 6 +-- .../presentation/composer_view_web.dart | 5 +-- .../web/bottom_bar_composer_widget.dart | 5 +-- 4 files changed, 21 insertions(+), 33 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index dfc8b0b0e0..3c404c95db 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -116,7 +116,6 @@ class ComposerController extends BaseController with DragDropFileMixin { final fromRecipientState = PrefixRecipientState.disabled.obs; final ccRecipientState = PrefixRecipientState.disabled.obs; final bccRecipientState = PrefixRecipientState.disabled.obs; - final isSendEmailLoading = false.obs; final identitySelected = Rxn(); final listFromIdentities = RxList(); @@ -182,6 +181,7 @@ class ComposerController extends BaseController with DragDropFileMixin { bool isAttachmentCollapsed = false; ButtonState _closeComposerButtonState = ButtonState.enabled; ButtonState _saveToDraftButtonState = ButtonState.enabled; + ButtonState _sendButtonState = ButtonState.enabled; late Worker uploadInlineImageWorker; late Worker dashboardViewStateWorker; @@ -309,9 +309,6 @@ class ComposerController extends BaseController with DragDropFileMixin { } else if (failure is GetEmailContentFailure || failure is TransformHtmlEmailContentFailure) { emailContentsViewState.value = Left(failure); - if (isSendEmailLoading.isTrue) { - isSendEmailLoading.value = false; - } } else if (failure is GetAllIdentitiesFailure) { if (identitySelected.value == null) { _autoFocusFieldWhenLauncher(); @@ -805,15 +802,15 @@ class ComposerController extends BaseController with DragDropFileMixin { } } - void validateInformationBeforeSending(BuildContext context) async { - if (isSendEmailLoading.isTrue) { + void handleClickSendButton(BuildContext context) async { + if (_sendButtonState == ButtonState.disabled) { + log('ComposerController::handleClickSendButton: SENDING EMAIL'); return; } + _sendButtonState = ButtonState.disabled; clearFocus(context); - isSendEmailLoading.value = true; - if (toEmailAddressController.text.isNotEmpty || ccEmailAddressController.text.isNotEmpty || bccEmailAddressController.text.isNotEmpty) { @@ -825,12 +822,11 @@ class ComposerController extends BaseController with DragDropFileMixin { showConfirmDialogAction(context, AppLocalizations.of(context).message_dialog_send_email_without_recipient, AppLocalizations.of(context).add_recipients, - onConfirmAction: () => isSendEmailLoading.value = false, title: AppLocalizations.of(context).sending_failed, icon: SvgPicture.asset(imagePaths.icSendToastError, fit: BoxFit.fill), hasCancelButton: false, showAsBottomSheet: true, - ).whenComplete(() => isSendEmailLoading.value = false); + ).whenComplete(() => _sendButtonState = ButtonState.enabled); return; } @@ -846,13 +842,12 @@ class ComposerController extends BaseController with DragDropFileMixin { toAddressExpandMode.value = ExpandMode.EXPAND; ccAddressExpandMode.value = ExpandMode.EXPAND; bccAddressExpandMode.value = ExpandMode.EXPAND; - isSendEmailLoading.value = false; }, showAsBottomSheet: true, title: AppLocalizations.of(context).sending_failed, icon: SvgPicture.asset(imagePaths.icSendToastError, fit: BoxFit.fill), hasCancelButton: false - ).whenComplete(() => isSendEmailLoading.value = false); + ).whenComplete(() => _sendButtonState = ButtonState.enabled); return; } @@ -860,12 +855,11 @@ class ComposerController extends BaseController with DragDropFileMixin { showConfirmDialogAction(context, AppLocalizations.of(context).message_dialog_send_email_without_a_subject, AppLocalizations.of(context).send_anyway, - onConfirmAction: () => _handleSendMessages(context), - onCancelAction: () => isSendEmailLoading.value = false, + onConfirmAction: _handleSendMessages, title: AppLocalizations.of(context).empty_subject, showAsBottomSheet: true, icon: SvgPicture.asset(imagePaths.icEmpty, fit: BoxFit.fill), - ).whenComplete(() => isSendEmailLoading.value = false); + ).whenComplete(() => _sendButtonState = ButtonState.enabled); return; } @@ -874,12 +868,11 @@ class ComposerController extends BaseController with DragDropFileMixin { context, AppLocalizations.of(context).messageDialogSendEmailUploadingAttachment, AppLocalizations.of(context).got_it, - onConfirmAction: () => isSendEmailLoading.value = false, title: AppLocalizations.of(context).sending_failed, showAsBottomSheet: true, icon: SvgPicture.asset(imagePaths.icSendToastError, fit: BoxFit.fill), hasCancelButton: false - ).whenComplete(() => isSendEmailLoading.value = false); + ).whenComplete(() => _sendButtonState = ButtonState.enabled); return; } @@ -889,15 +882,14 @@ class ComposerController extends BaseController with DragDropFileMixin { AppLocalizations.of(context).message_dialog_send_email_exceeds_maximum_size( filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), AppLocalizations.of(context).got_it, - onConfirmAction: () => isSendEmailLoading.value = false, title: AppLocalizations.of(context).sending_failed, icon: SvgPicture.asset(imagePaths.icSendToastError, fit: BoxFit.fill), hasCancelButton: false - ).whenComplete(() => isSendEmailLoading.value = false); + ).whenComplete(() => _sendButtonState = ButtonState.enabled); return; } - _handleSendMessages(context); + _handleSendMessages(); } Future _getContentInEditor() async { @@ -911,12 +903,13 @@ class ComposerController extends BaseController with DragDropFileMixin { } } - void _handleSendMessages(BuildContext context) async { + void _handleSendMessages() async { if (composerArguments.value == null || mailboxDashBoardController.sessionCurrent == null || mailboxDashBoardController.accountId.value == null ) { log('ComposerController::_handleSendMessages: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); + _sendButtonState = ButtonState.enabled; _closeComposerAction(); return; } @@ -957,6 +950,7 @@ class ComposerController extends BaseController with DragDropFileMixin { barrierColor: AppColor.colorDefaultCupertinoActionSheet, ); + _sendButtonState = ButtonState.enabled; _closeComposerAction(result: resultState); } @@ -1368,8 +1362,6 @@ class ComposerController extends BaseController with DragDropFileMixin { } void _closeComposerAction({dynamic result}) { - isSendEmailLoading.value = false; - if (PlatformInfo.isWeb) { mailboxDashBoardController.closeComposerOverlay(result: result); } else { diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index bdb3d63399..e9c707c188 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -60,7 +60,7 @@ class ComposerView extends GetWidget { Obx(() => LandscapeAppBarComposerWidget( isSendButtonEnabled: controller.isEnableEmailSendButton.value, onCloseViewAction: () => controller.handleClickCloseComposer(context), - sendMessageAction: () => controller.validateInformationBeforeSending(context), + sendMessageAction: () => controller.handleClickSendButton(context), openContextMenuAction: (position) { controller.openPopupMenuAction( context, @@ -74,7 +74,7 @@ class ComposerView extends GetWidget { Obx(() => AppBarComposerWidget( isSendButtonEnabled: controller.isEnableEmailSendButton.value, onCloseViewAction: () => controller.handleClickCloseComposer(context), - sendMessageAction: () => controller.validateInformationBeforeSending(context), + sendMessageAction: () => controller.handleClickSendButton(context), openContextMenuAction: (position) { controller.openPopupMenuAction( context, @@ -362,7 +362,7 @@ class ComposerView extends GetWidget { TabletBottomBarComposerWidget( deleteComposerAction: () => controller.handleClickDeleteComposer(context), saveToDraftAction: () => controller.saveToDraftAction(context), - sendMessageAction: () => controller.validateInformationBeforeSending(context), + sendMessageAction: () => controller.handleClickSendButton(context), requestReadReceiptAction: (position) { controller.openPopupMenuAction( context, diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index ccae484332..d1b10e5222 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -50,7 +50,7 @@ class ComposerView extends GetWidget { onCloseViewAction: () => controller.handleClickCloseComposer(context), attachFileAction: () => controller.openFilePickerByType(context, FileType.any), insertImageAction: () => controller.insertImage(context, constraints.maxWidth), - sendMessageAction: () => controller.validateInformationBeforeSending(context), + sendMessageAction: () => controller.handleClickSendButton(context), openContextMenuAction: (position) { controller.openPopupMenuAction( context, @@ -465,7 +465,6 @@ class ComposerView extends GetWidget { radius: ComposerStyle.popupMenuRadius ); }, - isSending: controller.isSendEmailLoading.value, )), ], ), @@ -756,7 +755,7 @@ class ComposerView extends GetWidget { showCodeViewAction: controller.richTextWebController.toggleCodeView, deleteComposerAction: () => controller.handleClickDeleteComposer(context), saveToDraftAction: () => controller.saveToDraftAction(context), - sendMessageAction: () => controller.validateInformationBeforeSending(context), + sendMessageAction: () => controller.handleClickSendButton(context), requestReadReceiptAction: (position) { controller.openPopupMenuAction( context, diff --git a/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart index 648ac86302..691f7136d3 100644 --- a/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart @@ -19,7 +19,6 @@ class BottomBarComposerWidget extends StatelessWidget { final VoidCallback saveToDraftAction; final VoidCallback sendMessageAction; final OnRequestReadReceiptAction? requestReadReceiptAction; - final bool isSending; final _imagePaths = Get.find(); @@ -35,7 +34,6 @@ class BottomBarComposerWidget extends StatelessWidget { required this.saveToDraftAction, required this.sendMessageAction, this.requestReadReceiptAction, - this.isSending = false, }); @override @@ -129,7 +127,7 @@ class BottomBarComposerWidget extends StatelessWidget { ), const SizedBox(width: BottomBarComposerWidgetStyle.sendButtonSpace), TMailButtonWidget( - text: isSending ? AppLocalizations.of(context).sending : AppLocalizations.of(context).send, + text: AppLocalizations.of(context).send, icon: _imagePaths.icSend, iconAlignment: TextDirection.rtl, padding: BottomBarComposerWidgetStyle.sendButtonPadding, @@ -139,7 +137,6 @@ class BottomBarComposerWidget extends StatelessWidget { backgroundColor: BottomBarComposerWidgetStyle.sendButtonBackgroundColor, borderRadius: BottomBarComposerWidgetStyle.sendButtonRadius, onTapActionCallback: sendMessageAction, - isLoading: isSending, ) ] ), From bda34ec9caa42d0cf51046f8d391fb22740a21a6 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 6 Mar 2024 00:47:57 +0700 Subject: [PATCH 13/80] TF-2667 Show confirm dialog when send email failed --- .../presentation/composer_controller.dart | 75 +++++++++++++++++-- .../mailbox/presentation/mailbox_view.dart | 4 +- .../presentation/mailbox_view_web.dart | 4 +- .../mailbox_dashboard_controller.dart | 5 -- .../settings/settings_first_level_view.dart | 4 +- lib/l10n/intl_messages.arb | 32 +++++++- lib/main/localizations/app_localizations.dart | 12 +++ 7 files changed, 119 insertions(+), 17 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 3c404c95db..d58cc02352 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -37,9 +37,11 @@ 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/model/contact_suggestion_source.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/get_autocomplete_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_device_contact_suggestions_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/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/usecases/create_new_and_send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/download_image_as_base64_interactor.dart'; @@ -855,7 +857,7 @@ class ComposerController extends BaseController with DragDropFileMixin { showConfirmDialogAction(context, AppLocalizations.of(context).message_dialog_send_email_without_a_subject, AppLocalizations.of(context).send_anyway, - onConfirmAction: _handleSendMessages, + onConfirmAction: () => _handleSendMessages(context), title: AppLocalizations.of(context).empty_subject, showAsBottomSheet: true, icon: SvgPicture.asset(imagePaths.icEmpty, fit: BoxFit.fill), @@ -889,7 +891,7 @@ class ComposerController extends BaseController with DragDropFileMixin { return; } - _handleSendMessages(); + _handleSendMessages(context); } Future _getContentInEditor() async { @@ -903,7 +905,7 @@ class ComposerController extends BaseController with DragDropFileMixin { } } - void _handleSendMessages() async { + void _handleSendMessages(BuildContext context) async { if (composerArguments.value == null || mailboxDashBoardController.sessionCurrent == null || mailboxDashBoardController.accountId.value == null @@ -916,7 +918,23 @@ class ComposerController extends BaseController with DragDropFileMixin { final emailContent = await _getContentInEditor(); - final resultState = await Get.dialog( + final resultState = await _showSendingMessageDialog(emailContent: emailContent); + + if (resultState is SendEmailSuccess) { + _sendButtonState = ButtonState.enabled; + _closeComposerAction(result: resultState); + } else if ((resultState is SendEmailFailure || resultState is GenerateEmailFailure) && context.mounted) { + _showConfirmDialogWhenSendMessageFailure( + context: context, + failure: resultState + ); + } else { + _sendButtonState = ButtonState.enabled; + } + } + + Future _showSendingMessageDialog({required String emailContent}) { + return Get.dialog( PointerInterceptor( child: SendingMessageDialogView( createEmailRequest: CreateEmailRequest( @@ -936,8 +954,8 @@ class ComposerController extends BaseController with DragDropFileMixin { outboxMailboxId: mailboxDashBoardController.outboxMailbox?.mailboxId, sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], draftsEmailId: composerArguments.value!.emailActionType == EmailActionType.editDraft - ? composerArguments.value!.presentationEmail?.id - : null, + ? composerArguments.value!.presentationEmail?.id + : null, answerForwardEmailId: composerArguments.value!.presentationEmail?.id, unsubscribeEmailId: composerArguments.value!.previousEmailId, messageId: composerArguments.value!.messageId, @@ -949,9 +967,50 @@ class ComposerController extends BaseController with DragDropFileMixin { ), barrierColor: AppColor.colorDefaultCupertinoActionSheet, ); + } - _sendButtonState = ButtonState.enabled; - _closeComposerAction(result: resultState); + void _showConfirmDialogWhenSendMessageFailure({ + required BuildContext context, + required FeatureFailure failure + }) { + showConfirmDialogAction( + context, + title: '', + AppLocalizations.of(context).warningMessageWhenSendEmailFailure, + AppLocalizations.of(context).edit, + cancelTitle: AppLocalizations.of(context).closeAnyway, + alignCenter: true, + onConfirmAction: () { + _sendButtonState = ButtonState.enabled; + popBack(); + _autoFocusFieldWhenLauncher(); + }, + onCancelAction: () async { + _sendButtonState = ButtonState.enabled; + await Future.delayed( + const Duration(milliseconds: 100), + _closeComposerAction + ); + }, + icon: SvgPicture.asset( + imagePaths.icQuotasWarning, + width: 40, + height: 40, + colorFilter: AppColor.colorBackgroundQuotasWarning.asFilter(), + ), + messageStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 14, + color: AppColor.colorTextBody + ), + actionStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.white + ), + cancelStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.black + ) + ); } void _checkContactPermission() async { diff --git a/lib/features/mailbox/presentation/mailbox_view.dart b/lib/features/mailbox/presentation/mailbox_view.dart index cd892d688e..ed13f9f8da 100644 --- a/lib/features/mailbox/presentation/mailbox_view.dart +++ b/lib/features/mailbox/presentation/mailbox_view.dart @@ -179,7 +179,9 @@ class MailboxView extends BaseMailboxView { return const SizedBox.shrink(); } return UserInformationWidget( - userName: controller.mailboxDashBoardController.sessionCurrent?.username, + userName: controller.mailboxDashBoardController.accountId.value != null + ? controller.mailboxDashBoardController.sessionCurrent?.username + : null, subtitle: AppLocalizations.of(context).manage_account, onSubtitleClick: controller.mailboxDashBoardController.goToSettings, border: const Border( diff --git a/lib/features/mailbox/presentation/mailbox_view_web.dart b/lib/features/mailbox/presentation/mailbox_view_web.dart index 4dbab42c82..976176ed0c 100644 --- a/lib/features/mailbox/presentation/mailbox_view_web.dart +++ b/lib/features/mailbox/presentation/mailbox_view_web.dart @@ -100,7 +100,9 @@ class MailboxView extends BaseMailboxView { child: Column(children: [ if (!controller.responsiveUtils.isDesktop(context)) Obx(() => UserInformationWidget( - userName: controller.mailboxDashBoardController.sessionCurrent?.username, + userName: controller.mailboxDashBoardController.accountId.value != null + ? controller.mailboxDashBoardController.sessionCurrent?.username + : null, subtitle: AppLocalizations.of(context).manage_account, onSubtitleClick: controller.mailboxDashBoardController.goToSettings, border: const Border( diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 784e1d67bd..42e14e4a07 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -30,7 +30,6 @@ import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dar import 'package:tmail_ui_user/features/composer/domain/exceptions/set_method_exception.dart'; import 'package:tmail_ui_user/features/composer/domain/extensions/email_request_extension.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; @@ -1263,8 +1262,6 @@ class MailboxDashBoardController extends ReloadableController { saveEmailToDraft(arguments: result); } else if (result is SendEmailSuccess) { consumeState(Stream.value(Right(result))); - } else if (result is SendEmailFailure || result is GenerateEmailFailure) { - consumeState(Stream.value(Left(result))); } } @@ -1400,8 +1397,6 @@ class MailboxDashBoardController extends ReloadableController { saveEmailToDraft(arguments: result); } else if (result is SendEmailSuccess) { consumeState(Stream.value(Right(result))); - } else if (result is SendEmailFailure || result is GenerateEmailFailure) { - consumeState(Stream.value(Left(result))); } } } diff --git a/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart b/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart index 10af956290..81a9914d74 100644 --- a/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart +++ b/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart @@ -19,7 +19,9 @@ class SettingsFirstLevelView extends GetWidget { controller: PlatformInfo.isMobile ? null : controller.settingScrollController, child: Column(children: [ Obx(() => UserInformationWidget( - userName: controller.manageAccountDashboardController.sessionCurrent?.username, + userName: controller.manageAccountDashboardController.accountId.value != null + ? controller.manageAccountDashboardController.sessionCurrent?.username + : null, padding: SettingsUtils.getPaddingInFirstLevel(context, controller.responsiveUtils), titlePadding: const EdgeInsetsDirectional.only(start: 16))), Divider( diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 8675ed7ae7..5eee433f3d 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-03-15T21:34:52.177175", + "@@last_modified": "2024-03-06T00:47:30.311599", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3773,5 +3773,35 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "status": "Status", + "@status": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "progress": "Progress", + "@progress": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendingMessage": "Sending message", + "@sendingMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "warningMessageWhenSendEmailFailure": "Sending of the message failed.\nAn error occurred while sending mail.", + "@warningMessageWhenSendEmailFailure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "closeAnyway": "Close anyway", + "@closeAnyway": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index fedc723135..9fe57f82b9 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3962,4 +3962,16 @@ class AppLocalizations { 'Sending message', name: 'sendingMessage'); } + + String get warningMessageWhenSendEmailFailure { + return Intl.message( + 'Sending of the message failed.\nAn error occurred while sending mail.', + name: 'warningMessageWhenSendEmailFailure'); + } + + String get closeAnyway { + return Intl.message( + 'Close anyway', + name: 'closeAnyway'); + } } \ No newline at end of file From 3b3d30050959309da0a83132e35e8f15d0ebba62 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 6 Mar 2024 01:50:54 +0700 Subject: [PATCH 14/80] TF-2667 Show confirm dialog when click close composer Signed-off-by: dab246 --- .../dialog/confirmation_dialog_builder.dart | 4 +- .../mixin/message_dialog_action_mixin.dart | 9 +- .../presentation/composer_controller.dart | 101 +++++++++++++++--- .../widgets/sending_message_dialog_view.dart | 4 +- lib/l10n/intl_messages.arb | 20 +++- lib/main/localizations/app_localizations.dart | 18 ++++ 6 files changed, 137 insertions(+), 19 deletions(-) diff --git a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart index 036f485238..054246ff63 100644 --- a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart +++ b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart @@ -27,7 +27,7 @@ class ConfirmDialogBuilder { EdgeInsets? _paddingContent; EdgeInsets? _paddingButton; EdgeInsets? _outsideDialogPadding; - EdgeInsets? _marginIcon; + EdgeInsetsGeometry? _marginIcon; EdgeInsets? _margin; double? _widthDialog; double? _heightDialog; @@ -107,7 +107,7 @@ class ConfirmDialogBuilder { _paddingButton = value; } - void marginIcon(EdgeInsets? value) { + void marginIcon(EdgeInsetsGeometry? value) { _marginIcon = value; } diff --git a/lib/features/base/mixin/message_dialog_action_mixin.dart b/lib/features/base/mixin/message_dialog_action_mixin.dart index 88cf77dac1..bb4f098417 100644 --- a/lib/features/base/mixin/message_dialog_action_mixin.dart +++ b/lib/features/base/mixin/message_dialog_action_mixin.dart @@ -15,6 +15,7 @@ mixin MessageDialogActionMixin { { Function? onConfirmAction, Function? onCancelAction, + OnCloseButtonAction? onCloseButtonAction, String? title, String? cancelTitle, bool hasCancelButton = true, @@ -28,6 +29,7 @@ mixin MessageDialogActionMixin { TextStyle? cancelStyle, Color? actionButtonColor, Color? cancelButtonColor, + EdgeInsetsGeometry? marginIcon, } ) async { final responsiveUtils = Get.find(); @@ -43,7 +45,7 @@ mixin MessageDialogActionMixin { ..addIcon(icon) ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) - ..marginIcon(icon != null ? const EdgeInsets.only(top: 24) : null) + ..marginIcon(icon != null ? (marginIcon ?? const EdgeInsets.only(top: 24)) : null) ..paddingTitle(icon != null ? const EdgeInsetsDirectional.only(top: 24, start: 24, end: 24) : const EdgeInsetsDirectional.symmetric(horizontal: 24) @@ -66,6 +68,7 @@ mixin MessageDialogActionMixin { onCancelAction?.call(); } ) + ..onCloseButtonAction(onCloseButtonAction) ).build() ), barrierColor: AppColor.colorDefaultCupertinoActionSheet, @@ -111,7 +114,7 @@ mixin MessageDialogActionMixin { onCancelAction?.call(); } ) - ..onCloseButtonAction(() => popBack())) + ..onCloseButtonAction(onCloseButtonAction ?? () => popBack())) .build() ), isScrollControlled: true, @@ -171,7 +174,7 @@ mixin MessageDialogActionMixin { onCancelAction?.call(); } ) - ..onCloseButtonAction(() => popBack())) + ..onCloseButtonAction(onCloseButtonAction ?? () => popBack())) .build() ), barrierColor: AppColor.colorDefaultCupertinoActionSheet, diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index d58cc02352..40f39a6bb2 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -428,7 +428,9 @@ class ComposerController extends BaseController with DragDropFileMixin { } void onCreatedMobileEditorAction(BuildContext context, HtmlEditorApi editorApi, String? content) { - initTextEditor(content); + if (identitySelected.value != null) { + initTextEditor(content); + } richTextMobileTabletController.htmlEditorApi = editorApi; keyboardRichTextController.onCreateHTMLEditor( editorApi, @@ -982,7 +984,6 @@ class ComposerController extends BaseController with DragDropFileMixin { alignCenter: true, onConfirmAction: () { _sendButtonState = ButtonState.enabled; - popBack(); _autoFocusFieldWhenLauncher(); }, onCancelAction: () async { @@ -1170,8 +1171,9 @@ class ComposerController extends BaseController with DragDropFileMixin { PresentationEmail? presentationEmail, Role? mailboxRole, }) async { - final newEmailBody = await _getEmailBodyText(context, asDrafts: true); + final newEmailBody = await _getContentInEditor(); final oldEmailBody = _initTextEditor ?? ''; + log('ComposerController::_validateEmailChange: newEmailBody = $newEmailBody | oldEmailBody = $oldEmailBody'); final isEmailBodyChanged = !oldEmailBody.trim().isSame(newEmailBody.trim()); final newEmailSubject = subjectEmail.value ?? ''; @@ -1936,7 +1938,9 @@ class ComposerController extends BaseController with DragDropFileMixin { HtmlEditorApi? get htmlEditorApi => richTextMobileTabletController.htmlEditorApi; void onChangeTextEditorWeb(String? text) { - initTextEditor(text); + if (identitySelected.value != null) { + initTextEditor(text); + } _textEditorWeb = text; } @@ -2053,19 +2057,92 @@ class ComposerController extends BaseController with DragDropFileMixin { log('ComposerController::handleClickCloseComposer: _closeComposerButtonState = disabled'); return; } - - if (!_isEmailBodyLoaded) { - log('ComposerController::handleClickCloseComposer: _isEmailBodyLoaded = false'); + + _closeComposerButtonState = ButtonState.disabled; + + if (composerArguments.value == null) { + log('ComposerController::handleClickCloseComposer: ARGUMENTS is NULL'); clearFocus(context); _closeComposerAction(); return; } - _closeComposerButtonState = ButtonState.disabled; - clearFocus(context); - final draftArgs = await _generateSaveAsDraftsArguments(context); - _closeComposerAction(result: draftArgs); - _closeComposerButtonState = ButtonState.enabled; + final isChanged = await _validateEmailChange( + context: context, + emailActionType: composerArguments.value!.emailActionType, + presentationEmail: composerArguments.value!.presentationEmail, + mailboxRole: composerArguments.value!.mailboxRole + ); + + if (isChanged && context.mounted) { + clearFocus(context); + _showConfirmDialogSaveMessage(context); + return; + } + + if (!_isEmailBodyLoaded && context.mounted) { + log('ComposerController::handleClickCloseComposer: EDITOR NOT LOADED'); + _closeComposerButtonState = ButtonState.enabled; + clearFocus(context); + _closeComposerAction(); + return; + } + + if (context.mounted) { + _closeComposerButtonState = ButtonState.enabled; + clearFocus(context); + _closeComposerAction(); + } + } + + void _showConfirmDialogSaveMessage(BuildContext context) { + showConfirmDialogAction( + context, + title: AppLocalizations.of(context).saveMessage.capitalizeFirstEach, + AppLocalizations.of(context).warningMessageWhenClickCloseComposer, + AppLocalizations.of(context).save, + cancelTitle: AppLocalizations.of(context).discardChanges, + alignCenter: true, + onConfirmAction: () { + _closeComposerButtonState = ButtonState.enabled; + }, + onCancelAction: () async { + _closeComposerButtonState = ButtonState.enabled; + await Future.delayed( + const Duration(milliseconds: 100), + _closeComposerAction + ); + }, + onCloseButtonAction: () { + _closeComposerButtonState = ButtonState.enabled; + popBack(); + _autoFocusFieldWhenLauncher(); + }, + marginIcon: EdgeInsets.zero, + icon: SvgPicture.asset( + imagePaths.icQuotasWarning, + width: 40, + height: 40, + colorFilter: AppColor.colorBackgroundQuotasWarning.asFilter(), + ), + titleStyle: Theme.of(context).textTheme.labelLarge?.copyWith( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black + ), + messageStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 14, + color: AppColor.colorTextBody + ), + actionStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.white + ), + cancelStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.black + ) + ); } void _getAlwaysReadReceiptSetting() { diff --git a/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart b/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart index a1aa4d173b..f80d9b5740 100644 --- a/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart +++ b/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:core/presentation/extensions/capitalize_extension.dart'; import 'package:core/presentation/extensions/color_extension.dart'; @@ -7,6 +8,7 @@ import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart' as dartz; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart'; @@ -82,7 +84,7 @@ class _SendingMessageDialogViewState extends State { borderRadius: BorderRadius.all(Radius.circular(12)), color: Colors.white, ), - width: 400, + width: min(context.width, 400), child: Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 5eee433f3d..dbbfcce02b 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-03-06T00:47:30.311599", + "@@last_modified": "2024-03-06T01:50:29.250412", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3803,5 +3803,23 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "saveMessage": "Save message", + "@saveMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "discardChanges": "Discard changes", + "@discardChanges": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "warningMessageWhenClickCloseComposer": "Save this message to your drafts folder and close composer?", + "@warningMessageWhenClickCloseComposer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 9fe57f82b9..0216e25014 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3974,4 +3974,22 @@ class AppLocalizations { 'Close anyway', name: 'closeAnyway'); } + + String get saveMessage { + return Intl.message( + 'Save message', + name: 'saveMessage'); + } + + String get discardChanges { + return Intl.message( + 'Discard changes', + name: 'discardChanges'); + } + + String get warningMessageWhenClickCloseComposer { + return Intl.message( + 'Save this message to your drafts folder and close composer?', + name: 'warningMessageWhenClickCloseComposer'); + } } \ No newline at end of file From 8351b26ccaa3c9e52d7306f0ba705ade0e25fa12 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 6 Mar 2024 03:02:18 +0700 Subject: [PATCH 15/80] TF-2667 Show saving message dialog when preform save action --- .../repository/composer_repository_impl.dart | 4 +- .../state/save_email_as_drafts_state.dart | 8 +- .../state/update_email_drafts_state.dart | 8 +- ...w_and_save_email_to_drafts_interactor.dart | 139 ++++++++++++ .../save_email_as_drafts_interactor.dart | 2 +- .../update_email_drafts_interactor.dart | 2 +- .../presentation/composer_bindings.dart | 9 +- .../presentation/composer_controller.dart | 202 +++++++++++------ .../widgets/saving_message_dialog_view.dart | 209 ++++++++++++++++++ .../widgets/sending_message_dialog_view.dart | 22 +- .../saving_message_to_drafts_dialog_view.dart | 208 +++++++++++++++++ .../mailbox_dashboard_controller.dart | 16 +- lib/l10n/intl_messages.arb | 26 ++- lib/main/localizations/app_localizations.dart | 24 ++ 14 files changed, 784 insertions(+), 95 deletions(-) create mode 100644 lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart create mode 100644 lib/features/composer/presentation/widgets/saving_message_dialog_view.dart create mode 100644 lib/features/composer/presentation/widgets/web/saving_message_to_drafts_dialog_view.dart diff --git a/lib/features/composer/data/repository/composer_repository_impl.dart b/lib/features/composer/data/repository/composer_repository_impl.dart index 1ee0c4b918..2f502bd40c 100644 --- a/lib/features/composer/data/repository/composer_repository_impl.dart +++ b/lib/features/composer/data/repository/composer_repository_impl.dart @@ -58,7 +58,9 @@ class ComposerRepositoryImpl extends ComposerRepository { emailAttachments.addAll(tupleContentInlineAttachments.value2); } - emailContent = await _removeCollapsedExpandedSignatureEffect(emailContent: emailContent); + if (createEmailRequest.draftsMailboxId == null) { + emailContent = await _removeCollapsedExpandedSignatureEffect(emailContent: emailContent); + } final userAgent = await _applicationManager.generateApplicationUserAgent(); final emailBodyPartId = PartId(_uuid.v1()); diff --git a/lib/features/composer/domain/state/save_email_as_drafts_state.dart b/lib/features/composer/domain/state/save_email_as_drafts_state.dart index e92f9d7192..d2844f905a 100644 --- a/lib/features/composer/domain/state/save_email_as_drafts_state.dart +++ b/lib/features/composer/domain/state/save_email_as_drafts_state.dart @@ -4,14 +4,14 @@ import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; -class SaveEmailAsDraftsLoading extends UIState {} +class SaveEmailAsDraftsLoading extends LoadingState {} class SaveEmailAsDraftsSuccess extends UIActionState { - final Email emailAsDrafts; + final EmailId emailId; SaveEmailAsDraftsSuccess( - this.emailAsDrafts, + this.emailId, { jmap.State? currentEmailState, jmap.State? currentMailboxState, @@ -19,7 +19,7 @@ class SaveEmailAsDraftsSuccess extends UIActionState { ) : super(currentEmailState, currentMailboxState); @override - List get props => [emailAsDrafts, ...super.props]; + List get props => [emailId, ...super.props]; } class SaveEmailAsDraftsFailure extends FeatureFailure { diff --git a/lib/features/composer/domain/state/update_email_drafts_state.dart b/lib/features/composer/domain/state/update_email_drafts_state.dart index af7ae68776..47ca09cca3 100644 --- a/lib/features/composer/domain/state/update_email_drafts_state.dart +++ b/lib/features/composer/domain/state/update_email_drafts_state.dart @@ -4,14 +4,14 @@ import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; -class UpdatingEmailDrafts extends UIState {} +class UpdatingEmailDrafts extends LoadingState {} class UpdateEmailDraftsSuccess extends UIActionState { - final Email emailAsDrafts; + final EmailId emailId; UpdateEmailDraftsSuccess( - this.emailAsDrafts, + this.emailId, { jmap.State? currentEmailState, jmap.State? currentMailboxState, @@ -19,7 +19,7 @@ class UpdateEmailDraftsSuccess extends UIActionState { ) : super(currentEmailState, currentMailboxState); @override - List get props => [emailAsDrafts, ...super.props]; + List get props => [emailId, ...super.props]; } class UpdateEmailDraftsFailure extends FeatureFailure { diff --git a/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart b/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart new file mode 100644 index 0000000000..c48920f721 --- /dev/null +++ b/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart @@ -0,0 +1,139 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart' as dartz; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; + +class CreateNewAndSaveEmailToDraftsInteractor { + final EmailRepository _emailRepository; + final MailboxRepository _mailboxRepository; + final ComposerRepository _composerRepository; + + CreateNewAndSaveEmailToDraftsInteractor( + this._emailRepository, + this._mailboxRepository, + this._composerRepository, + ); + + Stream> execute(CreateEmailRequest createEmailRequest) async* { + try { + yield dartz.Right(GenerateEmailLoading()); + + final listCurrentState = await _getStoredCurrentState( + session: createEmailRequest.session, + accountId: createEmailRequest.accountId + ); + + final emailCreated = await _createEmailObject(createEmailRequest); + + if (emailCreated != null) { + if (createEmailRequest.draftsEmailId == null) { + yield dartz.Right(SaveEmailAsDraftsLoading()); + + final emailDraftSaved = await _emailRepository.saveEmailAsDrafts( + createEmailRequest.session, + createEmailRequest.accountId, + emailCreated + ); + + yield dartz.Right( + SaveEmailAsDraftsSuccess( + emailDraftSaved.id!, + currentMailboxState: listCurrentState?.value1, + currentEmailState: listCurrentState?.value2 + ) + ); + } else { + yield dartz.Right(UpdatingEmailDrafts()); + + final emailDraftSaved = await _emailRepository.updateEmailDrafts( + createEmailRequest.session, + createEmailRequest.accountId, + emailCreated, + createEmailRequest.draftsEmailId! + ); + + await _deleteOldDraftsEmail( + session: createEmailRequest.session, + accountId: createEmailRequest.accountId, + draftEmailId: createEmailRequest.draftsEmailId! + ); + + yield dartz.Right( + UpdateEmailDraftsSuccess( + emailDraftSaved.id!, + currentMailboxState: listCurrentState?.value1, + currentEmailState: listCurrentState?.value2 + ) + ); + } + } else { + yield dartz.Left(GenerateEmailFailure(CannotCreateEmailObjectException())); + } + } catch (e) { + logError('CreateNewAndSaveEmailToDraftsInteractor::execute: Exception: $e'); + if (createEmailRequest.draftsEmailId == null) { + yield dartz.Left(SaveEmailAsDraftsFailure(e)); + } else { + yield dartz.Left(UpdateEmailDraftsFailure(e)); + } + } + } + + Future _createEmailObject(CreateEmailRequest createEmailRequest) async { + try { + final emailCreated = await _composerRepository.generateEmail(createEmailRequest); + return emailCreated; + } catch (e) { + logError('CreateNewAndSaveEmailToDraftsInteractor::_createEmailObject: Exception: $e'); + return null; + } + } + + Future?> _getStoredCurrentState({ + required Session session, + required AccountId accountId + }) async { + try { + final listState = await Future.wait([ + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), + ]); + + final mailboxState = listState.first; + final emailState = listState.last; + + return dartz.Tuple2(mailboxState, emailState); + } catch (e) { + logError('CreateNewAndSaveEmailToDraftsInteractor::_getStoredCurrentState: Exception: $e'); + return null; + } + } + + Future _deleteOldDraftsEmail({ + required Session session, + required AccountId accountId, + required EmailId draftEmailId + }) async { + try { + await _emailRepository.removeEmailDrafts( + session, + accountId, + draftEmailId + ); + } catch (e) { + logError('CreateNewAndSaveEmailToDraftsInteractor::_deleteOldDraftsEmail: Exception: $e'); + } + } +} \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/save_email_as_drafts_interactor.dart b/lib/features/composer/domain/usecases/save_email_as_drafts_interactor.dart index 511f3d181c..58bbe60f39 100644 --- a/lib/features/composer/domain/usecases/save_email_as_drafts_interactor.dart +++ b/lib/features/composer/domain/usecases/save_email_as_drafts_interactor.dart @@ -28,7 +28,7 @@ class SaveEmailAsDraftsInteractor { final emailAsDrafts = await _emailRepository.saveEmailAsDrafts(session, accountId, email); yield Right( SaveEmailAsDraftsSuccess( - emailAsDrafts, + emailAsDrafts.id!, currentEmailState: currentEmailState, currentMailboxState: currentMailboxState ) diff --git a/lib/features/composer/domain/usecases/update_email_drafts_interactor.dart b/lib/features/composer/domain/usecases/update_email_drafts_interactor.dart index 77c74ae594..7080666079 100644 --- a/lib/features/composer/domain/usecases/update_email_drafts_interactor.dart +++ b/lib/features/composer/domain/usecases/update_email_drafts_interactor.dart @@ -29,7 +29,7 @@ class UpdateEmailDraftsInteractor { final newEmailDrafts = await _emailRepository.updateEmailDrafts(session, accountId, newEmail, oldEmailId); yield Right( UpdateEmailDraftsSuccess( - newEmailDrafts, + newEmailDrafts.id!, currentEmailState: currentEmailState, currentMailboxState: currentMailboxState ) diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index 7f282169a2..d22bf4cbf1 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -1,6 +1,5 @@ import 'package:core/core.dart'; import 'package:core/utils/application_manager.dart'; -import 'package:core/utils/file_utils.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; import 'package:tmail_ui_user/features/composer/data/datasource/composer_datasource.dart'; @@ -11,6 +10,7 @@ import 'package:tmail_ui_user/features/composer/data/repository/composer_reposit import 'package:tmail_ui_user/features/composer/data/repository/contact_repository_impl.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/contact_repository.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/save_email_as_drafts_interactor.dart'; @@ -208,6 +208,11 @@ class ComposerBindings extends BaseBindings { Get.find(), Get.find(), )); + Get.lazyPut(() => CreateNewAndSaveEmailToDraftsInteractor( + Get.find(), + Get.find(), + Get.find(), + )); IdentityInteractorsBindings().dependencies(); } @@ -229,6 +234,8 @@ class ComposerBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), + Get.find(), )); } diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 40f39a6bb2..730db176d0 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -43,6 +43,7 @@ import 'package:tmail_ui_user/features/composer/domain/state/get_device_contact_ 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/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'; @@ -63,6 +64,7 @@ import 'package:tmail_ui_user/features/composer/presentation/model/save_to_draft 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/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/sending_message_dialog_view.dart'; import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; @@ -133,6 +135,7 @@ class ComposerController extends BaseController with DragDropFileMixin { final TransformHtmlEmailContentInteractor _transformHtmlEmailContentInteractor; final GetAlwaysReadReceiptSettingInteractor _getAlwaysReadReceiptSettingInteractor; final CreateNewAndSendEmailInteractor _createNewAndSendEmailInteractor; + final CreateNewAndSaveEmailToDraftsInteractor _createNewAndSaveEmailToDraftsInteractor; GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; @@ -202,6 +205,7 @@ class ComposerController extends BaseController with DragDropFileMixin { this._transformHtmlEmailContentInteractor, this._getAlwaysReadReceiptSettingInteractor, this._createNewAndSendEmailInteractor, + this._createNewAndSaveEmailToDraftsInteractor, ); @override @@ -348,11 +352,11 @@ class ComposerController extends BaseController with DragDropFileMixin { }, (success) { if (success is SaveEmailAsDraftsSuccess) { - _emailIdEditing = success.emailAsDrafts.id; + _emailIdEditing = success.emailId; _saveToDraftButtonState = ButtonState.enabled; log('ComposerController::_listenStreamEvent::dashboardViewStateWorker:SaveEmailAsDraftsSuccess:emailIdEditing: $_emailIdEditing'); } else if (success is UpdateEmailDraftsSuccess) { - _emailIdEditing = success.emailAsDrafts.id; + _emailIdEditing = success.emailId; _saveToDraftButtonState = ButtonState.enabled; log('ComposerController::_listenStreamEvent::dashboardViewStateWorker:UpdateEmailDraftsSuccess:emailIdEditing: $_emailIdEditing'); } @@ -1211,65 +1215,6 @@ class ComposerController extends BaseController with DragDropFileMixin { return false; } - Future _generateSaveAsDraftsArguments(BuildContext context) async { - log('ComposerController::_generateSaveAsDraftsArguments:'); - final arguments = composerArguments.value; - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; - final draftMailboxId = mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts]; - - if (arguments == null || - draftMailboxId == null || - session == null || - accountId == null - ) { - return null; - } - - if (_emailIdEditing != null && _emailIdEditing != arguments.presentationEmail?.id) { - final newEmail = await _generateEmail( - context, - session.username, - asDrafts: true, - draftMailboxId: draftMailboxId, - arguments: arguments, - ); - - return SaveToDraftArguments( - session: session, - accountId: accountId, - newEmail: newEmail, - oldEmailId: _emailIdEditing!); - } else { - final isChanged = await _validateEmailChange( - context: context, - emailActionType: arguments.emailActionType, - presentationEmail: arguments.presentationEmail, - mailboxRole: arguments.mailboxRole - ); - - if (isChanged && context.mounted) { - final newEmail = await _generateEmail( - context, - session.username, - asDrafts: true, - draftMailboxId: draftMailboxId, - arguments: arguments, - ); - - return SaveToDraftArguments( - session: session, - accountId: accountId, - newEmail: newEmail, - oldEmailId: arguments.emailActionType == EmailActionType.editDraft - ? arguments.presentationEmail?.id - : null); - } else { - return null; - } - } - } - void saveToDraftAction(BuildContext context) async { if (_saveToDraftButtonState == ButtonState.disabled) { log('ComposerController::saveToDraftAction: Saving to draft'); @@ -2103,9 +2048,10 @@ class ComposerController extends BaseController with DragDropFileMixin { AppLocalizations.of(context).save, cancelTitle: AppLocalizations.of(context).discardChanges, alignCenter: true, - onConfirmAction: () { - _closeComposerButtonState = ButtonState.enabled; - }, + onConfirmAction: () async => await Future.delayed( + const Duration(milliseconds: 100), + () => _handleSaveMessageToDraft(context) + ), onCancelAction: () async { _closeComposerButtonState = ButtonState.enabled; await Future.delayed( @@ -2180,4 +2126,132 @@ class ComposerController extends BaseController with DragDropFileMixin { onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo) ); } + + void _handleSaveMessageToDraft(BuildContext context) async { + if (composerArguments.value == null || + mailboxDashBoardController.sessionCurrent == null || + mailboxDashBoardController.accountId.value == null || + mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts] == null + ) { + log('ComposerController::_handleSaveMessageToDraft: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); + _closeComposerButtonState = ButtonState.enabled; + _closeComposerAction(); + return; + } + + final emailContent = await _getContentInEditor(); + final draftEmailId = _getDraftEmailId(); + log('ComposerController::_handleSaveMessageToDraft: draftEmailId = $draftEmailId'); + final resultState = await _showSavingMessageToDraftsDialog( + emailContent: emailContent, + draftEmailId: draftEmailId + ); + + if (resultState is SaveEmailAsDraftsSuccess || resultState is UpdateEmailDraftsSuccess) { + _closeComposerButtonState = ButtonState.enabled; + _closeComposerAction(result: resultState); + } else if ((resultState is SaveEmailAsDraftsFailure || + resultState is UpdateEmailDraftsFailure || + resultState is GenerateEmailFailure) && + context.mounted + ) { + _showConfirmDialogWhenSaveMessageToDraftsFailure( + context: context, + failure: resultState + ); + } else { + _closeComposerButtonState = ButtonState.enabled; + } + } + + EmailId? _getDraftEmailId() { + if (_emailIdEditing != null && + _emailIdEditing != composerArguments.value!.presentationEmail?.id) { + return _emailIdEditing; + } else if (composerArguments.value!.emailActionType == EmailActionType.editDraft) { + return composerArguments.value!.presentationEmail?.id; + } else { + return null; + } + } + + Future _showSavingMessageToDraftsDialog({ + required String emailContent, + EmailId? draftEmailId, + }) { + return Get.dialog( + PointerInterceptor( + child: SavingMessageDialogView( + createEmailRequest: CreateEmailRequest( + session: mailboxDashBoardController.sessionCurrent!, + accountId: mailboxDashBoardController.accountId.value!, + emailActionType: composerArguments.value!.emailActionType, + subject: subjectEmail.value ?? '', + emailContent: emailContent, + fromSender: composerArguments.value!.presentationEmail?.from ?? {}, + toRecipients: listToEmailAddress.toSet(), + ccRecipients: listCcEmailAddress.toSet(), + bccRecipients: listBccEmailAddress.toSet(), + isRequestReadReceipt: hasRequestReadReceipt.value, + identity: identitySelected.value, + attachments: uploadController.attachmentsUploaded, + inlineAttachments: uploadController.mapInlineAttachments, + sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], + draftsMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts], + draftsEmailId: draftEmailId, + answerForwardEmailId: composerArguments.value!.presentationEmail?.id, + unsubscribeEmailId: composerArguments.value!.previousEmailId, + messageId: composerArguments.value!.messageId, + references: composerArguments.value!.references, + emailSendingQueue: composerArguments.value!.sendingEmail + ), + createNewAndSaveEmailToDraftsInteractor: _createNewAndSaveEmailToDraftsInteractor + ), + ), + barrierColor: AppColor.colorDefaultCupertinoActionSheet, + ); + } + + void _showConfirmDialogWhenSaveMessageToDraftsFailure({ + required BuildContext context, + required FeatureFailure failure + }) { + showConfirmDialogAction( + context, + title: '', + AppLocalizations.of(context).warningMessageWhenSaveEmailToDraftsFailure, + AppLocalizations.of(context).edit, + cancelTitle: AppLocalizations.of(context).closeAnyway, + alignCenter: true, + onConfirmAction: () { + _closeComposerButtonState = ButtonState.enabled; + _autoFocusFieldWhenLauncher(); + }, + onCancelAction: () async { + _closeComposerButtonState = ButtonState.enabled; + await Future.delayed( + const Duration(milliseconds: 100), + _closeComposerAction + ); + }, + icon: SvgPicture.asset( + imagePaths.icQuotasWarning, + width: 40, + height: 40, + colorFilter: AppColor.colorBackgroundQuotasWarning.asFilter(), + ), + messageStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 14, + color: AppColor.colorTextBody + ), + actionStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.white + ), + cancelStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.black + ) + ); + } } \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/saving_message_dialog_view.dart b/lib/features/composer/presentation/widgets/saving_message_dialog_view.dart new file mode 100644 index 0000000000..63e9962c9c --- /dev/null +++ b/lib/features/composer/presentation/widgets/saving_message_dialog_view.dart @@ -0,0 +1,209 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:core/presentation/extensions/capitalize_extension.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart' as dartz; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_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/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +class SavingMessageDialogView extends StatefulWidget { + + final CreateEmailRequest createEmailRequest; + final CreateNewAndSaveEmailToDraftsInteractor createNewAndSaveEmailToDraftsInteractor; + + const SavingMessageDialogView({ + super.key, + required this.createEmailRequest, + required this.createNewAndSaveEmailToDraftsInteractor, + }); + + @override + State createState() => _SavingMessageDialogViewState(); +} + +class _SavingMessageDialogViewState extends State { + + StreamSubscription? _streamSubscription; + final ValueNotifier?> _viewStateNotifier = ValueNotifier(null); + + @override + void initState() { + super.initState(); + _streamSubscription = widget.createNewAndSaveEmailToDraftsInteractor + .execute(widget.createEmailRequest) + .listen( + _handleDataStream, + onError: _handleErrorStream + ); + } + + void _handleDataStream(dartz.Either newState) { + _viewStateNotifier.value = newState; + + newState.fold( + (failure) { + if (failure is SaveEmailAsDraftsFailure || + failure is UpdateEmailDraftsFailure || + failure is GenerateEmailFailure) { + popBack(result: failure); + } + }, + (success) { + if (success is SaveEmailAsDraftsSuccess || success is UpdateEmailDraftsSuccess) { + popBack(result: success); + } + } + ); + } + + void _handleErrorStream(Object error, StackTrace stackTrace) { + logError('_SavingMessageDialogViewState::_handleErrorStream: Exception = $error'); + popBack(result: SaveEmailAsDraftsFailure(error)); + } + + @override + Widget build(BuildContext context) { + return Dialog( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12))), + insetPadding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 16.0 + ), + alignment: Alignment.center, + child: Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: Colors.white, + ), + width: min(context.width, 400), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + padding: const EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 12), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: AppColor.colorItemSelected, + ), + alignment: Alignment.center, + child: Text( + AppLocalizations.of(context).savingMessage.capitalizeFirstEach, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 17 + ), + ), + ), + const Divider(), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 12, bottom: 4), + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).status}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + ), + const SizedBox(width: 8), + Expanded( + child: ValueListenableBuilder( + valueListenable: _viewStateNotifier, + builder: (context, value, child) { + if (value == null) { + return child!; + } + + return value.fold( + (failure) => child!, + (success) { + if (success is GenerateEmailLoading) { + return Text( + '${AppLocalizations.of(context).creatingMessage}...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ); + } else { + return Text( + '${AppLocalizations.of(context).savingMessageToDraftFolder}...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ); + } + } + ); + }, + child: Text( + '...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ), + ), + ) + ], + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 4, bottom: 24), + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).progress}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + ), + const SizedBox(width: 8), + Expanded( + child: LinearProgressIndicator( + color: Colors.white.withOpacity(0.6), + backgroundColor: AppColor.primaryColor, + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + ) + ], + ), + ) + ], + ) + ], + ), + ), + ); + } + + @override + void dispose() { + _streamSubscription?.cancel(); + _viewStateNotifier.dispose(); + super.dispose(); + } +} diff --git a/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart b/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart index f80d9b5740..d59cc9f33f 100644 --- a/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart +++ b/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart @@ -33,13 +33,13 @@ class SendingMessageDialogView extends StatefulWidget { class _SendingMessageDialogViewState extends State { - StreamSubscription? _sendingStreamSubscription; - final ValueNotifier?> _sendingNotifier = ValueNotifier(null); + StreamSubscription? _streamSubscription; + final ValueNotifier?> _viewStateNotifier = ValueNotifier(null); @override void initState() { super.initState(); - _sendingStreamSubscription = widget.createNewAndSendEmailInteractor + _streamSubscription = widget.createNewAndSendEmailInteractor .execute(widget.createEmailRequest) .listen( _handleDataStream, @@ -48,7 +48,7 @@ class _SendingMessageDialogViewState extends State { } void _handleDataStream(dartz.Either newState) { - _sendingNotifier.value = newState; + _viewStateNotifier.value = newState; newState.fold( (failure) { @@ -125,7 +125,7 @@ class _SendingMessageDialogViewState extends State { const SizedBox(width: 8), Expanded( child: ValueListenableBuilder( - valueListenable: _sendingNotifier, + valueListenable: _viewStateNotifier, builder: (context, value, child) { if (value == null) { return child!; @@ -136,13 +136,13 @@ class _SendingMessageDialogViewState extends State { (success) { if (success is GenerateEmailLoading) { return Text( - 'Creating email...', + '${AppLocalizations.of(context).creatingMessage}...', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: AppColor.labelColor, fontSize: 14 ), ); - } else if (success is SendEmailLoading) { + } else { return Text( '${AppLocalizations.of(context).sendingMessage}...', style: Theme.of(context).textTheme.labelSmall?.copyWith( @@ -150,14 +150,12 @@ class _SendingMessageDialogViewState extends State { fontSize: 14 ), ); - } else { - return child!; } } ); }, child: Text( - '${AppLocalizations.of(context).sendingMessage}...', + '...', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: AppColor.labelColor, fontSize: 14 @@ -201,8 +199,8 @@ class _SendingMessageDialogViewState extends State { @override void dispose() { - _sendingStreamSubscription?.cancel(); - _sendingNotifier.dispose(); + _streamSubscription?.cancel(); + _viewStateNotifier.dispose(); super.dispose(); } } diff --git a/lib/features/composer/presentation/widgets/web/saving_message_to_drafts_dialog_view.dart b/lib/features/composer/presentation/widgets/web/saving_message_to_drafts_dialog_view.dart new file mode 100644 index 0000000000..ea8db6d62a --- /dev/null +++ b/lib/features/composer/presentation/widgets/web/saving_message_to_drafts_dialog_view.dart @@ -0,0 +1,208 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:core/presentation/extensions/capitalize_extension.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart' as dartz; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +class SavingMessageToDraftsDialogView extends StatefulWidget { + + final CreateEmailRequest createEmailRequest; + final CreateNewAndSendEmailInteractor createNewAndSendEmailInteractor; + + const SavingMessageToDraftsDialogView({ + super.key, + required this.createEmailRequest, + required this.createNewAndSendEmailInteractor, + }); + + @override + State createState() => _SavingMessageToDraftsDialogViewState(); +} + +class _SavingMessageToDraftsDialogViewState extends State { + + StreamSubscription? _sendingStreamSubscription; + final ValueNotifier?> _sendingNotifier = ValueNotifier(null); + + @override + void initState() { + super.initState(); + _sendingStreamSubscription = widget.createNewAndSendEmailInteractor + .execute(widget.createEmailRequest) + .listen( + _handleDataStream, + onError: _handleErrorStream + ); + } + + void _handleDataStream(dartz.Either newState) { + _sendingNotifier.value = newState; + + newState.fold( + (failure) { + if (failure is SendEmailFailure || failure is GenerateEmailFailure) { + popBack(result: failure); + } + }, + (success) { + if (success is SendEmailSuccess) { + popBack(result: success); + } + } + ); + } + + void _handleErrorStream(Object error, StackTrace stackTrace) { + logError('_SendingMessageDialogViewState::_handleErrorStream: Exception = $error'); + popBack(result: SendEmailFailure(exception: error)); + } + + @override + Widget build(BuildContext context) { + return Dialog( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12))), + insetPadding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 16.0 + ), + alignment: Alignment.center, + child: Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: Colors.white, + ), + width: min(context.width, 400), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + padding: const EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 12), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: AppColor.colorItemSelected, + ), + alignment: Alignment.center, + child: Text( + AppLocalizations.of(context).sendingMessage.capitalizeFirstEach, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 17 + ), + ), + ), + const Divider(), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 12, bottom: 4), + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).status}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + ), + const SizedBox(width: 8), + Expanded( + child: ValueListenableBuilder( + valueListenable: _sendingNotifier, + builder: (context, value, child) { + if (value == null) { + return child!; + } + + return value.fold( + (failure) => child!, + (success) { + if (success is GenerateEmailLoading) { + return Text( + 'Creating email...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ); + } else if (success is SendEmailLoading) { + return Text( + '${AppLocalizations.of(context).sendingMessage}...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ); + } else { + return child!; + } + } + ); + }, + child: Text( + '${AppLocalizations.of(context).sendingMessage}...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ), + ), + ) + ], + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 4, bottom: 24), + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).progress}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + ), + const SizedBox(width: 8), + Expanded( + child: LinearProgressIndicator( + color: Colors.white.withOpacity(0.6), + backgroundColor: AppColor.primaryColor, + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + ) + ], + ), + ) + ], + ) + ], + ), + ), + ); + } + + @override + void dispose() { + _sendingStreamSubscription?.cancel(); + _sendingNotifier.dispose(); + super.dispose(); + } +} diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 42e14e4a07..76b385f635 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -656,7 +656,7 @@ class MailboxDashBoardController extends ReloadableController { currentOverlayContext!, AppLocalizations.of(currentContext!).drafts_saved, actionName: AppLocalizations.of(currentContext!).discard, - onActionClick: () => _discardEmail(success.emailAsDrafts), + onActionClick: () => _discardEmail(success.emailId), leadingSVGIcon: imagePaths.icMailboxDrafts, leadingSVGIconColor: Colors.white, backgroundColor: AppColor.toastSuccessBackgroundColor, @@ -699,11 +699,11 @@ class MailboxDashBoardController extends ReloadableController { } } - void _discardEmail(Email email) { + void _discardEmail(EmailId emailId) { final currentAccountId = accountId.value; final session = sessionCurrent; - if (currentAccountId != null && session != null && email.id != null) { - consumeState(_removeEmailDraftsInteractor.execute(session, currentAccountId, email.id!)); + if (currentAccountId != null && session != null) { + consumeState(_removeEmailDraftsInteractor.execute(session, currentAccountId, emailId)); } } @@ -1260,7 +1260,9 @@ class MailboxDashBoardController extends ReloadableController { handleSendEmailAction(result); } else if (result is SaveToDraftArguments) { saveEmailToDraft(arguments: result); - } else if (result is SendEmailSuccess) { + } else if (result is SendEmailSuccess || + result is SaveEmailAsDraftsSuccess || + result is UpdateEmailDraftsSuccess) { consumeState(Stream.value(Right(result))); } } @@ -1395,7 +1397,9 @@ class MailboxDashBoardController extends ReloadableController { handleSendEmailAction(result); } else if (result is SaveToDraftArguments) { saveEmailToDraft(arguments: result); - } else if (result is SendEmailSuccess) { + } else if (result is SendEmailSuccess || + result is SaveEmailAsDraftsSuccess || + result is UpdateEmailDraftsSuccess) { consumeState(Stream.value(Right(result))); } } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index dbbfcce02b..6d2997da9f 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-03-06T01:50:29.250412", + "@@last_modified": "2024-03-06T02:46:19.947772", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3821,5 +3821,29 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "savingMessage": "Saving message", + "@savingMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "creatingMessage": "Creating message", + "@creatingMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "savingMessageToDraftFolder": "Saving message to draft folder", + "@savingMessageToDraftFolder": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "warningMessageWhenSaveEmailToDraftsFailure": "Saving of the message to drafts folder failed.\nAn error occurred while saving mail.", + "@warningMessageWhenSaveEmailToDraftsFailure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 0216e25014..222f04fdba 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3992,4 +3992,28 @@ class AppLocalizations { 'Save this message to your drafts folder and close composer?', name: 'warningMessageWhenClickCloseComposer'); } + + String get savingMessage { + return Intl.message( + 'Saving message', + name: 'savingMessage'); + } + + String get creatingMessage { + return Intl.message( + 'Creating message', + name: 'creatingMessage'); + } + + String get savingMessageToDraftFolder { + return Intl.message( + 'Saving message to draft folder', + name: 'savingMessageToDraftFolder'); + } + + String get warningMessageWhenSaveEmailToDraftsFailure { + return Intl.message( + 'Saving of the message to drafts folder failed.\nAn error occurred while saving mail.', + name: 'warningMessageWhenSaveEmailToDraftsFailure'); + } } \ No newline at end of file From 67954f0bbbc608c51c0af4db708bd1e0c326bb30 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 6 Mar 2024 03:31:34 +0700 Subject: [PATCH 16/80] TF-2667 Refresh thread & mailbox view when saving message success --- .../save_email_as_drafts_interactor.dart | 40 ---- .../update_email_drafts_interactor.dart | 41 ---- .../presentation/composer_bindings.dart | 8 - .../presentation/composer_controller.dart | 98 +++++---- .../composer/presentation/composer_view.dart | 4 +- .../presentation/composer_view_web.dart | 4 +- .../model/save_to_draft_arguments.dart | 26 --- .../saving_message_to_drafts_dialog_view.dart | 208 ------------------ .../bindings/mailbox_dashboard_bindings.dart | 12 - .../mailbox_dashboard_controller.dart | 32 --- 10 files changed, 55 insertions(+), 418 deletions(-) delete mode 100644 lib/features/composer/domain/usecases/save_email_as_drafts_interactor.dart delete mode 100644 lib/features/composer/domain/usecases/update_email_drafts_interactor.dart delete mode 100644 lib/features/composer/presentation/model/save_to_draft_arguments.dart delete mode 100644 lib/features/composer/presentation/widgets/web/saving_message_to_drafts_dialog_view.dart diff --git a/lib/features/composer/domain/usecases/save_email_as_drafts_interactor.dart b/lib/features/composer/domain/usecases/save_email_as_drafts_interactor.dart deleted file mode 100644 index 58bbe60f39..0000000000 --- a/lib/features/composer/domain/usecases/save_email_as_drafts_interactor.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:core/core.dart'; -import 'package:dartz/dartz.dart'; -import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; -import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; - -class SaveEmailAsDraftsInteractor { - final EmailRepository _emailRepository; - final MailboxRepository _mailboxRepository; - - SaveEmailAsDraftsInteractor(this._emailRepository, this._mailboxRepository); - - Stream> execute(Session session, AccountId accountId, Email email) async* { - try { - yield Right(SaveEmailAsDraftsLoading()); - - final listState = await Future.wait([ - _mailboxRepository.getMailboxState(session, accountId), - _emailRepository.getEmailState(session, accountId), - ], eagerError: true); - - final currentMailboxState = listState.first; - final currentEmailState = listState.last; - - final emailAsDrafts = await _emailRepository.saveEmailAsDrafts(session, accountId, email); - yield Right( - SaveEmailAsDraftsSuccess( - emailAsDrafts.id!, - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState - ) - ); - } catch (e) { - yield Left(SaveEmailAsDraftsFailure(e)); - } - } -} \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/update_email_drafts_interactor.dart b/lib/features/composer/domain/usecases/update_email_drafts_interactor.dart deleted file mode 100644 index 7080666079..0000000000 --- a/lib/features/composer/domain/usecases/update_email_drafts_interactor.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:core/presentation/state/failure.dart'; -import 'package:core/presentation/state/success.dart'; -import 'package:dartz/dartz.dart'; -import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; -import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; - -class UpdateEmailDraftsInteractor { - final EmailRepository _emailRepository; - final MailboxRepository _mailboxRepository; - - UpdateEmailDraftsInteractor(this._emailRepository, this._mailboxRepository); - - Stream> execute(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId) async* { - try { - yield Right(UpdatingEmailDrafts()); - - final listState = await Future.wait([ - _mailboxRepository.getMailboxState(session, accountId), - _emailRepository.getEmailState(session, accountId), - ], eagerError: true); - - final currentMailboxState = listState.first; - final currentEmailState = listState.last; - - final newEmailDrafts = await _emailRepository.updateEmailDrafts(session, accountId, newEmail, oldEmailId); - yield Right( - UpdateEmailDraftsSuccess( - newEmailDrafts.id!, - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState - ) - ); - } catch (e) { - yield Left(UpdateEmailDraftsFailure(e)); - } - } -} \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index d22bf4cbf1..2c56aff05b 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -13,8 +13,6 @@ import 'package:tmail_ui_user/features/composer/domain/repository/contact_reposi import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/download_image_as_base64_interactor.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_as_drafts_interactor.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/update_email_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachment_interactor.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart'; @@ -191,13 +189,7 @@ class ComposerBindings extends BaseBindings { Get.lazyPut(() => LocalFilePickerInteractor()); Get.lazyPut(() => LocalImagePickerInteractor()); Get.lazyPut(() => UploadAttachmentInteractor(Get.find())); - Get.lazyPut(() => SaveEmailAsDraftsInteractor( - Get.find(), - Get.find())); Get.lazyPut(() => GetEmailContentInteractor(Get.find())); - Get.lazyPut(() => UpdateEmailDraftsInteractor( - Get.find(), - Get.find())); Get.lazyPut(() => RemoveComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => SaveComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => DownloadImageAsBase64Interactor(Get.find())); diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 730db176d0..d2eb6ef268 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -60,7 +60,6 @@ import 'package:tmail_ui_user/features/composer/presentation/model/create_email_ import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.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/save_to_draft_arguments.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/widgets/mobile/from_composer_bottom_sheet_builder.dart'; @@ -341,27 +340,6 @@ class ComposerController extends BaseController with DragDropFileMixin { } }); }); - - dashboardViewStateWorker = ever(mailboxDashBoardController.viewState, (state) { - state.fold( - (failure) { - if (failure is SaveEmailAsDraftsFailure || - failure is UpdateEmailDraftsFailure) { - _saveToDraftButtonState = ButtonState.enabled; - } - }, - (success) { - if (success is SaveEmailAsDraftsSuccess) { - _emailIdEditing = success.emailId; - _saveToDraftButtonState = ButtonState.enabled; - log('ComposerController::_listenStreamEvent::dashboardViewStateWorker:SaveEmailAsDraftsSuccess:emailIdEditing: $_emailIdEditing'); - } else if (success is UpdateEmailDraftsSuccess) { - _emailIdEditing = success.emailId; - _saveToDraftButtonState = ButtonState.enabled; - log('ComposerController::_listenStreamEvent::dashboardViewStateWorker:UpdateEmailDraftsSuccess:emailIdEditing: $_emailIdEditing'); - } - }); - }); } void _listenBrowserTabRefresh() { @@ -1215,38 +1193,62 @@ class ComposerController extends BaseController with DragDropFileMixin { return false; } - void saveToDraftAction(BuildContext context) async { + void handleClickSaveAsDraftsButton(BuildContext context) async { if (_saveToDraftButtonState == ButtonState.disabled) { - log('ComposerController::saveToDraftAction: Saving to draft'); + log('ComposerController::handleClickSaveAsDraftsButton: Saving to draft'); return; } _saveToDraftButtonState = ButtonState.disabled; - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; - final draftMailboxId = mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts]; - - if (draftMailboxId == null || session == null || accountId == null) { - log('ComposerController::saveToDraftAction: Param is NULL'); + if (composerArguments.value == null || + mailboxDashBoardController.sessionCurrent == null || + mailboxDashBoardController.accountId.value == null || + mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts] == null + ) { + log('ComposerController::handleClickSaveAsDraftsButton: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); + _saveToDraftButtonState = ButtonState.enabled; return; } - final newEmail = await _generateEmail( - context, - session.username, - asDrafts: true, - draftMailboxId: draftMailboxId, - arguments: mailboxDashBoardController.composerArguments); - - mailboxDashBoardController.saveEmailToDraft( - arguments: SaveToDraftArguments( - session: session, - accountId:accountId, - newEmail: newEmail, - oldEmailId: _emailIdEditing - ) + final emailContent = await _getContentInEditor(); + + final resultState = await _showSavingMessageToDraftsDialog( + emailContent: emailContent, + draftEmailId: _emailIdEditing ); + + if (resultState is SaveEmailAsDraftsSuccess) { + _saveToDraftButtonState = ButtonState.enabled; + _emailIdEditing = resultState.emailId; + mailboxDashBoardController.consumeState(Stream.value(Right(resultState))); + } else if (resultState is UpdateEmailDraftsSuccess) { + _saveToDraftButtonState = ButtonState.enabled; + _emailIdEditing = resultState.emailId; + mailboxDashBoardController.consumeState(Stream.value(Right(resultState))); + } else if ((resultState is SaveEmailAsDraftsFailure || + resultState is UpdateEmailDraftsFailure || + resultState is GenerateEmailFailure) && + context.mounted + ) { + _showConfirmDialogWhenSaveMessageToDraftsFailure( + context: context, + failure: resultState, + onConfirmAction: () { + _saveToDraftButtonState = ButtonState.enabled; + _autoFocusFieldWhenLauncher(); + }, + onCancelAction: () async { + _saveToDraftButtonState = ButtonState.enabled; + await Future.delayed( + const Duration(milliseconds: 100), + _closeComposerAction + ); + } + ); + } else { + _saveToDraftButtonState = ButtonState.enabled; + } } void _addAttachmentFromFileShare(List listSharedMediaFile) { @@ -2214,7 +2216,9 @@ class ComposerController extends BaseController with DragDropFileMixin { void _showConfirmDialogWhenSaveMessageToDraftsFailure({ required BuildContext context, - required FeatureFailure failure + required FeatureFailure failure, + VoidCallback? onConfirmAction, + VoidCallback? onCancelAction, }) { showConfirmDialogAction( context, @@ -2223,11 +2227,11 @@ class ComposerController extends BaseController with DragDropFileMixin { AppLocalizations.of(context).edit, cancelTitle: AppLocalizations.of(context).closeAnyway, alignCenter: true, - onConfirmAction: () { + onConfirmAction: onConfirmAction ?? () { _closeComposerButtonState = ButtonState.enabled; _autoFocusFieldWhenLauncher(); }, - onCancelAction: () async { + onCancelAction: onCancelAction ?? () async { _closeComposerButtonState = ButtonState.enabled; await Future.delayed( const Duration(milliseconds: 100), diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index e9c707c188..3c58fab309 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -361,7 +361,7 @@ class ComposerView extends GetWidget { ), TabletBottomBarComposerWidget( deleteComposerAction: () => controller.handleClickDeleteComposer(context), - saveToDraftAction: () => controller.saveToDraftAction(context), + saveToDraftAction: () => controller.handleClickSaveAsDraftsButton(context), sendMessageAction: () => controller.handleClickSendButton(context), requestReadReceiptAction: (position) { controller.openPopupMenuAction( @@ -453,7 +453,7 @@ class ComposerView extends GetWidget { padding: ComposerStyle.popupItemPadding, onCallbackAction: () { popBack(); - controller.saveToDraftAction(context); + controller.handleClickSaveAsDraftsButton(context); } ) ), diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index d1b10e5222..a7c7b0e186 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -754,7 +754,7 @@ class ComposerView extends GetWidget { insertImageAction: () => controller.insertImage(context, constraints.maxWidth), showCodeViewAction: controller.richTextWebController.toggleCodeView, deleteComposerAction: () => controller.handleClickDeleteComposer(context), - saveToDraftAction: () => controller.saveToDraftAction(context), + saveToDraftAction: () => controller.handleClickSaveAsDraftsButton(context), sendMessageAction: () => controller.handleClickSendButton(context), requestReadReceiptAction: (position) { controller.openPopupMenuAction( @@ -836,7 +836,7 @@ class ComposerView extends GetWidget { padding: ComposerStyle.popupItemPadding, onCallbackAction: () { popBack(); - controller.saveToDraftAction(context); + controller.handleClickSaveAsDraftsButton(context); } ) ), diff --git a/lib/features/composer/presentation/model/save_to_draft_arguments.dart b/lib/features/composer/presentation/model/save_to_draft_arguments.dart deleted file mode 100644 index b29a3855c1..0000000000 --- a/lib/features/composer/presentation/model/save_to_draft_arguments.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:tmail_ui_user/main/routes/router_arguments.dart'; - -class SaveToDraftArguments extends RouterArguments { - final Session session; - final AccountId accountId; - final Email newEmail; - final EmailId? oldEmailId; - - SaveToDraftArguments({ - required this.session, - required this.accountId, - required this.newEmail, - required this.oldEmailId - }); - - @override - List get props => [ - session, - accountId, - newEmail, - oldEmailId, - ]; -} diff --git a/lib/features/composer/presentation/widgets/web/saving_message_to_drafts_dialog_view.dart b/lib/features/composer/presentation/widgets/web/saving_message_to_drafts_dialog_view.dart deleted file mode 100644 index ea8db6d62a..0000000000 --- a/lib/features/composer/presentation/widgets/web/saving_message_to_drafts_dialog_view.dart +++ /dev/null @@ -1,208 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:core/presentation/extensions/capitalize_extension.dart'; -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/state/failure.dart'; -import 'package:core/presentation/state/success.dart'; -import 'package:core/utils/app_logger.dart'; -import 'package:dartz/dartz.dart' as dartz; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -import 'package:tmail_ui_user/main/routes/route_navigation.dart'; - -class SavingMessageToDraftsDialogView extends StatefulWidget { - - final CreateEmailRequest createEmailRequest; - final CreateNewAndSendEmailInteractor createNewAndSendEmailInteractor; - - const SavingMessageToDraftsDialogView({ - super.key, - required this.createEmailRequest, - required this.createNewAndSendEmailInteractor, - }); - - @override - State createState() => _SavingMessageToDraftsDialogViewState(); -} - -class _SavingMessageToDraftsDialogViewState extends State { - - StreamSubscription? _sendingStreamSubscription; - final ValueNotifier?> _sendingNotifier = ValueNotifier(null); - - @override - void initState() { - super.initState(); - _sendingStreamSubscription = widget.createNewAndSendEmailInteractor - .execute(widget.createEmailRequest) - .listen( - _handleDataStream, - onError: _handleErrorStream - ); - } - - void _handleDataStream(dartz.Either newState) { - _sendingNotifier.value = newState; - - newState.fold( - (failure) { - if (failure is SendEmailFailure || failure is GenerateEmailFailure) { - popBack(result: failure); - } - }, - (success) { - if (success is SendEmailSuccess) { - popBack(result: success); - } - } - ); - } - - void _handleErrorStream(Object error, StackTrace stackTrace) { - logError('_SendingMessageDialogViewState::_handleErrorStream: Exception = $error'); - popBack(result: SendEmailFailure(exception: error)); - } - - @override - Widget build(BuildContext context) { - return Dialog( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(12))), - insetPadding: const EdgeInsets.symmetric( - horizontal: 24.0, - vertical: 16.0 - ), - alignment: Alignment.center, - child: Container( - decoration: const BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(12)), - color: Colors.white, - ), - width: min(context.width, 400), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: double.infinity, - clipBehavior: Clip.antiAlias, - padding: const EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 12), - decoration: const BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(12)), - color: AppColor.colorItemSelected, - ), - alignment: Alignment.center, - child: Text( - AppLocalizations.of(context).sendingMessage.capitalizeFirstEach, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 17 - ), - ), - ), - const Divider(), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 12, bottom: 4), - child: Row( - children: [ - Text( - '${AppLocalizations.of(context).status}:', - style: Theme.of(context).textTheme.labelMedium?.copyWith( - color: Colors.black, - fontWeight: FontWeight.w500, - fontSize: 14 - ), - ), - const SizedBox(width: 8), - Expanded( - child: ValueListenableBuilder( - valueListenable: _sendingNotifier, - builder: (context, value, child) { - if (value == null) { - return child!; - } - - return value.fold( - (failure) => child!, - (success) { - if (success is GenerateEmailLoading) { - return Text( - 'Creating email...', - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: AppColor.labelColor, - fontSize: 14 - ), - ); - } else if (success is SendEmailLoading) { - return Text( - '${AppLocalizations.of(context).sendingMessage}...', - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: AppColor.labelColor, - fontSize: 14 - ), - ); - } else { - return child!; - } - } - ); - }, - child: Text( - '${AppLocalizations.of(context).sendingMessage}...', - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: AppColor.labelColor, - fontSize: 14 - ), - ), - ), - ) - ], - ), - ), - Padding( - padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 4, bottom: 24), - child: Row( - children: [ - Text( - '${AppLocalizations.of(context).progress}:', - style: Theme.of(context).textTheme.labelMedium?.copyWith( - color: Colors.black, - fontWeight: FontWeight.w500, - fontSize: 14 - ), - ), - const SizedBox(width: 8), - Expanded( - child: LinearProgressIndicator( - color: Colors.white.withOpacity(0.6), - backgroundColor: AppColor.primaryColor, - borderRadius: const BorderRadius.all(Radius.circular(12)), - ), - ) - ], - ), - ) - ], - ) - ], - ), - ), - ); - } - - @override - void dispose() { - _sendingStreamSubscription?.cancel(); - _sendingNotifier.dispose(); - super.dispose(); - } -} diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index 0c33fba6ed..0982261372 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -8,9 +8,7 @@ import 'package:tmail_ui_user/features/base/base_bindings.dart'; import 'package:tmail_ui_user/features/caching/clients/recent_search_cache_client.dart'; import 'package:tmail_ui_user/features/composer/data/repository/contact_repository_impl.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/contact_repository.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_as_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/update_email_drafts_interactor.dart'; import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource/print_file_datasource.dart'; @@ -170,8 +168,6 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), - Get.find(), - Get.find(), Get.find(), Get.find(), Get.find(), @@ -328,14 +324,6 @@ class MailboxDashBoardBindings extends BaseBindings { )); SendingQueueInteractorBindings().dependencies(); Get.lazyPut(() => StoreSessionInteractor(Get.find())); - Get.lazyPut(() => SaveEmailAsDraftsInteractor( - Get.find(), - Get.find() - )); - Get.lazyPut(() => UpdateEmailDraftsInteractor( - Get.find(), - Get.find() - )); Get.lazyPut(() => UnsubscribeEmailInteractor(Get.find())); Get.lazyPut(() => RestoredDeletedMessageInteractor( Get.find(), diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 76b385f635..cdf17317e8 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -35,13 +35,10 @@ import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_draft 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/usecases/get_autocomplete_interactor.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_as_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/update_email_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_bindings.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/compose_action_mode.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/save_to_draft_arguments.dart'; import 'package:tmail_ui_user/features/contact/presentation/model/contact_arguments.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; @@ -179,8 +176,6 @@ class MailboxDashBoardController extends ReloadableController { final GetAllSendingEmailInteractor _getAllSendingEmailInteractor; final StoreSessionInteractor _storeSessionInteractor; final EmptySpamFolderInteractor _emptySpamFolderInteractor; - final SaveEmailAsDraftsInteractor _saveEmailAsDraftsInteractor; - final UpdateEmailDraftsInteractor _updateEmailDraftsInteractor; final DeleteSendingEmailInteractor _deleteSendingEmailInteractor; final UnsubscribeEmailInteractor _unsubscribeEmailInteractor; final RestoredDeletedMessageInteractor _restoreDeletedMessageInteractor; @@ -258,8 +253,6 @@ class MailboxDashBoardController extends ReloadableController { this._getAllSendingEmailInteractor, this._storeSessionInteractor, this._emptySpamFolderInteractor, - this._saveEmailAsDraftsInteractor, - this._updateEmailDraftsInteractor, this._deleteSendingEmailInteractor, this._unsubscribeEmailInteractor, this._restoreDeletedMessageInteractor, @@ -1258,8 +1251,6 @@ class MailboxDashBoardController extends ReloadableController { composerOverlayState.value = ComposerOverlayState.inActive; if (result is SendingEmailArguments) { handleSendEmailAction(result); - } else if (result is SaveToDraftArguments) { - saveEmailToDraft(arguments: result); } else if (result is SendEmailSuccess || result is SaveEmailAsDraftsSuccess || result is UpdateEmailDraftsSuccess) { @@ -1395,8 +1386,6 @@ class MailboxDashBoardController extends ReloadableController { final result = await push(AppRoutes.composer, arguments: arguments); if (result is SendingEmailArguments) { handleSendEmailAction(result); - } else if (result is SaveToDraftArguments) { - saveEmailToDraft(arguments: result); } else if (result is SendEmailSuccess || result is SaveEmailAsDraftsSuccess || result is UpdateEmailDraftsSuccess) { @@ -2159,27 +2148,6 @@ class MailboxDashBoardController extends ReloadableController { attachmentDraggableAppState.value = DraggableAppState.inActive; } - void saveEmailToDraft({required SaveToDraftArguments arguments}) { - if (arguments.oldEmailId != null) { - consumeState( - _updateEmailDraftsInteractor.execute( - arguments.session, - arguments.accountId, - arguments.newEmail, - arguments.oldEmailId! - ) - ); - } else { - consumeState( - _saveEmailAsDraftsInteractor.execute( - arguments.session, - arguments.accountId, - arguments.newEmail, - ) - ); - } - } - void _handleSendEmailSuccess(SendEmailSuccess success) { if (PlatformInfo.isMobile && success.emailRequest.storedSendingId != null && From ddb924ef30eb63dd2c634319b78ffcdd6617d3c1 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 6 Mar 2024 03:42:45 +0700 Subject: [PATCH 17/80] TF-2667 Handle save message to cache when reload page --- .../dialog/confirmation_dialog_builder.dart | 9 +- .../mixin/message_dialog_action_mixin.dart | 2 +- ...save_composer_cache_on_web_interactor.dart | 27 +++ .../presentation/composer_bindings.dart | 7 +- .../presentation/composer_controller.dart | 199 ++++-------------- .../controller/base_rich_text_controller.dart | 33 --- ...move_composer_cache_on_web_interactor.dart | 2 +- ...save_composer_cache_on_web_interactor.dart | 20 -- .../bindings/mailbox_dashboard_bindings.dart | 2 - 9 files changed, 72 insertions(+), 229 deletions(-) create mode 100644 lib/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart delete mode 100644 lib/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.dart diff --git a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart index 054246ff63..72b63ececf 100644 --- a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart +++ b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart @@ -22,7 +22,6 @@ class ConfirmDialogBuilder { TextStyle? _styleTitle; TextStyle? _styleContent; double? _radiusButton; - double? heightButton; EdgeInsetsGeometry? _paddingTitle; EdgeInsets? _paddingContent; EdgeInsets? _paddingButton; @@ -46,7 +45,6 @@ class ConfirmDialogBuilder { { this.showAsBottomSheet = false, this.listTextSpan, - this.heightButton, this.maxWith = double.infinity, } ); @@ -240,7 +238,6 @@ class ConfirmDialogBuilder { name: _cancelText, bgColor: _colorCancelButton, radius: _radiusButton, - height: heightButton, textStyle: _styleTextCancelButton, action: _onCancelButtonAction)), if (_confirmText.isNotEmpty && _cancelText.isNotEmpty) const SizedBox(width: 16), @@ -249,7 +246,6 @@ class ConfirmDialogBuilder { name: _confirmText, bgColor: _colorConfirmButton, radius: _radiusButton, - height: heightButton, textStyle: _styleTextConfirmButton, action: _onConfirmButtonAction)) ] @@ -264,12 +260,10 @@ class ConfirmDialogBuilder { TextStyle? textStyle, Color? bgColor, double? radius, - double? height, Function? action }) { return SizedBox( width: double.infinity, - height: height ?? 48, child: ElevatedButton( onPressed: () => action?.call(), style: ButtonStyle( @@ -281,8 +275,7 @@ class ConfirmDialogBuilder { borderRadius: BorderRadius.circular(radius ?? 8), side: BorderSide(width: 0, color: bgColor ?? AppColor.colorTextButton), )), - padding: MaterialStateProperty.resolveWith( - (Set states) => const EdgeInsets.symmetric(horizontal: 16)), + padding: MaterialStateProperty.resolveWith((Set states) => const EdgeInsets.all(8)), elevation: MaterialStateProperty.resolveWith((Set states) => 0)), child: Text(name ?? '', textAlign: TextAlign.center, diff --git a/lib/features/base/mixin/message_dialog_action_mixin.dart b/lib/features/base/mixin/message_dialog_action_mixin.dart index bb4f098417..0e542d15fc 100644 --- a/lib/features/base/mixin/message_dialog_action_mixin.dart +++ b/lib/features/base/mixin/message_dialog_action_mixin.dart @@ -38,7 +38,7 @@ mixin MessageDialogActionMixin { if (alignCenter) { return await Get.dialog( PointerInterceptor( - child: (ConfirmDialogBuilder(imagePaths, listTextSpan: listTextSpan, heightButton: 44) + child: (ConfirmDialogBuilder(imagePaths, listTextSpan: listTextSpan) ..key(const Key('confirm_dialog_action')) ..title(title ?? '') ..content(message) diff --git a/lib/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart b/lib/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart new file mode 100644 index 0000000000..cbb6c924d1 --- /dev/null +++ b/lib/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart @@ -0,0 +1,27 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/save_composer_cache_state.dart'; + +class SaveComposerCacheOnWebInteractor { + final ComposerCacheRepository _composerCacheRepository; + final ComposerRepository _composerRepository; + + SaveComposerCacheOnWebInteractor( + this._composerCacheRepository, + this._composerRepository, + ); + + Future> execute(CreateEmailRequest createEmailRequest) async { + try { + final emailCreated = await _composerRepository.generateEmail(createEmailRequest); + _composerCacheRepository.saveComposerCacheOnWeb(emailCreated); + return Right(SaveComposerCacheSuccess()); + } catch (exception) { + return Left(SaveComposerCacheFailure(exception)); + } + } +} diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index 2c56aff05b..65d5f5b900 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -13,6 +13,7 @@ import 'package:tmail_ui_user/features/composer/domain/repository/contact_reposi import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/download_image_as_base64_interactor.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachment_interactor.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart'; @@ -43,7 +44,6 @@ import 'package:tmail_ui_user/features/mailbox/data/repository/mailbox_repositor import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.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/profiles/identities/identity_interactors_bindings.dart'; import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_manager.dart'; @@ -191,7 +191,10 @@ class ComposerBindings extends BaseBindings { Get.lazyPut(() => UploadAttachmentInteractor(Get.find())); Get.lazyPut(() => GetEmailContentInteractor(Get.find())); Get.lazyPut(() => RemoveComposerCacheOnWebInteractor(Get.find())); - Get.lazyPut(() => SaveComposerCacheOnWebInteractor(Get.find())); + Get.lazyPut(() => SaveComposerCacheOnWebInteractor( + Get.find(), + Get.find(), + )); Get.lazyPut(() => DownloadImageAsBase64Interactor(Get.find())); Get.lazyPut(() => TransformHtmlEmailContentInteractor(Get.find())); Get.lazyPut(() => GetAlwaysReadReceiptSettingInteractor(Get.find())); diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index d2eb6ef268..4ca93a9e9e 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -16,15 +16,9 @@ import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:html_editor_enhanced/html_editor.dart' as web_html_editor; -import 'package:http_parser/http_parser.dart'; -import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email_body_value.dart'; -import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; -import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -49,6 +43,7 @@ import 'package:tmail_ui_user/features/composer/domain/usecases/download_image_a import 'package:tmail_ui_user/features/composer/domain/usecases/get_all_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_device_contact_suggestions_interactor.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/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/email_action_type_extension.dart'; @@ -72,7 +67,6 @@ import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_i import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_composer_cache_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/model/draggable_app_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; @@ -343,14 +337,43 @@ class ComposerController extends BaseController with DragDropFileMixin { } void _listenBrowserTabRefresh() { - _subscriptionOnBeforeUnload = html.window.onBeforeUnload.listen((event) async { - _removeComposerCacheOnWebInteractor.execute(); - if (mailboxDashBoardController.sessionCurrent != null) { - final draftEmail = await _generateEmail( - currentContext!, - mailboxDashBoardController.sessionCurrent!.username); - _saveComposerCacheOnWebInteractor.execute(draftEmail); + _subscriptionOnBeforeUnload = html.window.onBeforeUnload.listen((event) async { + await _removeComposerCacheOnWebInteractor.execute(); + + if (composerArguments.value == null || + mailboxDashBoardController.sessionCurrent == null || + mailboxDashBoardController.accountId.value == null + ) { + log('ComposerController::_listenBrowserTabRefresh: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); + return; } + + final emailContent = await _getContentInEditor(); + + await _saveComposerCacheOnWebInteractor.execute(CreateEmailRequest( + session: mailboxDashBoardController.sessionCurrent!, + accountId: mailboxDashBoardController.accountId.value!, + emailActionType: composerArguments.value!.emailActionType, + subject: subjectEmail.value ?? '', + emailContent: emailContent, + fromSender: composerArguments.value!.presentationEmail?.from ?? {}, + toRecipients: listToEmailAddress.toSet(), + ccRecipients: listCcEmailAddress.toSet(), + bccRecipients: listBccEmailAddress.toSet(), + isRequestReadReceipt: hasRequestReadReceipt.value, + identity: identitySelected.value, + attachments: uploadController.attachmentsUploaded, + inlineAttachments: uploadController.mapInlineAttachments, + outboxMailboxId: mailboxDashBoardController.outboxMailbox?.mailboxId, + sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], + draftsMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts], + draftsEmailId: _getDraftEmailId(), + answerForwardEmailId: composerArguments.value!.presentationEmail?.id, + unsubscribeEmailId: composerArguments.value!.previousEmailId, + messageId: composerArguments.value!.messageId, + references: composerArguments.value!.references, + emailSendingQueue: composerArguments.value!.sendingEmail + )); }); _subscriptionOnDragEnter = html.window.onDragEnter.listen((event) { @@ -666,128 +689,6 @@ class ComposerController extends BaseController with DragDropFileMixin { } } - Future _generateEmail( - BuildContext context, - UserName userName, - { - bool asDrafts = false, - MailboxId? draftMailboxId, - MailboxId? outboxMailboxId, - ComposerArguments? arguments, - } - ) async { - Set listFromEmailAddress = {EmailAddress(null, userName.value)}; - if (identitySelected.value?.email?.isNotEmpty == true) { - listFromEmailAddress = { - EmailAddress( - identitySelected.value?.name, - identitySelected.value?.email - ) - }; - } - Set listReplyToEmailAddress = {EmailAddress(null, userName.value)}; - if (identitySelected.value?.replyTo?.isNotEmpty == true) { - listReplyToEmailAddress = identitySelected.value!.replyTo!; - } - - final attachments = {}; - attachments.addAll(uploadController.generateAttachments() ?? []); - - var emailBodyText = await _getEmailBodyText(context, asDrafts: asDrafts); - if (uploadController.mapInlineAttachments.isNotEmpty) { - final mapContents = await _getMapContent(emailBodyText); - emailBodyText = mapContents.value1; - final listInlineAttachment = mapContents.value2; - final listInlineEmailBodyPart = listInlineAttachment - .map((attachment) => attachment.toEmailBodyPart(charset: 'base64')) - .toSet(); - attachments.addAll(listInlineEmailBodyPart); - } - - final userAgent = await applicationManager.getUserAgent(); - log('ComposerController::_generateEmail(): userAgent: $userAgent'); - - Map mailboxIds = {}; - if (asDrafts && draftMailboxId != null) { - mailboxIds[draftMailboxId] = true; - } - if (outboxMailboxId != null) { - mailboxIds[outboxMailboxId] = true; - } - - Map? mapKeywords = {}; - if (asDrafts) { - mapKeywords[KeyWordIdentifier.emailDraft] = true; - mapKeywords[KeyWordIdentifier.emailSeen] = true; - } - - final inReplyTo = _generateInReplyTo(arguments); - final references = _generateReferences(arguments); - - final generatePartId = PartId(uuid.v1()); - - return Email( - mailboxIds: mailboxIds.isNotEmpty ? mailboxIds : null, - from: listFromEmailAddress, - to: listToEmailAddress.toSet(), - cc: listCcEmailAddress.toSet(), - bcc: listBccEmailAddress.toSet(), - replyTo: listReplyToEmailAddress, - inReplyTo: inReplyTo, - references: references, - keywords: mapKeywords.isNotEmpty ? mapKeywords : null, - subject: subjectEmail.value, - htmlBody: { - EmailBodyPart( - partId: generatePartId, - type: MediaType.parse('text/html') - )}, - bodyValues: { - generatePartId: EmailBodyValue(emailBodyText, false, false) - }, - attachments: attachments.isNotEmpty ? attachments : null, - headerMdn: hasRequestReadReceipt.value ? { IndividualHeaderIdentifier.headerMdn: getEmailAddressSender() } : {}, - ); - } - - MessageIdsHeaderValue? _generateInReplyTo(ComposerArguments? arguments) { - if (arguments?.emailActionType == EmailActionType.reply || - arguments?.emailActionType == EmailActionType.replyAll) { - return arguments?.messageId; - } - return null; - } - - MessageIdsHeaderValue? _generateReferences(ComposerArguments? arguments) { - if (arguments?.emailActionType == EmailActionType.reply || - arguments?.emailActionType == EmailActionType.replyAll || - arguments?.emailActionType == EmailActionType.forward) { - Set ids = {}; - if (arguments?.messageId?.ids.isNotEmpty == true) { - ids.addAll(arguments!.messageId!.ids); - } - if (arguments?.references?.ids.isNotEmpty == true) { - ids.addAll(arguments!.references!.ids); - } - if (ids.isNotEmpty) { - return MessageIdsHeaderValue(ids); - } - } - return null; - } - - Future>> _getMapContent(String emailBodyText) async { - if (kIsWeb) { - return await richTextWebController.refactorContentHasInlineImage( - emailBodyText, - uploadController.mapInlineAttachments); - } else { - return await richTextMobileTabletController.refactorContentHasInlineImage( - emailBodyText, - uploadController.mapInlineAttachments); - } - } - void handleClickSendButton(BuildContext context) async { if (_sendButtonState == ButtonState.disabled) { log('ComposerController::handleClickSendButton: SENDING EMAIL'); @@ -1897,32 +1798,6 @@ class ComposerController extends BaseController with DragDropFileMixin { void setSubjectEmail(String subject) => subjectEmail.value = subject; - Future _getEmailBodyText(BuildContext context, {bool asDrafts = false}) async { - var contentHtml = ''; - - if (PlatformInfo.isWeb) { - if (responsiveUtils.isDesktop(context) && - screenDisplayMode.value == ScreenDisplayMode.minimize) { - contentHtml = _textEditorWeb ?? ''; - } else { - if (asDrafts) { - contentHtml = await richTextWebController.editorController.getText(); - } else { - contentHtml = await richTextWebController.editorController.getTextWithSignatureContent(); - } - } - } else { - if (asDrafts) { - contentHtml = (await htmlEditorApi?.getText()) ?? ''; - } else { - contentHtml = (await htmlEditorApi?.getTextWithSignatureContent()) ?? ''; - } - } - - final newContentHtml = contentHtml.removeEditorStartTag(); - return newContentHtml; - } - void removeDraggableEmailAddress(DraggableEmailAddress draggableEmailAddress) { log('ComposerController::removeDraggableEmailAddress: $draggableEmailAddress'); switch(draggableEmailAddress.prefix) { diff --git a/lib/features/composer/presentation/controller/base_rich_text_controller.dart b/lib/features/composer/presentation/controller/base_rich_text_controller.dart index 262dbd5441..52d3f8c469 100644 --- a/lib/features/composer/presentation/controller/base_rich_text_controller.dart +++ b/lib/features/composer/presentation/controller/base_rich_text_controller.dart @@ -1,12 +1,7 @@ -import 'package:collection/collection.dart'; import 'package:core/presentation/views/dialog/color_picker_dialog_builder.dart'; -import 'package:core/utils/app_logger.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:html/parser.dart' show parse; -import 'package:model/email/attachment.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; @@ -37,32 +32,4 @@ abstract class BaseRichTextController extends GetxController { } ).show(); } - - Future>> refactorContentHasInlineImage( - String emailContent, - Map mapInlineAttachments - ) async { - final document = parse(emailContent); - final listImgTag = document.querySelectorAll('img[src^="data:image/"][id^="cid:"]'); - final listInlineAttachment = await Future.wait(listImgTag.map((imgTag) async { - final idImg = imgTag.attributes['id']; - final cid = idImg!.replaceFirst('cid:', '').trim(); - log('BaseRichTextController::refactorContentHasInlineImage(): $cid'); - imgTag.attributes['src'] = 'cid:$cid'; - imgTag.attributes.remove('id'); - return cid; - })).then((listCid) { - log('BaseRichTextController::refactorContentHasInlineImage(): $listCid'); - final listInlineAttachment = listCid - .whereNotNull() - .map((cid) => mapInlineAttachments[cid]) - .whereNotNull() - .toList(); - return listInlineAttachment; - }); - final newContent = document.body?.innerHtml ?? emailContent; - log('BaseRichTextController::refactorContentHasInlineImage(): $newContent'); - log('BaseRichTextController::refactorContentHasInlineImage(): listInlineAttachment: $listInlineAttachment'); - return Tuple2(newContent, listInlineAttachment); - } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart b/lib/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart index 6674a50292..5857b4eb46 100644 --- a/lib/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart +++ b/lib/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart @@ -8,7 +8,7 @@ class RemoveComposerCacheOnWebInteractor { RemoveComposerCacheOnWebInteractor(this.composerCacheRepository); - Either execute() { + Future> execute() async { try { composerCacheRepository.removeComposerCacheOnWeb(); return Right(RemoveComposerCacheSuccess()); diff --git a/lib/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.dart b/lib/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.dart deleted file mode 100644 index e08c19eef5..0000000000 --- a/lib/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:core/core.dart'; -import 'package:dartz/dartz.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/save_composer_cache_state.dart'; - -class SaveComposerCacheOnWebInteractor { - final ComposerCacheRepository composerCacheRepository; - - SaveComposerCacheOnWebInteractor(this.composerCacheRepository); - - Either execute(Email email) { - try { - composerCacheRepository.saveComposerCacheOnWeb(email); - return Right(SaveComposerCacheSuccess()); - } catch (exception) { - return Left(SaveComposerCacheFailure(exception)); - } - } -} diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index 0982261372..26ee066fa1 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -69,7 +69,6 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_unr import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/quick_search_email_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_recent_search_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/store_last_time_dismissed_spam_reported_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/store_spam_report_state_interactor.dart'; @@ -276,7 +275,6 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find()) ); Get.lazyPut(() => GetComposerCacheOnWebInteractor(Get.find())); - Get.lazyPut(() => SaveComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => RemoveComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => MarkAsEmailReadInteractor( Get.find(), From 3a9d5ba8fab6af1554ee6055f0208ff0797abce1 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 12 Mar 2024 12:53:41 +0700 Subject: [PATCH 18/80] TF-2667 Add cancel button in progress send message dialog --- .../exceptions/compose_email_exception.dart | 1 + .../domain/state/send_email_state.dart | 4 +- .../create_new_and_send_email_interactor.dart | 44 +++++++++++----- .../presentation/composer_controller.dart | 26 ++++++++-- .../widgets/sending_message_dialog_view.dart | 50 +++++++++++++++++-- .../data/datasource/email_datasource.dart | 12 ++++- .../email_datasource_impl.dart | 27 ++++++++-- .../email_hive_cache_datasource_impl.dart | 17 ++++++- .../email/data/network/email_api.dart | 16 ++++-- .../repository/email_repository_impl.dart | 27 ++++++++-- .../domain/repository/email_repository.dart | 12 ++++- .../presentation/thread_controller.dart | 4 +- lib/l10n/intl_messages.arb | 8 ++- lib/main/exceptions/remote_exception.dart | 4 +- .../exceptions/remote_exception_thrower.dart | 2 +- lib/main/localizations/app_localizations.dart | 7 +++ 16 files changed, 215 insertions(+), 46 deletions(-) create mode 100644 lib/features/composer/domain/exceptions/compose_email_exception.dart diff --git a/lib/features/composer/domain/exceptions/compose_email_exception.dart b/lib/features/composer/domain/exceptions/compose_email_exception.dart new file mode 100644 index 0000000000..f1116fe421 --- /dev/null +++ b/lib/features/composer/domain/exceptions/compose_email_exception.dart @@ -0,0 +1 @@ +class SendingEmailCanceledException implements Exception {} \ No newline at end of file diff --git a/lib/features/composer/domain/state/send_email_state.dart b/lib/features/composer/domain/state/send_email_state.dart index 01312e80ff..814e6c3cb6 100644 --- a/lib/features/composer/domain/state/send_email_state.dart +++ b/lib/features/composer/domain/state/send_email_state.dart @@ -53,4 +53,6 @@ class SendEmailFailure extends FeatureFailure { mailboxRequest, sendingEmailActionType, ]; -} \ No newline at end of file +} + +class CancelSendingEmail extends LoadingState {} \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart b/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart index 2c21547d97..cbff76b4db 100644 --- a/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart +++ b/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart @@ -2,10 +2,12 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart' as dartz; +import 'package:dio/dio.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/composer/domain/exceptions/compose_email_exception.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; @@ -15,6 +17,7 @@ import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions. import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; class CreateNewAndSendEmailInteractor { final EmailRepository _emailRepository; @@ -27,7 +30,10 @@ class CreateNewAndSendEmailInteractor { this._composerRepository, ); - Stream> execute(CreateEmailRequest createEmailRequest) async* { + Stream> execute({ + required CreateEmailRequest createEmailRequest, + CancelToken? cancelToken + }) async* { SendingEmailArguments? sendingEmailArguments; try { yield dartz.Right(GenerateEmailLoading()); @@ -46,14 +52,16 @@ class CreateNewAndSendEmailInteractor { sendingEmailArguments.session, sendingEmailArguments.accountId, sendingEmailArguments.emailRequest, - mailboxRequest: sendingEmailArguments.mailboxRequest + mailboxRequest: sendingEmailArguments.mailboxRequest, + cancelToken: cancelToken ); if (sendingEmailArguments.emailRequest.emailIdDestroyed != null) { await _deleteOldDraftsEmail( session: sendingEmailArguments.session, accountId: sendingEmailArguments.accountId, - draftEmailId: sendingEmailArguments.emailRequest.emailIdDestroyed! + draftEmailId: sendingEmailArguments.emailRequest.emailIdDestroyed!, + cancelToken: cancelToken ); } @@ -69,13 +77,23 @@ class CreateNewAndSendEmailInteractor { } } catch (e) { logError('CreateNewAndSendEmailInteractor::execute: Exception: $e'); - yield dartz.Left(SendEmailFailure( - exception: e, - session: sendingEmailArguments?.session, - accountId: sendingEmailArguments?.accountId, - emailRequest: sendingEmailArguments?.emailRequest, - mailboxRequest: sendingEmailArguments?.mailboxRequest, - )); + if (e is UnknownError && e.message is List) { + yield dartz.Left(SendEmailFailure( + exception: SendingEmailCanceledException(), + session: sendingEmailArguments?.session, + accountId: sendingEmailArguments?.accountId, + emailRequest: sendingEmailArguments?.emailRequest, + mailboxRequest: sendingEmailArguments?.mailboxRequest, + )); + } else { + yield dartz.Left(SendEmailFailure( + exception: e, + session: sendingEmailArguments?.session, + accountId: sendingEmailArguments?.accountId, + emailRequest: sendingEmailArguments?.emailRequest, + mailboxRequest: sendingEmailArguments?.mailboxRequest, + )); + } } } @@ -113,13 +131,15 @@ class CreateNewAndSendEmailInteractor { Future _deleteOldDraftsEmail({ required Session session, required AccountId accountId, - required EmailId draftEmailId + required EmailId draftEmailId, + CancelToken? cancelToken }) async { try { await _emailRepository.deleteEmailPermanently( session, accountId, - draftEmailId + draftEmailId, + cancelToken: cancelToken ); } catch (e) { logError('CreateNewAndSendEmailInteractor::_deleteOldDraftsEmail: Exception: $e'); diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 4ca93a9e9e..d1cfb15ff3 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -7,6 +7,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:device_info_plus/device_info_plus.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'; @@ -29,6 +30,7 @@ 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/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/model/contact_suggestion_source.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'; @@ -802,12 +804,17 @@ class ComposerController extends BaseController with DragDropFileMixin { } final emailContent = await _getContentInEditor(); - - final resultState = await _showSendingMessageDialog(emailContent: emailContent); - + final cancelToken = CancelToken(); + final resultState = await _showSendingMessageDialog( + emailContent: emailContent, + cancelToken: cancelToken + ); + log('ComposerController::_handleSendMessages: resultState = $resultState'); if (resultState is SendEmailSuccess) { _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) && context.mounted) { _showConfirmDialogWhenSendMessageFailure( context: context, @@ -818,7 +825,10 @@ class ComposerController extends BaseController with DragDropFileMixin { } } - Future _showSendingMessageDialog({required String emailContent}) { + Future _showSendingMessageDialog({ + required String emailContent, + CancelToken? cancelToken + }) { return Get.dialog( PointerInterceptor( child: SendingMessageDialogView( @@ -847,13 +857,19 @@ class ComposerController extends BaseController with DragDropFileMixin { references: composerArguments.value!.references, emailSendingQueue: composerArguments.value!.sendingEmail ), - createNewAndSendEmailInteractor: _createNewAndSendEmailInteractor + createNewAndSendEmailInteractor: _createNewAndSendEmailInteractor, + onCancelSendingEmailAction: _handleCancelSendingMessage, + cancelToken: cancelToken, ), ), barrierColor: AppColor.colorDefaultCupertinoActionSheet, ); } + void _handleCancelSendingMessage({CancelToken? cancelToken}) { + cancelToken?.cancel([SendingEmailCanceledException()]); + } + void _showConfirmDialogWhenSendMessageFailure({ required BuildContext context, required FeatureFailure failure diff --git a/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart b/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart index d59cc9f33f..f74979398d 100644 --- a/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart +++ b/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart @@ -5,26 +5,36 @@ import 'package:core/presentation/extensions/capitalize_extension.dart'; import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart' as dartz; +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/composer/domain/exceptions/compose_email_exception.dart'; import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.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'; +typedef OnCancelSendingEmailAction = Function({CancelToken? cancelToken}); + class SendingMessageDialogView extends StatefulWidget { final CreateEmailRequest createEmailRequest; final CreateNewAndSendEmailInteractor createNewAndSendEmailInteractor; + final OnCancelSendingEmailAction? onCancelSendingEmailAction; + final CancelToken? cancelToken; const SendingMessageDialogView({ super.key, required this.createEmailRequest, required this.createNewAndSendEmailInteractor, + this.onCancelSendingEmailAction, + this.cancelToken, }); @override @@ -40,7 +50,10 @@ class _SendingMessageDialogViewState extends State { void initState() { super.initState(); _streamSubscription = widget.createNewAndSendEmailInteractor - .execute(widget.createEmailRequest) + .execute( + createEmailRequest: widget.createEmailRequest, + cancelToken: widget.cancelToken + ) .listen( _handleDataStream, onError: _handleErrorStream @@ -66,7 +79,11 @@ class _SendingMessageDialogViewState extends State { void _handleErrorStream(Object error, StackTrace stackTrace) { logError('_SendingMessageDialogViewState::_handleErrorStream: Exception = $error'); - popBack(result: SendEmailFailure(exception: error)); + if (error is UnknownError && error.message is List) { + popBack(result: SendEmailFailure(exception: SendingEmailCanceledException())); + } else { + popBack(result: SendEmailFailure(exception: error)); + } } @override @@ -142,6 +159,14 @@ class _SendingMessageDialogViewState extends State { fontSize: 14 ), ); + } else if (success is CancelSendingEmail) { + return Text( + '${AppLocalizations.of(context).canceling}...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ); } else { return Text( '${AppLocalizations.of(context).sendingMessage}...', @@ -167,7 +192,7 @@ class _SendingMessageDialogViewState extends State { ), ), Padding( - padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 4, bottom: 24), + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 4, bottom: 16), child: Row( children: [ Text( @@ -188,7 +213,24 @@ class _SendingMessageDialogViewState extends State { ) ], ), - ) + ), + if (widget.onCancelSendingEmailAction != null) + Align( + alignment: AlignmentDirectional.centerEnd, + child: TMailButtonWidget.fromText( + text: AppLocalizations.of(context).cancel, + textStyle: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.black87, + fontSize: 15 + ), + padding: const EdgeInsetsDirectional.symmetric(horizontal: 20, vertical: 8), + margin: const EdgeInsetsDirectional.only(start: 12, end: 12, bottom: 16), + onTapActionCallback: () { + _viewStateNotifier.value = dartz.Right(CancelSendingEmail()); + widget.onCancelSendingEmailAction!(cancelToken: widget.cancelToken); + }, + ), + ) ], ) ], diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index 9255b5fe2d..943448579b 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -32,7 +32,10 @@ abstract class EmailDataSource { Session session, AccountId accountId, EmailRequest emailRequest, - {CreateNewMailboxRequest? mailboxRequest} + { + CreateNewMailboxRequest? mailboxRequest, + CancelToken? cancelToken + } ); Future> markAsRead(Session session, AccountId accountId, List emails, ReadActions readActions); @@ -79,7 +82,12 @@ abstract class EmailDataSource { Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds); - Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId); + Future deleteEmailPermanently( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ); Future storeDetailedNewEmail(Session session, AccountId accountId, DetailedEmail detailedEmail); diff --git a/lib/features/email/data/datasource_impl/email_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_datasource_impl.dart index 36fa298aa0..f2dc682909 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -46,10 +46,19 @@ class EmailDataSourceImpl extends EmailDataSource { Session session, AccountId accountId, EmailRequest emailRequest, - {CreateNewMailboxRequest? mailboxRequest} + { + CreateNewMailboxRequest? mailboxRequest, + CancelToken? cancelToken, + } ) async { try { - return await emailAPI.sendEmail(session, accountId, emailRequest, mailboxRequest: mailboxRequest); + return await emailAPI.sendEmail( + session, + accountId, + emailRequest, + mailboxRequest: mailboxRequest, + cancelToken: cancelToken + ); } catch (error, stackTrace) { return await _sendEmailExceptionThrower.throwException(error, stackTrace); } @@ -157,9 +166,19 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId) { + Future deleteEmailPermanently( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) { return Future.sync(() async { - return await emailAPI.deleteEmailPermanently(session, accountId, emailId); + return await emailAPI.deleteEmailPermanently( + session, + accountId, + emailId, + cancelToken: cancelToken + ); }).catchError(_exceptionThrower.throwException); } diff --git a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart index ae2c3d25f7..b19e067472 100644 --- a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart @@ -71,7 +71,12 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { ); @override - Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId) { + Future deleteEmailPermanently( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) { throw UnimplementedError(); } @@ -134,7 +139,15 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { } @override - Future sendEmail(Session session, AccountId accountId, EmailRequest emailRequest, {CreateNewMailboxRequest? mailboxRequest}) { + Future sendEmail( + Session session, + AccountId accountId, + EmailRequest emailRequest, + { + CreateNewMailboxRequest? mailboxRequest, + CancelToken? cancelToken + } + ) { throw UnimplementedError(); } diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 55dadc49a0..85f6b9d388 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -117,7 +117,10 @@ class EmailAPI with HandleSetErrorMixin { Session session, AccountId accountId, EmailRequest emailRequest, - {CreateNewMailboxRequest? mailboxRequest} + { + CreateNewMailboxRequest? mailboxRequest, + CancelToken? cancelToken, + } ) async { final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); @@ -199,7 +202,7 @@ class EmailAPI with HandleSetErrorMixin { final response = await (requestBuilder ..usings(capabilities)) .build() - .execute(); + .execute(cancelToken: cancelToken); final setEmailResponse = response.parse( setEmailInvocation.methodCallId, @@ -606,7 +609,12 @@ class EmailAPI with HandleSetErrorMixin { return List.empty(); } - Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId) async { + Future deleteEmailPermanently( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) async { final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); final setEmailMethod = SetEmailMethod(accountId) ..addDestroy({emailId.id}); @@ -619,7 +627,7 @@ class EmailAPI with HandleSetErrorMixin { final response = await (requestBuilder ..usings(capabilities)) .build() - .execute(); + .execute(cancelToken: cancelToken); final setEmailResponse = response.parse( setEmailInvocation.methodCallId, diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index 422682ca20..9e1f000420 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -58,9 +58,18 @@ class EmailRepositoryImpl extends EmailRepository { Session session, AccountId accountId, EmailRequest emailRequest, - {CreateNewMailboxRequest? mailboxRequest} + { + CreateNewMailboxRequest? mailboxRequest, + CancelToken? cancelToken + } ) { - return emailDataSource[DataSourceType.network]!.sendEmail(session, accountId, emailRequest, mailboxRequest: mailboxRequest); + return emailDataSource[DataSourceType.network]!.sendEmail( + session, + accountId, + emailRequest, + mailboxRequest: mailboxRequest, + cancelToken: cancelToken, + ); } @override @@ -172,8 +181,18 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId) { - return emailDataSource[DataSourceType.network]!.deleteEmailPermanently(session, accountId, emailId); + Future deleteEmailPermanently( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) { + return emailDataSource[DataSourceType.network]!.deleteEmailPermanently( + session, + accountId, + emailId, + cancelToken: cancelToken + ); } @override diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart index a2906e0afa..533b82960d 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -34,7 +34,10 @@ abstract class EmailRepository { Session session, AccountId accountId, EmailRequest emailRequest, - {CreateNewMailboxRequest? mailboxRequest} + { + CreateNewMailboxRequest? mailboxRequest, + CancelToken? cancelToken + } ); Future> markAsRead(Session session, AccountId accountId, List emails, ReadActions readActions); @@ -87,7 +90,12 @@ abstract class EmailRepository { Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds); - Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId); + Future deleteEmailPermanently( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ); Future getEmailState(Session session, AccountId accountId); diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 32edf5aefd..a5861163e3 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -415,10 +415,10 @@ class ThreadController extends BaseController with EmailActionController { } _getAllEmailAction(); } else if (error is MethodLevelErrors) { - if (currentOverlayContext != null && error.message?.isNotEmpty == true) { + if (currentOverlayContext != null && error.message != null) { appToast.showToastErrorMessage( currentOverlayContext!, - error.message! + error.message?.toString() ?? '' ); } clearState(); diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 6d2997da9f..e71195ee25 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-03-06T02:46:19.947772", + "@@last_modified": "2024-03-12T12:50:35.525381", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3845,5 +3845,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "canceling": "Canceling", + "@canceling": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/exceptions/remote_exception.dart b/lib/main/exceptions/remote_exception.dart index 69a8732f8b..235be7a938 100644 --- a/lib/main/exceptions/remote_exception.dart +++ b/lib/main/exceptions/remote_exception.dart @@ -11,7 +11,7 @@ abstract class RemoteException with EquatableMixin implements Exception { static const badCredentials = 'Bad credentials'; static const socketException = 'Socket exception'; - final String? message; + final Object? message; final int? code; const RemoteException({this.code, this.message}); @@ -25,7 +25,7 @@ class BadCredentialsException extends RemoteException { } class UnknownError extends RemoteException { - const UnknownError({int? code, String? message}) : super(code: code, message: message); + const UnknownError({int? code, Object? message}) : super(code: code, message: message); @override List get props => [code, message]; diff --git a/lib/main/exceptions/remote_exception_thrower.dart b/lib/main/exceptions/remote_exception_thrower.dart index 1a37115aa7..885133b92b 100644 --- a/lib/main/exceptions/remote_exception_thrower.dart +++ b/lib/main/exceptions/remote_exception_thrower.dart @@ -53,7 +53,7 @@ class RemoteExceptionThrower extends ExceptionThrower { if (error.error is SocketException) { throw const SocketError(); } else if (error.error != null) { - throw UnknownError(message: error.error!.toString()); + throw UnknownError(message: error.error); } else { throw const UnknownError(); } diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 222f04fdba..73c17632b3 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4016,4 +4016,11 @@ class AppLocalizations { 'Saving of the message to drafts folder failed.\nAn error occurred while saving mail.', name: 'warningMessageWhenSaveEmailToDraftsFailure'); } + + String get canceling { + return Intl.message( + 'Canceling', + name: 'canceling' + ); + } } \ No newline at end of file From 21f3fcf2909c5e8ebee7b1b2d5e4376e20137c99 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 12 Mar 2024 13:43:14 +0700 Subject: [PATCH 19/80] TF-2667 Add cancel button in progress saving message to drafts dialog --- core/lib/utils/application_manager.dart | 20 +++-- .../exceptions/compose_email_exception.dart | 4 +- .../state/save_email_as_drafts_state.dart | 4 +- ...w_and_save_email_to_drafts_interactor.dart | 37 ++++++--- .../presentation/composer_controller.dart | 25 +++++- .../widgets/saving_message_dialog_view.dart | 76 ++++++++++++++----- .../widgets/sending_message_dialog_view.dart | 42 +++++----- .../data/datasource/email_datasource.dart | 22 +++++- .../email_datasource_impl.dart | 44 +++++++++-- .../email_hive_cache_datasource_impl.dart | 22 +++++- .../email/data/network/email_api.dart | 23 ++++-- .../repository/email_repository_impl.dart | 44 +++++++++-- .../domain/repository/email_repository.dart | 22 +++++- .../mailbox_dashboard_controller.dart | 6 +- 14 files changed, 297 insertions(+), 94 deletions(-) diff --git a/core/lib/utils/application_manager.dart b/core/lib/utils/application_manager.dart index b452f6670f..5f9c944e7c 100644 --- a/core/lib/utils/application_manager.dart +++ b/core/lib/utils/application_manager.dart @@ -25,14 +25,12 @@ class ApplicationManager { Future getUserAgent() async { try { - String userAgent; + String userAgent = ''; if (PlatformInfo.isWeb) { final webBrowserInfo = await _deviceInfoPlugin.webBrowserInfo; userAgent = webBrowserInfo.userAgent ?? ''; - } else { - await FkUserAgent.init(); + } else if (PlatformInfo.isMobile) { userAgent = FkUserAgent.userAgent ?? ''; - FkUserAgent.release(); } log('ApplicationManager::getUserAgent: $userAgent'); return userAgent; @@ -42,9 +40,21 @@ class ApplicationManager { } } + Future initUserAgent() async { + if (PlatformInfo.isMobile) { + await FkUserAgent.init(); + } + } + + Future releaseUserAgent() async { + if (PlatformInfo.isMobile) { + FkUserAgent.release(); + } + } + Future generateApplicationUserAgent() async { final userAgent = await getUserAgent(); final version = await getVersion(); - return 'Team-Mail/$version $userAgent'; + return 'Twake-Mail/$version $userAgent'; } } \ No newline at end of file diff --git a/lib/features/composer/domain/exceptions/compose_email_exception.dart b/lib/features/composer/domain/exceptions/compose_email_exception.dart index f1116fe421..b0d29ed9d2 100644 --- a/lib/features/composer/domain/exceptions/compose_email_exception.dart +++ b/lib/features/composer/domain/exceptions/compose_email_exception.dart @@ -1 +1,3 @@ -class SendingEmailCanceledException implements Exception {} \ No newline at end of file +class SendingEmailCanceledException implements Exception {} + +class SavingEmailToDraftsCanceledException implements Exception {} \ No newline at end of file diff --git a/lib/features/composer/domain/state/save_email_as_drafts_state.dart b/lib/features/composer/domain/state/save_email_as_drafts_state.dart index d2844f905a..ae82f2d468 100644 --- a/lib/features/composer/domain/state/save_email_as_drafts_state.dart +++ b/lib/features/composer/domain/state/save_email_as_drafts_state.dart @@ -25,4 +25,6 @@ class SaveEmailAsDraftsSuccess extends UIActionState { class SaveEmailAsDraftsFailure extends FeatureFailure { SaveEmailAsDraftsFailure(dynamic exception) : super(exception: exception); -} \ No newline at end of file +} + +class CancelSavingEmailToDrafts extends LoadingState {} \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart b/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart index c48920f721..1102b225fe 100644 --- a/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart +++ b/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart @@ -2,10 +2,12 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart' as dartz; +import 'package:dio/dio.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/composer/domain/exceptions/compose_email_exception.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; @@ -14,6 +16,7 @@ import 'package:tmail_ui_user/features/composer/presentation/model/create_email_ import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; class CreateNewAndSaveEmailToDraftsInteractor { final EmailRepository _emailRepository; @@ -26,7 +29,10 @@ class CreateNewAndSaveEmailToDraftsInteractor { this._composerRepository, ); - Stream> execute(CreateEmailRequest createEmailRequest) async* { + Stream> execute({ + required CreateEmailRequest createEmailRequest, + CancelToken? cancelToken, + }) async* { try { yield dartz.Right(GenerateEmailLoading()); @@ -44,7 +50,8 @@ class CreateNewAndSaveEmailToDraftsInteractor { final emailDraftSaved = await _emailRepository.saveEmailAsDrafts( createEmailRequest.session, createEmailRequest.accountId, - emailCreated + emailCreated, + cancelToken: cancelToken ); yield dartz.Right( @@ -61,13 +68,15 @@ class CreateNewAndSaveEmailToDraftsInteractor { createEmailRequest.session, createEmailRequest.accountId, emailCreated, - createEmailRequest.draftsEmailId! + createEmailRequest.draftsEmailId!, + cancelToken: cancelToken ); await _deleteOldDraftsEmail( session: createEmailRequest.session, accountId: createEmailRequest.accountId, - draftEmailId: createEmailRequest.draftsEmailId! + draftEmailId: createEmailRequest.draftsEmailId!, + cancelToken: cancelToken ); yield dartz.Right( @@ -83,10 +92,18 @@ class CreateNewAndSaveEmailToDraftsInteractor { } } catch (e) { logError('CreateNewAndSaveEmailToDraftsInteractor::execute: Exception: $e'); - if (createEmailRequest.draftsEmailId == null) { - yield dartz.Left(SaveEmailAsDraftsFailure(e)); + if (e is UnknownError && e.message is List) { + if (createEmailRequest.draftsEmailId == null) { + yield dartz.Left(SaveEmailAsDraftsFailure(SavingEmailToDraftsCanceledException())); + } else { + yield dartz.Left(UpdateEmailDraftsFailure(SavingEmailToDraftsCanceledException())); + } } else { - yield dartz.Left(UpdateEmailDraftsFailure(e)); + if (createEmailRequest.draftsEmailId == null) { + yield dartz.Left(SaveEmailAsDraftsFailure(e)); + } else { + yield dartz.Left(UpdateEmailDraftsFailure(e)); + } } } } @@ -124,13 +141,15 @@ class CreateNewAndSaveEmailToDraftsInteractor { Future _deleteOldDraftsEmail({ required Session session, required AccountId accountId, - required EmailId draftEmailId + required EmailId draftEmailId, + CancelToken? cancelToken }) async { try { await _emailRepository.removeEmailDrafts( session, accountId, - draftEmailId + draftEmailId, + cancelToken: cancelToken ); } catch (e) { logError('CreateNewAndSaveEmailToDraftsInteractor::_deleteOldDraftsEmail: Exception: $e'); diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index d1cfb15ff3..f9b7c42244 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1129,10 +1129,11 @@ class ComposerController extends BaseController with DragDropFileMixin { } final emailContent = await _getContentInEditor(); - + final cancelToken = CancelToken(); final resultState = await _showSavingMessageToDraftsDialog( emailContent: emailContent, - draftEmailId: _emailIdEditing + draftEmailId: _emailIdEditing, + cancelToken: cancelToken ); if (resultState is SaveEmailAsDraftsSuccess) { @@ -1143,6 +1144,9 @@ class ComposerController extends BaseController with DragDropFileMixin { _saveToDraftButtonState = ButtonState.enabled; _emailIdEditing = resultState.emailId; mailboxDashBoardController.consumeState(Stream.value(Right(resultState))); + } 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) && @@ -2035,14 +2039,20 @@ class ComposerController extends BaseController with DragDropFileMixin { final emailContent = await _getContentInEditor(); final draftEmailId = _getDraftEmailId(); log('ComposerController::_handleSaveMessageToDraft: draftEmailId = $draftEmailId'); + final cancelToken = CancelToken(); final resultState = await _showSavingMessageToDraftsDialog( emailContent: emailContent, - draftEmailId: draftEmailId + 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; + _closeComposerAction(); } else if ((resultState is SaveEmailAsDraftsFailure || resultState is UpdateEmailDraftsFailure || resultState is GenerateEmailFailure) && @@ -2071,6 +2081,7 @@ class ComposerController extends BaseController with DragDropFileMixin { Future _showSavingMessageToDraftsDialog({ required String emailContent, EmailId? draftEmailId, + CancelToken? cancelToken, }) { return Get.dialog( PointerInterceptor( @@ -2098,13 +2109,19 @@ class ComposerController extends BaseController with DragDropFileMixin { references: composerArguments.value!.references, emailSendingQueue: composerArguments.value!.sendingEmail ), - createNewAndSaveEmailToDraftsInteractor: _createNewAndSaveEmailToDraftsInteractor + createNewAndSaveEmailToDraftsInteractor: _createNewAndSaveEmailToDraftsInteractor, + onCancelSavingEmailToDraftsAction: _handleCancelSavingMessageToDrafts, + cancelToken: cancelToken, ), ), barrierColor: AppColor.colorDefaultCupertinoActionSheet, ); } + void _handleCancelSavingMessageToDrafts({CancelToken? cancelToken}) { + cancelToken?.cancel([SavingEmailToDraftsCanceledException()]); + } + void _showConfirmDialogWhenSaveMessageToDraftsFailure({ required BuildContext context, required FeatureFailure failure, diff --git a/lib/features/composer/presentation/widgets/saving_message_dialog_view.dart b/lib/features/composer/presentation/widgets/saving_message_dialog_view.dart index 63e9962c9c..04a94b804c 100644 --- a/lib/features/composer/presentation/widgets/saving_message_dialog_view.dart +++ b/lib/features/composer/presentation/widgets/saving_message_dialog_view.dart @@ -5,27 +5,37 @@ import 'package:core/presentation/extensions/capitalize_extension.dart'; import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart' as dartz; +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/composer/domain/exceptions/compose_email_exception.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/update_email_drafts_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/presentation/model/create_email_request.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'; +typedef OnCancelSavingEmailToDraftsAction = Function({CancelToken? cancelToken}); + class SavingMessageDialogView extends StatefulWidget { final CreateEmailRequest createEmailRequest; final CreateNewAndSaveEmailToDraftsInteractor createNewAndSaveEmailToDraftsInteractor; + final OnCancelSavingEmailToDraftsAction? onCancelSavingEmailToDraftsAction; + final CancelToken? cancelToken; const SavingMessageDialogView({ super.key, required this.createEmailRequest, required this.createNewAndSaveEmailToDraftsInteractor, + this.onCancelSavingEmailToDraftsAction, + this.cancelToken, }); @override @@ -41,7 +51,10 @@ class _SavingMessageDialogViewState extends State { void initState() { super.initState(); _streamSubscription = widget.createNewAndSaveEmailToDraftsInteractor - .execute(widget.createEmailRequest) + .execute( + createEmailRequest: widget.createEmailRequest, + cancelToken: widget.cancelToken + ) .listen( _handleDataStream, onError: _handleErrorStream @@ -69,7 +82,11 @@ class _SavingMessageDialogViewState extends State { void _handleErrorStream(Object error, StackTrace stackTrace) { logError('_SavingMessageDialogViewState::_handleErrorStream: Exception = $error'); - popBack(result: SaveEmailAsDraftsFailure(error)); + if (error is UnknownError && error.message is List) { + popBack(result: SaveEmailAsDraftsFailure(SavingEmailToDraftsCanceledException())); + } else { + popBack(result: SaveEmailAsDraftsFailure(error)); + } } @override @@ -137,23 +154,13 @@ class _SavingMessageDialogViewState extends State { return value.fold( (failure) => child!, (success) { - if (success is GenerateEmailLoading) { - return Text( - '${AppLocalizations.of(context).creatingMessage}...', - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: AppColor.labelColor, - fontSize: 14 - ), - ); - } else { - return Text( - '${AppLocalizations.of(context).savingMessageToDraftFolder}...', - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: AppColor.labelColor, - fontSize: 14 - ), - ); - } + return Text( + '${_getStatusMessage(success)}...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ); } ); }, @@ -170,7 +177,7 @@ class _SavingMessageDialogViewState extends State { ), ), Padding( - padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 4, bottom: 24), + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 4, bottom: 16), child: Row( children: [ Text( @@ -191,7 +198,24 @@ class _SavingMessageDialogViewState extends State { ) ], ), - ) + ), + if (widget.onCancelSavingEmailToDraftsAction != null) + Align( + alignment: AlignmentDirectional.centerEnd, + child: TMailButtonWidget.fromText( + text: AppLocalizations.of(context).cancel, + textStyle: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.black87, + fontSize: 15 + ), + padding: const EdgeInsetsDirectional.symmetric(horizontal: 20, vertical: 8), + margin: const EdgeInsetsDirectional.only(start: 12, end: 12, bottom: 16), + onTapActionCallback: () { + _viewStateNotifier.value = dartz.Right(CancelSavingEmailToDrafts()); + widget.onCancelSavingEmailToDraftsAction!(cancelToken: widget.cancelToken); + }, + ), + ) ], ) ], @@ -200,6 +224,16 @@ class _SavingMessageDialogViewState extends State { ); } + String _getStatusMessage(Success success) { + if (success is GenerateEmailLoading) { + return AppLocalizations.of(context).creatingMessage; + } else if (success is CancelSavingEmailToDrafts) { + return AppLocalizations.of(context).canceling; + } else { + return AppLocalizations.of(context).savingMessageToDraftFolder; + } + } + @override void dispose() { _streamSubscription?.cancel(); diff --git a/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart b/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart index f74979398d..774b7600c6 100644 --- a/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart +++ b/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart @@ -151,31 +151,13 @@ class _SendingMessageDialogViewState extends State { return value.fold( (failure) => child!, (success) { - if (success is GenerateEmailLoading) { - return Text( - '${AppLocalizations.of(context).creatingMessage}...', - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: AppColor.labelColor, - fontSize: 14 - ), - ); - } else if (success is CancelSendingEmail) { - return Text( - '${AppLocalizations.of(context).canceling}...', - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: AppColor.labelColor, - fontSize: 14 - ), - ); - } else { - return Text( - '${AppLocalizations.of(context).sendingMessage}...', - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: AppColor.labelColor, - fontSize: 14 - ), - ); - } + return Text( + '${_getStatusMessage(success)}...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ); } ); }, @@ -239,6 +221,16 @@ class _SendingMessageDialogViewState extends State { ); } + String _getStatusMessage(Success success) { + if (success is GenerateEmailLoading) { + return AppLocalizations.of(context).creatingMessage; + } else if (success is CancelSendingEmail) { + return AppLocalizations.of(context).canceling; + } else { + return AppLocalizations.of(context).sendingMessage; + } + } + @override void dispose() { _streamSubscription?.cancel(); diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index 943448579b..612468edf6 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -74,11 +74,27 @@ abstract class EmailDataSource { MarkStarAction markStarAction ); - Future saveEmailAsDrafts(Session session, AccountId accountId, Email email); + Future saveEmailAsDrafts( + Session session, + AccountId accountId, + Email email, + {CancelToken? cancelToken} + ); - Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId); + Future removeEmailDrafts( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ); - Future updateEmailDrafts(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId); + Future updateEmailDrafts( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, + {CancelToken? cancelToken} + ); Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds); diff --git a/lib/features/email/data/datasource_impl/email_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_datasource_impl.dart index f2dc682909..407c1c1a07 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -116,23 +116,55 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future saveEmailAsDrafts(Session session, AccountId accountId, Email email) { + Future saveEmailAsDrafts( + Session session, + AccountId accountId, + Email email, + {CancelToken? cancelToken} + ) { return Future.sync(() async { - return await emailAPI.saveEmailAsDrafts(session, accountId, email); + return await emailAPI.saveEmailAsDrafts( + session, + accountId, + email, + cancelToken: cancelToken + ); }).catchError(_exceptionThrower.throwException); } @override - Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId) { + Future removeEmailDrafts( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) { return Future.sync(() async { - return await emailAPI.removeEmailDrafts(session, accountId, emailId); + return await emailAPI.removeEmailDrafts( + session, + accountId, + emailId, + cancelToken: cancelToken + ); }).catchError(_exceptionThrower.throwException); } @override - Future updateEmailDrafts(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId) { + Future updateEmailDrafts( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, + {CancelToken? cancelToken} + ) { return Future.sync(() async { - return await emailAPI.updateEmailDrafts(session, accountId, newEmail, oldEmailId); + return await emailAPI.updateEmailDrafts( + session, + accountId, + newEmail, + oldEmailId, + cancelToken: cancelToken + ); }).catchError(_exceptionThrower.throwException); } diff --git a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart index b19e067472..fe9a6eade6 100644 --- a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart @@ -129,12 +129,22 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { } @override - Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId) { + Future removeEmailDrafts( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) { throw UnimplementedError(); } @override - Future saveEmailAsDrafts(Session session, AccountId accountId, Email email) { + Future saveEmailAsDrafts( + Session session, + AccountId accountId, + Email email, + {CancelToken? cancelToken} + ) { throw UnimplementedError(); } @@ -178,7 +188,13 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { } @override - Future updateEmailDrafts(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId) { + Future updateEmailDrafts( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, + {CancelToken? cancelToken} + ) { throw UnimplementedError(); } diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 85f6b9d388..44a1d59ace 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -479,7 +479,12 @@ class EmailAPI with HandleSetErrorMixin { }); } - Future saveEmailAsDrafts(Session session, AccountId accountId, Email email) async { + Future saveEmailAsDrafts( + Session session, + AccountId accountId, + Email email, + {CancelToken? cancelToken} + ) async { final idCreateMethod = Id(_uuid.v1()); final setEmailMethod = SetEmailMethod(accountId) ..addCreate(idCreateMethod, email); @@ -494,7 +499,7 @@ class EmailAPI with HandleSetErrorMixin { final response = await (requestBuilder ..usings(capabilities)) .build() - .execute(); + .execute(cancelToken: cancelToken); final setEmailResponse = response.parse( setEmailInvocation.methodCallId, @@ -511,7 +516,12 @@ class EmailAPI with HandleSetErrorMixin { } } - Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId) async { + Future removeEmailDrafts( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) async { final setEmailMethod = SetEmailMethod(accountId) ..addDestroy({emailId.id}); @@ -525,7 +535,7 @@ class EmailAPI with HandleSetErrorMixin { final response = await (requestBuilder ..usings(capabilities)) .build() - .execute(); + .execute(cancelToken: cancelToken); final setEmailResponse = response.parse( setEmailInvocation.methodCallId, @@ -542,7 +552,8 @@ class EmailAPI with HandleSetErrorMixin { Session session, AccountId accountId, Email newEmail, - EmailId oldEmailId + EmailId oldEmailId, + {CancelToken? cancelToken} ) async { final idCreateMethod = Id(_uuid.v1()); final setEmailMethod = SetEmailMethod(accountId) @@ -559,7 +570,7 @@ class EmailAPI with HandleSetErrorMixin { final response = await (requestBuilder ..usings(capabilities)) .build() - .execute(); + .execute(cancelToken: cancelToken); final setEmailResponse = response.parse( setEmailInvocation.methodCallId, diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index 9e1f000420..2d699dc11e 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -141,18 +141,50 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future saveEmailAsDrafts(Session session, AccountId accountId, Email email) { - return emailDataSource[DataSourceType.network]!.saveEmailAsDrafts(session, accountId, email); + Future saveEmailAsDrafts( + Session session, + AccountId accountId, + Email email, + {CancelToken? cancelToken} + ) { + return emailDataSource[DataSourceType.network]!.saveEmailAsDrafts( + session, + accountId, + email, + cancelToken: cancelToken + ); } @override - Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId) { - return emailDataSource[DataSourceType.network]!.removeEmailDrafts(session, accountId, emailId); + Future removeEmailDrafts( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) { + return emailDataSource[DataSourceType.network]!.removeEmailDrafts( + session, + accountId, + emailId, + cancelToken: cancelToken + ); } @override - Future updateEmailDrafts(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId) { - return emailDataSource[DataSourceType.network]!.updateEmailDrafts(session, accountId, newEmail, oldEmailId); + Future updateEmailDrafts( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, + {CancelToken? cancelToken} + ) { + return emailDataSource[DataSourceType.network]!.updateEmailDrafts( + session, + accountId, + newEmail, + oldEmailId, + cancelToken: cancelToken + ); } @override diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart index 533b82960d..38488b0581 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -82,11 +82,27 @@ abstract class EmailRepository { TransformConfiguration transformConfiguration ); - Future saveEmailAsDrafts(Session session, AccountId accountId, Email email); + Future saveEmailAsDrafts( + Session session, + AccountId accountId, + Email email, + {CancelToken? cancelToken} + ); - Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId); + Future removeEmailDrafts( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ); - Future updateEmailDrafts(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId); + Future updateEmailDrafts( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, + {CancelToken? cancelToken} + ); Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds); diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index cdf17317e8..629e3a680a 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -260,9 +260,12 @@ class MailboxDashBoardController extends ReloadableController { ); @override - void onInit() { + void onInit() async { _registerStreamListener(); BackButtonInterceptor.add(_onBackButtonInterceptor, name: AppRoutes.dashboard); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await applicationManager.initUserAgent(); + }); super.onInit(); } @@ -2466,6 +2469,7 @@ class MailboxDashBoardController extends ReloadableController { _refreshActionEventController.close(); _notificationManager.closeStream(); _fcmService.closeStream(); + applicationManager.releaseUserAgent(); BackButtonInterceptor.removeByName(AppRoutes.dashboard); super.onClose(); } From 2a26967473b2268e87a24deae3ae08e44c1798bb Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 19 Mar 2024 00:54:21 +0700 Subject: [PATCH 20/80] TF-2667 Validate email in recipient form before send email --- core/lib/core.dart | 2 + .../domain/exceptions/address_exception.dart | 13 + core/lib/utils/mail/domain.dart | 107 +++++ core/lib/utils/mail/mail_address.dart | 398 ++++++++++++++++++ core/test/utils/mail_address_test.dart | 62 +++ .../presentation/composer_controller.dart | 3 +- .../widgets/recipient_tag_item_widget.dart | 5 +- .../email/presentation/utils/email_utils.dart | 11 +- 8 files changed, 597 insertions(+), 4 deletions(-) create mode 100644 core/lib/domain/exceptions/address_exception.dart create mode 100644 core/lib/utils/mail/domain.dart create mode 100644 core/lib/utils/mail/mail_address.dart create mode 100644 core/test/utils/mail_address_test.dart diff --git a/core/lib/core.dart b/core/lib/core.dart index b25e073b86..9bc6ca3923 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -46,6 +46,8 @@ export 'utils/option_param_mixin.dart'; export 'utils/print_utils.dart'; export 'utils/broadcast_channel/broadcast_channel.dart'; export 'utils/list_utils.dart'; +export 'utils/mail/domain.dart'; +export 'utils/mail/mail_address.dart'; // Views export 'presentation/views/text/slogan_builder.dart'; diff --git a/core/lib/domain/exceptions/address_exception.dart b/core/lib/domain/exceptions/address_exception.dart new file mode 100644 index 0000000000..51889bbf76 --- /dev/null +++ b/core/lib/domain/exceptions/address_exception.dart @@ -0,0 +1,13 @@ +import 'package:equatable/equatable.dart'; + +class AddressException with EquatableMixin implements Exception { + final String message; + + AddressException(this.message); + + @override + String toString() => message; + + @override + List get props => [message]; +} diff --git a/core/lib/utils/mail/domain.dart b/core/lib/utils/mail/domain.dart new file mode 100644 index 0000000000..86acbca7c1 --- /dev/null +++ b/core/lib/utils/mail/domain.dart @@ -0,0 +1,107 @@ +import 'package:equatable/equatable.dart'; + +class Domain with EquatableMixin { + static final RegExp _dashMatcher = RegExp(r"[-_]"); + static final RegExp _digitMatcher = RegExp(r"[0-9]"); + static final RegExp _partCharMatcher = RegExp(r"[a-zA-Z0-9\-._]"); + + static final Domain localhost = Domain.of('localhost'); + static const int maximumDomainLength = 253; + + static String _removeBrackets(String domainName) { + if (!(domainName.startsWith("[") && domainName.endsWith("]"))) { + return domainName; + } + return domainName.substring(1, domainName.length - 1); + } + + static Domain of(String domain) { + assert( + domain.length <= maximumDomainLength, + 'Domain name length should not exceed $maximumDomainLength characters' + ); + + String domainWithoutBrackets = _removeBrackets(domain); + assert( + _partCharMatcher + .allMatches(domainWithoutBrackets) + .every((match) => match.group(0) != null), + 'Domain parts ASCII chars must be a-z A-Z 0-9 - or _' + ); + + int pos = 0; + int nextDot = domainWithoutBrackets.indexOf('.'); + + while (nextDot > -1) { + if (pos + 1 > domainWithoutBrackets.length) { + throw ArgumentError('Last domain part should not be empty'); + } + _assertValidPart(domainWithoutBrackets, pos, nextDot); + pos = nextDot + 1; + nextDot = domainWithoutBrackets.indexOf('.', pos); + } + _assertValidPart(domainWithoutBrackets, pos, domainWithoutBrackets.length); + _assertValidLastPart(domainWithoutBrackets, pos); + return Domain._(domainWithoutBrackets); + } + + static void _assertValidPart(String domainPart, int begin, int end) { + assert(begin != end, "Domain part should not be empty"); + assert(!_dashMatcher.hasMatch(domainPart[begin]), + "Domain part should not start with '-' or '_'"); + assert(!_dashMatcher.hasMatch(domainPart[end - 1]), + "Domain part should not end with '-' or '_'"); + assert( + end - begin <= 63, "Domain part should not not exceed 63 characters"); + } + + static void _assertValidLastPart(String domainPart, int pos) { + bool onlyDigits = _digitMatcher.hasMatch(domainPart[pos]); + bool invalid = onlyDigits && !_validIPAddress(domainPart); + assert(!invalid, "The last domain part must not start with 0-9"); + } + + static bool _validIPAddress(String value) { + try { + Uri.parseIPv6Address(value); + return true; + } catch (e) { + return false; + } + } + + final String domainName; + final String normalizedDomainName; + + Domain._(this.domainName) + : normalizedDomainName = _removeBrackets(domainName.toLowerCase()); + + String name() { + return domainName; + } + + String asString() { + return normalizedDomainName; + } + + @override + bool operator ==(Object other) { + if (other is Domain) { + return normalizedDomainName == other.normalizedDomainName; + } + return false; + } + + @override + int get hashCode { + return normalizedDomainName.hashCode; + } + + @override + String toString() { + return "Domain : $domainName"; + } + + @override + List get props => [domainName]; +} diff --git a/core/lib/utils/mail/mail_address.dart b/core/lib/utils/mail/mail_address.dart new file mode 100644 index 0000000000..f35b91e4b8 --- /dev/null +++ b/core/lib/utils/mail/mail_address.dart @@ -0,0 +1,398 @@ +import 'package:core/domain/exceptions/address_exception.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/mail/domain.dart'; +import 'package:equatable/equatable.dart'; + +class MailAddress with EquatableMixin { + static final List SPECIAL = [ + '<', + '>', + '(', + ')', + '[', + ']', + '\\', + '.', + ',', + ';', + ':', + '@', + '\"' + ]; + + final String localPart; + final Domain domain; + + MailAddress({required this.localPart, required this.domain}); + + String asString() { + return '$localPart@${domain.asString()}'; + } + + String asPrettyString() { + return '<${asString()}>'; + } + + static MailAddress validate(String address) { + String localPart; + Domain domain; + + address = address.trim(); + int pos = 0; + + // Test if mail address has source routing information (RFC-821) and get rid of it!! + // must be called first!! (or at least prior to updating pos) + _stripSourceRoute(address, pos); + + StringBuffer localPartSB = StringBuffer(); + StringBuffer domainSB = StringBuffer(); + // Begin parsing + // ::= "@" + + try { + // parse local-part + // ::= | + if (address[pos] == '\"') { + pos = _parseQuotedLocalPartOrThrowException(localPartSB, address, pos); + } else { + pos = parseUnquotedLocalPartOrThrowException(localPartSB, address, pos); + } + // find @ + if (pos >= address.length || address[pos] != '@') { + throw AddressException('Did not find @ between local-part and domain at position ${pos + 1} in "$address"'); + } + pos++; + // parse domain + // ::= | "." + // ::= | "#" | "[" "]" + while (true) { + if (pos >= address.length) { + break; + } + var postChar = address[pos]; + if (postChar == '#') { + pos = _parseNumber(domainSB, address, pos); + } else if (postChar == '[') { + pos = _parseDomainLiteral(domainSB, address, pos); + } else { + pos = _parseDomain(domainSB, address, pos); + } + if (pos >= address.length) { + break; + } + postChar = address[pos]; + if (postChar == '.') { + var lastChar = address[pos - 1]; + if (lastChar == '@' || lastChar == '.') { + throw AddressException('Subdomain expected before "." or duplicate "." in "address"'); + } + domainSB.write('.'); + pos++; + continue; + } + break; + } + if (domainSB.length == 0) { + throw AddressException('No domain found at position ${pos + 1} in "$address"'); + } + } catch (e) { + log('MailAddress::validate: Exception = $e'); + if (e is AddressException) { + rethrow; + } else { + throw AddressException('Out of data at position ${pos + 1} in "$address"'); + } + } + + localPart = localPartSB.toString(); + + if (localPart.startsWith('.') || + localPart.endsWith('.') || + _haveDoubleDot(localPart)) { + throw AddressException('Addresses cannot start end with "." or contain two consecutive dots'); + } + + domain = _createDomain(domainSB.toString()); + + log('MailAddress::validate: localPart = $localPart | domain = $domain'); + + return MailAddress(localPart: localPart, domain: domain); + } + + static bool _haveDoubleDot(String localPart) { + return localPart.contains('..'); + } + + static Domain _createDomain(String domain) { + try { + return Domain.of(domain); + } catch (e) { + throw AddressException(e.toString()); + } + } + + static int _parseNumber(StringBuffer dSB, String address, int pos) { + // ::= | + + // we were passed the string with pos pointing the the # char. + // take the first char (#), put it in the result buffer and increment pos + var postChar = address[pos]; + dSB.write(postChar); + pos++; + // We keep the position from the class level pos field + while (true) { + if (pos >= address.length) { + break; + } + // ::= any one of the ten digits 0 through 9 + var d = address[pos]; + if (d == '.') { + break; + } + if (d.compareTo('0') < 0 || d.compareTo('9') > 0) { + throw AddressException('In domain, did not find a number in # address at position ${pos + 1} in "$address"'); + } + dSB.write(d); + pos++; + } + if (dSB.length < 2) { + throw AddressException('In domain, did not find a number in # address at position ${pos + 1} in "$address"'); + } + return pos; + } + + static int _parseDomainLiteral(StringBuffer dSB, String address, int pos) { + // we were passed the string with pos pointing the the [ char. + // take the first char ([), put it in the result buffer and increment pos + var posChar = address[pos]; + dSB.write(posChar); + pos++; + + // ::= "." "." "." + for (int octet = 0; octet < 4; octet++) { + // ::= one, two, or three digits representing a decimal + // integer value in the range 0 through 255 + // ::= any one of the ten digits 0 through 9 + StringBuffer snumSB = StringBuffer(); + for (int digits = 0; digits < 3; digits++) { + String currentChar = address[pos]; + if (currentChar == '.' || currentChar == ']') { + break; + } else if (currentChar.compareTo('0') < 0 || + currentChar.compareTo('9') > 0) { + throw AddressException('Invalid number at position ${pos + 1} in "$address"'); + } + snumSB.write(currentChar); + pos++; + } + if (snumSB.length == 0) { + throw AddressException('Number not found at position ${pos + 1} in "$address"'); + } + try { + int snum = int.parse(snumSB.toString()); + if (snum > 255) { + throw AddressException('Invalid number at position ${pos + 1} in "$address"'); + } + } catch (e) { + throw AddressException('Invalid number at position ${pos + 1} in "$address"'); + } + dSB.write(snumSB.toString()); + var posChar = address[pos]; + if (posChar == ']') { + if (octet < 3) { + throw AddressException('End of number reached too quickly at ${pos + 1} in "$address"'); + } + break; + } + if (posChar == '.') { + dSB.write('.'); + pos++; + } + } + posChar = address[pos]; + if (posChar != ']') { + throw AddressException('Did not find closing bracket \"]\" in domain at position ${pos + 1} in "$address"'); + } + dSB.write(']'); + pos++; + return pos; + } + + static int _parseDomain(StringBuffer dSB, String address, int pos) { + StringBuffer resultSB = StringBuffer(); + // ::= + // ::= | + // ::= | + // ::= | | "-" + // ::= any one of the 52 alphabetic characters A through Z + // in upper case and a through z in lower case + // ::= any one of the ten digits 0 through 9 + + // basically, this is a series of letters, digits, and hyphens, + // but it can't start with a digit or hypthen + // and can't end with a hyphen + + // in practice though, we should relax this as domain names can start + // with digits as well as letters. So only check that doesn't start + // or end with hyphen. + while (true) { + if (pos >= address.length) { + break; + } + var ch = address[pos]; + if ((ch.compareTo('0') >= 0 && ch.compareTo('9') <= 0) || + (ch.compareTo('a') >= 0 && ch.compareTo('z') <= 0) || + (ch.compareTo('A') >= 0 && ch.compareTo('Z') <= 0) || + (ch == '-')) { + resultSB.write(ch); + pos++; + continue; + } + if (ch == '.') { + break; + } + throw AddressException('Invalid character at $pos in "$address"'); + } + String result = resultSB.toString(); + if (result.startsWith('-') || result.endsWith('-')) { + throw AddressException('Domain name cannot begin or end with a hyphen \"-\" at position ${pos + 1} in "$address"'); + } + dSB.write(result); + return pos; + } + + static int _stripSourceRoute(String address, int pos) { + var posChar = address[pos]; + if (pos < address.length && posChar == '@') { + int i = address.indexOf(':'); + if (i != -1) { + pos = i + 1; + } + } + return pos; + } + + static int _parseUnquotedLocalPart(StringBuffer lpSB, String address, int pos) { + // ::= | "." + bool lastCharDot = false; + while (true) { + if (pos >= address.length) { + break; + } + // ::= | + // ::= | "\" + var postChar = address[pos]; + if (postChar == '\\') { + lpSB.write('\\'); + pos++; + // ::= any one of the 128 ASCII characters (no exceptions) + var x = address[pos]; + if (x.codeUnitAt(0) < 0 || x.codeUnitAt(0) > 127) { + throw AddressException('Invalid \\ syntax character at position ${pos + 1} in "$address"'); + } + lpSB.write(x); + pos++; + lastCharDot = false; + } else if (postChar == '.') { + if (pos == 0) { + throw AddressException('Local part must not start with a "."'); + } + lpSB.write('.'); + pos++; + lastCharDot = true; + } else if (postChar == '@') { + // End of local-part + break; + } else { + // ::= any one of the 128 ASCII characters, but not any + // or + // ::= "<" | ">" | "(" | ")" | "[" | "]" | "\" | "." + // | "," | ";" | ":" | "@" """ | the control + // characters (ASCII codes 0 through 31 inclusive and + // 127) + // ::= the space character (ASCII code 32) + var c = address[pos]; + if (c.codeUnitAt(0) <= 31 || c.codeUnitAt(0) >= 127 || c == ' ') { + throw AddressException('Invalid character in local-part (user account) at position ${pos + 1} in "$address"'); + } + int i = 0; + while (i < SPECIAL.length) { + if (c == SPECIAL[i]) { + throw AddressException('Invalid character in local-part (user account) at position ${pos + 1} in "$address"'); + } + i++; + } + lpSB.write(c); + pos++; + lastCharDot = false; + } + } + if (lastCharDot) { + throw AddressException('local-part (user account) ended with a \".\", which is invalid in address "$address"'); + } + return pos; + } + + static int parseUnquotedLocalPartOrThrowException(StringBuffer localPartSB, String address, int pos) { + pos = _parseUnquotedLocalPart(localPartSB, address, pos); + if (localPartSB.length == 0) { + throw AddressException('No local-part (user account) found at position ${pos + 1} in "$address"'); + } + return pos; + } + + static int _parseQuotedLocalPartOrThrowException(StringBuffer localPartSB, String address, int pos) { + pos = _parseQuotedLocalPart(localPartSB, address, pos); + if (localPartSB.length == 2) { + throw AddressException('No quoted local-part (user account) found at position ${pos + 2} in "$address"'); + } + return pos; + } + + static int _parseQuotedLocalPart(StringBuffer lpSB, String address, int pos) { + lpSB.write('\"'); + pos++; + // ::= """ """ + // ::= "\" | "\" | | + while (true) { + if (pos >= address.length) { + break; + } + var postChar = address[pos]; + if (postChar == '\"') { + lpSB.write('\"'); + // end of quoted string... move forward + pos++; + break; + } + if (postChar == '\\') { + lpSB.write('\\'); + pos++; + // ::= any one of the 128 ASCII characters (no exceptions) + var x = address[pos]; + if (x.codeUnitAt(0) < 0 || x.codeUnitAt(0) > 127) { + throw AddressException('Invalid \\ syntax character at position ${pos + 1} in "$address"'); + } + lpSB.write(x); + pos++; + } else { + // ::= any one of the 128 ASCII characters except , + // , quote ("), or backslash (\) + var q = address[pos]; + if (q.codeUnitAt(0) <= 0 || + q == '\n' || + q == '\r' || + q == '\"' || + q == '\\') { + throw AddressException('Unquoted local-part (user account) must be one of the 128 ASCII characters exception , , quote (\"), or backslash (\\) at position ${pos + 1} in "$address"'); + } + lpSB.write(q); + pos++; + } + } + return pos; + } + + @override + List get props => [localPart, domain]; +} diff --git a/core/test/utils/mail_address_test.dart b/core/test/utils/mail_address_test.dart new file mode 100644 index 0000000000..7df98548a2 --- /dev/null +++ b/core/test/utils/mail_address_test.dart @@ -0,0 +1,62 @@ +import 'package:core/domain/exceptions/address_exception.dart'; +import 'package:core/utils/mail/mail_address.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('MailAddress test', () { + test('validate method should be return MailAddress when address valid', () { + String validAddress = 'user@example.com'; + MailAddress mailAddress = MailAddress.validate(validAddress); + expect(mailAddress.localPart, equals('user')); + expect(mailAddress.domain.name(), equals('example.com')); + }); + + test('validate method should be throw AddressException when address missing @', () { + String invalidAddress = 'userexample.com'; + expect( + () => MailAddress.validate(invalidAddress), + throwsA(isA().having( + (e) => e.message, + 'message', + 'Did not find @ between local-part and domain at position 16 in "userexample.com"') + ) + ); + }); + + test('validate method should be throw AddressException when address empty local-part', () { + String invalidAddress = '@example.com'; + expect( + () => MailAddress.validate(invalidAddress), + throwsA(isA().having( + (e) => e.message, + 'message', + 'No local-part (user account) found at position 1 in "@example.com"') + ) + ); + }); + + test('validate method should be throw AddressException when address empty domain', () { + String invalidAddress = 'user@'; + expect( + () => MailAddress.validate(invalidAddress), + throwsA(isA().having( + (e) => e.message, + 'message', + 'No domain found at position 6 in "user@"') + ) + ); + }); + + test('validate method should be throw AddressException when address with a hyphen "-"', () { + String invalidAddress = 'user@-example.com'; + expect( + () => MailAddress.validate(invalidAddress), + throwsA(isA().having( + (e) => e.message, + 'message', + 'Domain name cannot begin or end with a hyphen "-" at position 14 in "user@-example.com"') + ) + ); + }); + }); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index f9b7c42244..6d0758f93b 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -68,6 +68,7 @@ import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_c import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_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/model/draggable_app_state.dart'; @@ -721,7 +722,7 @@ class ComposerController extends BaseController with DragDropFileMixin { final allListEmailAddress = listToEmailAddress + listCcEmailAddress + listBccEmailAddress; final listEmailAddressInvalid = allListEmailAddress - .where((emailAddress) => !GetUtils.isEmail(emailAddress.emailAddress)) + .where((emailAddress) => !EmailUtils.isEmailAddressValid(emailAddress.emailAddress)) .toList(); if (listEmailAddressInvalid.isNotEmpty) { showConfirmDialogAction(context, diff --git a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart index 2fea5a509c..57b981ed93 100644 --- a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart @@ -16,6 +16,7 @@ import 'package:tmail_ui_user/features/composer/presentation/model/draggable_ema import 'package:tmail_ui_user/features/composer/presentation/styles/recipient_tag_item_widget_style.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/draggable_recipient_tag_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; class RecipientTagItemWidget extends StatelessWidget { @@ -172,7 +173,7 @@ class RecipientTagItemWidget extends StatelessWidget { Color _getTagBackgroundColor() { if (isLatestTagFocused && isLatestEmail) { return AppColor.colorItemRecipientSelected; - } else if (GetUtils.isEmail(currentEmailAddress.emailAddress)) { + } else if (EmailUtils.isEmailAddressValid(currentEmailAddress.emailAddress)) { return AppColor.colorEmailAddressTag; } else { return Colors.white; @@ -182,7 +183,7 @@ class RecipientTagItemWidget extends StatelessWidget { BorderSide _getTagBorderSide() { if (isLatestTagFocused && isLatestEmail) { return const BorderSide(width: 1, color: AppColor.primaryColor); - } else if (GetUtils.isEmail(currentEmailAddress.emailAddress)) { + } else if (EmailUtils.isEmailAddressValid(currentEmailAddress.emailAddress)) { return const BorderSide(width: 1, color: AppColor.colorEmailAddressTag); } else { return const BorderSide( diff --git a/lib/features/email/presentation/utils/email_utils.dart b/lib/features/email/presentation/utils/email_utils.dart index 6b8bca98ca..f75e85f7a5 100644 --- a/lib/features/email/presentation/utils/email_utils.dart +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -68,7 +68,16 @@ class EmailUtils { required String internalDomain }) { log('EmailUtils::isSameDomain: emailAddress = $emailAddress | internalDomain = $internalDomain'); - return GetUtils.isEmail(emailAddress) && + return EmailUtils.isEmailAddressValid(emailAddress) && emailAddress.split('@').last.toLowerCase() == internalDomain.toLowerCase(); } + + static bool isEmailAddressValid(String address) { + try { + return GetUtils.isEmail(address) && MailAddress.validate(address).asString().isNotEmpty; + } catch(e) { + logError('EmailUtils::isEmailAddressValid: Exception = $e'); + return false; + } + } } \ No newline at end of file From 92edb7a0b1b4bfa164bbff299c675c9489163e00 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 19 Mar 2024 00:56:23 +0700 Subject: [PATCH 21/80] TF-2667 Remove log unnecessary --- model/lib/extensions/presentation_email_extension.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/model/lib/extensions/presentation_email_extension.dart b/model/lib/extensions/presentation_email_extension.dart index 1ca8061f4b..fc3bf0b6f8 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -32,7 +32,6 @@ extension PresentationEmailExtension on PresentationEmail { int numberOfAllEmailAddress() => to.numberEmailAddress() + cc.numberEmailAddress() + bcc.numberEmailAddress(); String getReceivedAt(String newLocale, {String? pattern}) { - log('PresentationEmailExtension::getReceivedAt: newLocale = $newLocale | pattern = $pattern'); final emailTime = receivedAt; if (emailTime != null) { return emailTime.formatDateToLocal( From b4a29fe8cb6a8c32715550543561c30cc971081e Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 19 Mar 2024 09:10:13 +0700 Subject: [PATCH 22/80] TF-2667 Clean `WARNING/INFO` when run dart analyze --- lib/features/composer/presentation/composer_controller.dart | 4 +--- lib/features/composer/presentation/composer_view_web.dart | 4 ++-- lib/features/email/presentation/utils/email_utils.dart | 5 ++++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 6d0758f93b..bce051211a 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -6,7 +6,6 @@ import 'package:collection/collection.dart'; import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:desktop_drop/desktop_drop.dart'; -import 'package:device_info_plus/device_info_plus.dart'; import 'package:dio/dio.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:file_picker/file_picker.dart'; @@ -78,7 +77,6 @@ import 'package:tmail_ui_user/features/manage_account/presentation/extensions/id 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/sending_queue/domain/model/sending_email.dart'; -import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; import 'package:tmail_ui_user/features/server_settings/domain/state/get_always_read_receipt_setting_state.dart'; import 'package:tmail_ui_user/features/server_settings/domain/usecases/get_always_read_receipt_setting_interactor.dart'; import 'package:tmail_ui_user/features/upload/domain/exceptions/pick_file_exception.dart'; @@ -212,7 +210,7 @@ class ComposerController extends BaseController with DragDropFileMixin { _listenStreamEvent(); if (PlatformInfo.isWeb) { WidgetsBinding.instance.addPostFrameCallback((_) { - _listenBrowserEventAction(); + _listenBrowserTabRefresh(); }); } _getAlwaysReadReceiptSetting(); diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index a7c7b0e186..5433d57408 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -455,8 +455,8 @@ class ComposerView extends GetWidget { insertImageAction: () => controller.insertImage(context, constraints.maxWidth), showCodeViewAction: controller.richTextWebController.toggleCodeView, deleteComposerAction: () => controller.handleClickDeleteComposer(context), - saveToDraftAction: () => controller.saveToDraftAction(context), - sendMessageAction: () => controller.validateInformationBeforeSending(context), + saveToDraftAction: () => controller.handleClickSaveAsDraftsButton(context), + sendMessageAction: () => controller.handleClickSendButton(context), requestReadReceiptAction: (position) { controller.openPopupMenuAction( context, diff --git a/lib/features/email/presentation/utils/email_utils.dart b/lib/features/email/presentation/utils/email_utils.dart index f75e85f7a5..63877adc28 100644 --- a/lib/features/email/presentation/utils/email_utils.dart +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -1,6 +1,9 @@ import 'package:collection/collection.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/mail/mail_address.dart'; import 'package:get/get_utils/src/get_utils/get_utils.dart'; -import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; From 299883c7c425f00b1db2755f8bde523161235c0e Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 22 Mar 2024 01:24:03 +0700 Subject: [PATCH 23/80] TF-2667 Write unit test for `Domain` & `MailAddress` class --- core/lib/utils/mail/domain.dart | 64 ++++---- core/lib/utils/mail/mail_address.dart | 47 ++++-- core/test/utils/domain_test.dart | 79 ++++++++++ core/test/utils/mail_address_test.dart | 139 ++++++++++++++++-- .../email/presentation/utils/email_utils.dart | 2 +- 5 files changed, 270 insertions(+), 61 deletions(-) create mode 100644 core/test/utils/domain_test.dart diff --git a/core/lib/utils/mail/domain.dart b/core/lib/utils/mail/domain.dart index 86acbca7c1..f57d210f37 100644 --- a/core/lib/utils/mail/domain.dart +++ b/core/lib/utils/mail/domain.dart @@ -1,33 +1,39 @@ +import 'dart:io'; + +import 'package:core/utils/app_logger.dart'; import 'package:equatable/equatable.dart'; class Domain with EquatableMixin { - static final RegExp _dashMatcher = RegExp(r"[-_]"); - static final RegExp _digitMatcher = RegExp(r"[0-9]"); - static final RegExp _partCharMatcher = RegExp(r"[a-zA-Z0-9\-._]"); + static final RegExp _dashMatcher = RegExp(r'[-_]'); + static final RegExp _digitMatcher = RegExp(r'\d'); + static final RegExp _partCharMatcher = RegExp(r'[A-Za-z0-9_\-.]'); static final Domain localhost = Domain.of('localhost'); static const int maximumDomainLength = 253; static String _removeBrackets(String domainName) { - if (!(domainName.startsWith("[") && domainName.endsWith("]"))) { + if (!(domainName.startsWith('[') && domainName.endsWith(']'))) { return domainName; } return domainName.substring(1, domainName.length - 1); } - static Domain of(String domain) { - assert( - domain.length <= maximumDomainLength, - 'Domain name length should not exceed $maximumDomainLength characters' - ); + static bool _allCharactersMatchRegex(String input, RegExp regex) { + for (int i = 0; i < input.length; i++) { + if (!regex.hasMatch(input[i])) { + return false; + } + } + return true; + } + + static Domain of(String? domain) { + assert(domain != null, 'Domain can not be null'); + assert(domain!.isNotEmpty, 'Domain can not be empty'); + assert(domain!.length <= maximumDomainLength, 'Domain name length should not exceed $maximumDomainLength characters'); - String domainWithoutBrackets = _removeBrackets(domain); - assert( - _partCharMatcher - .allMatches(domainWithoutBrackets) - .every((match) => match.group(0) != null), - 'Domain parts ASCII chars must be a-z A-Z 0-9 - or _' - ); + String domainWithoutBrackets = _removeBrackets(domain!); + assert(_allCharactersMatchRegex(domainWithoutBrackets, _partCharMatcher), 'Domain parts ASCII chars must be a-z A-Z 0-9 - or _'); int pos = 0; int nextDot = domainWithoutBrackets.indexOf('.'); @@ -42,17 +48,15 @@ class Domain with EquatableMixin { } _assertValidPart(domainWithoutBrackets, pos, domainWithoutBrackets.length); _assertValidLastPart(domainWithoutBrackets, pos); + return Domain._(domainWithoutBrackets); } static void _assertValidPart(String domainPart, int begin, int end) { assert(begin != end, "Domain part should not be empty"); - assert(!_dashMatcher.hasMatch(domainPart[begin]), - "Domain part should not start with '-' or '_'"); - assert(!_dashMatcher.hasMatch(domainPart[end - 1]), - "Domain part should not end with '-' or '_'"); - assert( - end - begin <= 63, "Domain part should not not exceed 63 characters"); + assert(!_dashMatcher.hasMatch(domainPart[begin]), "Domain part should not start with '-' or '_'"); + assert(!_dashMatcher.hasMatch(domainPart[end - 1]), "Domain part should not end with '-' or '_'"); + assert(end - begin <= 63, "Domain part should not not exceed 63 characters"); } static void _assertValidLastPart(String domainPart, int pos) { @@ -63,9 +67,10 @@ class Domain with EquatableMixin { static bool _validIPAddress(String value) { try { - Uri.parseIPv6Address(value); + InternetAddress(value); return true; } catch (e) { + logError('Domain::validIPAddress: Exception = $e'); return false; } } @@ -73,16 +78,11 @@ class Domain with EquatableMixin { final String domainName; final String normalizedDomainName; - Domain._(this.domainName) - : normalizedDomainName = _removeBrackets(domainName.toLowerCase()); + Domain._(this.domainName) : normalizedDomainName = _removeBrackets(domainName.toLowerCase()); - String name() { - return domainName; - } + String name() => domainName; - String asString() { - return normalizedDomainName; - } + String asString() => normalizedDomainName; @override bool operator ==(Object other) { @@ -104,4 +104,4 @@ class Domain with EquatableMixin { @override List get props => [domainName]; -} +} \ No newline at end of file diff --git a/core/lib/utils/mail/mail_address.dart b/core/lib/utils/mail/mail_address.dart index f35b91e4b8..a791220063 100644 --- a/core/lib/utils/mail/mail_address.dart +++ b/core/lib/utils/mail/mail_address.dart @@ -25,19 +25,15 @@ class MailAddress with EquatableMixin { MailAddress({required this.localPart, required this.domain}); - String asString() { - return '$localPart@${domain.asString()}'; - } - - String asPrettyString() { - return '<${asString()}>'; - } - - static MailAddress validate(String address) { + factory MailAddress.validateAddress(String address) { + log('MailAddress::validate: Address = $address'); String localPart; Domain domain; address = address.trim(); + if (address.isEmpty) { + throw AddressException('Addresses should not be empty'); + } int pos = 0; // Test if mail address has source routing information (RFC-821) and get rid of it!! @@ -96,7 +92,7 @@ class MailAddress with EquatableMixin { throw AddressException('No domain found at position ${pos + 1} in "$address"'); } } catch (e) { - log('MailAddress::validate: Exception = $e'); + logError('MailAddress::validate: Exception = $e'); if (e is AddressException) { rethrow; } else { @@ -114,11 +110,38 @@ class MailAddress with EquatableMixin { domain = _createDomain(domainSB.toString()); - log('MailAddress::validate: localPart = $localPart | domain = $domain'); - return MailAddress(localPart: localPart, domain: domain); } + factory MailAddress.validateLocalPartAndDomain({required String localPart, required dynamic domain}) { + if (domain is Domain) { + return MailAddress.validateAddress('$localPart@${domain.name()}'); + } else { + return MailAddress.validateAddress('$localPart@$domain'); + } + } + + String asString() { + return '$localPart@${domain.asString()}'; + } + + String asPrettyString() { + return '<${asString()}>'; + } + + Domain getDomain() { + return domain; + } + + String getLocalPart() { + return localPart; + } + + @override + String toString() { + return '$localPart@${domain.asString()}'; + } + static bool _haveDoubleDot(String localPart) { return localPart.contains('..'); } diff --git a/core/test/utils/domain_test.dart b/core/test/utils/domain_test.dart new file mode 100644 index 0000000000..17c804dbed --- /dev/null +++ b/core/test/utils/domain_test.dart @@ -0,0 +1,79 @@ + +import 'package:core/utils/mail/domain.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Domain.of(args) should not be case sensitive', () { + expect(Domain.of('Domain'), equals(Domain.of('domain'))); + }); + + group('Domain.of(arg) should throw an AssertionError with a list of invalid domains', () { + final listDomainInValid = [ + 'domain\$bad.com', + '', + 'aab..ddd', + 'aab.cc.1com', + 'abc.abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcd.com', + 'domain\$bad.com', + 'domain/bad.com', + 'domain\\bad.com', + 'domain@bad.com', + 'domain@bad.com', + 'domain%bad.com', + '#domain.com', + 'bad-.com', + 'bad_.com', + '-bad.com', + 'bad_.com', + '[domain.tld', + 'domain.tld]', + 'a[aaa]a', + '[aaa]a', + 'a[aaa]', + '[]' + ]; + for (var arg in listDomainInValid) { + test(arg, () { + expect(() => Domain.of(arg), throwsA(const TypeMatcher())); + }); + } + }); + + group('Domain.of(arg) should not throw any exceptions with the list of valid domains', () { + final listDomainValid = [ + '127.0.0.1', + 'domain.tld', + 'do-main.tld', + 'do_main.tld', + 'ab.dc.de.fr', + '123.456.789.a23', + 'acv.abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabc.fr', + 'ab--cv.fr', + 'ab__cd.fr', + 'domain', + '[domain]', + '127.0.0.1' + ]; + for (var arg in listDomainValid) { + test(arg, () { + expect(() => Domain.of(arg), returnsNormally); + }); + } + }); + + test('Domain.of(args) should remove brackets', () { + expect(Domain.of('[domain]'), equals(Domain.of('domain'))); + }); + + test('Domain.of(args) should throw AssertionError when args is null', () { + expect(() => Domain.of(null), throwsA(const TypeMatcher())); + }); + + test('Domain.of(args) should allow 253 long domain', () { + expect(Domain.of('${'aaaaaaaaa.' * 25}aaa').domainName.length, 253); + }); + + test('Domain.of(args) should throw AssertionError when too long', () { + expect(() => Domain.of('a' * 254), throwsA(const TypeMatcher())); + }); +} diff --git a/core/test/utils/mail_address_test.dart b/core/test/utils/mail_address_test.dart index 7df98548a2..2265458a8c 100644 --- a/core/test/utils/mail_address_test.dart +++ b/core/test/utils/mail_address_test.dart @@ -1,20 +1,85 @@ import 'package:core/domain/exceptions/address_exception.dart'; +import 'package:core/utils/mail/domain.dart'; import 'package:core/utils/mail/mail_address.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - group('MailAddress test', () { - test('validate method should be return MailAddress when address valid', () { - String validAddress = 'user@example.com'; - MailAddress mailAddress = MailAddress.validate(validAddress); + const String GOOD_LOCAL_PART = "\"quoted@local part\""; + const String GOOD_QUOTED_LOCAL_PART = "\"quoted@local part\"@james.apache.org"; + const String GOOD_ADDRESS = "server-dev@james.apache.org"; + final Domain GOOD_DOMAIN = Domain.of("james.apache.org"); + + final List goodAddresses = [ + GOOD_ADDRESS, + GOOD_QUOTED_LOCAL_PART, + "server-dev@james-apache.org", + "server-dev@[127.0.0.1]", + "server.dev@james.apache.org", + "\\.server-dev@james.apache.org", + "Abc@10.42.0.1", + "Abc.123@example.com", + "user+mailbox/department=shipping@example.com", + "user+mailbox@example.com", + "\"Abc@def\"@example.com", + "\"Fred Bloggs\"@example.com", + "\"Joe.\\Blow\"@example.com", + "!#\$%&'*+-/=?^_`.{|}~@example.com" + ]; + + final List badAddresses = [ + "", + "server-dev", + "server-dev@", + "[]", + "server-dev@[]", + "server-dev@#", + "quoted local-part@james.apache.org", + "quoted@local-part@james.apache.org", + "local-part.@james.apache.org", + ".local-part@james.apache.org", + "local-part@.james.apache.org", + "local-part@james.apache.org.", + "local-part@james.apache..org", + "server-dev@-james.apache.org", + "server-dev@james.apache.org-", + "server-dev@#james.apache.org", + "server-dev@#123james.apache.org", + "server-dev@#-123.james.apache.org", + "server-dev@james. apache.org", + "server-dev@james\\.apache.org", + "server-dev@[300.0.0.1]", + "server-dev@[127.0.1]", + "server-dev@[0127.0.0.1]", + "server-dev@[127.0.1.1a]", + "server-dev@[127\\.0.1.1]", + "server-dev@#123", + "server-dev@#123.apache.org", + "server-dev@[127.0.1.1.1]", + "server-dev@[127.0.1.-1]", + "\"a..b\"@domain.com", // jakarta.mail is unable to handle this so we better reject it + "server-dev\\.@james.apache.org", // jakarta.mail is unable to handle this so we better reject it + "a..b@domain.com", + // According to wikipedia these addresses are valid but as jakarta.mail is unable + // to work with them we shall rather reject them (note that this is not breaking retro-compatibility) + "Loïc.Accentué@voilà.fr8", + "pelé@exemple.com", + "δοκιμή@παράδειγμα.δοκιμή", + "我買@屋企.香港", + "二ノ宮@黒川.日本", + "медведь@с-балалайкой.рф", + "संपर्क@डाटामेल.भारत" + ]; + + group('MailAddress simple test', () { + test('MailAddress.validateAddress() should be return MailAddress when address valid', () { + MailAddress mailAddress = MailAddress.validateAddress('user@example.com'); expect(mailAddress.localPart, equals('user')); expect(mailAddress.domain.name(), equals('example.com')); }); - test('validate method should be throw AddressException when address missing @', () { - String invalidAddress = 'userexample.com'; + test('MailAddress.validateAddress() should be throw AddressException when address missing @', () { expect( - () => MailAddress.validate(invalidAddress), + () => MailAddress.validateAddress('userexample.com'), throwsA(isA().having( (e) => e.message, 'message', @@ -23,10 +88,9 @@ void main() { ); }); - test('validate method should be throw AddressException when address empty local-part', () { - String invalidAddress = '@example.com'; + test('MailAddress.validateAddress() should be throw AddressException when address empty local-part', () { expect( - () => MailAddress.validate(invalidAddress), + () => MailAddress.validateAddress('@example.com'), throwsA(isA().having( (e) => e.message, 'message', @@ -35,10 +99,9 @@ void main() { ); }); - test('validate method should be throw AddressException when address empty domain', () { - String invalidAddress = 'user@'; + test('MailAddress.validateAddress() should be throw AddressException when address empty domain', () { expect( - () => MailAddress.validate(invalidAddress), + () => MailAddress.validateAddress('user@'), throwsA(isA().having( (e) => e.message, 'message', @@ -47,10 +110,9 @@ void main() { ); }); - test('validate method should be throw AddressException when address with a hyphen "-"', () { - String invalidAddress = 'user@-example.com'; + test('MailAddress.validateAddress() should be throw AddressException when address with a hyphen "-"', () { expect( - () => MailAddress.validate(invalidAddress), + () => MailAddress.validateAddress('user@-example.com'), throwsA(isA().having( (e) => e.message, 'message', @@ -59,4 +121,49 @@ void main() { ); }); }); + + group('MailAddress advanced test', () { + group('MailAddress.validateAddress() should not throw any exceptions with the list of good address', () { + for (var arg in goodAddresses) { + test(arg, () { + expect(() => MailAddress.validateAddress(arg), returnsNormally); + }); + } + }); + + group('MailAddress.validateAddress() should throw an AddressException with a list of bad address', () { + for (var arg in badAddresses) { + test(arg, () { + expect(() => MailAddress.validateAddress(arg), throwsA(const TypeMatcher())); + }); + } + }); + + test('MailAddress.validateLocalPartAndDomain() should not throw any exceptions with good address have LocalPart and Domain', () { + expect(() => MailAddress.validateLocalPartAndDomain(localPart: 'local-part', domain: 'domain'), returnsNormally); + }); + + test('MailAddress.validateLocalPartAndDomain() should throw an AddressException with bad address have LocalPart and Domain', () { + expect(() => MailAddress.validateLocalPartAndDomain(localPart: 'local-part', domain: '-domain'), throwsA(const TypeMatcher())); + }); + + test('MailAddress.validateAddress() should not throw any exceptions with mail address is GOOD_QUOTED_LOCAL_PART', () { + expect(() => MailAddress.validateAddress(GOOD_QUOTED_LOCAL_PART), returnsNormally); + }); + + test('MailAddress.getDomain() should return GOOD_DOMAIN with address is GOOD_ADDRESS', () { + final mailAddress = MailAddress.validateAddress(GOOD_ADDRESS); + expect(mailAddress.getDomain(), equals(GOOD_DOMAIN)); + }); + + test('MailAddress.getLocalPart() should return GOOD_LOCAL_PART with address is GOOD_QUOTED_LOCAL_PART', () { + final mailAddress = MailAddress.validateAddress(GOOD_QUOTED_LOCAL_PART); + expect(mailAddress.getLocalPart(), equals(GOOD_LOCAL_PART)); + }); + + test('MailAddress.toString() should return GOOD_ADDRESS with address is GOOD_ADDRESS', () { + final mailAddress = MailAddress.validateAddress(GOOD_ADDRESS); + expect(mailAddress.toString(), equals(GOOD_ADDRESS)); + }); + }); } \ No newline at end of file diff --git a/lib/features/email/presentation/utils/email_utils.dart b/lib/features/email/presentation/utils/email_utils.dart index 63877adc28..d4eb188455 100644 --- a/lib/features/email/presentation/utils/email_utils.dart +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -77,7 +77,7 @@ class EmailUtils { static bool isEmailAddressValid(String address) { try { - return GetUtils.isEmail(address) && MailAddress.validate(address).asString().isNotEmpty; + return GetUtils.isEmail(address) && MailAddress.validateAddress(address).asString().isNotEmpty; } catch(e) { logError('EmailUtils::isEmailAddressValid: Exception = $e'); return false; From 0fa8a98c6809d9c7b142451d99381c8cfce08dd5 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 22 Mar 2024 01:38:21 +0700 Subject: [PATCH 24/80] TF-2667 Fix crash when click on From me in search filter --- lib/features/contact/presentation/contact_controller.dart | 6 +++--- lib/features/contact/presentation/contact_view.dart | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/features/contact/presentation/contact_controller.dart b/lib/features/contact/presentation/contact_controller.dart index 4dc1415749..c2abee38c5 100644 --- a/lib/features/contact/presentation/contact_controller.dart +++ b/lib/features/contact/presentation/contact_controller.dart @@ -29,6 +29,7 @@ class ContactController extends BaseController { ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.tMailContact; final searchQuery = SearchQuery.initial().obs; + final session = Rxn(); final listContactSearched = RxList(); final scrollListViewController = ScrollController(); @@ -38,7 +39,6 @@ class ContactController extends BaseController { final Debouncer _deBouncerTime = Debouncer(const Duration(milliseconds: 300), initialValue: ''); AccountId? _accountId; - Session? session; ContactArguments? arguments; EmailAddress? contactSelected; @@ -63,14 +63,14 @@ class ContactController extends BaseController { textInputSearchFocus.requestFocus(); if (arguments != null) { _accountId = arguments!.accountId; - session = arguments!.session; + session.value = arguments!.session; final listContactSelected = arguments!.listContactSelected; log('ContactController::onReady(): arguments: $arguments'); log('ContactController::onReady(): listContactSelected: $listContactSelected'); if (listContactSelected.isNotEmpty) { contactSelected = EmailAddress(listContactSelected.first, listContactSelected.first); } - injectAutoCompleteBindings(session, _accountId); + injectAutoCompleteBindings(session.value, _accountId); } if (PlatformInfo.isMobile) { Future.delayed( diff --git a/lib/features/contact/presentation/contact_view.dart b/lib/features/contact/presentation/contact_view.dart index 2818c5b9a8..1d691c4523 100644 --- a/lib/features/contact/presentation/contact_view.dart +++ b/lib/features/contact/presentation/contact_view.dart @@ -62,8 +62,6 @@ class ContactView extends GetWidget { searchInputController: controller.textInputSearchController, hasBackButton: false, hasSearchButton: true, - padding: EdgeInsets.zero, - heightSearchBar: 44, margin: ContactUtils.getPaddingSearchInputForm(context, controller.responsiveUtils), decoration: const BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(10)), @@ -80,10 +78,11 @@ class ContactView extends GetWidget { ), if (PlatformInfo.isWeb) Obx(() { - if (controller.session?.username.value.isNotEmpty == true) { + final username = controller.session.value?.username.value ?? ''; + if (username.isNotEmpty) { final userEmailAddress = EmailAddress( AppLocalizations.of(context).me, - controller.session?.username.value); + username); final fromMeSuggestionEmailAddress = SuggestionEmailAddress(userEmailAddress, state: SuggestionEmailState.valid); return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), From 6d890bba76d7ebe4133f2e51e1755b58438c4fe1 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 22 Mar 2024 02:06:06 +0700 Subject: [PATCH 25/80] TF-2667 Remove composer cache after close composer view --- .../bindings/mailbox_dashboard_bindings.dart | 2 ++ .../controller/mailbox_dashboard_controller.dart | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index 26ee066fa1..d4df25acde 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -171,6 +171,7 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); Get.put(AdvancedFilterController()); } @@ -276,6 +277,7 @@ class MailboxDashBoardBindings extends BaseBindings { ); Get.lazyPut(() => GetComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => RemoveComposerCacheOnWebInteractor(Get.find())); + Get.lazyPut(() => RemoveComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => MarkAsEmailReadInteractor( Get.find(), Get.find() diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 629e3a680a..cfd05c0de5 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -78,6 +78,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_app_da import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_composer_cache_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_composer_cache_on_web_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/app_grid_dashboard_controller.dart'; @@ -180,6 +181,7 @@ class MailboxDashBoardController extends ReloadableController { final UnsubscribeEmailInteractor _unsubscribeEmailInteractor; final RestoredDeletedMessageInteractor _restoreDeletedMessageInteractor; final GetRestoredDeletedMessageInterator _getRestoredDeletedMessageInteractor; + final RemoveComposerCacheOnWebInteractor _removeComposerCacheOnWebInteractor; GetAllVacationInteractor? _getAllVacationInteractor; UpdateVacationInteractor? _updateVacationInteractor; @@ -257,6 +259,7 @@ class MailboxDashBoardController extends ReloadableController { this._unsubscribeEmailInteractor, this._restoreDeletedMessageInteractor, this._getRestoredDeletedMessageInteractor, + this._removeComposerCacheOnWebInteractor, ); @override @@ -1248,7 +1251,7 @@ class MailboxDashBoardController extends ReloadableController { composerOverlayState.value = ComposerOverlayState.active; } - void closeComposerOverlay({dynamic result}) { + void closeComposerOverlay({dynamic result}) async { composerArguments = null; ComposerBindings().dispose(); composerOverlayState.value = ComposerOverlayState.inActive; @@ -1259,6 +1262,8 @@ class MailboxDashBoardController extends ReloadableController { result is UpdateEmailDraftsSuccess) { consumeState(Stream.value(Right(result))); } + + await _removeComposerCacheOnWeb(); } void dispatchRoute(DashboardRoutes route) { @@ -1394,6 +1399,8 @@ class MailboxDashBoardController extends ReloadableController { result is UpdateEmailDraftsSuccess) { consumeState(Stream.value(Right(result))); } + + await _removeComposerCacheOnWeb(); } } @@ -2457,7 +2464,11 @@ class MailboxDashBoardController extends ReloadableController { isRecoveringDeletedMessage.value = true; } - String get userEmail => userProfile.value?.email ?? ''; + String get userEmail => sessionCurrent?.username.value ?? ''; + + Future _removeComposerCacheOnWeb() async { + await _removeComposerCacheOnWebInteractor.execute(); + } @override void onClose() { From 9d10ffb0749be52306678d45b0fc48f31eecdbeb Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 22 Mar 2024 02:24:49 +0700 Subject: [PATCH 26/80] TF-2667 Handle error better when perform save as drafts --- ...w_and_save_email_to_drafts_interactor.dart | 25 -------- .../email/data/network/email_api.dart | 57 ++++++++----------- 2 files changed, 24 insertions(+), 58 deletions(-) diff --git a/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart b/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart index 1102b225fe..0c20eeff1a 100644 --- a/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart +++ b/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart @@ -72,13 +72,6 @@ class CreateNewAndSaveEmailToDraftsInteractor { cancelToken: cancelToken ); - await _deleteOldDraftsEmail( - session: createEmailRequest.session, - accountId: createEmailRequest.accountId, - draftEmailId: createEmailRequest.draftsEmailId!, - cancelToken: cancelToken - ); - yield dartz.Right( UpdateEmailDraftsSuccess( emailDraftSaved.id!, @@ -137,22 +130,4 @@ class CreateNewAndSaveEmailToDraftsInteractor { return null; } } - - Future _deleteOldDraftsEmail({ - required Session session, - required AccountId accountId, - required EmailId draftEmailId, - CancelToken? cancelToken - }) async { - try { - await _emailRepository.removeEmailDrafts( - session, - accountId, - draftEmailId, - cancelToken: cancelToken - ); - } catch (e) { - logError('CreateNewAndSaveEmailToDraftsInteractor::_deleteOldDraftsEmail: Exception: $e'); - } - } } \ No newline at end of file diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 44a1d59ace..4a148a663e 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -541,11 +541,14 @@ class EmailAPI with HandleSetErrorMixin { setEmailInvocation.methodCallId, SetEmailResponse.deserialize); - return Future.sync(() async { - return setEmailResponse?.destroyed?.contains(emailId.id) == true; - }).catchError((error) { - throw error; - }); + final isEmailDestroyed = setEmailResponse?.destroyed?.contains(emailId.id) ?? false; + final mapErrors = handleSetResponse([setEmailResponse]); + + if (isEmailDestroyed && mapErrors.isEmpty) { + return isEmailDestroyed; + } else { + throw SetMethodException(mapErrors); + } } Future updateEmailDrafts( @@ -555,37 +558,25 @@ class EmailAPI with HandleSetErrorMixin { EmailId oldEmailId, {CancelToken? cancelToken} ) async { - final idCreateMethod = Id(_uuid.v1()); - final setEmailMethod = SetEmailMethod(accountId) - ..addCreate(idCreateMethod, newEmail) - ..addDestroy({oldEmailId.id}); - - final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - - final setEmailInvocation = requestBuilder.invocation(setEmailMethod); - - final capabilities = setEmailMethod.requiredCapabilities - .toCapabilitiesSupportTeamMailboxes(session, accountId); - - final response = await (requestBuilder - ..usings(capabilities)) - .build() - .execute(cancelToken: cancelToken); - - final setEmailResponse = response.parse( - setEmailInvocation.methodCallId, - SetEmailResponse.deserialize + final emailCreated = await saveEmailAsDrafts( + session, + accountId, + newEmail, + cancelToken: cancelToken ); - final emailUpdated = setEmailResponse?.created?[idCreateMethod]; - final isEmailDeleted = setEmailResponse?.destroyed?.contains(oldEmailId.id); - final mapErrors = handleSetResponse([setEmailResponse]); - - if (emailUpdated != null && isEmailDeleted == true && mapErrors.isEmpty) { - return emailUpdated; - } else { - throw SetMethodException(mapErrors); + try { + await removeEmailDrafts( + session, + accountId, + oldEmailId, + cancelToken: cancelToken + ); + } catch (e) { + logError('EmailAPI::updateEmailDrafts: Exception = $e'); } + + return emailCreated; } Future> deleteMultipleEmailsPermanently( From 1d9fcc312b6825c95512d125d7c831d3aa2ccf90 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 22 Mar 2024 03:02:07 +0700 Subject: [PATCH 27/80] TF-2667 Fix width size inline image in email view after send email --- .../lib/data/network/download/download_client.dart | 14 ++++++++++---- .../composer/presentation/composer_controller.dart | 11 +++++++++-- .../composer/presentation/composer_view_web.dart | 9 ++++++--- .../controller/rich_text_web_controller.dart | 4 ++-- .../presentation/identity_creator_controller.dart | 2 +- 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/core/lib/data/network/download/download_client.dart b/core/lib/data/network/download/download_client.dart index 781eeab111..8686739f1c 100644 --- a/core/lib/data/network/download/download_client.dart +++ b/core/lib/data/network/download/download_client.dart @@ -63,7 +63,8 @@ class DownloadClient { 'bytesData': bytesData, 'mimeType': 'image/$fileExtension', 'cid': cid, - 'fileName': fileName + 'fileName': fileName, + 'maxWidth': maxWidth }); return base64Uri; @@ -77,7 +78,8 @@ class DownloadClient { 'bytesData': bytesDataCompressed, 'mimeType': 'image/$fileExtension', 'cid': cid, - 'fileName': fileName + 'fileName': fileName, + 'maxWidth': maxWidth }); return base64Uri; @@ -86,7 +88,8 @@ class DownloadClient { 'bytesData': bytesData, 'mimeType': 'image/$fileExtension', 'cid': cid, - 'fileName': fileName + 'fileName': fileName, + 'maxWidth': maxWidth }); return base64Uri; @@ -103,11 +106,14 @@ class DownloadClient { var mimeType = entryParam['mimeType']; final cid = entryParam['cid']; var fileName = entryParam['fileName']; + var maxWidth = entryParam['maxWidth'] != null + ? '${entryParam['maxWidth']}px' + : '100%'; final base64Data = base64Encode(bytesData); if (fileName.contains('.')) { fileName = fileName.split('.').first; } - final base64Uri = '$fileName'; + final base64Uri = '$fileName'; return base64Uri; } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index bce051211a..835b82e306 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1587,7 +1587,7 @@ class ComposerController extends BaseController with DragDropFileMixin { if (responsiveUtils.isMobile(context)) { maxWithEditor = maxWith - 40; } else { - maxWithEditor = maxWith - 120; + maxWithEditor = maxWith - 70; } consumeState(_localImagePickerInteractor.execute()); @@ -2000,8 +2000,15 @@ class ComposerController extends BaseController with DragDropFileMixin { void onLocalFileDropZoneListener({ required BuildContext context, - required DropDoneDetails details + required DropDoneDetails details, + required double maxWidth }) async { + if (responsiveUtils.isMobile(context)) { + maxWithEditor = maxWidth - 40; + } else { + maxWithEditor = maxWidth - 70; + } + final listFileInfo = await onDragDone(context: context, details: details); if (listFileInfo.isEmpty && context.mounted) { diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index 5433d57408..394f991255 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -248,7 +248,8 @@ class ComposerView extends GetWidget { onLocalFileDropZoneListener: (details) => controller.onLocalFileDropZoneListener( context: context, - details: details + details: details, + maxWidth: constraintsEditor.maxWidth, ), ) ), @@ -503,7 +504,8 @@ class ComposerView extends GetWidget { onLocalFileDropZoneListener: (details) => controller.onLocalFileDropZoneListener( context: context, - details: details + details: details, + maxWidth: constraintsEditor.maxWidth, ), ) ), @@ -731,7 +733,8 @@ class ComposerView extends GetWidget { onLocalFileDropZoneListener: (details) => controller.onLocalFileDropZoneListener( context: context, - details: details + details: details, + maxWidth: constraintsEditor.maxWidth, ), ) ), diff --git a/lib/features/composer/presentation/controller/rich_text_web_controller.dart b/lib/features/composer/presentation/controller/rich_text_web_controller.dart index bb8bd07fb7..76f5cf473b 100644 --- a/lib/features/composer/presentation/controller/rich_text_web_controller.dart +++ b/lib/features/composer/presentation/controller/rich_text_web_controller.dart @@ -279,11 +279,11 @@ class RichTextWebController extends BaseRichTextController { menuOrderListController.hideMenu(); } - void insertImageAsBase64({required PlatformFile platformFile}) { + void insertImageAsBase64({required PlatformFile platformFile, int? maxWidth}) { if (platformFile.bytes != null) { final base64Data = base64Encode(platformFile.bytes!); editorController.insertHtml( - 'Image in my signature' + 'Image in my signature' ); } else { logError("RichTextWebController::insertImageAsBase64: bytes is null"); diff --git a/lib/features/identity_creator/presentation/identity_creator_controller.dart b/lib/features/identity_creator/presentation/identity_creator_controller.dart index a76fd9364f..11e0d8ca27 100644 --- a/lib/features/identity_creator/presentation/identity_creator_controller.dart +++ b/lib/features/identity_creator/presentation/identity_creator_controller.dart @@ -546,7 +546,7 @@ class IdentityCreatorController extends BaseController { } } else { if (PlatformInfo.isWeb) { - richTextWebController.insertImageAsBase64(platformFile: file); + richTextWebController.insertImageAsBase64(platformFile: file, maxWidth: maxWidth); } else if (PlatformInfo.isMobile) { richTextMobileTabletController.insertImageData(platformFile: file, maxWidth: maxWidth); if (file.path != null) { From 8f19d48e1775b7039d388cd9575b32ef74a02a90 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 22 Mar 2024 03:03:13 +0700 Subject: [PATCH 28/80] TF-2667 Remove function not used --- .../presentation/composer_controller.dart | 54 ------------------- 1 file changed, 54 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 835b82e306..f88f4684fd 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -15,7 +15,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; -import 'package:html_editor_enhanced/html_editor.dart' as web_html_editor; 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'; @@ -48,7 +47,6 @@ import 'package:tmail_ui_user/features/composer/domain/usecases/save_composer_ca 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/email_action_type_extension.dart'; -import 'package:tmail_ui_user/features/composer/presentation/extensions/file_upload_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/list_shared_media_file_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/mixin/drag_drog_file_mixin.dart'; @@ -1724,58 +1722,6 @@ class ComposerController extends BaseController with DragDropFileMixin { onEditorFocusChange(true); } - void handleImageUploadSuccess ( - BuildContext context, - List listFileUpload - ) async { - log('ComposerController::handleImageUploadSuccess: COUNT_FILE_UPLOADED = ${listFileUpload.length}'); - List listFileInfo = []; - - await Future.forEach(listFileUpload, (fileUpload) async { - if (fileUpload.base64?.isNotEmpty == true) { - final fileInfo = await fileUpload.toFileInfo(); - if (fileInfo != null) { - if (fileInfo.mimeType.startsWith(MediaTypeExtension.imageType) == true) { - listFileInfo.add(fileInfo.withInline()); - } else { - listFileInfo.add(fileInfo); - } - } - } - }); - - if (listFileInfo.isEmpty && context.mounted) { - appToast.showToastErrorMessage( - context, - AppLocalizations.of(context).can_not_upload_this_file_as_attachments - ); - return; - } - - final listAttachments = listFileInfo - .where((fileInfo) => fileInfo.isInline != true) - .toList(); - - uploadController.validateTotalSizeAttachmentsBeforeUpload( - totalSizePreparedFiles: listFileInfo.totalSize, - totalSizePreparedFilesWithDispositionAttachment: listAttachments.totalSize, - onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo) - ); - } - - void handleImageUploadFailure({ - required BuildContext context, - required web_html_editor.UploadError uploadError, - List? listFileUpload, - String? base64Str, - }) { - logError('ComposerController::handleImageUploadFailure: COUNT_FILE_FAILED = ${listFileUpload?.length} | ERROR = $uploadError'); - appToast.showToastErrorMessage( - context, - '${AppLocalizations.of(context).can_not_upload_this_file_as_attachments}. (${uploadError.name})' - ); - } - FocusNode? getNextFocusOfToEmailAddress() { if (ccRecipientState.value == PrefixRecipientState.enabled) { return ccAddressFocusNode; From fe794690433171ee62d8b980f909f52410f3a0d9 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 22 Mar 2024 11:20:53 +0700 Subject: [PATCH 29/80] TF-2667 Prevent close sending/saving dialog when click system back on mobile --- .../presentation/composer_controller.dart | 129 ++++++++++-------- .../mailbox_dashboard_controller.dart | 5 + 2 files changed, 74 insertions(+), 60 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index f88f4684fd..d8ca6f5467 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -826,39 +826,44 @@ class ComposerController extends BaseController with DragDropFileMixin { required String emailContent, CancelToken? cancelToken }) { - return Get.dialog( - PointerInterceptor( - child: SendingMessageDialogView( - createEmailRequest: CreateEmailRequest( - session: mailboxDashBoardController.sessionCurrent!, - accountId: mailboxDashBoardController.accountId.value!, - emailActionType: composerArguments.value!.emailActionType, - subject: subjectEmail.value ?? '', - emailContent: emailContent, - fromSender: composerArguments.value!.presentationEmail?.from ?? {}, - toRecipients: listToEmailAddress.toSet(), - ccRecipients: listCcEmailAddress.toSet(), - bccRecipients: listBccEmailAddress.toSet(), - isRequestReadReceipt: hasRequestReadReceipt.value, - identity: identitySelected.value, - attachments: uploadController.attachmentsUploaded, - inlineAttachments: uploadController.mapInlineAttachments, - outboxMailboxId: mailboxDashBoardController.outboxMailbox?.mailboxId, - sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], - draftsEmailId: composerArguments.value!.emailActionType == EmailActionType.editDraft - ? composerArguments.value!.presentationEmail?.id - : null, - answerForwardEmailId: composerArguments.value!.presentationEmail?.id, - unsubscribeEmailId: composerArguments.value!.previousEmailId, - messageId: composerArguments.value!.messageId, - references: composerArguments.value!.references, - emailSendingQueue: composerArguments.value!.sendingEmail - ), - createNewAndSendEmailInteractor: _createNewAndSendEmailInteractor, - onCancelSendingEmailAction: _handleCancelSendingMessage, - cancelToken: cancelToken, + final childWidget = PointerInterceptor( + child: SendingMessageDialogView( + createEmailRequest: CreateEmailRequest( + session: mailboxDashBoardController.sessionCurrent!, + accountId: mailboxDashBoardController.accountId.value!, + emailActionType: composerArguments.value!.emailActionType, + subject: subjectEmail.value ?? '', + emailContent: emailContent, + fromSender: composerArguments.value!.presentationEmail?.from ?? {}, + toRecipients: listToEmailAddress.toSet(), + ccRecipients: listCcEmailAddress.toSet(), + bccRecipients: listBccEmailAddress.toSet(), + isRequestReadReceipt: hasRequestReadReceipt.value, + identity: identitySelected.value, + attachments: uploadController.attachmentsUploaded, + inlineAttachments: uploadController.mapInlineAttachments, + outboxMailboxId: mailboxDashBoardController.outboxMailbox?.mailboxId, + sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], + draftsEmailId: composerArguments.value!.emailActionType == EmailActionType.editDraft + ? composerArguments.value!.presentationEmail?.id + : null, + answerForwardEmailId: composerArguments.value!.presentationEmail?.id, + unsubscribeEmailId: composerArguments.value!.previousEmailId, + messageId: composerArguments.value!.messageId, + references: composerArguments.value!.references, + emailSendingQueue: composerArguments.value!.sendingEmail ), + createNewAndSendEmailInteractor: _createNewAndSendEmailInteractor, + onCancelSendingEmailAction: _handleCancelSendingMessage, + cancelToken: cancelToken, ), + ); + + return Get.dialog( + PlatformInfo.isMobile + ? PopScope(canPop: false, child: childWidget) + : childWidget, + barrierDismissible: false, barrierColor: AppColor.colorDefaultCupertinoActionSheet, ); } @@ -2035,37 +2040,41 @@ class ComposerController extends BaseController with DragDropFileMixin { EmailId? draftEmailId, CancelToken? cancelToken, }) { - return Get.dialog( - PointerInterceptor( - child: SavingMessageDialogView( - createEmailRequest: CreateEmailRequest( - session: mailboxDashBoardController.sessionCurrent!, - accountId: mailboxDashBoardController.accountId.value!, - emailActionType: composerArguments.value!.emailActionType, - subject: subjectEmail.value ?? '', - emailContent: emailContent, - fromSender: composerArguments.value!.presentationEmail?.from ?? {}, - toRecipients: listToEmailAddress.toSet(), - ccRecipients: listCcEmailAddress.toSet(), - bccRecipients: listBccEmailAddress.toSet(), - isRequestReadReceipt: hasRequestReadReceipt.value, - identity: identitySelected.value, - attachments: uploadController.attachmentsUploaded, - inlineAttachments: uploadController.mapInlineAttachments, - sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], - draftsMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts], - draftsEmailId: draftEmailId, - answerForwardEmailId: composerArguments.value!.presentationEmail?.id, - unsubscribeEmailId: composerArguments.value!.previousEmailId, - messageId: composerArguments.value!.messageId, - references: composerArguments.value!.references, - emailSendingQueue: composerArguments.value!.sendingEmail - ), - createNewAndSaveEmailToDraftsInteractor: _createNewAndSaveEmailToDraftsInteractor, - onCancelSavingEmailToDraftsAction: _handleCancelSavingMessageToDrafts, - cancelToken: cancelToken, + final childWidget = PointerInterceptor( + child: SavingMessageDialogView( + createEmailRequest: CreateEmailRequest( + session: mailboxDashBoardController.sessionCurrent!, + accountId: mailboxDashBoardController.accountId.value!, + emailActionType: composerArguments.value!.emailActionType, + subject: subjectEmail.value ?? '', + emailContent: emailContent, + fromSender: composerArguments.value!.presentationEmail?.from ?? {}, + toRecipients: listToEmailAddress.toSet(), + ccRecipients: listCcEmailAddress.toSet(), + bccRecipients: listBccEmailAddress.toSet(), + isRequestReadReceipt: hasRequestReadReceipt.value, + identity: identitySelected.value, + attachments: uploadController.attachmentsUploaded, + inlineAttachments: uploadController.mapInlineAttachments, + sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], + draftsMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts], + draftsEmailId: draftEmailId, + answerForwardEmailId: composerArguments.value!.presentationEmail?.id, + unsubscribeEmailId: composerArguments.value!.previousEmailId, + messageId: composerArguments.value!.messageId, + references: composerArguments.value!.references, + emailSendingQueue: composerArguments.value!.sendingEmail ), + createNewAndSaveEmailToDraftsInteractor: _createNewAndSaveEmailToDraftsInteractor, + onCancelSavingEmailToDraftsAction: _handleCancelSavingMessageToDrafts, + cancelToken: cancelToken, ), + ); + return Get.dialog( + PlatformInfo.isMobile + ? PopScope(canPop: false, child: childWidget) + : childWidget, + barrierDismissible: false, barrierColor: AppColor.colorDefaultCupertinoActionSheet, ); } diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index cfd05c0de5..e1a7aa38e6 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -1391,7 +1391,12 @@ class MailboxDashBoardController extends ReloadableController { openComposerOverlay(arguments); } } else { + BackButtonInterceptor.removeByName(AppRoutes.dashboard); + final result = await push(AppRoutes.composer, arguments: arguments); + + BackButtonInterceptor.add(_onBackButtonInterceptor, name: AppRoutes.dashboard); + if (result is SendingEmailArguments) { handleSendEmailAction(result); } else if (result is SendEmailSuccess || From 0b77f74f1b2c367ca004f40c9510d2c148f3df5a Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 22 Mar 2024 15:35:25 +0700 Subject: [PATCH 30/80] TF-2667 Store email to sending queue when sending with no connection on mobile --- .../presentation/composer_controller.dart | 2 +- .../mailbox_dashboard_controller.dart | 22 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index d8ca6f5467..08e3745c6a 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -807,7 +807,7 @@ class ComposerController extends BaseController with DragDropFileMixin { cancelToken: cancelToken ); log('ComposerController::_handleSendMessages: resultState = $resultState'); - if (resultState is SendEmailSuccess) { + if (resultState is SendEmailSuccess || mailboxDashBoardController.validateSendingEmailFailedWhenNetworkIsLostOnMobile(resultState)) { _sendButtonState = ButtonState.enabled; _closeComposerAction(result: resultState); } else if (resultState is SendEmailFailure && resultState.exception is SendingEmailCanceledException) { diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index e1a7aa38e6..b6ca0d6441 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -392,7 +392,10 @@ class MailboxDashBoardController extends ReloadableController { @override void handleExceptionAction({Failure? failure, Exception? exception}) { super.handleExceptionAction(failure: failure, exception: exception); - if (failure is SendEmailFailure && exception is NoNetworkError) { + if (failure is SendEmailFailure && + exception is NoNetworkError && + PlatformInfo.isMobile + ) { log('MailboxDashBoardController::handleExceptionAction(): $failure'); _storeSendingEmailInCaseOfSendingFailureInMobile(failure); } @@ -1261,6 +1264,8 @@ class MailboxDashBoardController extends ReloadableController { result is SaveEmailAsDraftsSuccess || result is UpdateEmailDraftsSuccess) { consumeState(Stream.value(Right(result))); + } else if (validateSendingEmailFailedWhenNetworkIsLostOnMobile(result)) { + _storeSendingEmailInCaseOfSendingFailureInMobile(result); } await _removeComposerCacheOnWeb(); @@ -1403,6 +1408,8 @@ class MailboxDashBoardController extends ReloadableController { result is SaveEmailAsDraftsSuccess || result is UpdateEmailDraftsSuccess) { consumeState(Stream.value(Right(result))); + } else if (validateSendingEmailFailedWhenNetworkIsLostOnMobile(result)) { + _storeSendingEmailInCaseOfSendingFailureInMobile(result); } await _removeComposerCacheOnWeb(); @@ -1813,7 +1820,9 @@ class MailboxDashBoardController extends ReloadableController { void _handleSendEmailFailure(SendEmailFailure failure) { logError('MailboxDashBoardController::_handleSendEmailFailure():failure: $failure'); - _storeSendingEmailInCaseOfSendingFailureInMobile(failure); + if (PlatformInfo.isMobile) { + _storeSendingEmailInCaseOfSendingFailureInMobile(failure); + } if (currentContext == null) { clearState(); return; @@ -1943,8 +1952,7 @@ class MailboxDashBoardController extends ReloadableController { } void _storeSendingEmailInCaseOfSendingFailureInMobile(SendEmailFailure failure) { - if (PlatformInfo.isMobile && - failure.session != null && + if (failure.session != null && failure.accountId != null && failure.emailRequest != null ) { @@ -2475,6 +2483,12 @@ class MailboxDashBoardController extends ReloadableController { await _removeComposerCacheOnWebInteractor.execute(); } + bool validateSendingEmailFailedWhenNetworkIsLostOnMobile(FeatureFailure failure) { + return failure is SendEmailFailure && + failure.exception is NoNetworkError && + PlatformInfo.isMobile; + } + @override void onClose() { _emailReceiveManager.closeEmailReceiveManagerStream(); From 30038febaf9bb06cba8fa6ca08d0573723008bb9 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 12 Mar 2024 23:41:01 +0700 Subject: [PATCH 31/80] TF-2684 Add long press action for TMailButtonWidget --- core/lib/presentation/action/action_callback_define.dart | 3 ++- .../lib/presentation/views/button/tmail_button_widget.dart | 7 +++++++ .../views/container/tmail_container_widget.dart | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/core/lib/presentation/action/action_callback_define.dart b/core/lib/presentation/action/action_callback_define.dart index 8bce05143e..95baa0b255 100644 --- a/core/lib/presentation/action/action_callback_define.dart +++ b/core/lib/presentation/action/action_callback_define.dart @@ -2,4 +2,5 @@ import 'package:flutter/material.dart'; typedef OnTapActionCallback = void Function(); -typedef OnTapActionAtPositionCallback = void Function(RelativeRect position); \ No newline at end of file +typedef OnTapActionAtPositionCallback = void Function(RelativeRect position); +typedef OnLongPressActionCallback = void Function(); \ No newline at end of file diff --git a/core/lib/presentation/views/button/tmail_button_widget.dart b/core/lib/presentation/views/button/tmail_button_widget.dart index 3f3a645c1d..1a8f8e1516 100644 --- a/core/lib/presentation/views/button/tmail_button_widget.dart +++ b/core/lib/presentation/views/button/tmail_button_widget.dart @@ -10,6 +10,7 @@ class TMailButtonWidget extends StatelessWidget { final OnTapActionCallback? onTapActionCallback; final OnTapActionAtPositionCallback? onTapActionAtPositionCallback; + final OnLongPressActionCallback? onLongPressActionCallback; final double borderRadius; final double? width; @@ -44,6 +45,7 @@ class TMailButtonWidget extends StatelessWidget { required this.text, this.onTapActionCallback, this.onTapActionAtPositionCallback, + this.onLongPressActionCallback, this.borderRadius = 20, this.width, this.maxWidth = double.infinity, @@ -77,6 +79,7 @@ class TMailButtonWidget extends StatelessWidget { final Key? key, OnTapActionCallback? onTapActionCallback, OnTapActionAtPositionCallback? onTapActionAtPositionCallback, + OnLongPressActionCallback? onLongPressActionCallback, double borderRadius = 20, double? width, double maxWidth = double.infinity, @@ -99,6 +102,7 @@ class TMailButtonWidget extends StatelessWidget { text: '', onTapActionCallback: onTapActionCallback, onTapActionAtPositionCallback: onTapActionAtPositionCallback, + onLongPressActionCallback: onLongPressActionCallback, borderRadius: borderRadius, width: width, maxWidth : maxWidth, @@ -124,6 +128,7 @@ class TMailButtonWidget extends StatelessWidget { final Key? key, OnTapActionCallback? onTapActionCallback, OnTapActionAtPositionCallback? onTapActionAtPositionCallback, + OnLongPressActionCallback? onLongPressActionCallback, double borderRadius = 20, double? width, double maxWidth = double.infinity, @@ -145,6 +150,7 @@ class TMailButtonWidget extends StatelessWidget { text: text, onTapActionCallback: onTapActionCallback, onTapActionAtPositionCallback: onTapActionAtPositionCallback, + onLongPressActionCallback: onLongPressActionCallback, borderRadius: borderRadius, width: width, maxWidth : maxWidth, @@ -335,6 +341,7 @@ class TMailButtonWidget extends StatelessWidget { return TMailContainerWidget( onTapActionCallback: onTapActionCallback, onTapActionAtPositionCallback: onTapActionAtPositionCallback, + onLongPressActionCallback: onLongPressActionCallback, borderRadius: borderRadius, width: width, maxWidth: maxWidth, diff --git a/core/lib/presentation/views/container/tmail_container_widget.dart b/core/lib/presentation/views/container/tmail_container_widget.dart index 307bdc3df8..525e92f24b 100644 --- a/core/lib/presentation/views/container/tmail_container_widget.dart +++ b/core/lib/presentation/views/container/tmail_container_widget.dart @@ -7,6 +7,7 @@ class TMailContainerWidget extends StatelessWidget { final OnTapActionCallback? onTapActionCallback; final OnTapActionAtPositionCallback? onTapActionAtPositionCallback; + final OnLongPressActionCallback? onLongPressActionCallback; final Widget child; final double borderRadius; @@ -26,6 +27,7 @@ class TMailContainerWidget extends StatelessWidget { required this.child, this.onTapActionCallback, this.onTapActionAtPositionCallback, + this.onLongPressActionCallback, this.borderRadius = 20, this.width, this.maxWidth = double.infinity, @@ -58,6 +60,7 @@ class TMailContainerWidget extends StatelessWidget { onTapActionAtPositionCallback!.call(position); } }, + onLongPress: onLongPressActionCallback, borderRadius: BorderRadius.all(Radius.circular(borderRadius)), child: tooltipMessage != null ? Tooltip( From f246465a2f61f36643c8fda5082c19f34bf8753f Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 13 Mar 2024 01:08:24 +0700 Subject: [PATCH 32/80] TF-2684 Use scrollbar theme replace customize scrollbar --- .../extensions/color_extension.dart | 1 - core/lib/presentation/utils/theme_utils.dart | 6 +- .../presentation/contact_controller.dart | 2 - .../contact/presentation/contact_view.dart | 44 ++++---- .../presentation/destination_picker_view.dart | 2 + .../controller/single_email_controller.dart | 2 - .../presentation/mailbox_view_web.dart | 12 +- .../mailbox_visibility_view.dart | 7 +- .../menu/settings/settings_controller.dart | 8 -- .../settings/settings_first_level_view.dart | 17 +-- .../email/presentation/search_email_view.dart | 106 +++++++++--------- .../search_mailbox_controller.dart | 2 - .../presentation/search_mailbox_view.dart | 9 +- .../thread/presentation/thread_view.dart | 59 +++++----- 14 files changed, 117 insertions(+), 160 deletions(-) diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index f1dd580168..14c2f4e1b8 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -160,7 +160,6 @@ extension AppColor on Color { static const colorBackgroundQuotasWarning = Color(0xFFFFC107); static const colorQuotaWarning = Color(0xFFF05C44); static const colorQuotaError = Color(0xffE64646); - static const colorThumbScrollBar = Color(0xFFAEB7C2); static const colorCreateNewIdentityButton = Color(0xFFEBEDF0); static const colorSpamReportBannerBackground = Color(0xFFBFDEFF); static const colorSpamReportBannerStrokeBorder = Color(0x1F000000); diff --git a/core/lib/presentation/utils/theme_utils.dart b/core/lib/presentation/utils/theme_utils.dart index 7755ba0c0a..2be885946f 100644 --- a/core/lib/presentation/utils/theme_utils.dart +++ b/core/lib/presentation/utils/theme_utils.dart @@ -14,9 +14,9 @@ class ThemeUtils { dividerTheme: _dividerTheme, visualDensity: VisualDensity.adaptivePlatformDensity, scrollbarTheme: ScrollbarThemeData( - thickness: MaterialStateProperty.all(2.0), - radius: const Radius.circular(5.0), - thumbColor: MaterialStateProperty.all(AppColor.colorThumbScrollBar)), + thickness: MaterialStateProperty.all(8.0), + radius: const Radius.circular(8.0), + thumbColor: MaterialStateProperty.all(AppColor.thumbScrollbarColor)), ); } diff --git a/lib/features/contact/presentation/contact_controller.dart b/lib/features/contact/presentation/contact_controller.dart index c2abee38c5..2d243e1f52 100644 --- a/lib/features/contact/presentation/contact_controller.dart +++ b/lib/features/contact/presentation/contact_controller.dart @@ -31,7 +31,6 @@ class ContactController extends BaseController { final searchQuery = SearchQuery.initial().obs; final session = Rxn(); final listContactSearched = RxList(); - final scrollListViewController = ScrollController(); GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; @@ -85,7 +84,6 @@ class ContactController extends BaseController { textInputSearchFocus.dispose(); textInputSearchController.dispose(); _deBouncerTime.cancel(); - scrollListViewController.dispose(); super.onClose(); } diff --git a/lib/features/contact/presentation/contact_view.dart b/lib/features/contact/presentation/contact_view.dart index 1d691c4523..a67fc2fca7 100644 --- a/lib/features/contact/presentation/contact_view.dart +++ b/lib/features/contact/presentation/contact_view.dart @@ -137,30 +137,26 @@ class ContactView extends GetWidget { return Container( color: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 4), - child: ScrollbarListView( - scrollController: controller.scrollListViewController, - child: ListView.separated( - itemCount: controller.listContactSearched.length, - controller: controller.scrollListViewController, - separatorBuilder: (context, index) { - return Padding( - padding: ContactUtils.getPaddingDividerSearchResultList(context, controller.responsiveUtils), - child: const Divider(height: 1, color: AppColor.colorDivider), - ); - }, - itemBuilder: (context, index) { - final emailAddress = controller.listContactSearched[index]; - final suggestionEmailAddress = _toSuggestionEmailAddress( - emailAddress, - controller.contactSelected != null ? [controller.contactSelected!] : [] - ); - return ContactSuggestionBoxItem( - suggestionEmailAddress, - padding: ContactUtils.getPaddingSearchResultList(context, controller.responsiveUtils), - selectedContactCallbackAction: (contact) => controller.selectContact(context, contact), - ); - } - ), + child: ListView.separated( + itemCount: controller.listContactSearched.length, + separatorBuilder: (context, index) { + return Padding( + padding: ContactUtils.getPaddingDividerSearchResultList(context, controller.responsiveUtils), + child: const Divider(height: 1, color: AppColor.colorDivider), + ); + }, + itemBuilder: (context, index) { + final emailAddress = controller.listContactSearched[index]; + final suggestionEmailAddress = _toSuggestionEmailAddress( + emailAddress, + controller.contactSelected != null ? [controller.contactSelected!] : [] + ); + return ContactSuggestionBoxItem( + suggestionEmailAddress, + padding: ContactUtils.getPaddingSearchResultList(context, controller.responsiveUtils), + selectedContactCallbackAction: (contact) => controller.selectContact(context, contact), + ); + } ) ); } diff --git a/lib/features/destination_picker/presentation/destination_picker_view.dart b/lib/features/destination_picker/presentation/destination_picker_view.dart index c0781617f5..57cce40785 100644 --- a/lib/features/destination_picker/presentation/destination_picker_view.dart +++ b/lib/features/destination_picker/presentation/destination_picker_view.dart @@ -154,6 +154,7 @@ class DestinationPickerView extends GetWidget PointerDeviceKind.mouse, PointerDeviceKind.trackpad }, + scrollbars: false ), scrollController: controller.destinationListScrollController, child: RefreshIndicator( @@ -179,6 +180,7 @@ class DestinationPickerView extends GetWidget PointerDeviceKind.mouse, PointerDeviceKind.trackpad }, + scrollbars: false ), scrollController: controller.destinationListScrollController, child: RefreshIndicator( diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 18cb841f6b..05b04c452c 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -115,7 +115,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final _downloadManager = Get.find(); final _printUtils = Get.find(); final _attachmentListScrollController = ScrollController(); - final emailContentScrollController = ScrollController(); final GetEmailContentInteractor _getEmailContentInteractor; final MarkAsEmailReadInteractor _markAsEmailReadInteractor; @@ -190,7 +189,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void onClose() { _downloadProgressStateController.close(); _attachmentListScrollController.dispose(); - emailContentScrollController.dispose(); super.onClose(); } diff --git a/lib/features/mailbox/presentation/mailbox_view_web.dart b/lib/features/mailbox/presentation/mailbox_view_web.dart index 976176ed0c..14847e24fe 100644 --- a/lib/features/mailbox/presentation/mailbox_view_web.dart +++ b/lib/features/mailbox/presentation/mailbox_view_web.dart @@ -88,6 +88,7 @@ class MailboxView extends BaseMailboxView { PointerDeviceKind.mouse, PointerDeviceKind.trackpad }, + scrollbars: false ), child: RefreshIndicator( color: AppColor.primaryColor, @@ -131,7 +132,11 @@ class MailboxView extends BaseMailboxView { const SizedBox(height: 8), const Divider(color: AppColor.colorDividerMailbox, height: 1), Padding( - padding: const EdgeInsetsDirectional.symmetric(vertical: 4), + padding: EdgeInsetsDirectional.only( + top: 4, + bottom: 4, + start: controller.responsiveUtils.isDesktop(context) ? 0 : 16 + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -249,7 +254,10 @@ class MailboxView extends BaseMailboxView { controller.imagePaths, categories, controller, - toggleMailboxCategories: controller.toggleMailboxCategories + toggleMailboxCategories: controller.toggleMailboxCategories, + padding: controller.responsiveUtils.isDesktop(context) + ? null + : const EdgeInsetsDirectional.only(start: 16) ), AnimatedContainer( duration: const Duration(milliseconds: 400), diff --git a/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_view.dart b/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_view.dart index e7a49f215c..af45f06363 100644 --- a/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_view.dart +++ b/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_view.dart @@ -43,12 +43,7 @@ class MailboxVisibilityView extends GetWidget _buildLoadingView(), Expanded(child: Padding( padding: MailboxVisibilityUtils.getPaddingListView(context, controller.responsiveUtils), - child: PlatformInfo.isMobile - ? _buildListMailbox(context) - : ScrollbarListView( - scrollController: controller.mailboxListScrollController, - child: _buildListMailbox(context) - ) + child: _buildListMailbox(context) )) ] ), diff --git a/lib/features/manage_account/presentation/menu/settings/settings_controller.dart b/lib/features/manage_account/presentation/menu/settings/settings_controller.dart index 7374417010..d08766d9f4 100644 --- a/lib/features/manage_account/presentation/menu/settings/settings_controller.dart +++ b/lib/features/manage_account/presentation/menu/settings/settings_controller.dart @@ -1,6 +1,5 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/manage_account_dashboard_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/account_menu_item.dart'; @@ -9,15 +8,8 @@ class SettingsController extends GetxController { final manageAccountDashboardController = Get.find(); final responsiveUtils = Get.find(); final imagePaths = Get.find(); - final settingScrollController = ScrollController(); void selectSettings(AccountMenuItem accountMenuItem) => manageAccountDashboardController.selectSettings(accountMenuItem); void backToUniversalSettings() => manageAccountDashboardController.backToUniversalSettings(); - - @override - void onClose() { - settingScrollController.dispose(); - super.onClose(); - } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart b/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart index 81a9914d74..fb145a0208 100644 --- a/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart +++ b/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart @@ -1,8 +1,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/widgets/user_information_widget.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings/settings_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings_utils.dart'; @@ -15,8 +13,7 @@ class SettingsFirstLevelView extends GetWidget { @override Widget build(BuildContext context) { - final child = SingleChildScrollView( - controller: PlatformInfo.isMobile ? null : controller.settingScrollController, + return SingleChildScrollView( child: Column(children: [ Obx(() => UserInformationWidget( userName: controller.manageAccountDashboardController.accountId.value != null @@ -156,17 +153,5 @@ class SettingsFirstLevelView extends GetWidget { ), ]), ); - - if (PlatformInfo.isMobile) { - return child; - } else { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: ScrollbarListView( - scrollController: controller.settingScrollController, - child: child - ), - ); - } } } diff --git a/lib/features/search/email/presentation/search_email_view.dart b/lib/features/search/email/presentation/search_email_view.dart index c311767f71..2f2d5600bb 100644 --- a/lib/features/search/email/presentation/search_email_view.dart +++ b/lib/features/search/email/presentation/search_email_view.dart @@ -10,7 +10,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; -import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; @@ -497,7 +496,7 @@ class SearchEmailView extends GetWidget searchQuery: controller.searchQuery, isShowingEmailContent: controller.mailboxDashBoardController.selectedEmail.value?.id == currentPresentationEmail.id, isSearchEmailRunning: true, - padding: SearchEmailUtils.getPaddingItemListMobile(context, controller.responsiveUtils), + padding: SearchEmailUtils.getPaddingSearchResultList(context, controller.responsiveUtils), mailboxContain: currentPresentationEmail.mailboxContain, emailActionClick: (action, email) { controller.pressEmailAction( @@ -533,61 +532,58 @@ class SearchEmailView extends GetWidget } }, ) - : ScrollbarListView( - scrollController: controller.resultSearchScrollController, - child: ListView.separated( - controller: controller.resultSearchScrollController, - physics: const AlwaysScrollableScrollPhysics(), - key: const PageStorageKey('list_presentation_email_in_search_view'), - itemCount: listPresentationEmail.length, - itemBuilder: (context, index) { - final currentPresentationEmail = listPresentationEmail[index]; - return Obx(() => EmailTileBuilder( - presentationEmail: currentPresentationEmail, - selectAllMode: controller.selectionMode.value, - searchQuery: controller.searchQuery, - isShowingEmailContent: controller.mailboxDashBoardController.selectedEmail.value?.id == currentPresentationEmail.id, - isSearchEmailRunning: true, - padding: SearchEmailUtils.getPaddingSearchResultList(context, controller.responsiveUtils), - mailboxContain: currentPresentationEmail.mailboxContain, - emailActionClick: (action, email) { - controller.pressEmailAction( - context, - action, - email, - mailboxContain: currentPresentationEmail.mailboxContain - ); - }, - onMoreActionClick: (email, position) { - if (controller.responsiveUtils.isScreenWithShortestSide(context)) { - controller.openContextMenuAction( - context, - _contextMenuActionTile(context, email) - ); - } else { - controller.openPopupMenuAction( - context, - position, - _popupMenuActionTile(context, email) - ); - } - }, - - )); - }, - separatorBuilder: (context, index) { - return Padding( - padding: ItemEmailTileStyles.getPaddingDividerWeb(context, controller.responsiveUtils), - child: Divider( - color: index < listPresentationEmail.length - 1 && - controller.selectionMode.value == SelectMode.INACTIVE - ? null - : Colors.white - ) + : ListView.separated( + controller: controller.resultSearchScrollController, + physics: const AlwaysScrollableScrollPhysics(), + key: const PageStorageKey('list_presentation_email_in_search_view'), + itemCount: listPresentationEmail.length, + itemBuilder: (context, index) { + final currentPresentationEmail = listPresentationEmail[index]; + return Obx(() => EmailTileBuilder( + presentationEmail: currentPresentationEmail, + selectAllMode: controller.selectionMode.value, + searchQuery: controller.searchQuery, + isShowingEmailContent: controller.mailboxDashBoardController.selectedEmail.value?.id == currentPresentationEmail.id, + isSearchEmailRunning: true, + padding: SearchEmailUtils.getPaddingSearchResultList(context, controller.responsiveUtils), + mailboxContain: currentPresentationEmail.mailboxContain, + emailActionClick: (action, email) { + controller.pressEmailAction( + context, + action, + email, + mailboxContain: currentPresentationEmail.mailboxContain ); }, - ) - ) + onMoreActionClick: (email, position) { + if (controller.responsiveUtils.isScreenWithShortestSide(context)) { + controller.openContextMenuAction( + context, + _contextMenuActionTile(context, email) + ); + } else { + controller.openPopupMenuAction( + context, + position, + _popupMenuActionTile(context, email) + ); + } + }, + + )); + }, + separatorBuilder: (context, index) { + return Padding( + padding: ItemEmailTileStyles.getPaddingDividerWeb(context, controller.responsiveUtils), + child: Divider( + color: index < listPresentationEmail.length - 1 && + controller.selectionMode.value == SelectMode.INACTIVE + ? null + : Colors.white + ) + ); + }, + ) ); } diff --git a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart index a04f107793..83a0fe9d88 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart @@ -81,7 +81,6 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa final currentSearchQuery = RxString(''); final listMailboxSearched = RxList(); final textInputSearchController = TextEditingController(); - final scrollbarController = ScrollController(); late Debouncer _deBouncerTime; PresentationMailbox? get selectedMailbox => dashboardController.selectedMailbox.value; @@ -730,7 +729,6 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa void onClose() { textInputSearchController.dispose(); _deBouncerTime.cancel(); - scrollbarController.dispose(); super.onClose(); } } \ No newline at end of file diff --git a/lib/features/search/mailbox/presentation/search_mailbox_view.dart b/lib/features/search/mailbox/presentation/search_mailbox_view.dart index ff83bc6b66..beeb620712 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_view.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_view.dart @@ -11,7 +11,6 @@ import 'package:get/get.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; -import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/search_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/context_item_mailbox_action.dart'; @@ -58,12 +57,7 @@ class SearchMailboxView extends GetWidget const Divider(color: AppColor.colorDividerComposer, height: 1), _buildLoadingView(), Expanded( - child: PlatformInfo.isMobile - ? _buildMailboxListView(context) - : ScrollbarListView( - scrollController: controller.scrollbarController, - child: _buildMailboxListView(context) - ) + child: _buildMailboxListView(context) ) ]), ); @@ -179,7 +173,6 @@ class SearchMailboxView extends GetWidget padding: SearchMailboxUtils.getPaddingListViewMailboxSearched(context, controller.responsiveUtils), key: const Key('list_mailbox_searched'), itemCount: controller.listMailboxSearched.length, - controller: controller.scrollbarController, shrinkWrap: true, primary: false, itemBuilder: (context, index) { diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index 0444ce05ea..e25e86b366 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -381,38 +381,35 @@ class ThreadView extends GetWidget focusNode: controller.focusNodeKeyBoard, autofocus: true, onKey: controller.handleKeyEvent, - child: ScrollbarListView( - scrollController: controller.listEmailController, - child: ListView.separated( - key: const PageStorageKey('list_presentation_email_in_threads'), - controller: controller.listEmailController, - physics: const AlwaysScrollableScrollPhysics(), - itemCount: listPresentationEmail.length + 2, - itemBuilder: (context, index) => Obx(() { - if (index == listPresentationEmail.length) { - return _buildLoadMoreButton( - context, - controller.loadingMoreStatus.value); - } - if (index == listPresentationEmail.length + 1) { - return _buildLoadMoreProgressBar(controller.loadingMoreStatus.value); - } - return _buildEmailItem( + child: ListView.separated( + key: const PageStorageKey('list_presentation_email_in_threads'), + controller: controller.listEmailController, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: listPresentationEmail.length + 2, + itemBuilder: (context, index) => Obx(() { + if (index == listPresentationEmail.length) { + return _buildLoadMoreButton( context, - listPresentationEmail[index]); - }), - separatorBuilder: (context, index) { - return Padding( - padding: ItemEmailTileStyles.getPaddingDividerWeb(context, controller.responsiveUtils), - child: Divider( - color: index < listPresentationEmail.length - 1 && - controller.mailboxDashBoardController.currentSelectMode.value == SelectMode.INACTIVE - ? null - : Colors.white, - ) - ); - }, - ), + controller.loadingMoreStatus.value); + } + if (index == listPresentationEmail.length + 1) { + return _buildLoadMoreProgressBar(controller.loadingMoreStatus.value); + } + return _buildEmailItem( + context, + listPresentationEmail[index]); + }), + separatorBuilder: (context, index) { + return Padding( + padding: ItemEmailTileStyles.getPaddingDividerWeb(context, controller.responsiveUtils), + child: Divider( + color: index < listPresentationEmail.length - 1 && + controller.mailboxDashBoardController.currentSelectMode.value == SelectMode.INACTIVE + ? null + : Colors.white, + ) + ); + }, ), ) ); From 38ca8cfcb6e7a6240244ce2dfb03c036aaced80d Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 13 Mar 2024 01:08:51 +0700 Subject: [PATCH 33/80] TF-2684 Enable selectable in blue bar --- .../base/widget/hyper_link_widget.dart | 4 +- .../email/presentation/email_view.dart | 398 ++++++++--------- .../styles/email_subject_styles.dart | 1 - .../calendar_event/attendee_widget.dart | 4 +- .../calendar_event_action_banner_widget.dart | 4 +- .../calendar_event_information_widget.dart | 8 +- .../event_attendee_information_widget.dart | 4 +- .../calendar_event/organizer_widget.dart | 4 +- .../widgets/email_receiver_widget.dart | 422 +++++++++--------- .../widgets/email_subject_widget.dart | 4 +- .../widgets/prefix_recipient_widget.dart | 26 ++ 11 files changed, 460 insertions(+), 419 deletions(-) create mode 100644 lib/features/email/presentation/widgets/prefix_recipient_widget.dart diff --git a/lib/features/base/widget/hyper_link_widget.dart b/lib/features/base/widget/hyper_link_widget.dart index a06d0ac5c5..55cf8d4122 100644 --- a/lib/features/base/widget/hyper_link_widget.dart +++ b/lib/features/base/widget/hyper_link_widget.dart @@ -11,8 +11,8 @@ class HyperLinkWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return RichText( - text: TextSpan( + return Text.rich( + TextSpan( text: urlString, style: const TextStyle( color: HyperLinkWidgetStyles.textColor, diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 94c9d11df6..742d73c61a 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -46,215 +46,215 @@ class EmailView extends GetWidget { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: controller.responsiveUtils.isWebDesktop(context) - ? AppColor.colorBgDesktop - : Colors.white, - appBar: PlatformInfo.isIOS - ? PreferredSize( - preferredSize: const Size(double.infinity, 100), + return SelectionArea( + child: Scaffold( + backgroundColor: controller.responsiveUtils.isWebDesktop(context) + ? AppColor.colorBgDesktop + : Colors.white, + appBar: PlatformInfo.isIOS + ? PreferredSize( + preferredSize: const Size(double.infinity, 100), + child: Obx(() { + if (controller.currentEmail != null) { + return SafeArea( + top: false, + bottom: false, + child: EmailViewAppBarWidget( + key: const Key('email_view_app_bar_widget'), + presentationEmail: controller.currentEmail!, + mailboxContain: _getMailboxContain(controller.currentEmail!), + isSearchActivated: controller.mailboxDashBoardController.searchController.isSearchEmailRunning, + onBackAction: () => controller.closeEmailView(context: context), + onEmailActionClick: (email, action) => controller.handleEmailAction(context, email, action), + onMoreActionClick: (presentationEmail, position) => _handleMoreEmailAction(context: context, presentationEmail: presentationEmail, position: position) + ), + ); + } else { + return const SizedBox.shrink(); + } + }) + ) + : null, + body: SafeArea( + right: controller.responsiveUtils.isLandscapeMobile(context), + left: controller.responsiveUtils.isLandscapeMobile(context), + bottom: !PlatformInfo.isIOS, + child: Container( + clipBehavior: Clip.antiAlias, + decoration: controller.responsiveUtils.isWebDesktop(context) + ? BoxDecoration( + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColor.colorBorderBodyThread, width: 1), + color: Colors.white) + : const BoxDecoration(color: Colors.white), + margin: _getMarginEmailView(context), child: Obx(() { - if (controller.currentEmail != null) { - return SafeArea( - top: false, - bottom: false, - child: EmailViewAppBarWidget( - key: const Key('email_view_app_bar_widget'), - presentationEmail: controller.currentEmail!, - mailboxContain: _getMailboxContain(controller.currentEmail!), - isSearchActivated: controller.mailboxDashBoardController.searchController.isSearchEmailRunning, - onBackAction: () => controller.closeEmailView(context: context), - onEmailActionClick: (email, action) => controller.handleEmailAction(context, email, action), - onMoreActionClick: (presentationEmail, position) => _handleMoreEmailAction(context: context, presentationEmail: presentationEmail, position: position) - ), - ); - } else { - return const SizedBox.shrink(); - } - }) - ) - : null, - body: SafeArea( - right: controller.responsiveUtils.isLandscapeMobile(context), - left: controller.responsiveUtils.isLandscapeMobile(context), - bottom: !PlatformInfo.isIOS, - child: Container( - clipBehavior: Clip.antiAlias, - decoration: controller.responsiveUtils.isWebDesktop(context) - ? BoxDecoration( - borderRadius: BorderRadius.circular(20), - border: Border.all(color: AppColor.colorBorderBodyThread, width: 1), - color: Colors.white) - : const BoxDecoration(color: Colors.white), - margin: _getMarginEmailView(context), - child: Obx(() { - final currentEmail = controller.currentEmail; - if (currentEmail != null) { - return Column(children: [ - if (!PlatformInfo.isIOS) - Obx(() => EmailViewAppBarWidget( - key: const Key('email_view_app_bar_widget'), - presentationEmail: currentEmail, - mailboxContain: _getMailboxContain(currentEmail), - isSearchActivated: controller.mailboxDashBoardController.searchController.isSearchEmailRunning, - onBackAction: () => controller.closeEmailView(context: context), - onEmailActionClick: (email, action) => controller.handleEmailAction(context, email, action), - onMoreActionClick: (presentationEmail, position) => _handleMoreEmailAction(context: context, presentationEmail: presentationEmail, position: position), - optionsWidget: PlatformInfo.isWeb && controller.emailSupervisorController.supportedPageView.isTrue - ? _buildNavigatorPageViewWidgets(context) - : null, - )), - Obx(() { - final vacation = controller.mailboxDashBoardController.vacationResponse.value; - if (vacation?.vacationResponderIsValid == true && - ( - controller.responsiveUtils.isMobile(context) || - controller.responsiveUtils.isTablet(context) || - controller.responsiveUtils.isLandscapeMobile(context) - ) - ) { - return VacationNotificationMessageWidget( - margin: const EdgeInsets.only(left: 12, right: 12, bottom: 5), - vacationResponse: vacation!, - actionGotoVacationSetting: controller.mailboxDashBoardController.goToVacationSetting, - actionEndNow: controller.mailboxDashBoardController.disableVacationResponder - ); - } else { - return const SizedBox.shrink(); - } - }), - Expanded( - child: LayoutBuilder(builder: (context, constraints) { - log('EmailView::build: EMAIL_BODY_MAX_HEIGHT = ${constraints.maxHeight}'); - return Obx(() { - if (controller.emailSupervisorController.supportedPageView.isTrue) { - final currentListEmail = controller.emailSupervisorController.currentListEmail; - return PageView.builder( - physics: controller.emailSupervisorController.scrollPhysicsPageView.value, - itemCount: currentListEmail.length, - allowImplicitScrolling: true, - controller: controller.emailSupervisorController.pageController, - onPageChanged: controller.emailSupervisorController.onPageChanged, - itemBuilder: (context, index) { - final currentEmail = currentListEmail[index]; - if (PlatformInfo.isMobile) { - return SingleChildScrollView( - physics : const ClampingScrollPhysics(), - child: Container( - width: double.infinity, - alignment: Alignment.center, - color: Colors.white, - child: Obx(() => _buildEmailMessage( - context: context, - presentationEmail: currentEmail, - calendarEvent: controller.calendarEvent, - maxBodyHeight: constraints.maxHeight - )) - ) - ); - } else { - return Obx(() { - final calendarEvent = controller.calendarEvent; - if (currentEmail.hasCalendarEvent && calendarEvent != null) { - return Padding( - padding: const EdgeInsetsDirectional.symmetric(horizontal: 4), - child: ScrollbarListView( - scrollController: controller.emailContentScrollController, - child: SingleChildScrollView( - physics : const ClampingScrollPhysics(), - controller: controller.emailContentScrollController, - child: Container( - width: double.infinity, - alignment: Alignment.center, - color: Colors.white, - child: _buildEmailMessage( - context: context, - presentationEmail: currentEmail, - calendarEvent: calendarEvent, - emailAddressSender: currentEmail.listEmailAddressSender.getListAddress(), + final currentEmail = controller.currentEmail; + if (currentEmail != null) { + return Column(children: [ + if (!PlatformInfo.isIOS) + Obx(() => EmailViewAppBarWidget( + key: const Key('email_view_app_bar_widget'), + presentationEmail: currentEmail, + mailboxContain: _getMailboxContain(currentEmail), + isSearchActivated: controller.mailboxDashBoardController.searchController.isSearchEmailRunning, + onBackAction: () => controller.closeEmailView(context: context), + onEmailActionClick: (email, action) => controller.handleEmailAction(context, email, action), + onMoreActionClick: (presentationEmail, position) => _handleMoreEmailAction(context: context, presentationEmail: presentationEmail, position: position), + optionsWidget: PlatformInfo.isWeb && controller.emailSupervisorController.supportedPageView.isTrue + ? _buildNavigatorPageViewWidgets(context) + : null, + )), + Obx(() { + final vacation = controller.mailboxDashBoardController.vacationResponse.value; + if (vacation?.vacationResponderIsValid == true && + ( + controller.responsiveUtils.isMobile(context) || + controller.responsiveUtils.isTablet(context) || + controller.responsiveUtils.isLandscapeMobile(context) + ) + ) { + return VacationNotificationMessageWidget( + margin: const EdgeInsets.only(left: 12, right: 12, bottom: 5), + vacationResponse: vacation!, + actionGotoVacationSetting: controller.mailboxDashBoardController.goToVacationSetting, + actionEndNow: controller.mailboxDashBoardController.disableVacationResponder + ); + } else { + return const SizedBox.shrink(); + } + }), + Expanded( + child: LayoutBuilder(builder: (context, constraints) { + log('EmailView::build: EMAIL_BODY_MAX_HEIGHT = ${constraints.maxHeight}'); + return Obx(() { + if (controller.emailSupervisorController.supportedPageView.isTrue) { + final currentListEmail = controller.emailSupervisorController.currentListEmail; + return PageView.builder( + physics: controller.emailSupervisorController.scrollPhysicsPageView.value, + itemCount: currentListEmail.length, + allowImplicitScrolling: true, + controller: controller.emailSupervisorController.pageController, + onPageChanged: controller.emailSupervisorController.onPageChanged, + itemBuilder: (context, index) { + final currentEmail = currentListEmail[index]; + if (PlatformInfo.isMobile) { + return SingleChildScrollView( + physics : const ClampingScrollPhysics(), + child: Container( + width: double.infinity, + alignment: Alignment.center, + color: Colors.white, + child: Obx(() => _buildEmailMessage( + context: context, + presentationEmail: currentEmail, + calendarEvent: controller.calendarEvent.value, + maxBodyHeight: constraints.maxHeight + )) + ) + ); + } else { + return Obx(() { + final calendarEvent = controller.calendarEvent.value; + if (currentEmail.hasCalendarEvent && calendarEvent != null) { + return Padding( + padding: const EdgeInsetsDirectional.symmetric(horizontal: 4), + child: ScrollbarListView( + scrollController: controller.emailContentScrollController, + child: SingleChildScrollView( + physics : const ClampingScrollPhysics(), + child: Container( + width: double.infinity, + alignment: Alignment.center, + color: Colors.white, + child: _buildEmailMessage( + context: context, + presentationEmail: currentEmail, + calendarEvent: calendarEvent, + emailAddressSender: currentEmail.listEmailAddressSender.getListAddress(), + ) ) - ) + ), ), - ), - ); - } else { - return _buildEmailMessage( - context: context, - presentationEmail: currentEmail, - maxBodyHeight: constraints.maxHeight - ); - } - }); + ); + } else { + return _buildEmailMessage( + context: context, + presentationEmail: currentEmail, + maxBodyHeight: constraints.maxHeight + ); + } + }); + } } - } - ); - } else { - if (PlatformInfo.isMobile) { - return SingleChildScrollView( - physics : const ClampingScrollPhysics(), - child: Container( - width: double.infinity, - alignment: Alignment.center, - color: Colors.white, - child: Obx(() => _buildEmailMessage( - context: context, - presentationEmail: currentEmail, - calendarEvent: controller.calendarEvent, - maxBodyHeight: constraints.maxHeight - )) - ) ); } else { - return Obx(() { - final calendarEvent = controller.calendarEvent; - if (currentEmail.hasCalendarEvent && calendarEvent != null) { - return Padding( - padding: const EdgeInsetsDirectional.symmetric(horizontal: 4), - child: ScrollbarListView( - scrollController: controller.emailContentScrollController, - child: SingleChildScrollView( - physics : const ClampingScrollPhysics(), - controller: controller.emailContentScrollController, - child: Container( - width: double.infinity, - alignment: Alignment.center, - color: Colors.white, - child: _buildEmailMessage( - context: context, - presentationEmail: currentEmail, - calendarEvent: calendarEvent, - emailAddressSender: currentEmail.listEmailAddressSender.getListAddress(), - maxBodyHeight: constraints.maxHeight + if (PlatformInfo.isMobile) { + return SingleChildScrollView( + physics : const ClampingScrollPhysics(), + child: Container( + width: double.infinity, + alignment: Alignment.center, + color: Colors.white, + child: Obx(() => _buildEmailMessage( + context: context, + presentationEmail: currentEmail, + calendarEvent: controller.calendarEvent.value, + maxBodyHeight: constraints.maxHeight + )) + ) + ); + } else { + return Obx(() { + final calendarEvent = controller.calendarEvent.value; + if (currentEmail.hasCalendarEvent && calendarEvent != null) { + return Padding( + padding: const EdgeInsetsDirectional.symmetric(horizontal: 4), + child: ScrollbarListView( + scrollController: controller.emailContentScrollController, + child: SingleChildScrollView( + physics : const ClampingScrollPhysics(), + child: Container( + width: double.infinity, + alignment: Alignment.center, + color: Colors.white, + child: _buildEmailMessage( + context: context, + presentationEmail: currentEmail, + calendarEvent: calendarEvent, + emailAddressSender: currentEmail.listEmailAddressSender.getListAddress(), + maxBodyHeight: constraints.maxHeight + ) ) - ) + ), ), - ), - ); - } else { - return _buildEmailMessage( - context: context, - presentationEmail: currentEmail, - maxBodyHeight: constraints.maxHeight - ); - } - }); + ); + } else { + return _buildEmailMessage( + context: context, + presentationEmail: currentEmail, + maxBodyHeight: constraints.maxHeight + ); + } + }); + } } - } - }); - }), - ), - EmailViewBottomBarWidget( - key: const Key('email_view_button_bar'), - presentationEmail: currentEmail, - emailActionCallback: controller.pressEmailAction - ), - ]); - } else { - return const EmailViewEmptyWidget(); - } - }) + }); + }), + ), + EmailViewBottomBarWidget( + key: const Key('email_view_button_bar'), + presentationEmail: currentEmail, + emailActionCallback: controller.pressEmailAction + ), + ]); + } else { + return const EmailViewEmptyWidget(); + } + }) + ) ) - ) + ), ); } diff --git a/lib/features/email/presentation/styles/email_subject_styles.dart b/lib/features/email/presentation/styles/email_subject_styles.dart index 177944a342..c8d7a8481c 100644 --- a/lib/features/email/presentation/styles/email_subject_styles.dart +++ b/lib/features/email/presentation/styles/email_subject_styles.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; class EmailSubjectStyles { static const double textSize = 20; static const int? maxLines = PlatformInfo.isWeb ? 2 : null; - static const int? minLines = PlatformInfo.isWeb ? 1 : null; static const Color textColor = AppColor.colorNameEmail; static const Color cursorColor = AppColor.colorTextButton; diff --git a/lib/features/email/presentation/widgets/calendar_event/attendee_widget.dart b/lib/features/email/presentation/widgets/calendar_event/attendee_widget.dart index 607e2a1353..8834905b8a 100644 --- a/lib/features/email/presentation/widgets/calendar_event/attendee_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/attendee_widget.dart @@ -16,8 +16,8 @@ class AttendeeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return RichText( - text: TextSpan( + return Text.rich( + TextSpan( style: const TextStyle( fontSize: AttendeeWidgetStyles.textSize, fontWeight: FontWeight.w500, diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_banner_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_banner_widget.dart index 36c6507467..7fe70559fe 100644 --- a/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_banner_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_banner_widget.dart @@ -48,8 +48,8 @@ class CalendarEventActionBannerWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - RichText( - text: TextSpan( + Text.rich( + TextSpan( style: TextStyle( fontSize: CalendarEventActionBannerStyles.titleTextSize, fontWeight: FontWeight.w400, diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart index 852d6566de..2c26565c9b 100644 --- a/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart @@ -76,8 +76,8 @@ class CalendarEventInformationWidget extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - RichText( - text: TextSpan( + Text.rich( + TextSpan( style: const TextStyle( fontSize: CalendarEventInformationWidgetStyles.invitationMessageTextSize, fontWeight: FontWeight.w500, @@ -147,8 +147,8 @@ class CalendarEventInformationWidget extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - RichText( - text: TextSpan( + Text.rich( + TextSpan( style: const TextStyle( fontSize: CalendarEventInformationWidgetStyles.invitationMessageTextSize, fontWeight: FontWeight.w500, diff --git a/lib/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart index f38edc797a..9ab413796f 100644 --- a/lib/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart @@ -36,8 +36,8 @@ class EventAttendeeInformationWidget extends StatelessWidget { ), ), ), - Expanded(child: RichText( - text: TextSpan( + Expanded(child: Text.rich( + TextSpan( style: const TextStyle( fontSize: EventAttendeeInformationWidgetStyles.textSize, fontWeight: FontWeight.w500, diff --git a/lib/features/email/presentation/widgets/calendar_event/organizer_widget.dart b/lib/features/email/presentation/widgets/calendar_event/organizer_widget.dart index 4ac0dce035..6ed9f4ae5a 100644 --- a/lib/features/email/presentation/widgets/calendar_event/organizer_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/organizer_widget.dart @@ -15,8 +15,8 @@ class OrganizerWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return RichText( - text: TextSpan( + return Text.rich( + TextSpan( style: const TextStyle( fontSize: OrganizerWidgetStyles.textSize, fontWeight: FontWeight.w500, diff --git a/lib/features/email/presentation/widgets/email_receiver_widget.dart b/lib/features/email/presentation/widgets/email_receiver_widget.dart index acee9f0884..7c1b859f56 100644 --- a/lib/features/email/presentation/widgets/email_receiver_widget.dart +++ b/lib/features/email/presentation/widgets/email_receiver_widget.dart @@ -1,10 +1,9 @@ +import 'package:collection/collection.dart'; import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; -import 'package:core/utils/direction_utils.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -14,10 +13,9 @@ import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:model/extensions/list_email_address_extension.dart'; import 'package:model/extensions/presentation_email_extension.dart'; -import 'package:tmail_ui_user/features/base/widget/material_text_button.dart'; -import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_sender_builder.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/prefix_recipient_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; @@ -47,233 +45,253 @@ class _EmailReceiverWidgetState extends State { final _imagePaths = Get.find(); final _responsiveUtils = Get.find(); - final _scrollController = ScrollController(); bool _isDisplayAll = false; @override Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: Padding( - padding: EdgeInsets.only(top: _isDisplayAll - ? DirectionUtils.isDirectionRTLByLanguage(context) ? 3 : 5.5 - : 0), - child: PlatformInfo.isWeb - ? Container( - constraints: BoxConstraints( - maxHeight: _isDisplayAll && widget.maxHeight != null - ? widget.maxHeight! / 2 - _offsetTop - : double.infinity - ), - child: ScrollbarListView( - scrollController: _scrollController, - child: SingleChildScrollView( - controller: _scrollController, - child: _buildEmailAddressOfReceiver( - context, - widget.emailSelected, - _isDisplayAll, - widget.maxWidth - ), - ), + if (PlatformInfo.isWeb) { + if (_isDisplayAll) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + constraints: BoxConstraints(maxHeight: _maxHeight), + child: ListView( + primary: false, + shrinkWrap: true, + padding: EdgeInsets.zero, + children: [ + if (widget.emailSelected.to.numberEmailAddress() > 0) + _buildRecipientsWidgetToDisplayFull( + context: context, + prefixEmailAddress: PrefixEmailAddress.to, + listEmailAddress: PrefixEmailAddress.to.listEmailAddress(widget.emailSelected) + ), + if (widget.emailSelected.cc.numberEmailAddress() > 0) + _buildRecipientsWidgetToDisplayFull( + context: context, + prefixEmailAddress: PrefixEmailAddress.cc, + listEmailAddress: PrefixEmailAddress.cc.listEmailAddress(widget.emailSelected) + ), + if (widget.emailSelected.bcc.numberEmailAddress() > 0) + _buildRecipientsWidgetToDisplayFull( + context: context, + prefixEmailAddress: PrefixEmailAddress.bcc, + listEmailAddress: PrefixEmailAddress.bcc.listEmailAddress(widget.emailSelected) + ), + ], ), ) - : _buildEmailAddressOfReceiver( - context, - widget.emailSelected, - _isDisplayAll, - widget.maxWidth - ), - )), - if (_isDisplayAll) - Padding( - padding: EdgeInsets.symmetric( - vertical: DirectionUtils.isDirectionRTLByLanguage(context) ? 0 : 6), - child: MaterialTextButton( - padding: DirectionUtils.isDirectionRTLByLanguage(context) - ? const EdgeInsets.symmetric(horizontal: 8, vertical: 4) - : null, - onTap: () => setState(() => _isDisplayAll = false), - label: AppLocalizations.of(context).hide, - ) - ) - ] - ); - } - - Widget _buildEmailAddressOfReceiver( - BuildContext context, - PresentationEmail presentationEmail, - bool isDisplayFull, - double maxWidth - ) { - if (!isDisplayFull && presentationEmail.numberOfAllEmailAddress() > 1) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 40, - color: Colors.white, - constraints: BoxConstraints(maxWidth: _getMaxWidthEmailAddressDisplayed(context, maxWidth)), - child: ListView( - scrollDirection: Axis.horizontal, - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - children: [ - if (presentationEmail.to.numberEmailAddress() > 0) - _buildEmailAddressByPrefix( - context, - presentationEmail, - PrefixEmailAddress.to, - isDisplayFull - ), - if (presentationEmail.cc.numberEmailAddress() > 0) - _buildEmailAddressByPrefix( - context, - presentationEmail, - PrefixEmailAddress.cc, - isDisplayFull - ), - if (presentationEmail.bcc.numberEmailAddress() > 0) - _buildEmailAddressByPrefix( - context, - presentationEmail, - PrefixEmailAddress.bcc, - isDisplayFull - ), - ] ), - ), - Transform( - transform: Matrix4.translationValues(0.0, -5.0, 0.0), - child: TMailButtonWidget.fromIcon( - icon: _imagePaths.icChevronDown, + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).hide, + textStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + color: AppColor.primaryColor, + fontSize: 15 + ), backgroundColor: Colors.transparent, - onTapActionCallback: () => setState(() => _isDisplayAll = true), + onTapActionCallback: () => setState(() => _isDisplayAll = false), + ) + ] + ); + } else { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: _getMaxWidth(context), + maxHeight: 34, + ), + child: ListView( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + shrinkWrap: true, + children: [ + if (widget.emailSelected.to.numberEmailAddress() > 0) + ..._buildRecipientsWidget( + context: context, + prefixEmailAddress: PrefixEmailAddress.to, + listEmailAddress: PrefixEmailAddress.to.listEmailAddress(widget.emailSelected) + ), + if (widget.emailSelected.cc.numberEmailAddress() > 0) + ..._buildRecipientsWidget( + context: context, + prefixEmailAddress: PrefixEmailAddress.cc, + listEmailAddress: PrefixEmailAddress.cc.listEmailAddress(widget.emailSelected) + ), + if (widget.emailSelected.bcc.numberEmailAddress() > 0) + ..._buildRecipientsWidget( + context: context, + prefixEmailAddress: PrefixEmailAddress.bcc, + listEmailAddress: PrefixEmailAddress.bcc.listEmailAddress(widget.emailSelected) + ), + ] + ), ), - ) - ] - ); + if (widget.emailSelected.numberOfAllEmailAddress() > 1) + TMailButtonWidget.fromIcon( + icon: _imagePaths.icChevronDown, + backgroundColor: Colors.transparent, + onTapActionCallback: () => setState(() => _isDisplayAll = true), + ) + ] + ); + } } else { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (presentationEmail.to.numberEmailAddress() > 0) - _buildEmailAddressByPrefix( - context, - presentationEmail, - PrefixEmailAddress.to, - isDisplayFull - ), - if (presentationEmail.cc.numberEmailAddress() > 0) - _buildEmailAddressByPrefix( - context, - presentationEmail, - PrefixEmailAddress.cc, - isDisplayFull - ), - if (presentationEmail.bcc.numberEmailAddress() > 0) - _buildEmailAddressByPrefix( - context, - presentationEmail, - PrefixEmailAddress.bcc, - isDisplayFull + if (_isDisplayAll) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + constraints: BoxConstraints(maxHeight: _maxHeight), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.emailSelected.to.numberEmailAddress() > 0) + _buildRecipientsWidgetToDisplayFull( + context: context, + prefixEmailAddress: PrefixEmailAddress.to, + listEmailAddress: PrefixEmailAddress.to.listEmailAddress(widget.emailSelected) + ), + if (widget.emailSelected.cc.numberEmailAddress() > 0) + _buildRecipientsWidgetToDisplayFull( + context: context, + prefixEmailAddress: PrefixEmailAddress.cc, + listEmailAddress: PrefixEmailAddress.cc.listEmailAddress(widget.emailSelected) + ), + if (widget.emailSelected.bcc.numberEmailAddress() > 0) + _buildRecipientsWidgetToDisplayFull( + context: context, + prefixEmailAddress: PrefixEmailAddress.bcc, + listEmailAddress: PrefixEmailAddress.bcc.listEmailAddress(widget.emailSelected) + ), + ], + ), + ) + ), + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).hide, + textStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + color: AppColor.primaryColor, + fontSize: 15 + ), + backgroundColor: Colors.transparent, + onTapActionCallback: () => setState(() => _isDisplayAll = false), + ) + ] + ); + } else { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 40, + constraints: BoxConstraints(maxWidth: _getMaxWidth(context)), + child: ListView( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + shrinkWrap: true, + children: [ + if (widget.emailSelected.to.numberEmailAddress() > 0) + ..._buildRecipientsWidget( + context: context, + prefixEmailAddress: PrefixEmailAddress.to, + listEmailAddress: PrefixEmailAddress.to.listEmailAddress(widget.emailSelected) + ), + if (widget.emailSelected.cc.numberEmailAddress() > 0) + ..._buildRecipientsWidget( + context: context, + prefixEmailAddress: PrefixEmailAddress.cc, + listEmailAddress: PrefixEmailAddress.cc.listEmailAddress(widget.emailSelected) + ), + if (widget.emailSelected.bcc.numberEmailAddress() > 0) + ..._buildRecipientsWidget( + context: context, + prefixEmailAddress: PrefixEmailAddress.bcc, + listEmailAddress: PrefixEmailAddress.bcc.listEmailAddress(widget.emailSelected) + ), + ] + ), ), - ], - ); + if (widget.emailSelected.numberOfAllEmailAddress() > 1) + TMailButtonWidget.fromIcon( + icon: _imagePaths.icChevronDown, + backgroundColor: Colors.transparent, + onTapActionCallback: () => setState(() => _isDisplayAll = true), + ) + ] + ); + } } } - Widget _buildEmailAddressByPrefix( - BuildContext context, - PresentationEmail presentationEmail, - PrefixEmailAddress prefixEmailAddress, - bool isDisplayFull - ) { + List _buildRecipientsTag({required List listEmailAddress}) { + return listEmailAddress + .mapIndexed((index, emailAddress) => TMailButtonWidget.fromText( + text: index == listEmailAddress.length - 1 + ? emailAddress.asString() + : '${emailAddress.asString()},', + textStyle: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.black, + fontSize: 16, + ), + padding: const EdgeInsetsDirectional.symmetric(vertical: 5, horizontal: 8), + backgroundColor: Colors.transparent, + onTapActionCallback: () => widget.openEmailAddressDetailAction?.call(context, emailAddress), + onLongPressActionCallback: () => AppUtils.copyEmailAddressToClipboard(context, emailAddress.emailAddress), + )) + .toList(); + } + + + Widget _buildRecipientsWidgetToDisplayFull({ + required BuildContext context, + required PrefixEmailAddress prefixEmailAddress, + required List listEmailAddress, + }) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.only(top: 5), - child: Text( - '${prefixEmailAddress.asName(context)}:', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: AppColor.colorEmailAddressFull - ) - ), - ), - if (!isDisplayFull && presentationEmail.numberOfAllEmailAddress() > 1) - _buildListEmailAddressWidget( - context, - prefixEmailAddress.listEmailAddress(presentationEmail), - isDisplayFull + PrefixRecipientWidget(prefixEmailAddress: prefixEmailAddress), + Expanded( + child: Wrap( + children: _buildRecipientsTag(listEmailAddress: listEmailAddress) ) - else - Expanded(child: _buildListEmailAddressWidget( - context, - prefixEmailAddress.listEmailAddress(presentationEmail), - isDisplayFull - )) - ] + ) + ], ); } - Widget _buildListEmailAddressWidget( - BuildContext context, - List listEmailAddress, - bool isDisplayFull - ) { - final lastEmailAddress = listEmailAddress.last; - final emailAddressWidgets = listEmailAddress.map((emailAddress) { - return MaterialTextButton( - label: lastEmailAddress == emailAddress - ? emailAddress.asString() - : '${emailAddress.asString()},', - onTap: () => widget.openEmailAddressDetailAction?.call(context, emailAddress), - onLongPress: () { - AppUtils.copyEmailAddressToClipboard(context, emailAddress.emailAddress); - }, - borderRadius: 8, - labelColor: Colors.black, - labelSize: 16, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, - ); - }).toList(); - - if (isDisplayFull) { - return Wrap(children: emailAddressWidgets); - } else { - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - physics: const NeverScrollableScrollPhysics(), - child: Row( - crossAxisAlignment:CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: emailAddressWidgets - ), - ); - } + List _buildRecipientsWidget({ + required BuildContext context, + required PrefixEmailAddress prefixEmailAddress, + required List listEmailAddress, + }) { + return [ + PrefixRecipientWidget(prefixEmailAddress: prefixEmailAddress), + ..._buildRecipientsTag(listEmailAddress: listEmailAddress) + ]; } - double _getMaxWidthEmailAddressDisplayed(BuildContext context, double maxWidth) { + double _getMaxWidth(BuildContext context) { if (_responsiveUtils.isPortraitMobile(context)) { - return maxWidth - _maxSizeFullDisplayEmailAddressArrowDownButton; + return widget.maxWidth - _maxSizeFullDisplayEmailAddressArrowDownButton; } else if (_responsiveUtils.isWebDesktop(context)) { - return maxWidth / 2; + return widget.maxWidth / 2; } else { - return maxWidth * 3/4; + return widget.maxWidth * 3/4; } } - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); + double get _maxHeight { + return _isDisplayAll && widget.maxHeight != null + ? widget.maxHeight! / 2 - _offsetTop + : double.infinity; } } \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/email_subject_widget.dart b/lib/features/email/presentation/widgets/email_subject_widget.dart index e7611f9634..6132c433c6 100644 --- a/lib/features/email/presentation/widgets/email_subject_widget.dart +++ b/lib/features/email/presentation/widgets/email_subject_widget.dart @@ -12,11 +12,9 @@ class EmailSubjectWidget extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: EmailSubjectStyles.padding, - child: SelectableText( + child: Text( presentationEmail.getEmailTitle(), maxLines: EmailSubjectStyles.maxLines, - minLines: EmailSubjectStyles.minLines, - cursorColor: EmailSubjectStyles.cursorColor, style: const TextStyle( fontSize: EmailSubjectStyles.textSize, color: EmailSubjectStyles.textColor, diff --git a/lib/features/email/presentation/widgets/prefix_recipient_widget.dart b/lib/features/email/presentation/widgets/prefix_recipient_widget.dart new file mode 100644 index 0000000000..03fe772c2a --- /dev/null +++ b/lib/features/email/presentation/widgets/prefix_recipient_widget.dart @@ -0,0 +1,26 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:model/email/prefix_email_address.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart'; + +class PrefixRecipientWidget extends StatelessWidget { + final PrefixEmailAddress prefixEmailAddress; + + const PrefixRecipientWidget({super.key, required this.prefixEmailAddress}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + '${prefixEmailAddress.asName(context)}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 16, + fontWeight: FontWeight.w500, + color: AppColor.colorEmailAddressFull + ) + ), + ); + } +} \ No newline at end of file From ec83287cfb216ded1cc9e3c2661beb284a4a0c60 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 13 Mar 2024 02:38:07 +0700 Subject: [PATCH 34/80] TF-2684 Add button `Mail to attendees` to open composer with `to: all attendees` --- lib/features/email/domain/model/event_action.dart | 9 ++++++++- .../presentation/controller/single_email_controller.dart | 4 +++- .../calendar_event_action_button_widget.dart | 4 +++- .../calendar_event_information_widget.dart | 1 + .../presentation/widgets/email_receiver_widget.dart | 1 + lib/l10n/intl_messages.arb | 8 +++++++- lib/main/localizations/app_localizations.dart | 7 +++++++ 7 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/features/email/domain/model/event_action.dart b/lib/features/email/domain/model/event_action.dart index 144985f9d9..adf04ec476 100644 --- a/lib/features/email/domain/model/event_action.dart +++ b/lib/features/email/domain/model/event_action.dart @@ -8,7 +8,8 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; enum EventActionType { yes, maybe, - no; + no, + mailToAttendees; String getLabelButton(BuildContext context) { switch(this) { @@ -18,6 +19,8 @@ enum EventActionType { return AppLocalizations.of(context).maybe; case EventActionType.no: return AppLocalizations.of(context).no; + case EventActionType.mailToAttendees: + return AppLocalizations.of(context).mailToAttendees; } } @@ -62,6 +65,10 @@ class EventAction with EquatableMixin { EventAction(this.actionType, this.link); + factory EventAction.mailToAttendees() { + return EventAction(EventActionType.mailToAttendees, ''); + } + @override List get props => [actionType, link]; } \ No newline at end of file diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 05b04c452c..f7516f01ba 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -16,6 +16,8 @@ import 'package:jmap_dart_client/jmap/core/id.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/calendar/calendar_event.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_organizer.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/mdn/disposition.dart'; @@ -1659,7 +1661,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { break; } } - + void _acceptCalendarEventAction(EmailId emailId) { if (_acceptCalendarEventInteractor == null || _displayingEventBlobId == null diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart index ffe7bbe5d8..eb3df766b7 100644 --- a/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart @@ -14,6 +14,7 @@ class CalendarEventActionButtonWidget extends StatelessWidget { final OnCalendarEventReplyActionClick onCalendarEventReplyActionClick; final bool calendarEventReplying; final PresentationEmail? presentationEmail; + final VoidCallback? onMailToAttendeesAction; final _responsiveUtils = Get.find(); @@ -23,6 +24,7 @@ class CalendarEventActionButtonWidget extends StatelessWidget { required this.calendarEventReplying, this.margin, this.presentationEmail, + this.onMailToAttendeesAction, }); @override @@ -57,7 +59,7 @@ class CalendarEventActionButtonWidget extends StatelessWidget { color: _getButtonBorderColor(action) ), onTapActionCallback: _getCallbackFunction(action), - ), + ) )) .toList(), ), diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart index 2c26565c9b..f73562fd50 100644 --- a/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart @@ -8,6 +8,7 @@ import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_ev import 'package:tmail_ui_user/features/email/presentation/styles/calendar_event_information_widget_styles.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_date_icon_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_location_detail_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_location_information_widget.dart'; diff --git a/lib/features/email/presentation/widgets/email_receiver_widget.dart b/lib/features/email/presentation/widgets/email_receiver_widget.dart index 7c1b859f56..4c385aa602 100644 --- a/lib/features/email/presentation/widgets/email_receiver_widget.dart +++ b/lib/features/email/presentation/widgets/email_receiver_widget.dart @@ -151,6 +151,7 @@ class _EmailReceiverWidgetState extends State { constraints: BoxConstraints(maxHeight: _maxHeight), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ if (widget.emailSelected.to.numberEmailAddress() > 0) _buildRecipientsWidgetToDisplayFull( diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index e71195ee25..4fc020a496 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-03-12T12:50:35.525381", + "@@last_modified": "2024-03-13T01:12:22.224880", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3851,5 +3851,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "mailToAttendees": "Mail to attendees", + "@mailToAttendees": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 73c17632b3..f2147bf8ec 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4023,4 +4023,11 @@ class AppLocalizations { name: 'canceling' ); } + + String get mailToAttendees { + return Intl.message( + 'Mail to attendees', + name: 'mailToAttendees' + ); + } } \ No newline at end of file From efb70f872ffdc49e325d77832e26e592dc7cd1d9 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 27 Mar 2024 01:42:10 +0700 Subject: [PATCH 35/80] TF-2684 Fix difficult to select text in blue bar on responsive tablet --- .../contact/presentation/contact_view.dart | 1 - .../email_supervisor_controller.dart | 15 +++--- .../email/presentation/email_view.dart | 8 +-- ...formation_sender_and_receiver_builder.dart | 49 ++++++++----------- .../mailbox_visibility_view.dart | 2 - .../thread/presentation/thread_view.dart | 1 - 6 files changed, 33 insertions(+), 43 deletions(-) diff --git a/lib/features/contact/presentation/contact_view.dart b/lib/features/contact/presentation/contact_view.dart index a67fc2fca7..fe2e23dcee 100644 --- a/lib/features/contact/presentation/contact_view.dart +++ b/lib/features/contact/presentation/contact_view.dart @@ -6,7 +6,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; -import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/suggestion_email_address.dart'; import 'package:tmail_ui_user/features/contact/presentation/contact_controller.dart'; import 'package:tmail_ui_user/features/contact/presentation/utils/contact_utils.dart'; diff --git a/lib/features/email/presentation/controller/email_supervisor_controller.dart b/lib/features/email/presentation/controller/email_supervisor_controller.dart index cda09da80a..bee43932af 100644 --- a/lib/features/email/presentation/controller/email_supervisor_controller.dart +++ b/lib/features/email/presentation/controller/email_supervisor_controller.dart @@ -1,6 +1,6 @@ import 'dart:collection'; + import 'package:collection/collection.dart'; -import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -114,7 +114,7 @@ class EmailSupervisorController extends GetxController { void _jumpToPage(int page) { if (PlatformInfo.isWeb) { - pageController?.jumpToPage(page); + onPageChanged(page); } else { pageController?.animateToPage( page, @@ -124,11 +124,12 @@ class EmailSupervisorController extends GetxController { } void updateScrollPhysicPageView(bool isScrollPageViewActivated) { - log('EmailSupervisorController::updateScrollPhysicPageView:isScrollPageViewActivated: $isScrollPageViewActivated'); - if (PlatformInfo.isWeb || !isScrollPageViewActivated) { - scrollPhysicsPageView.value = const NeverScrollableScrollPhysics(); - } else { - scrollPhysicsPageView.value = null; + if (PlatformInfo.isMobile) { + if (!isScrollPageViewActivated) { + scrollPhysicsPageView.value = const NeverScrollableScrollPhysics(); + } else { + scrollPhysicsPageView.value = null; + } } } diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 742d73c61a..092853e036 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -2,7 +2,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart'; import 'package:core/presentation/views/html_viewer/html_content_viewer_widget.dart'; -import 'package:core/utils/app_logger.dart'; import 'package:core/utils/direction_utils.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; @@ -126,10 +125,11 @@ class EmailView extends GetWidget { }), Expanded( child: LayoutBuilder(builder: (context, constraints) { - log('EmailView::build: EMAIL_BODY_MAX_HEIGHT = ${constraints.maxHeight}'); return Obx(() { - if (controller.emailSupervisorController.supportedPageView.isTrue) { - final currentListEmail = controller.emailSupervisorController.currentListEmail; + bool supportedPageView = controller.emailSupervisorController.supportedPageView.isTrue && PlatformInfo.isMobile; + final currentListEmail = controller.emailSupervisorController.currentListEmail; + + if (supportedPageView) { return PageView.builder( physics: controller.emailSupervisorController.scrollPhysicsPageView.value, itemCount: currentListEmail.length, diff --git a/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart b/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart index 55bb5541b1..d28f0d1db2 100644 --- a/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart +++ b/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart @@ -37,8 +37,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - color: Colors.white, + return Padding( padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 16), child: Row( crossAxisAlignment: emailSelected.numberOfAllEmailAddress() > 0 @@ -55,32 +54,26 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { children: [ if (emailSelected.from?.isNotEmpty == true) Row(children: [ - Expanded( - child: Row( - children: [ - Flexible(child: Transform( - transform: Matrix4.translationValues(-5.0, 0.0, 0.0), - child: EmailSenderBuilder( - emailAddress: emailSelected.from!.first, - openEmailAddressDetailAction: openEmailAddressDetailAction, - ) - )), - if (!emailSelected.isSubscribed && emailUnsubscribe != null && !responsiveUtils.isPortraitMobile(context)) - TMailButtonWidget.fromText( - text: AppLocalizations.of(context).unsubscribe, - textStyle: const TextStyle( - fontWeight: FontWeight.normal, - fontSize: 14, - color: AppColor.colorTextBody, - decoration: TextDecoration.underline, - ), - padding: const EdgeInsetsDirectional.symmetric(vertical: 5, horizontal: 8), - backgroundColor: Colors.transparent, - onTapActionCallback: () => onEmailActionClick?.call(emailSelected, EmailActionType.unsubscribe), - ), - ], - ) - ), + Flexible(child: Transform( + transform: Matrix4.translationValues(-5.0, 0.0, 0.0), + child: EmailSenderBuilder( + emailAddress: emailSelected.from!.first, + openEmailAddressDetailAction: openEmailAddressDetailAction, + ) + )), + if (!emailSelected.isSubscribed && emailUnsubscribe != null && !responsiveUtils.isPortraitMobile(context)) + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).unsubscribe, + textStyle: const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 14, + color: AppColor.colorTextBody, + decoration: TextDecoration.underline, + ), + padding: const EdgeInsetsDirectional.symmetric(vertical: 5, horizontal: 8), + backgroundColor: Colors.transparent, + onTapActionCallback: () => onEmailActionClick?.call(emailSelected, EmailActionType.unsubscribe), + ), ReceivedTimeBuilder(emailSelected: emailSelected), ]), if (emailSelected.numberOfAllEmailAddress() > 0) diff --git a/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_view.dart b/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_view.dart index af45f06363..482d2b82e1 100644 --- a/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_view.dart +++ b/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_view.dart @@ -1,12 +1,10 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/views/list/tree_view.dart'; -import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:model/mailbox/expand_mode.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; -import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index e25e86b366..3d03d3453e 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -8,7 +8,6 @@ import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/popup_menu_widget_mixin.dart'; import 'package:tmail_ui_user/features/base/widget/compose_floating_button.dart'; -import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; From ae723692e9b4bb1f106426ceb313657656dc36fd Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 19 Mar 2024 01:51:55 +0700 Subject: [PATCH 36/80] TF-2717 Replace `from cc bcc` buttons in `to` by `arrow down` button on mobile --- .../presentation/composer_controller.dart | 6 ++ .../composer/presentation/composer_view.dart | 2 + .../recipient_composer_widget_style.dart | 1 + .../widgets/recipient_composer_widget.dart | 83 ++++++++++++------- 4 files changed, 63 insertions(+), 29 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 08e3745c6a..5ea9257307 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -2127,4 +2127,10 @@ class ComposerController extends BaseController with DragDropFileMixin { ) ); } + + void handleEnableRecipientsInputAction(bool isEnabled) { + fromRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; + ccRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; + bccRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; + } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index 3c58fab309..7c408ae9b3 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -127,6 +127,7 @@ class ComposerView extends GetWidget { onUpdateListEmailAddressAction: controller.updateListEmailAddress, onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputAction, )), Obx(() { if (controller.ccRecipientState.value == PrefixRecipientState.enabled) { @@ -280,6 +281,7 @@ class ComposerView extends GetWidget { onUpdateListEmailAddressAction: controller.updateListEmailAddress, onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputAction, ), if (controller.ccRecipientState.value == PrefixRecipientState.enabled) RecipientComposerWidget( diff --git a/lib/features/composer/presentation/styles/recipient_composer_widget_style.dart b/lib/features/composer/presentation/styles/recipient_composer_widget_style.dart index f7543db209..359c864ce8 100644 --- a/lib/features/composer/presentation/styles/recipient_composer_widget_style.dart +++ b/lib/features/composer/presentation/styles/recipient_composer_widget_style.dart @@ -25,6 +25,7 @@ class RecipientComposerWidgetStyle { static const EdgeInsetsGeometry prefixButtonPadding = EdgeInsetsDirectional.symmetric(vertical: 3, horizontal: 5); static const EdgeInsetsGeometry labelMargin = EdgeInsetsDirectional.only(top: 16); static const EdgeInsetsGeometry recipientMargin = EdgeInsetsDirectional.only(top: 12); + static const EdgeInsetsGeometry enableRecipientButtonMargin = EdgeInsetsDirectional.only(top: 10); static const TextStyle prefixButtonTextStyle = TextStyle( fontSize: 15, diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index db4a347b51..b7c77eee47 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'dart:math'; import 'package:collection/collection.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; @@ -34,6 +35,7 @@ typedef OnShowFullListEmailAddressAction = void Function(PrefixEmailAddress pref typedef OnFocusEmailAddressChangeAction = void Function(PrefixEmailAddress prefix, bool isFocus); typedef OnRemoveDraggableEmailAddressAction = void Function(DraggableEmailAddress draggableEmailAddress); typedef OnDeleteTagAction = void Function(EmailAddress emailAddress); +typedef OnEnableAllRecipientsInputAction = void Function(bool isEnabled); class RecipientComposerWidget extends StatefulWidget { @@ -58,6 +60,7 @@ class RecipientComposerWidget extends StatefulWidget { final VoidCallback? onFocusNextAddressAction; final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? margin; + final OnEnableAllRecipientsInputAction? onEnableAllRecipientsInputAction; const RecipientComposerWidget({ super.key, @@ -82,6 +85,7 @@ class RecipientComposerWidget extends StatefulWidget { this.onFocusEmailAddressChangeAction, this.onFocusNextAddressAction, this.onRemoveDraggableEmailAddressAction, + this.onEnableAllRecipientsInputAction, }); @override @@ -240,7 +244,6 @@ class _RecipientComposerWidgetState extends State { textInputAction: TextInputAction.done, debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, tagSpacing: RecipientComposerWidgetStyle.tagSpacing, - autofocus: widget.prefix != PrefixEmailAddress.to && _currentListEmailAddress.isEmpty, minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, resetTextOnSubmitted: true, autoScrollToInput: false, @@ -299,34 +302,52 @@ class _RecipientComposerWidgetState extends State { ) ), const SizedBox(width: RecipientComposerWidgetStyle.space), - if (widget.prefix == PrefixEmailAddress.to && widget.fromState == PrefixRecipientState.disabled) - TMailButtonWidget.fromText( - text: AppLocalizations.of(context).from_email_address_prefix, - textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, - backgroundColor: Colors.transparent, - padding: RecipientComposerWidgetStyle.prefixButtonPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.from), - ), - if (widget.prefix == PrefixEmailAddress.to && widget.ccState == PrefixRecipientState.disabled) - TMailButtonWidget.fromText( - text: AppLocalizations.of(context).cc_email_address_prefix, - textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, - backgroundColor: Colors.transparent, - padding: RecipientComposerWidgetStyle.prefixButtonPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.cc), - ), - if (widget.prefix == PrefixEmailAddress.to && widget.bccState == PrefixRecipientState.disabled) - TMailButtonWidget.fromText( - text: AppLocalizations.of(context).bcc_email_address_prefix, - textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, - backgroundColor: Colors.transparent, - padding: RecipientComposerWidgetStyle.prefixButtonPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.bcc), - ), - if (widget.prefix != PrefixEmailAddress.to) + if (widget.prefix == PrefixEmailAddress.to) + if (PlatformInfo.isWeb) + ...[ + if (widget.fromState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).from_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, + backgroundColor: Colors.transparent, + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.from), + ), + if (widget.ccState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).cc_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, + backgroundColor: Colors.transparent, + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.cc), + ), + if (widget.bccState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).bcc_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, + backgroundColor: Colors.transparent, + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.bcc), + ), + ] + else if (PlatformInfo.isMobile) + ...[ + TMailButtonWidget.fromIcon( + icon: _isAllRecipientInputEnabled + ? _imagePaths.icChevronUp + : _imagePaths.icChevronDown, + backgroundColor: Colors.transparent, + iconSize: 20, + padding: const EdgeInsets.all(5), + iconColor: AppColor.colorLabelComposer, + margin: RecipientComposerWidgetStyle.enableRecipientButtonMargin, + onTapActionCallback: () => widget.onEnableAllRecipientsInputAction?.call(_isAllRecipientInputEnabled), + ) + ] + else if (PlatformInfo.isWeb) TMailButtonWidget.fromIcon( icon: _imagePaths.icClose, backgroundColor: Colors.transparent, @@ -344,6 +365,10 @@ class _RecipientComposerWidgetState extends State { bool get _isCollapse => _currentListEmailAddress.length > 1 && widget.expandMode == ExpandMode.COLLAPSE; + bool get _isAllRecipientInputEnabled => widget.fromState == PrefixRecipientState.enabled && + widget.ccState == PrefixRecipientState.enabled && + widget.bccState == PrefixRecipientState.enabled; + List get _collapsedListEmailAddress => _isCollapse ? _currentListEmailAddress.sublist(0, 1) : _currentListEmailAddress; From 67b473873b95808df328a233782f978fbcd4843b Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 19 Mar 2024 10:17:25 +0700 Subject: [PATCH 37/80] TF-2717 Allow show full signature in composer on mobile --- lib/features/composer/presentation/composer_controller.dart | 2 +- pubspec.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 5ea9257307..c8f39b7dbd 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1571,7 +1571,7 @@ class ComposerController extends BaseController with DragDropFileMixin { if (PlatformInfo.isWeb) { richTextWebController.editorController.insertSignature(signature); } else { - await htmlEditorApi?.insertSignature(signature); + await htmlEditorApi?.insertSignature(signature, allowCollapsed: false); } } diff --git a/pubspec.lock b/pubspec.lock index 7d5ccb764f..4c98d82511 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -425,11 +425,11 @@ packages: source: path version: "1.0.0+1" enough_html_editor: - dependency: transitive + dependency: "direct overridden" description: path: "." - ref: email_supported - resolved-ref: f13a35eb76fafb2ee1e686ca19c4c742d0efa78a + ref: "improve/allow-enable-disable-colllapsed-signature" + resolved-ref: b0038f647bf90ba40bf21efe7290a22dec4d2867 url: "https://github.com/linagora/enough_html_editor.git" source: git version: "0.0.5" From 9d89b4a2df89fb221415bf79980cfcabed370f16 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 19 Mar 2024 12:20:48 +0700 Subject: [PATCH 38/80] TF-2717 Display attachment bar below subject field on mobile --- .../presentation/composer_controller.dart | 7 - .../composer/presentation/composer_view.dart | 40 +++--- .../presentation/composer_view_web.dart | 6 +- .../attachment_item_composer_widget.dart | 5 +- .../mobile_attachment_composer_widget.dart | 136 +++++++++++++++--- .../widgets/recipient_composer_widget.dart | 4 +- lib/l10n/intl_messages.arb | 18 ++- lib/main/localizations/app_localizations.dart | 13 ++ 8 files changed, 171 insertions(+), 58 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index c8f39b7dbd..263e4857a4 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -98,7 +98,6 @@ class ComposerController extends BaseController with DragDropFileMixin { final networkConnectionController = Get.find(); final _dynamicUrlInterceptors = Get.find(); - final expandModeAttachments = ExpandMode.EXPAND.obs; final composerArguments = Rxn(); final isEnableEmailSendButton = false.obs; final isInitialRecipient = false.obs; @@ -1320,12 +1319,6 @@ class ComposerController extends BaseController with DragDropFileMixin { mailboxDashBoardController.closeComposerOverlay(); } - void toggleDisplayAttachments() { - final newExpandMode = expandModeAttachments.value == ExpandMode.COLLAPSE - ? ExpandMode.EXPAND : ExpandMode.COLLAPSE; - expandModeAttachments.value = newExpandMode; - } - void addEmailAddressType(PrefixEmailAddress prefixEmailAddress) { switch(prefixEmailAddress) { case PrefixEmailAddress.from: diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index 7c408ae9b3..c42f3b650c 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -185,6 +185,16 @@ class ComposerView extends GetWidget { margin: ComposerStyle.mobileSubjectMargin, onTapOutside: controller.onTapOutsideSubject, ), + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return MobileAttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + onDeleteAttachmentAction: controller.deleteAttachmentUploaded, + ); + } else { + return const SizedBox.shrink(); + } + }), Obx(() => Center( child: InsertImageLoadingBarWidget( uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, @@ -201,16 +211,6 @@ class ComposerView extends GetWidget { onLoadCompletedEditorAction: controller.onLoadCompletedMobileEditorAction, ), )), - Obx(() { - if (controller.uploadController.listUploadAttachments.isNotEmpty) { - return MobileAttachmentComposerWidget( - listFileUploaded: controller.uploadController.listUploadAttachments, - onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), - ); - } else { - return const SizedBox.shrink(); - } - }), const SizedBox(height: ComposerStyle.keyboardMaxHeight), ], ), @@ -331,6 +331,16 @@ class ComposerView extends GetWidget { margin: ComposerStyle.mobileSubjectMargin, onTapOutside: controller.onTapOutsideSubject, ), + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return MobileAttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + onDeleteAttachmentAction: controller.deleteAttachmentUploaded, + ); + } else { + return const SizedBox.shrink(); + } + }), Obx(() => Center( child: InsertImageLoadingBarWidget( uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, @@ -347,16 +357,6 @@ class ComposerView extends GetWidget { onLoadCompletedEditorAction: controller.onLoadCompletedMobileEditorAction, ), )), - Obx(() { - if (controller.uploadController.listUploadAttachments.isNotEmpty) { - return MobileAttachmentComposerWidget( - listFileUploaded: controller.uploadController.listUploadAttachments, - onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), - ); - } else { - return const SizedBox.shrink(); - } - }) ], ), ) diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index 394f991255..8a49da6b6d 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -190,7 +190,7 @@ class ComposerView extends GetWidget { return AttachmentComposerWidget( listFileUploaded: controller.uploadController.listUploadAttachments, isCollapsed: controller.isAttachmentCollapsed, - onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), + onDeleteAttachmentAction: controller.deleteAttachmentUploaded, onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, ); } else { @@ -423,7 +423,7 @@ class ComposerView extends GetWidget { return AttachmentComposerWidget( listFileUploaded: controller.uploadController.listUploadAttachments, isCollapsed: controller.isAttachmentCollapsed, - onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), + onDeleteAttachmentAction: controller.deleteAttachmentUploaded, onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, ); } else { @@ -675,7 +675,7 @@ class ComposerView extends GetWidget { return AttachmentComposerWidget( listFileUploaded: controller.uploadController.listUploadAttachments, isCollapsed: controller.isAttachmentCollapsed, - onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), + onDeleteAttachmentAction: controller.deleteAttachmentUploaded, onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, ); } else { diff --git a/lib/features/composer/presentation/widgets/attachment_item_composer_widget.dart b/lib/features/composer/presentation/widgets/attachment_item_composer_widget.dart index c77b6e7a4c..548b934747 100644 --- a/lib/features/composer/presentation/widgets/attachment_item_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/attachment_item_composer_widget.dart @@ -9,9 +9,10 @@ import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; import 'package:tmail_ui_user/features/composer/presentation/styles/attachment_item_composer_widget_style.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/attachment_progress_loading_composer_widget.dart'; +import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart'; -typedef OnDeleteAttachmentAction = void Function(UploadFileState fileState); +typedef OnDeleteAttachmentAction = void Function(UploadTaskId uploadTaskId); class AttachmentItemComposerWidget extends StatelessWidget with AppLoaderMixin { @@ -100,7 +101,7 @@ class AttachmentItemComposerWidget extends StatelessWidget with AppLoaderMixin { borderRadius: AttachmentItemComposerWidgetStyle.deleteIconRadius, padding: AttachmentItemComposerWidgetStyle.deleteIconPadding, iconColor: AttachmentItemComposerWidgetStyle.deleteIconColor, - onTapActionCallback: () => onDeleteAttachmentAction?.call(fileState), + onTapActionCallback: () => onDeleteAttachmentAction?.call(fileState.uploadTaskId), ) ], ), diff --git a/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart b/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart index e219c6f3f5..5c14f4bc47 100644 --- a/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart @@ -1,4 +1,7 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/presentation/views/list/sliver_grid_delegate_fixed_height.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -6,20 +9,47 @@ import 'package:tmail_ui_user/features/composer/presentation/styles/attachment_i import 'package:tmail_ui_user/features/composer/presentation/styles/mobile/mobile_attachment_composer_widget_style.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/attachment_item_composer_widget.dart'; import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart'; +import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_status.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -class MobileAttachmentComposerWidget extends StatelessWidget { +class MobileAttachmentComposerWidget extends StatefulWidget { final List listFileUploaded; final OnDeleteAttachmentAction onDeleteAttachmentAction; - final _responsiveUtils = Get.find(); - - MobileAttachmentComposerWidget({ + const MobileAttachmentComposerWidget({ super.key, required this.listFileUploaded, required this.onDeleteAttachmentAction, }); + @override + State createState() => _MobileAttachmentComposerWidgetState(); +} + +class _MobileAttachmentComposerWidgetState extends State { + static const int _maxCountDisplayedAttachments = 2; + + final _responsiveUtils = Get.find(); + final _imagePaths = Get.find(); + + List _listFileDisplayed = []; + bool _isCollapsed = false; + + @override + void initState() { + super.initState(); + _updateListFileDisplayed(); + } + + @override + void didUpdateWidget(covariant MobileAttachmentComposerWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.listFileUploaded != widget.listFileUploaded) { + _updateListFileDisplayed(); + } + } + @override Widget build(BuildContext context) { return Container( @@ -33,10 +63,9 @@ class MobileAttachmentComposerWidget extends StatelessWidget { SizedBox( width: _responsiveUtils.getSizeScreenWidth(context) * 0.7, child: GridView.builder( - reverse: true, primary: false, shrinkWrap: true, - itemCount: listFileUploaded.length, + itemCount: _listFileDisplayed.length, gridDelegate: const SliverGridDelegateFixedHeight( height: MobileAttachmentComposerWidgetStyle.listItemHeight, crossAxisCount: MobileAttachmentComposerWidgetStyle.maxItemRow, @@ -44,32 +73,93 @@ class MobileAttachmentComposerWidget extends StatelessWidget { ), itemBuilder: (context, index) { return AttachmentItemComposerWidget( - fileState: listFileUploaded[index], + fileState: _listFileDisplayed[index], itemMargin: MobileAttachmentComposerWidgetStyle.itemMargin, - onDeleteAttachmentAction: onDeleteAttachmentAction + onDeleteAttachmentAction: widget.onDeleteAttachmentAction ); } ), ) else - SizedBox( - width: AttachmentItemComposerWidgetStyle.width, - child: ListView.builder( - reverse: true, - shrinkWrap: true, - primary: false, - itemCount: listFileUploaded.length, - itemBuilder: (context, index) { - return AttachmentItemComposerWidget( - fileState: listFileUploaded[index], - itemMargin: MobileAttachmentComposerWidgetStyle.itemMargin, - onDeleteAttachmentAction: onDeleteAttachmentAction - ); - } + ...[ + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Flexible(child: SizedBox( + width: AttachmentItemComposerWidgetStyle.width, + child: ListView.builder( + shrinkWrap: true, + primary: false, + itemCount: _listFileDisplayed.length, + itemBuilder: (context, index) { + return AttachmentItemComposerWidget( + fileState: _listFileDisplayed[index], + itemMargin: MobileAttachmentComposerWidgetStyle.itemMargin, + onDeleteAttachmentAction: widget.onDeleteAttachmentAction + ); + } + ), + )), + if (!_isCollapsed && _isExceededDisplayedAttachments) + TMailButtonWidget( + text: AppLocalizations.of(context).showLess, + icon: _imagePaths.icChevronUp, + iconAlignment: TextDirection.rtl, + iconSpace: 2, + iconSize: 24, + iconColor: AppColor.primaryColor, + backgroundColor: Colors.transparent, + textStyle: Theme.of(context).textTheme.labelSmall?.copyWith( + fontSize: 15, + color: AppColor.primaryColor + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + margin: const EdgeInsetsDirectional.only(start: 8), + onTapActionCallback: _toggleListAttachments + ) + ], ), - ) + if (_isCollapsed && _isExceededDisplayedAttachments) + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).showMore(_countRemainingAttachments), + backgroundColor: Colors.transparent, + textStyle: Theme.of(context).textTheme.labelSmall?.copyWith( + fontSize: 15, + color: AppColor.primaryColor + ), + margin: const EdgeInsetsDirectional.only(top: 5), + onTapActionCallback: _toggleListAttachments + ) + ] ] ), ); } + + bool get _listFileUploadedSuccess => + widget.listFileUploaded.every((uploadFile) => uploadFile.uploadStatus.completed); + + bool get _isExceededDisplayedAttachments => + _listFileUploadedSuccess && + widget.listFileUploaded.length > _maxCountDisplayedAttachments; + + int get _countRemainingAttachments => + widget.listFileUploaded.length - _maxCountDisplayedAttachments; + + void _updateListFileDisplayed() { + final reversedList = widget.listFileUploaded.reversed.toList(); + if (_isCollapsed) { + _listFileDisplayed = reversedList.sublist(0, _maxCountDisplayedAttachments); + } else { + _listFileDisplayed = reversedList; + } + } + + void _toggleListAttachments() { + setState(() { + _isCollapsed = !_isCollapsed; + _updateListFileDisplayed(); + }); + } } diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index b7c77eee47..d2427489ec 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -338,9 +338,9 @@ class _RecipientComposerWidgetState extends State { TMailButtonWidget.fromIcon( icon: _isAllRecipientInputEnabled ? _imagePaths.icChevronUp - : _imagePaths.icChevronDown, + : _imagePaths.icChevronDownOutline, backgroundColor: Colors.transparent, - iconSize: 20, + iconSize: 24, padding: const EdgeInsets.all(5), iconColor: AppColor.colorLabelComposer, margin: RecipientComposerWidgetStyle.enableRecipientButtonMargin, diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 4fc020a496..b563d04a00 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-03-13T01:12:22.224880", + "@@last_modified": "2024-03-19T12:10:23.549474", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3857,5 +3857,21 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "showMore": "Show more (+{count})", + "@showMore": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "showLess": "Show less", + "@showLess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index f2147bf8ec..5dcf3481c2 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4030,4 +4030,17 @@ class AppLocalizations { name: 'mailToAttendees' ); } + + String showMore(int count) { + return Intl.message( + 'Show more (+$count)', + name: 'showMore', + args: [count]); + } + + String get showLess { + return Intl.message( + 'Show less', + name: 'showLess'); + } } \ No newline at end of file From 3101b89f7e814a8617bc9bcabadff7e68937bec1 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 25 Mar 2024 14:52:23 +0700 Subject: [PATCH 39/80] TF-2717 Display attachment bar below subject field on landscape mobile --- .../attachment_item_composer_widget.dart | 4 +- .../mobile_attachment_composer_widget.dart | 83 ++++++++++++++----- 2 files changed, 67 insertions(+), 20 deletions(-) diff --git a/lib/features/composer/presentation/widgets/attachment_item_composer_widget.dart b/lib/features/composer/presentation/widgets/attachment_item_composer_widget.dart index 548b934747..18f4802548 100644 --- a/lib/features/composer/presentation/widgets/attachment_item_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/attachment_item_composer_widget.dart @@ -21,6 +21,7 @@ class AttachmentItemComposerWidget extends StatelessWidget with AppLoaderMixin { final UploadFileState fileState; final double? maxWidth; final EdgeInsetsGeometry? itemMargin; + final EdgeInsetsGeometry? itemPadding; final OnDeleteAttachmentAction? onDeleteAttachmentAction; final Widget? buttonAction; @@ -29,6 +30,7 @@ class AttachmentItemComposerWidget extends StatelessWidget with AppLoaderMixin { required this.fileState, this.maxWidth, this.itemMargin, + this.itemPadding, this.buttonAction, this.onDeleteAttachmentAction, }); @@ -42,7 +44,7 @@ class AttachmentItemComposerWidget extends StatelessWidget with AppLoaderMixin { color: AttachmentItemComposerWidgetStyle.backgroundColor ), width: AttachmentItemComposerWidgetStyle.width, - padding: AttachmentItemComposerWidgetStyle.padding, + padding: itemPadding ?? AttachmentItemComposerWidgetStyle.padding, margin: itemMargin, child: Row( children: [ diff --git a/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart b/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart index 5c14f4bc47..ad28011125 100644 --- a/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart @@ -60,25 +60,70 @@ class _MobileAttachmentComposerWidgetState extends State Date: Tue, 19 Mar 2024 14:08:03 +0700 Subject: [PATCH 40/80] TF-2717 Display attachment bar below subject field on tablet --- ...bile_attachment_composer_widget_style.dart | 1 + .../mobile_attachment_composer_widget.dart | 153 +++++++++--------- 2 files changed, 78 insertions(+), 76 deletions(-) diff --git a/lib/features/composer/presentation/styles/mobile/mobile_attachment_composer_widget_style.dart b/lib/features/composer/presentation/styles/mobile/mobile_attachment_composer_widget_style.dart index 4c401ff6eb..30eb22df90 100644 --- a/lib/features/composer/presentation/styles/mobile/mobile_attachment_composer_widget_style.dart +++ b/lib/features/composer/presentation/styles/mobile/mobile_attachment_composer_widget_style.dart @@ -6,5 +6,6 @@ class MobileAttachmentComposerWidgetStyle { static const double listItemHeight = 50; static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 16); + static const EdgeInsetsGeometry tabletPadding = EdgeInsetsDirectional.only(start: 16, end: 16, bottom: 8); static const EdgeInsetsGeometry itemMargin = EdgeInsetsDirectional.only(top: 8); } \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart b/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart index ad28011125..6d0e4817c7 100644 --- a/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart @@ -53,80 +53,15 @@ class _MobileAttachmentComposerWidgetState extends State _maxCountDisplayedAttachments) { _listFileDisplayed = reversedList.sublist(0, _maxCountDisplayedAttachments); } else { _listFileDisplayed = reversedList; From 661502d355f539219cc91144e2d88de98c5104bc Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 25 Mar 2024 15:17:26 +0700 Subject: [PATCH 41/80] TF-2717 Write widgets test for RecipientComposerWidget --- .../composer/presentation/composer_view.dart | 14 +- .../presentation/composer_view_web.dart | 18 + .../view/mobile/mobile_container_view.dart | 4 +- .../widgets/recipient_composer_widget.dart | 486 +++++++++--------- .../recipient_suggestion_item_widget.dart | 9 +- .../widgets/recipient_tag_item_widget.dart | 26 +- .../recipient_composer_widget_test.dart | 184 +++++++ 7 files changed, 487 insertions(+), 254 deletions(-) create mode 100644 test/features/features/composer/recipient_composer_widget_test.dart diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index c42f3b650c..765f9c5f07 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -49,7 +49,7 @@ class ComposerView extends GetWidget { ? controller.insertImage(context, constraints.maxWidth) : null, backgroundColor: MobileAppBarComposerWidgetStyle.backgroundColor, - childBuilder: (context) => SafeArea( + childBuilder: (context, constraints) => SafeArea( left: !controller.responsiveUtils.isLandscapeMobile(context), right: !controller.responsiveUtils.isLandscapeMobile(context), child: Container( @@ -110,6 +110,8 @@ class ComposerView extends GetWidget { Obx(() => RecipientComposerWidget( prefix: PrefixEmailAddress.to, listEmailAddress: controller.listToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, @@ -134,6 +136,8 @@ class ComposerView extends GetWidget { return RecipientComposerWidget( prefix: PrefixEmailAddress.cc, listEmailAddress: controller.listCcEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, @@ -158,6 +162,8 @@ class ComposerView extends GetWidget { return RecipientComposerWidget( prefix: PrefixEmailAddress.bcc, listEmailAddress: controller.listBccEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, @@ -264,6 +270,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.to, listEmailAddress: controller.listToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, @@ -287,6 +295,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.cc, listEmailAddress: controller.listCcEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, @@ -306,6 +316,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.bcc, listEmailAddress: controller.listBccEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index 8a49da6b6d..e2d3531482 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -87,6 +87,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.to, listEmailAddress: controller.listToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, @@ -110,6 +112,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.cc, listEmailAddress: controller.listCcEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, @@ -130,6 +134,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.bcc, listEmailAddress: controller.listBccEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, @@ -305,6 +311,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.to, listEmailAddress: controller.listToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, @@ -328,6 +336,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.cc, listEmailAddress: controller.listCcEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, @@ -348,6 +358,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.bcc, listEmailAddress: controller.listBccEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, @@ -562,6 +574,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.to, listEmailAddress: controller.listToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, @@ -585,6 +599,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.cc, listEmailAddress: controller.listCcEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, @@ -605,6 +621,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.bcc, listEmailAddress: controller.listBccEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, diff --git a/lib/features/composer/presentation/view/mobile/mobile_container_view.dart b/lib/features/composer/presentation/view/mobile/mobile_container_view.dart index 6854dbeecb..2c6c23f27d 100644 --- a/lib/features/composer/presentation/view/mobile/mobile_container_view.dart +++ b/lib/features/composer/presentation/view/mobile/mobile_container_view.dart @@ -12,7 +12,7 @@ typedef OnInsertImageAction = Function(BoxConstraints constraints); class MobileContainerView extends StatelessWidget { - final Widget Function(BuildContext context) childBuilder; + final Widget Function(BuildContext context, BoxConstraints constraints) childBuilder; final rich_composer.RichTextController keyboardRichTextController; final VoidCallback onCloseViewAction; final VoidCallback? onAttachFileAction; @@ -69,7 +69,7 @@ class MobileContainerView extends StatelessWidget { paddingChild: isKeyboardVisible ? MobileContainerViewStyle.keyboardToolbarPadding : EdgeInsets.zero, - child: childBuilder(context), + child: childBuilder(context, constraints), ); }); }) diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index d2427489ec..c94831593d 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -11,7 +11,6 @@ import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/email/prefix_email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; @@ -41,6 +40,8 @@ class RecipientComposerWidget extends StatefulWidget { final PrefixEmailAddress prefix; final List listEmailAddress; + final ImagePaths imagePaths; + final double maxWidth; final ExpandMode expandMode; final PrefixRecipientState fromState; final PrefixRecipientState ccState; @@ -66,6 +67,8 @@ class RecipientComposerWidget extends StatefulWidget { super.key, required this.prefix, required this.listEmailAddress, + required this.imagePaths, + required this.maxWidth, this.ccState = PrefixRecipientState.disabled, this.bccState = PrefixRecipientState.disabled, this.fromState = PrefixRecipientState.disabled, @@ -99,8 +102,6 @@ class _RecipientComposerWidgetState extends State { bool _isDragging = false; late List _currentListEmailAddress; - final _imagePaths = Get.find(); - @override void initState() { super.initState(); @@ -117,250 +118,255 @@ class _RecipientComposerWidgetState extends State { @override Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints) { - return Container( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: RecipientComposerWidgetStyle.borderColor, - width: 1 - ) + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: RecipientComposerWidgetStyle.borderColor, + width: 1 ) - ), - padding: widget.padding, - margin: widget.margin, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: RecipientComposerWidgetStyle.labelMargin, - child: Text( - '${widget.prefix.asName(context)}:', - style: RecipientComposerWidgetStyle.labelTextStyle - ), + ) + ), + padding: widget.padding, + margin: widget.margin, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: RecipientComposerWidgetStyle.labelMargin, + child: Text( + '${widget.prefix.asName(context)}:', + key: Key('prefix_${widget.prefix.name}_recipient_composer_widget'), + style: RecipientComposerWidgetStyle.labelTextStyle ), - const SizedBox(width: RecipientComposerWidgetStyle.space), - Expanded( - child: FocusScope( - child: Focus( - onFocusChange: (focus) => widget.onFocusEmailAddressChangeAction?.call(widget.prefix, focus), - onKey: (focusNode, event) { - if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) { - widget.nextFocusNode?.requestFocus(); - widget.onFocusNextAddressAction?.call(); - return KeyEventResult.handled; + ), + const SizedBox(width: RecipientComposerWidgetStyle.space), + Expanded( + child: FocusScope( + child: Focus( + onFocusChange: (focus) => widget.onFocusEmailAddressChangeAction?.call(widget.prefix, focus), + onKey: (focusNode, event) { + if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) { + widget.nextFocusNode?.requestFocus(); + widget.onFocusNextAddressAction?.call(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: StatefulBuilder( + builder: (context, stateSetter) { + if (PlatformInfo.isWeb) { + return DragTarget( + builder: (context, candidateData, rejectedData) { + return TagEditor( + key: widget.keyTagEditor, + length: _collapsedListEmailAddress.length, + controller: widget.controller, + focusNode: widget.focusNode, + enableBorder: _isDragging, + borderRadius: RecipientComposerWidgetStyle.enableBorderRadius, + enableBorderColor: RecipientComposerWidgetStyle.enableBorderColor, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.done, + debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, + tagSpacing: RecipientComposerWidgetStyle.tagSpacing, + autofocus: widget.prefix != PrefixEmailAddress.to && _currentListEmailAddress.isEmpty, + minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, + resetTextOnSubmitted: true, + autoScrollToInput: false, + cursorColor: RecipientComposerWidgetStyle.cursorColor, + suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, + suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, + suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, + suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, + suggestionBoxWidth: _getSuggestionBoxWidth(widget.maxWidth), + textStyle: RecipientComposerWidgetStyle.inputTextStyle, + onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), + onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), + onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), + onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), + onTapOutside: (_) {}, + inputDecoration: const InputDecoration(border: InputBorder.none), + tagBuilder: (context, index) { + final currentEmailAddress = _currentListEmailAddress[index]; + final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; + + return RecipientTagItemWidget( + index: index, + imagePaths: widget.imagePaths, + prefix: widget.prefix, + currentEmailAddress: currentEmailAddress, + currentListEmailAddress: _currentListEmailAddress, + collapsedListEmailAddress: _collapsedListEmailAddress, + isLatestEmail: isLatestEmail, + isCollapsed: _isCollapse, + isLatestTagFocused: _lastTagFocused, + maxWidth: widget.maxWidth, + onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), + onShowFullAction: widget.onShowFullListEmailAddressAction, + ); + }, + onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), + findSuggestions: _findSuggestions, + useDefaultHighlight: false, + suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { + return RecipientSuggestionItemWidget( + imagePaths: widget.imagePaths, + suggestionState: suggestionEmailAddress.state, + emailAddress: suggestionEmailAddress.emailAddress, + suggestionValid: suggestionValid, + highlight: highlight, + onSelectedAction: (emailAddress) { + stateSetter(() => _currentListEmailAddress.add(emailAddress)); + _updateListEmailAddressAction(); + tagEditorState.resetTextField(); + tagEditorState.closeSuggestionBox(); + }, + ); + }, + ); + }, + onAccept: (draggableEmailAddress) => _handleAcceptDraggableEmailAddressAction(draggableEmailAddress, stateSetter), + onLeave: (draggableEmailAddress) { + if (_isDragging) { + stateSetter(() => _isDragging = false); + } + }, + onMove: (details) { + if (!_isDragging) { + stateSetter(() => _isDragging = true); + } + }, + ); + } else { + return TagEditor( + key: widget.keyTagEditor, + length: _collapsedListEmailAddress.length, + controller: widget.controller, + focusNode: widget.focusNode, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.done, + debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, + tagSpacing: RecipientComposerWidgetStyle.tagSpacing, + minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, + resetTextOnSubmitted: true, + autoScrollToInput: false, + cursorColor: RecipientComposerWidgetStyle.cursorColor, + suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, + suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, + suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, + suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, + suggestionBoxWidth: _getSuggestionBoxWidth(widget.maxWidth), + textStyle: RecipientComposerWidgetStyle.inputTextStyle, + onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), + onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), + onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), + onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), + onTapOutside: (_) {}, + inputDecoration: const InputDecoration(border: InputBorder.none), + tagBuilder: (context, index) { + final currentEmailAddress = _currentListEmailAddress[index]; + final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; + + return RecipientTagItemWidget( + index: index, + imagePaths: widget.imagePaths, + prefix: widget.prefix, + currentEmailAddress: currentEmailAddress, + currentListEmailAddress: _currentListEmailAddress, + collapsedListEmailAddress: _collapsedListEmailAddress, + isLatestEmail: isLatestEmail, + isCollapsed: _isCollapse, + isLatestTagFocused: _lastTagFocused, + maxWidth: widget.maxWidth, + onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), + onShowFullAction: widget.onShowFullListEmailAddressAction, + ); + }, + onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), + findSuggestions: _findSuggestions, + useDefaultHighlight: false, + suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { + return RecipientSuggestionItemWidget( + imagePaths: widget.imagePaths, + suggestionState: suggestionEmailAddress.state, + emailAddress: suggestionEmailAddress.emailAddress, + suggestionValid: suggestionValid, + highlight: highlight, + onSelectedAction: (emailAddress) { + stateSetter(() => _currentListEmailAddress.add(emailAddress)); + _updateListEmailAddressAction(); + tagEditorState.resetTextField(); + tagEditorState.closeSuggestionBox(); + }, + ); + }, + ); } - return KeyEventResult.ignored; }, - child: StatefulBuilder( - builder: (context, stateSetter) { - if (PlatformInfo.isWeb) { - return DragTarget( - builder: (context, candidateData, rejectedData) { - return TagEditor( - key: widget.keyTagEditor, - length: _collapsedListEmailAddress.length, - controller: widget.controller, - focusNode: widget.focusNode, - enableBorder: _isDragging, - borderRadius: RecipientComposerWidgetStyle.enableBorderRadius, - enableBorderColor: RecipientComposerWidgetStyle.enableBorderColor, - keyboardType: TextInputType.emailAddress, - textInputAction: TextInputAction.done, - debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, - tagSpacing: RecipientComposerWidgetStyle.tagSpacing, - autofocus: widget.prefix != PrefixEmailAddress.to && _currentListEmailAddress.isEmpty, - minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, - resetTextOnSubmitted: true, - autoScrollToInput: false, - cursorColor: RecipientComposerWidgetStyle.cursorColor, - suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, - suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, - suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, - suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, - suggestionBoxWidth: _getSuggestionBoxWidth(constraints.maxWidth), - textStyle: RecipientComposerWidgetStyle.inputTextStyle, - onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), - onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), - onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), - onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), - onTapOutside: (_) {}, - inputDecoration: const InputDecoration(border: InputBorder.none), - tagBuilder: (context, index) { - final currentEmailAddress = _currentListEmailAddress[index]; - final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; - - return RecipientTagItemWidget( - prefix: widget.prefix, - currentEmailAddress: currentEmailAddress, - currentListEmailAddress: _currentListEmailAddress, - collapsedListEmailAddress: _collapsedListEmailAddress, - isLatestEmail: isLatestEmail, - isCollapsed: _isCollapse, - isLatestTagFocused: _lastTagFocused, - maxWidth: constraints.maxWidth, - onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), - onShowFullAction: widget.onShowFullListEmailAddressAction, - ); - }, - onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), - findSuggestions: _findSuggestions, - useDefaultHighlight: false, - suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { - return RecipientSuggestionItemWidget( - suggestionState: suggestionEmailAddress.state, - emailAddress: suggestionEmailAddress.emailAddress, - suggestionValid: suggestionValid, - highlight: highlight, - onSelectedAction: (emailAddress) { - stateSetter(() => _currentListEmailAddress.add(emailAddress)); - _updateListEmailAddressAction(); - tagEditorState.resetTextField(); - tagEditorState.closeSuggestionBox(); - }, - ); - }, - ); - }, - onAccept: (draggableEmailAddress) => _handleAcceptDraggableEmailAddressAction(draggableEmailAddress, stateSetter), - onLeave: (draggableEmailAddress) { - if (_isDragging) { - stateSetter(() => _isDragging = false); - } - }, - onMove: (details) { - if (!_isDragging) { - stateSetter(() => _isDragging = true); - } - }, - ); - } else { - return TagEditor( - key: widget.keyTagEditor, - length: _collapsedListEmailAddress.length, - controller: widget.controller, - focusNode: widget.focusNode, - keyboardType: TextInputType.emailAddress, - textInputAction: TextInputAction.done, - debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, - tagSpacing: RecipientComposerWidgetStyle.tagSpacing, - minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, - resetTextOnSubmitted: true, - autoScrollToInput: false, - cursorColor: RecipientComposerWidgetStyle.cursorColor, - suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, - suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, - suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, - suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, - suggestionBoxWidth: _getSuggestionBoxWidth(constraints.maxWidth), - textStyle: RecipientComposerWidgetStyle.inputTextStyle, - onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), - onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), - onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), - onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), - onTapOutside: (_) {}, - inputDecoration: const InputDecoration(border: InputBorder.none), - tagBuilder: (context, index) { - final currentEmailAddress = _currentListEmailAddress[index]; - final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; - - return RecipientTagItemWidget( - prefix: widget.prefix, - currentEmailAddress: currentEmailAddress, - currentListEmailAddress: _currentListEmailAddress, - collapsedListEmailAddress: _collapsedListEmailAddress, - isLatestEmail: isLatestEmail, - isCollapsed: _isCollapse, - isLatestTagFocused: _lastTagFocused, - maxWidth: constraints.maxWidth, - onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), - onShowFullAction: widget.onShowFullListEmailAddressAction, - ); - }, - onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), - findSuggestions: _findSuggestions, - useDefaultHighlight: false, - suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { - return RecipientSuggestionItemWidget( - suggestionState: suggestionEmailAddress.state, - emailAddress: suggestionEmailAddress.emailAddress, - suggestionValid: suggestionValid, - highlight: highlight, - onSelectedAction: (emailAddress) { - stateSetter(() => _currentListEmailAddress.add(emailAddress)); - _updateListEmailAddressAction(); - tagEditorState.resetTextField(); - tagEditorState.closeSuggestionBox(); - }, - ); - }, - ); - } - }, - ) ) ) - ), - const SizedBox(width: RecipientComposerWidgetStyle.space), - if (widget.prefix == PrefixEmailAddress.to) - if (PlatformInfo.isWeb) - ...[ - if (widget.fromState == PrefixRecipientState.disabled) - TMailButtonWidget.fromText( - text: AppLocalizations.of(context).from_email_address_prefix, - textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, - backgroundColor: Colors.transparent, - padding: RecipientComposerWidgetStyle.prefixButtonPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.from), - ), - if (widget.ccState == PrefixRecipientState.disabled) - TMailButtonWidget.fromText( - text: AppLocalizations.of(context).cc_email_address_prefix, - textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, - backgroundColor: Colors.transparent, - padding: RecipientComposerWidgetStyle.prefixButtonPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.cc), - ), - if (widget.bccState == PrefixRecipientState.disabled) - TMailButtonWidget.fromText( - text: AppLocalizations.of(context).bcc_email_address_prefix, - textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, - backgroundColor: Colors.transparent, - padding: RecipientComposerWidgetStyle.prefixButtonPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.bcc), - ), - ] - else if (PlatformInfo.isMobile) - ...[ - TMailButtonWidget.fromIcon( - icon: _isAllRecipientInputEnabled - ? _imagePaths.icChevronUp - : _imagePaths.icChevronDownOutline, + ) + ), + const SizedBox(width: RecipientComposerWidgetStyle.space), + if (widget.prefix == PrefixEmailAddress.to) + if (PlatformInfo.isWeb) + ...[ + if (widget.fromState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).from_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, backgroundColor: Colors.transparent, - iconSize: 24, - padding: const EdgeInsets.all(5), - iconColor: AppColor.colorLabelComposer, - margin: RecipientComposerWidgetStyle.enableRecipientButtonMargin, - onTapActionCallback: () => widget.onEnableAllRecipientsInputAction?.call(_isAllRecipientInputEnabled), - ) - ] - else if (PlatformInfo.isWeb) - TMailButtonWidget.fromIcon( - icon: _imagePaths.icClose, - backgroundColor: Colors.transparent, - iconColor: RecipientComposerWidgetStyle.deleteRecipientFieldIconColor, - iconSize: RecipientComposerWidgetStyle.deleteRecipientFieldIconSize, - padding: RecipientComposerWidgetStyle.deleteRecipientFieldIconPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onDeleteEmailAddressTypeAction?.call(widget.prefix), - ) - ] - ), - ); - }); + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.from), + ), + if (widget.ccState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).cc_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, + backgroundColor: Colors.transparent, + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.cc), + ), + if (widget.bccState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).bcc_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, + backgroundColor: Colors.transparent, + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.bcc), + ), + ] + else if (PlatformInfo.isMobile) + ...[ + TMailButtonWidget.fromIcon( + icon: _isAllRecipientInputEnabled + ? widget.imagePaths.icChevronUp + : widget.imagePaths.icChevronDownOutline, + backgroundColor: Colors.transparent, + iconSize: 24, + padding: const EdgeInsets.all(5), + iconColor: AppColor.colorLabelComposer, + margin: RecipientComposerWidgetStyle.enableRecipientButtonMargin, + onTapActionCallback: () => widget.onEnableAllRecipientsInputAction?.call(_isAllRecipientInputEnabled), + ) + ] + else if (PlatformInfo.isWeb) + TMailButtonWidget.fromIcon( + icon: widget.imagePaths.icClose, + backgroundColor: Colors.transparent, + iconColor: RecipientComposerWidgetStyle.deleteRecipientFieldIconColor, + iconSize: RecipientComposerWidgetStyle.deleteRecipientFieldIconSize, + padding: RecipientComposerWidgetStyle.deleteRecipientFieldIconPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onDeleteEmailAddressTypeAction?.call(widget.prefix), + ) + ] + ), + ); } bool get _isCollapse => _currentListEmailAddress.length > 1 && widget.expandMode == ExpandMode.COLLAPSE; diff --git a/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart b/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart index 87b83b29c4..29dc018b0d 100644 --- a/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart @@ -3,7 +3,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:super_tag_editor/widgets/rich_text_widget.dart'; @@ -17,16 +16,16 @@ class RecipientSuggestionItemWidget extends StatelessWidget { final SuggestionEmailState suggestionState; final EmailAddress emailAddress; + final ImagePaths imagePaths; final String? suggestionValid; final bool highlight; final OnSelectedRecipientSuggestionAction? onSelectedAction; - final _imagePaths = Get.find(); - - RecipientSuggestionItemWidget({ + const RecipientSuggestionItemWidget({ super.key, required this.suggestionState, required this.emailAddress, + required this.imagePaths, this.suggestionValid, this.highlight = false, this.onSelectedAction, @@ -61,7 +60,7 @@ class RecipientSuggestionItemWidget extends StatelessWidget { ) : null, trailing: SvgPicture.asset( - _imagePaths.icFilterSelected, + imagePaths.icFilterSelected, width: RecipientSuggestionItemWidgetStyle.selectedIconSize, height: RecipientSuggestionItemWidgetStyle.selectedIconSize, fit: BoxFit.fill diff --git a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart index 57b981ed93..d7867b2813 100644 --- a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart @@ -8,7 +8,6 @@ import 'package:core/utils/direction_utils.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/email/prefix_email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; @@ -23,7 +22,9 @@ class RecipientTagItemWidget extends StatelessWidget { final bool isCollapsed; final bool isLatestTagFocused; final bool isLatestEmail; + final ImagePaths imagePaths; final double? maxWidth; + final int index; final PrefixEmailAddress prefix; final EmailAddress currentEmailAddress; final List currentListEmailAddress; @@ -31,14 +32,14 @@ class RecipientTagItemWidget extends StatelessWidget { final OnShowFullListEmailAddressAction? onShowFullAction; final OnDeleteTagAction? onDeleteTagAction; - final _imagePaths = Get.find(); - - RecipientTagItemWidget({ + const RecipientTagItemWidget({ super.key, + required this.index, required this.prefix, required this.currentEmailAddress, required this.currentListEmailAddress, required this.collapsedListEmailAddress, + required this.imagePaths, this.isCollapsed = false, this.isLatestTagFocused = false, this.isLatestEmail = false, @@ -50,6 +51,7 @@ class RecipientTagItemWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Container( + key: Key('recipient_tag_item_${prefix.name}_$index'), constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), child: Row( mainAxisSize: MainAxisSize.min, @@ -77,12 +79,17 @@ class RecipientTagItemWidget extends StatelessWidget { ), padding: EdgeInsets.zero, label: Text( + key: Key('label_recipient_tag_item_${prefix.name}_$index'), currentEmailAddress.asString(), maxLines: 1, overflow: CommonTextStyle.defaultTextOverFlow, softWrap: CommonTextStyle.defaultSoftWrap, ), - deleteIcon: SvgPicture.asset(_imagePaths.icClose, fit: BoxFit.fill), + deleteIcon: SvgPicture.asset( + imagePaths.icClose, + key: Key('delete_icon_recipient_tag_item_${prefix.name}_$index'), + fit: BoxFit.fill + ), labelStyle: RecipientTagItemWidgetStyle.labelTextStyle, backgroundColor: _getTagBackgroundColor(), side: _getTagBorderSide(), @@ -91,6 +98,7 @@ class RecipientTagItemWidget extends StatelessWidget { ), avatar: currentEmailAddress.displayName.isNotEmpty ? GradientCircleAvatarIcon( + key: Key('avatar_icon_recipient_tag_item_${prefix.name}_$index'), colors: currentEmailAddress.avatarColors, label: currentEmailAddress.displayName.firstLetterToUpperCase, labelFontSize: RecipientTagItemWidgetStyle.avatarLabelFontSize, @@ -116,13 +124,18 @@ class RecipientTagItemWidget extends StatelessWidget { vertical: DirectionUtils.isDirectionRTLByHasAnyRtl(currentEmailAddress.asString()) ? 0 : 2 ), label: Text( + key: Key('label_recipient_tag_item_${prefix.name}_$index'), currentEmailAddress.asString(), maxLines: 1, overflow: CommonTextStyle.defaultTextOverFlow, softWrap: CommonTextStyle.defaultSoftWrap, ), padding: EdgeInsets.zero, - deleteIcon: SvgPicture.asset(_imagePaths.icClose, fit: BoxFit.fill), + deleteIcon: SvgPicture.asset( + imagePaths.icClose, + key: Key('delete_icon_recipient_tag_item_${prefix.name}_$index'), + fit: BoxFit.fill + ), labelStyle: RecipientTagItemWidgetStyle.labelTextStyle, backgroundColor: _getTagBackgroundColor(), side: _getTagBorderSide(), @@ -131,6 +144,7 @@ class RecipientTagItemWidget extends StatelessWidget { ), avatar: currentEmailAddress.displayName.isNotEmpty ? GradientCircleAvatarIcon( + key: Key('avatar_icon_recipient_tag_item_${prefix.name}_$index'), colors: currentEmailAddress.avatarColors, label: currentEmailAddress.displayName.firstLetterToUpperCase, labelFontSize: RecipientTagItemWidgetStyle.avatarLabelFontSize, diff --git a/test/features/features/composer/recipient_composer_widget_test.dart b/test/features/features/composer/recipient_composer_widget_test.dart new file mode 100644 index 0000000000..b17b3b992d --- /dev/null +++ b/test/features/features/composer/recipient_composer_widget_test.dart @@ -0,0 +1,184 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/email/prefix_email_address.dart'; +import 'package:super_tag_editor/tag_editor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_tag_item_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations_delegate.dart'; +import 'package:tmail_ui_user/main/localizations/localization_service.dart'; + +void main() { + group('recipient_composer_widget test', () { + final imagePaths = ImagePaths(); + final keyEmailTagEditor = GlobalKey(); + const prefix = PrefixEmailAddress.to; + + Widget makeTestableWidget({required Widget child}) { + return GetMaterialApp( + localizationsDelegates: const [ + AppLocalizationsDelegate(), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: LocalizationService.supportedLocales, + home: Scaffold(body: child), + ); + } + + testWidgets('RecipientComposerWidget renders correctly', (tester) async { + final listEmailAddress = []; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final recipientComposerWidgetFinder = find.byType(RecipientComposerWidget); + + expect(recipientComposerWidgetFinder, findsOneWidget); + }); + + testWidgets('RecipientComposerWidget renders list email address correctly', (tester) async { + final listEmailAddress = [ + EmailAddress(null, 'test1@example.com'), + EmailAddress(null, 'test2@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + expect(find.byType(RecipientTagItemWidget), findsNWidgets(2)); + expect(find.text('test1@example.com'), findsOneWidget); + expect(find.text('test2@example.com'), findsOneWidget); + }); + + testWidgets('RecipientTagItemWidget should have a `maxWidth` equal to the RecipientComposerWidget\'s `maxWidth`', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + + final Container recipientTagItemWidget = tester.widget(recipientTagItemWidgetFinder); + + expect(recipientTagItemWidgetFinder, findsOneWidget); + expect(recipientTagItemWidget.constraints!.maxWidth, 360); + }); + + testWidgets('RecipientTagItemWidget should have all the components (AvatarIcon, Label, DeleteIcon)', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + final labelRecipientTagItemWidgetFinder = find.byKey(Key('label_recipient_tag_item_${prefix.name}_0')); + final deleteIconRecipientTagItemWidgetFinder = find.byKey(Key('delete_icon_recipient_tag_item_${prefix.name}_0')); + final avatarIconRecipientTagItemWidgetFinder = find.byKey(Key('avatar_icon_recipient_tag_item_${prefix.name}_0')); + + final Size recipientTagItemWidgetSize = tester.getSize(recipientTagItemWidgetFinder); + final Size labelRecipientTagItemWidgetSize = tester.getSize(labelRecipientTagItemWidgetFinder); + final Size deleteIconRecipientTagItemWidgetSize = tester.getSize(deleteIconRecipientTagItemWidgetFinder); + final Size avatarIconRecipientTagItemWidgetSize = tester.getSize(avatarIconRecipientTagItemWidgetFinder); + + log('recipient_composer_widget_test::main: TagSize = $recipientTagItemWidgetSize | LabelTagSize = $labelRecipientTagItemWidgetSize | DeleteIconTagSize = $deleteIconRecipientTagItemWidgetSize | AvatarIconTagSize = $avatarIconRecipientTagItemWidgetSize'); + + expect(labelRecipientTagItemWidgetFinder, findsOneWidget); + expect(deleteIconRecipientTagItemWidgetFinder, findsOneWidget); + expect(avatarIconRecipientTagItemWidgetFinder, findsOneWidget); + + expect( + labelRecipientTagItemWidgetSize.width + deleteIconRecipientTagItemWidgetSize.width + avatarIconRecipientTagItemWidgetSize.width, + lessThanOrEqualTo(recipientTagItemWidgetSize.width) + ); + }); + + testWidgets('RecipientComponentWidget should have all the components (PrefixLabel, RecipientTagItemWidget)', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final prefixRecipientComposerWidgetFinder = find.byKey(Key('prefix_${prefix.name}_recipient_composer_widget')); + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + + final Size prefixRecipientComposerWidgetSize = tester.getSize(prefixRecipientComposerWidgetFinder); + final Size recipientTagItemWidgetSize = tester.getSize(prefixRecipientComposerWidgetFinder); + + log('recipient_composer_widget_test::main: PrefixLabelSize = $prefixRecipientComposerWidgetSize | TagSize = $recipientTagItemWidgetSize'); + + expect(prefixRecipientComposerWidgetFinder, findsOneWidget); + expect(recipientTagItemWidgetFinder, findsOneWidget); + expect( + prefixRecipientComposerWidgetSize.width + recipientTagItemWidgetSize.width, + lessThanOrEqualTo(360) + ); + }); + }); +} From 31e5934a67de9735a8159e82c5cfa3fca064a30a Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 25 Mar 2024 18:54:53 +0700 Subject: [PATCH 42/80] TF-2717 Write widgets test for case email address too long in recipient_tag_item --- .../recipient_composer_widget_test.dart | 84 ++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/test/features/features/composer/recipient_composer_widget_test.dart b/test/features/features/composer/recipient_composer_widget_test.dart index b17b3b992d..87bea4e2f0 100644 --- a/test/features/features/composer/recipient_composer_widget_test.dart +++ b/test/features/features/composer/recipient_composer_widget_test.dart @@ -105,7 +105,8 @@ void main() { expect(recipientTagItemWidget.constraints!.maxWidth, 360); }); - testWidgets('RecipientTagItemWidget should have all the components (AvatarIcon, Label, DeleteIcon)', (tester) async { + testWidgets('WHEN EmailAddress has address is not empty AND display name is not empty\n' + 'RecipientTagItemWidget should have all the components (AvatarIcon, Label, DeleteIcon)', (tester) async { final listEmailAddress = [ EmailAddress('test1', 'test1@example.com'), ]; @@ -180,5 +181,86 @@ void main() { lessThanOrEqualTo(360) ); }); + + testWidgets('WHEN EmailAddress has address is too long AND display name is NULL\n' + 'RecipientTagItemWidget should have all the components (Label, DeleteIcon)', (tester) async { + final listEmailAddress = [ + EmailAddress(null, 'test123456789123456789@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 392.7, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + final labelRecipientTagItemWidgetFinder = find.byKey(Key('label_recipient_tag_item_${prefix.name}_0')); + final deleteIconRecipientTagItemWidgetFinder = find.byKey(Key('delete_icon_recipient_tag_item_${prefix.name}_0')); + + final Size recipientTagItemWidgetSize = tester.getSize(recipientTagItemWidgetFinder); + final Size labelRecipientTagItemWidgetSize = tester.getSize(labelRecipientTagItemWidgetFinder); + final Size deleteIconRecipientTagItemWidgetSize = tester.getSize(deleteIconRecipientTagItemWidgetFinder); + + log('recipient_composer_widget_test::main: TagSize = $recipientTagItemWidgetSize | LabelTagSize = $labelRecipientTagItemWidgetSize | DeleteIconTagSize = $deleteIconRecipientTagItemWidgetSize'); + + expect(labelRecipientTagItemWidgetFinder, findsOneWidget); + expect(deleteIconRecipientTagItemWidgetFinder, findsOneWidget); + + expect( + labelRecipientTagItemWidgetSize.width + deleteIconRecipientTagItemWidgetSize.width, + lessThanOrEqualTo(recipientTagItemWidgetSize.width) + ); + }); + + testWidgets('WHEN EmailAddress has address is too long AND display name is too long\n' + 'RecipientTagItemWidget should have all the components (AvatarIcon, Label, DeleteIcon)', (tester) async { + final listEmailAddress = [ + EmailAddress('test12345678912345678909123456789', 'test1234567891234567895678909123456789@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 392.7, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + final labelRecipientTagItemWidgetFinder = find.byKey(Key('label_recipient_tag_item_${prefix.name}_0')); + final deleteIconRecipientTagItemWidgetFinder = find.byKey(Key('delete_icon_recipient_tag_item_${prefix.name}_0')); + final avatarIconRecipientTagItemWidgetFinder = find.byKey(Key('avatar_icon_recipient_tag_item_${prefix.name}_0')); + + final Size recipientTagItemWidgetSize = tester.getSize(recipientTagItemWidgetFinder); + final Size labelRecipientTagItemWidgetSize = tester.getSize(labelRecipientTagItemWidgetFinder); + final Size deleteIconRecipientTagItemWidgetSize = tester.getSize(deleteIconRecipientTagItemWidgetFinder); + final Size avatarIconRecipientTagItemWidgetSize = tester.getSize(avatarIconRecipientTagItemWidgetFinder); + + log('recipient_composer_widget_test::main: TagSize = $recipientTagItemWidgetSize | LabelTagSize = $labelRecipientTagItemWidgetSize | DeleteIconTagSize = $deleteIconRecipientTagItemWidgetSize | AvatarIconTagSize = $avatarIconRecipientTagItemWidgetSize'); + + expect(labelRecipientTagItemWidgetFinder, findsOneWidget); + expect(deleteIconRecipientTagItemWidgetFinder, findsOneWidget); + expect(avatarIconRecipientTagItemWidgetFinder, findsOneWidget); + + expect( + labelRecipientTagItemWidgetSize.width + deleteIconRecipientTagItemWidgetSize.width + avatarIconRecipientTagItemWidgetSize.width, + lessThanOrEqualTo(recipientTagItemWidgetSize.width) + ); + }); }); } From 9ef4de21c7bc3c3473aac3c384d45d792194eecb Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 27 Mar 2024 00:06:53 +0700 Subject: [PATCH 43/80] TF-2717 Write widget test for multiple recipients when focus and unfocus --- .../widgets/recipient_tag_item_widget.dart | 1 + .../recipient_composer_widget_test.dart | 98 ++++++++++++++++++- 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart index d7867b2813..cc46781e2f 100644 --- a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart @@ -157,6 +157,7 @@ class RecipientTagItemWidget extends StatelessWidget { ), if (isCollapsed) TMailButtonWidget.fromText( + key: Key('counter_recipient_tag_item_${prefix.name}_$index'), margin: _counterMargin, text: '+$countRecipients', onTapActionCallback: () => onShowFullAction?.call(prefix), diff --git a/test/features/features/composer/recipient_composer_widget_test.dart b/test/features/features/composer/recipient_composer_widget_test.dart index 87bea4e2f0..d0cd27273c 100644 --- a/test/features/features/composer/recipient_composer_widget_test.dart +++ b/test/features/features/composer/recipient_composer_widget_test.dart @@ -6,6 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/email/prefix_email_address.dart'; +import 'package:model/mailbox/expand_mode.dart'; import 'package:super_tag_editor/tag_editor.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_tag_item_widget.dart'; @@ -170,7 +171,7 @@ void main() { final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); final Size prefixRecipientComposerWidgetSize = tester.getSize(prefixRecipientComposerWidgetFinder); - final Size recipientTagItemWidgetSize = tester.getSize(prefixRecipientComposerWidgetFinder); + final Size recipientTagItemWidgetSize = tester.getSize(recipientTagItemWidgetFinder); log('recipient_composer_widget_test::main: PrefixLabelSize = $prefixRecipientComposerWidgetSize | TagSize = $recipientTagItemWidgetSize'); @@ -212,6 +213,7 @@ void main() { log('recipient_composer_widget_test::main: TagSize = $recipientTagItemWidgetSize | LabelTagSize = $labelRecipientTagItemWidgetSize | DeleteIconTagSize = $deleteIconRecipientTagItemWidgetSize'); + expect(recipientTagItemWidgetFinder, findsOneWidget); expect(labelRecipientTagItemWidgetFinder, findsOneWidget); expect(deleteIconRecipientTagItemWidgetFinder, findsOneWidget); @@ -262,5 +264,99 @@ void main() { lessThanOrEqualTo(recipientTagItemWidgetSize.width) ); }); + + testWidgets('WHEN To has multiple recipients AND expandMode is COLLAPSE\n' + 'RecipientTagItemWidget should have all the components (AvatarIcon, Label, DeleteIcon, CounterTag)', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + EmailAddress('test2', 'test2@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + expandMode: ExpandMode.COLLAPSE, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final prefixRecipientComposerWidgetFinder = find.byKey(Key('prefix_${prefix.name}_recipient_composer_widget')); + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + + final Size prefixRecipientComposerWidgetSize = tester.getSize(prefixRecipientComposerWidgetFinder); + final Size recipientTagItemWidgetSize = tester.getSize(recipientTagItemWidgetFinder); + + log('recipient_composer_widget_test::main: PrefixLabelSize = $prefixRecipientComposerWidgetSize | TagSize = $recipientTagItemWidgetSize'); + + expect(prefixRecipientComposerWidgetFinder, findsOneWidget); + expect(recipientTagItemWidgetFinder, findsOneWidget); + expect( + prefixRecipientComposerWidgetSize.width + recipientTagItemWidgetSize.width, + lessThanOrEqualTo(360) + ); + + final labelRecipientTagItemWidgetFinder = find.byKey(Key('label_recipient_tag_item_${prefix.name}_0')); + final deleteIconRecipientTagItemWidgetFinder = find.byKey(Key('delete_icon_recipient_tag_item_${prefix.name}_0')); + final avatarIconRecipientTagItemWidgetFinder = find.byKey(Key('avatar_icon_recipient_tag_item_${prefix.name}_0')); + final counterRecipientTagItemWidgetFinder = find.byKey(Key('counter_recipient_tag_item_${prefix.name}_0')); + + final Size labelRecipientTagItemWidgetSize = tester.getSize(labelRecipientTagItemWidgetFinder); + final Size deleteIconRecipientTagItemWidgetSize = tester.getSize(deleteIconRecipientTagItemWidgetFinder); + final Size avatarIconRecipientTagItemWidgetSize = tester.getSize(avatarIconRecipientTagItemWidgetFinder); + final Size counterRecipientTagItemWidgetSize = tester.getSize(counterRecipientTagItemWidgetFinder); + + log('recipient_composer_widget_test::main: LabelTagSize = $labelRecipientTagItemWidgetSize | DeleteIconTagSize = $deleteIconRecipientTagItemWidgetSize | AvatarIconTagSize = $avatarIconRecipientTagItemWidgetSize | CounterTagSize = $counterRecipientTagItemWidgetSize'); + + expect(labelRecipientTagItemWidgetFinder, findsOneWidget); + expect(deleteIconRecipientTagItemWidgetFinder, findsOneWidget); + expect(avatarIconRecipientTagItemWidgetFinder, findsOneWidget); + expect(counterRecipientTagItemWidgetFinder, findsOneWidget); + + final totalSizeOfAllComponents = labelRecipientTagItemWidgetSize.width + + deleteIconRecipientTagItemWidgetSize.width + + avatarIconRecipientTagItemWidgetSize.width; + counterRecipientTagItemWidgetSize.width; + + expect( + totalSizeOfAllComponents, + lessThanOrEqualTo(recipientTagItemWidgetSize.width) + ); + }); + + testWidgets('WHEN To has multiple recipients AND expandMode is EXPAND\n' + 'RecipientTagItemWidget should have all the components (AvatarIcon, Label, DeleteIcon)', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + EmailAddress('test2', 'test2@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + expandMode: ExpandMode.EXPAND, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final prefixRecipientComposerWidgetFinder = find.byKey(Key('prefix_${prefix.name}_recipient_composer_widget')); + final recipientTagItemWidgetFinder = find.byType(RecipientTagItemWidget); + + expect(prefixRecipientComposerWidgetFinder, findsOneWidget); + expect(recipientTagItemWidgetFinder, findsNWidgets(2)); + }); }); } From 2246986d75e0cbfc933c1f6e8466bc06c917b0f8 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 27 Mar 2024 01:19:39 +0700 Subject: [PATCH 44/80] TF-2717 Write widget test for text overflow of label tag --- .../recipient_composer_widget_test.dart | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/test/features/features/composer/recipient_composer_widget_test.dart b/test/features/features/composer/recipient_composer_widget_test.dart index d0cd27273c..2c2832a130 100644 --- a/test/features/features/composer/recipient_composer_widget_test.dart +++ b/test/features/features/composer/recipient_composer_widget_test.dart @@ -358,5 +358,91 @@ void main() { expect(prefixRecipientComposerWidgetFinder, findsOneWidget); expect(recipientTagItemWidgetFinder, findsNWidgets(2)); }); + + testWidgets('WHEN EmailAddress has address is too long AND display name is NULL\n' + 'RecipientTagItemWidget SHOULD have text that overflows', (tester) async { + final listEmailAddress = [ + EmailAddress(null, 'test12345678901234567890@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final labelRecipientTagItemWidgetFinder = find.byKey(Key('label_recipient_tag_item_${prefix.name}_0')); + + final labelRecipientTagItemWidget = tester.widget(labelRecipientTagItemWidgetFinder); + final labelTagWidth = tester.getSize(labelRecipientTagItemWidgetFinder).width; + + expect(labelRecipientTagItemWidget.overflow, equals(TextOverflow.ellipsis)); + + final TextPainter textPainter = TextPainter( + maxLines: labelRecipientTagItemWidget.maxLines, + textDirection: labelRecipientTagItemWidget.textDirection ?? TextDirection.ltr, + text: TextSpan( + text: labelRecipientTagItemWidget.data, + style: labelRecipientTagItemWidget.style, + locale: labelRecipientTagItemWidget.locale + ), + ); + textPainter.layout(maxWidth: labelTagWidth); + bool isExceededTextOverflow = textPainter.didExceedMaxLines; + log('recipient_composer_widget_test::main: LABEL_TAB_WIDTH = $labelTagWidth | TextPainterWidth = ${textPainter.width} | isExceededTextOverflow = $isExceededTextOverflow'); + + expect(isExceededTextOverflow, equals(true)); + }); + + testWidgets('WHEN EmailAddress has address is too long AND display name is NULL\n' + 'RecipientTagItemWidget SHOULD have text display full', (tester) async { + final listEmailAddress = [ + EmailAddress(null, 'test123@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final labelRecipientTagItemWidgetFinder = find.byKey(Key('label_recipient_tag_item_${prefix.name}_0')); + + final labelRecipientTagItemWidget = tester.widget(labelRecipientTagItemWidgetFinder); + final labelTagWidth = tester.getSize(labelRecipientTagItemWidgetFinder).width; + + expect(labelRecipientTagItemWidget.overflow, equals(TextOverflow.ellipsis)); + + final TextPainter textPainter = TextPainter( + maxLines: labelRecipientTagItemWidget.maxLines, + textDirection: labelRecipientTagItemWidget.textDirection ?? TextDirection.ltr, + text: TextSpan( + text: labelRecipientTagItemWidget.data, + style: labelRecipientTagItemWidget.style, + locale: labelRecipientTagItemWidget.locale + ), + ); + textPainter.layout(maxWidth: labelTagWidth); + bool isExceededTextOverflow = textPainter.didExceedMaxLines; + log('recipient_composer_widget_test::main: LABEL_TAB_WIDTH = $labelTagWidth | TextPainterWidth = ${textPainter.width} | isExceededTextOverflow = $isExceededTextOverflow'); + + expect(isExceededTextOverflow, equals(false)); + }); }); } From c2c3314fa904c554b08ba8aaccdce47cac22d39a Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 27 Mar 2024 01:33:46 +0700 Subject: [PATCH 45/80] TF-2717 Write widget test for change language --- .../widgets/recipient_composer_widget.dart | 6 +- .../recipient_composer_widget_test.dart | 73 +++++++++++++++++-- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index c94831593d..9f65a6d1e1 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -371,9 +371,9 @@ class _RecipientComposerWidgetState extends State { bool get _isCollapse => _currentListEmailAddress.length > 1 && widget.expandMode == ExpandMode.COLLAPSE; - bool get _isAllRecipientInputEnabled => widget.fromState == PrefixRecipientState.enabled && - widget.ccState == PrefixRecipientState.enabled && - widget.bccState == PrefixRecipientState.enabled; + bool get _isAllRecipientInputEnabled => widget.fromState == PrefixRecipientState.enabled + && widget.ccState == PrefixRecipientState.enabled + && widget.bccState == PrefixRecipientState.enabled; List get _collapsedListEmailAddress => _isCollapse ? _currentListEmailAddress.sublist(0, 1) diff --git a/test/features/features/composer/recipient_composer_widget_test.dart b/test/features/features/composer/recipient_composer_widget_test.dart index 2c2832a130..e97d063257 100644 --- a/test/features/features/composer/recipient_composer_widget_test.dart +++ b/test/features/features/composer/recipient_composer_widget_test.dart @@ -11,6 +11,7 @@ import 'package:super_tag_editor/tag_editor.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_tag_item_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations_delegate.dart'; +import 'package:tmail_ui_user/main/localizations/language_code_constants.dart'; import 'package:tmail_ui_user/main/localizations/localization_service.dart'; void main() { @@ -144,7 +145,7 @@ void main() { expect( labelRecipientTagItemWidgetSize.width + deleteIconRecipientTagItemWidgetSize.width + avatarIconRecipientTagItemWidgetSize.width, - lessThanOrEqualTo(recipientTagItemWidgetSize.width) + lessThan(recipientTagItemWidgetSize.width) ); }); @@ -179,7 +180,7 @@ void main() { expect(recipientTagItemWidgetFinder, findsOneWidget); expect( prefixRecipientComposerWidgetSize.width + recipientTagItemWidgetSize.width, - lessThanOrEqualTo(360) + lessThan(360) ); }); @@ -219,7 +220,7 @@ void main() { expect( labelRecipientTagItemWidgetSize.width + deleteIconRecipientTagItemWidgetSize.width, - lessThanOrEqualTo(recipientTagItemWidgetSize.width) + lessThan(recipientTagItemWidgetSize.width) ); }); @@ -261,7 +262,7 @@ void main() { expect( labelRecipientTagItemWidgetSize.width + deleteIconRecipientTagItemWidgetSize.width + avatarIconRecipientTagItemWidgetSize.width, - lessThanOrEqualTo(recipientTagItemWidgetSize.width) + lessThan(recipientTagItemWidgetSize.width) ); }); @@ -299,7 +300,7 @@ void main() { expect(recipientTagItemWidgetFinder, findsOneWidget); expect( prefixRecipientComposerWidgetSize.width + recipientTagItemWidgetSize.width, - lessThanOrEqualTo(360) + lessThan(360) ); final labelRecipientTagItemWidgetFinder = find.byKey(Key('label_recipient_tag_item_${prefix.name}_0')); @@ -326,7 +327,7 @@ void main() { expect( totalSizeOfAllComponents, - lessThanOrEqualTo(recipientTagItemWidgetSize.width) + lessThan(recipientTagItemWidgetSize.width) ); }); @@ -402,7 +403,7 @@ void main() { expect(isExceededTextOverflow, equals(true)); }); - testWidgets('WHEN EmailAddress has address is too long AND display name is NULL\n' + testWidgets('WHEN EmailAddress has address short AND display name is NULL\n' 'RecipientTagItemWidget SHOULD have text display full', (tester) async { final listEmailAddress = [ EmailAddress(null, 'test123@example.com'), @@ -444,5 +445,63 @@ void main() { expect(isExceededTextOverflow, equals(false)); }); + + testWidgets('RecipientComponentWidget should display prefix To label correctly when the locale is fr-FR', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + ]; + + Get.updateLocale(const Locale(LanguageCodeConstants.french, 'FR')); + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final prefixRecipientComposerWidgetFinder = find.byKey(Key('prefix_${prefix.name}_recipient_composer_widget')); + final prefixRecipientComposerWidget = tester.widget(prefixRecipientComposerWidgetFinder); + + log('recipient_composer_widget_test::main: PREFIX_LABEL = ${prefixRecipientComposerWidget.data}'); + + expect(prefixRecipientComposerWidget.data, equals('À:')); + }); + + testWidgets('RecipientComponentWidget should display prefix To label correctly when the locale is vi-VN', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + ]; + + Get.updateLocale(const Locale(LanguageCodeConstants.vietnamese, 'VN')); + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final prefixRecipientComposerWidgetFinder = find.byKey(Key('prefix_${prefix.name}_recipient_composer_widget')); + final prefixRecipientComposerWidget = tester.widget(prefixRecipientComposerWidgetFinder); + + log('recipient_composer_widget_test::main: PREFIX_LABEL = ${prefixRecipientComposerWidget.data}'); + + expect(prefixRecipientComposerWidget.data, equals('Đến:')); + }); }); } From b9c4492d1fd9b49774bcb3e8ef8d9a087af064b1 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 1 Apr 2024 08:40:14 +0700 Subject: [PATCH 46/80] TF-2717 Use `cnb_supported` branch for rich_text_composer dependency --- pubspec.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 4c98d82511..66deb9a3fc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -425,11 +425,11 @@ packages: source: path version: "1.0.0+1" enough_html_editor: - dependency: "direct overridden" + dependency: transitive description: path: "." - ref: "improve/allow-enable-disable-colllapsed-signature" - resolved-ref: b0038f647bf90ba40bf21efe7290a22dec4d2867 + ref: cnb_supported + resolved-ref: "669cfd989498cf398c88a6618eebb089967b9e28" url: "https://github.com/linagora/enough_html_editor.git" source: git version: "0.0.5" @@ -1549,8 +1549,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "867d22aaa2da242bb1c5929e2804e3cb1d4acaf6" + ref: cnb_supported + resolved-ref: ef8bcdd1824badd7262aa63952d4b83a47ef0dc4 url: "https://github.com/linagora/rich-text-composer.git" source: git version: "0.0.2" From df721016335e9a979ae09b407a2e8929947aa4cc Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 1 Apr 2024 09:33:57 +0700 Subject: [PATCH 47/80] TF-2717 Get identity when open app, if not get identity, get whenever open composer --- .../presentation/composer_controller.dart | 20 +++++++++- .../composer_arguments_extension.dart | 24 +++++++++++ .../model/composer_arguments.dart | 4 ++ .../bindings/mailbox_dashboard_bindings.dart | 12 +++++- .../mailbox_dashboard_controller.dart | 40 ++++++++++++++++--- 5 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 lib/features/email/presentation/extensions/composer_arguments_extension.dart diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 263e4857a4..dfe25299c6 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -450,10 +450,13 @@ class ComposerController extends BaseController with DragDropFileMixin { subjectEmailInputFocusNode?.unfocus(); } - void onLoadCompletedMobileEditorAction(HtmlEditorApi editorApi, WebUri? url) { + void onLoadCompletedMobileEditorAction(HtmlEditorApi editorApi, WebUri? url) async { _isEmailBodyLoaded = true; if (identitySelected.value == null) { _getAllIdentities(); + } else { + await _selectIdentity(identitySelected.value); + _autoFocusFieldWhenLauncher(); } } @@ -465,6 +468,8 @@ class ComposerController extends BaseController with DragDropFileMixin { if (arguments is ComposerArguments) { composerArguments.value = arguments; + _initIdentities(arguments.identities); + injectAutoCompleteBindings( mailboxDashBoardController.sessionCurrent, mailboxDashBoardController.accountId.value @@ -592,7 +597,15 @@ class ComposerController extends BaseController with DragDropFileMixin { } } + void _initIdentities(List? identities) { + if (identities?.isNotEmpty == true) { + listFromIdentities.value = identities!; + identitySelected.value = identities.first; + } + } + void _getAllIdentities() { + log('ComposerController::_getAllIdentities: Fetch again identity !'); final accountId = mailboxDashBoardController.accountId.value; final session = mailboxDashBoardController.sessionCurrent; if (accountId != null && session != null) { @@ -1692,7 +1705,7 @@ class ComposerController extends BaseController with DragDropFileMixin { bccAddressFocusNode?.hasFocus == true || subjectEmailInputFocusNode?.hasFocus == true; - void handleInitHtmlEditorWeb(String initContent) { + void handleInitHtmlEditorWeb(String initContent) async { log('ComposerController::handleInitHtmlEditorWeb:'); _isEmailBodyLoaded = true; richTextWebController.editorController.setFullScreen(); @@ -1701,6 +1714,9 @@ class ComposerController extends BaseController with DragDropFileMixin { richTextWebController.setEnableCodeView(); if (identitySelected.value == null) { _getAllIdentities(); + } else { + await _selectIdentity(identitySelected.value); + _autoFocusFieldWhenLauncher(); } } diff --git a/lib/features/email/presentation/extensions/composer_arguments_extension.dart b/lib/features/email/presentation/extensions/composer_arguments_extension.dart new file mode 100644 index 0000000000..625576a76e --- /dev/null +++ b/lib/features/email/presentation/extensions/composer_arguments_extension.dart @@ -0,0 +1,24 @@ +import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; + +extension ComposerArgumentsExtension on ComposerArguments { + + ComposerArguments withIdentity({List? identities}) { + return ComposerArguments( + emailActionType: emailActionType, + presentationEmail: presentationEmail, + emailContents: emailContents, + attachments: attachments, + mailboxRole: mailboxRole, + listEmailAddress: listEmailAddress, + listSharedMediaFile: listSharedMediaFile, + sendingEmail: sendingEmail, + subject: subject, + body: body, + messageId: messageId, + references: references, + previousEmailId: previousEmailId, + identities: identities, + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/model/composer_arguments.dart b/lib/features/email/presentation/model/composer_arguments.dart index 527ad67af7..0b6291fd13 100644 --- a/lib/features/email/presentation/model/composer_arguments.dart +++ b/lib/features/email/presentation/model/composer_arguments.dart @@ -1,3 +1,4 @@ +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'; @@ -22,6 +23,7 @@ class ComposerArguments extends RouterArguments { final MessageIdsHeaderValue? messageId; final MessageIdsHeaderValue? references; final EmailId? previousEmailId; + final List? identities; ComposerArguments({ this.emailActionType = EmailActionType.compose, @@ -37,6 +39,7 @@ class ComposerArguments extends RouterArguments { this.messageId, this.references, this.previousEmailId, + this.identities, }); factory ComposerArguments.fromSendingEmail(SendingEmail sendingEmail) => @@ -170,5 +173,6 @@ class ComposerArguments extends RouterArguments { body, messageId, references, + identities, ]; } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index d4df25acde..375143f3ec 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -78,6 +78,10 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/repository/identity_repository.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/profiles/identities/identity_interactors_bindings.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/utils/identity_utils.dart'; import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_manager.dart'; import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_worker_queue.dart'; import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_manager.dart'; @@ -172,6 +176,7 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); Get.put(AdvancedFilterController()); } @@ -277,7 +282,6 @@ class MailboxDashBoardBindings extends BaseBindings { ); Get.lazyPut(() => GetComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => RemoveComposerCacheOnWebInteractor(Get.find())); - Get.lazyPut(() => RemoveComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => MarkAsEmailReadInteractor( Get.find(), Get.find() @@ -333,6 +337,12 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find() )); + + IdentityInteractorsBindings().dependencies(); + Get.lazyPut(() => GetAllIdentitiesInteractor( + Get.find(), + Get.find() + )); } @override diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index b6ca0d6441..99ae8dab82 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -18,6 +18,7 @@ import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/core/unsigned_int.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'; @@ -38,6 +39,7 @@ import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_bindings.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/compose_action_mode.dart'; import 'package:tmail_ui_user/features/contact/presentation/model/contact_arguments.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; @@ -63,6 +65,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_int import 'package:tmail_ui_user/features/email/domain/usecases/restore_deleted_message_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/unsubscribe_email_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/composer_arguments_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/email_recovery/presentation/model/email_recovery_arguments.dart'; @@ -96,8 +99,10 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/sear import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart'; import 'package:tmail_ui_user/features/mailto/presentation/model/mailto_arguments.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/state/get_all_vacation_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/update_vacation_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/domain/usecases/get_all_vacation_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/update_vacation_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/datetime_extension.dart'; @@ -182,6 +187,7 @@ class MailboxDashBoardController extends ReloadableController { final RestoredDeletedMessageInteractor _restoreDeletedMessageInteractor; final GetRestoredDeletedMessageInterator _getRestoredDeletedMessageInteractor; final RemoveComposerCacheOnWebInteractor _removeComposerCacheOnWebInteractor; + final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; GetAllVacationInteractor? _getAllVacationInteractor; UpdateVacationInteractor? _updateVacationInteractor; @@ -221,6 +227,7 @@ class MailboxDashBoardController extends ReloadableController { final listResultSearch = RxList(); PresentationMailbox? outboxMailbox; ComposerArguments? composerArguments; + List? _identities; late StreamSubscription _emailAddressStreamSubscription; late StreamSubscription _emailContentStreamSubscription; @@ -260,6 +267,7 @@ class MailboxDashBoardController extends ReloadableController { this._restoreDeletedMessageInteractor, this._getRestoredDeletedMessageInteractor, this._removeComposerCacheOnWebInteractor, + this._getAllIdentitiesInteractor, ); @override @@ -285,8 +293,8 @@ class MailboxDashBoardController extends ReloadableController { _getEmailCacheOnWebInteractor.execute().fold( (failure) {}, (success) { - if(success is GetComposerCacheSuccess){ - openComposerOverlay(ComposerArguments.fromSessionStorageBrowser(success.composerCache)); + if (success is GetComposerCacheSuccess) { + goToComposer(ComposerArguments.fromSessionStorageBrowser(success.composerCache)); } }, ); @@ -362,6 +370,8 @@ class MailboxDashBoardController extends ReloadableController { _handleRestoreDeletedMessageSuccess(success.emailRecoveryAction.id!); } else if (success is GetRestoredDeletedMessageSuccess) { _handleGetRestoredDeletedMessageSuccess(success); + } else if (success is GetAllIdentitiesSuccess) { + _handleGetAllIdentitiesSuccess(success); } } @@ -520,6 +530,7 @@ class MailboxDashBoardController extends ReloadableController { _getVacationResponse(); spamReportController.getSpamReportStateAction(); + _getAllIdentities(); if (PlatformInfo.isMobile) { getAllSendingEmails(); @@ -1391,14 +1402,16 @@ class MailboxDashBoardController extends ReloadableController { } void goToComposer(ComposerArguments arguments) async { + final argumentsWithIdentity = arguments.withIdentity(identities: _identities); + if (PlatformInfo.isWeb) { if (composerOverlayState.value == ComposerOverlayState.inActive) { - openComposerOverlay(arguments); + openComposerOverlay(argumentsWithIdentity); } } else { BackButtonInterceptor.removeByName(AppRoutes.dashboard); - final result = await push(AppRoutes.composer, arguments: arguments); + final result = await push(AppRoutes.composer, arguments: argumentsWithIdentity); BackButtonInterceptor.add(_onBackButtonInterceptor, name: AppRoutes.dashboard); @@ -2483,12 +2496,29 @@ class MailboxDashBoardController extends ReloadableController { await _removeComposerCacheOnWebInteractor.execute(); } - bool validateSendingEmailFailedWhenNetworkIsLostOnMobile(FeatureFailure failure) { + bool validateSendingEmailFailedWhenNetworkIsLostOnMobile(dynamic failure) { return failure is SendEmailFailure && failure.exception is NoNetworkError && PlatformInfo.isMobile; } + void _getAllIdentities() { + if (accountId.value != null && sessionCurrent != null) { + consumeState(_getAllIdentitiesInteractor.execute( + sessionCurrent!, + accountId.value! + )); + } + } + + void _handleGetAllIdentitiesSuccess(GetAllIdentitiesSuccess success) async { + final listIdentitiesMayDeleted = success.identities?.toListMayDeleted() ?? []; + if (listIdentitiesMayDeleted.isNotEmpty) { + _identities = listIdentitiesMayDeleted; + } + log('MailboxDashBoardController::_handleGetAllIdentitiesSuccess: IDENTITIES_SIZE = ${_identities?.length}'); + } + @override void onClose() { _emailReceiveManager.closeEmailReceiveManagerStream(); From 62bf78e7d62ecd25fc7629367bdbe3d049d98318 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 1 Apr 2024 09:52:28 +0700 Subject: [PATCH 48/80] TF-2717 Get identity again when back to dashboard from settings --- .../presentation/controller/mailbox_dashboard_controller.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 99ae8dab82..8af9ff2a2a 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -1464,6 +1464,8 @@ class MailboxDashBoardController extends ReloadableController { () => _replaceBrowserHistory(uri: result.value2) ); } + + _getAllIdentities(); } void selectQuickSearchFilter(QuickSearchFilter filter) { @@ -1542,6 +1544,8 @@ class MailboxDashBoardController extends ReloadableController { () => _replaceBrowserHistory(uri: result.value2) ); } + + _getAllIdentities(); } void _handleUpdateVacationSuccess(UpdateVacationSuccess success) { From 1d33b31f0182482f169abcbe27089de4879b64a5 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 1 Apr 2024 10:19:51 +0700 Subject: [PATCH 49/80] TF-2717 Disable delete in keyboard shortcut on mobile --- .../composer/presentation/composer_controller.dart | 13 ++++++++++++- .../composer/presentation/composer_view_web.dart | 9 +++++++++ .../widgets/recipient_composer_widget.dart | 4 ++++ pubspec.lock | 2 +- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index dfe25299c6..43851f3e93 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -155,6 +155,9 @@ class ComposerController extends BaseController with DragDropFileMixin { FocusNode? ccAddressFocusNode; FocusNode? bccAddressFocusNode; FocusNode? searchIdentitiesFocusNode; + FocusNode? toAddressFocusNodeKeyboard; + FocusNode? ccAddressFocusNodeKeyboard; + FocusNode? bccAddressFocusNodeKeyboard; StreamSubscription? _subscriptionOnBeforeUnload; StreamSubscription? _subscriptionOnDragEnter; @@ -249,6 +252,12 @@ class ComposerController extends BaseController with DragDropFileMixin { ccAddressFocusNode = null; bccAddressFocusNode?.dispose(); bccAddressFocusNode = null; + toAddressFocusNodeKeyboard?.dispose(); + toAddressFocusNodeKeyboard = null; + ccAddressFocusNodeKeyboard?.dispose(); + ccAddressFocusNodeKeyboard = null; + bccAddressFocusNodeKeyboard?.dispose(); + bccAddressFocusNodeKeyboard = null; searchIdentitiesFocusNode?.dispose(); searchIdentitiesFocusNode = null; subjectEmailInputController.dispose(); @@ -417,6 +426,9 @@ class ComposerController extends BaseController with DragDropFileMixin { ccAddressFocusNode = FocusNode(); bccAddressFocusNode = FocusNode(); searchIdentitiesFocusNode = FocusNode(); + toAddressFocusNodeKeyboard = FocusNode(); + ccAddressFocusNodeKeyboard = FocusNode(); + bccAddressFocusNodeKeyboard = FocusNode(); subjectEmailInputFocusNode?.addListener(() { log('ComposerController::createFocusNodeInput():subjectEmailInputFocusNode: ${subjectEmailInputFocusNode?.hasFocus}'); @@ -1313,7 +1325,6 @@ class ComposerController extends BaseController with DragDropFileMixin { } void displayScreenTypeComposerAction(ScreenDisplayMode displayMode) async { - createFocusNodeInput(); _updateTextForEditor(); screenDisplayMode.value = displayMode; diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index e2d3531482..30d990171f 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -95,6 +95,7 @@ class ComposerView extends GetWidget { expandMode: controller.toAddressExpandMode.value, controller: controller.toEmailAddressController, focusNode: controller.toAddressFocusNode, + focusNodeKeyboard: controller.toAddressFocusNodeKeyboard, keyTagEditor: controller.keyToEmailTagEditor, isInitial: controller.isInitialRecipient.value, padding: ComposerStyle.mobileRecipientPadding, @@ -117,6 +118,7 @@ class ComposerView extends GetWidget { expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, + focusNodeKeyboard: controller.ccAddressFocusNodeKeyboard, keyTagEditor: controller.keyCcEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.getNextFocusOfCcEmailAddress(), @@ -139,6 +141,7 @@ class ComposerView extends GetWidget { expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, + focusNodeKeyboard: controller.bccAddressFocusNodeKeyboard, keyTagEditor: controller.keyBccEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, @@ -319,6 +322,7 @@ class ComposerView extends GetWidget { expandMode: controller.toAddressExpandMode.value, controller: controller.toEmailAddressController, focusNode: controller.toAddressFocusNode, + focusNodeKeyboard: controller.toAddressFocusNodeKeyboard, keyTagEditor: controller.keyToEmailTagEditor, isInitial: controller.isInitialRecipient.value, padding: ComposerStyle.desktopRecipientPadding, @@ -341,6 +345,7 @@ class ComposerView extends GetWidget { expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, + focusNodeKeyboard: controller.ccAddressFocusNodeKeyboard, keyTagEditor: controller.keyCcEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.getNextFocusOfCcEmailAddress(), @@ -363,6 +368,7 @@ class ComposerView extends GetWidget { expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, + focusNodeKeyboard: controller.bccAddressFocusNodeKeyboard, keyTagEditor: controller.keyBccEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, @@ -582,6 +588,7 @@ class ComposerView extends GetWidget { expandMode: controller.toAddressExpandMode.value, controller: controller.toEmailAddressController, focusNode: controller.toAddressFocusNode, + focusNodeKeyboard: controller.toAddressFocusNodeKeyboard, keyTagEditor: controller.keyToEmailTagEditor, isInitial: controller.isInitialRecipient.value, padding: ComposerStyle.tabletRecipientPadding, @@ -604,6 +611,7 @@ class ComposerView extends GetWidget { expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, + focusNodeKeyboard: controller.ccAddressFocusNodeKeyboard, keyTagEditor: controller.keyCcEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.getNextFocusOfCcEmailAddress(), @@ -626,6 +634,7 @@ class ComposerView extends GetWidget { expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, + focusNodeKeyboard: controller.bccAddressFocusNodeKeyboard, keyTagEditor: controller.keyBccEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index 9f65a6d1e1..3d82574d9d 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -48,6 +48,7 @@ class RecipientComposerWidget extends StatefulWidget { final PrefixRecipientState bccState; final bool? isInitial; final FocusNode? focusNode; + final FocusNode? focusNodeKeyboard; final GlobalKey? keyTagEditor; final FocusNode? nextFocusNode; final TextEditingController? controller; @@ -89,6 +90,7 @@ class RecipientComposerWidget extends StatefulWidget { this.onFocusNextAddressAction, this.onRemoveDraggableEmailAddressAction, this.onEnableAllRecipientsInputAction, + this.focusNodeKeyboard, }); @override @@ -164,6 +166,7 @@ class _RecipientComposerWidgetState extends State { controller: widget.controller, focusNode: widget.focusNode, enableBorder: _isDragging, + focusNodeKeyboard: widget.focusNodeKeyboard, borderRadius: RecipientComposerWidgetStyle.enableBorderRadius, enableBorderColor: RecipientComposerWidgetStyle.enableBorderColor, keyboardType: TextInputType.emailAddress, @@ -244,6 +247,7 @@ class _RecipientComposerWidgetState extends State { length: _collapsedListEmailAddress.length, controller: widget.controller, focusNode: widget.focusNode, + focusNodeKeyboard: widget.focusNodeKeyboard, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.done, debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, diff --git a/pubspec.lock b/pubspec.lock index 66deb9a3fc..bc1ede15ad 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1738,7 +1738,7 @@ packages: description: path: "." ref: master - resolved-ref: dd81c8f1e870bacdffddda5b360c5fbe8a188bbb + resolved-ref: "0fb93e1ea5cff5b1065deeb46193d6fe0db7403e" url: "https://github.com/dab246/super_tag_editor.git" source: git version: "0.2.0" From 772db693d8215a9bbb133c9b80432c1786e50ab7 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 3 Apr 2024 08:30:51 +0700 Subject: [PATCH 50/80] TF-2717 Auto hide text input recipient when unfocus --- .../presentation/widgets/recipient_composer_widget.dart | 2 ++ pubspec.lock | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index 3d82574d9d..030ca52b52 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -177,6 +177,7 @@ class _RecipientComposerWidgetState extends State { minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, resetTextOnSubmitted: true, autoScrollToInput: false, + autoHideTextInputField: true, cursorColor: RecipientComposerWidgetStyle.cursorColor, suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, @@ -255,6 +256,7 @@ class _RecipientComposerWidgetState extends State { minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, resetTextOnSubmitted: true, autoScrollToInput: false, + autoHideTextInputField: true, cursorColor: RecipientComposerWidgetStyle.cursorColor, suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, diff --git a/pubspec.lock b/pubspec.lock index bc1ede15ad..d5b834d63b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1738,7 +1738,7 @@ packages: description: path: "." ref: master - resolved-ref: "0fb93e1ea5cff5b1065deeb46193d6fe0db7403e" + resolved-ref: f8f6470b9cf7d1d9b9b07752d9e09c558e9819a1 url: "https://github.com/dab246/super_tag_editor.git" source: git version: "0.2.0" From 0f853fb5a51f15ddd48fdbd1ccd557af47c4d7ab Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 3 Apr 2024 09:29:33 +0700 Subject: [PATCH 51/80] TF-2717 Use `defaultTargetPlatform` to easily write tests for widget --- core/lib/utils/platform_info.dart | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/core/lib/utils/platform_info.dart b/core/lib/utils/platform_info.dart index 6978807b34..adebd0cad1 100644 --- a/core/lib/utils/platform_info.dart +++ b/core/lib/utils/platform_info.dart @@ -1,17 +1,15 @@ -import 'dart:io'; - import 'package:core/utils/app_logger.dart'; import 'package:core/utils/web_renderer/canvas_kit.dart'; import 'package:flutter/foundation.dart'; abstract class PlatformInfo { static const bool isWeb = kIsWeb; - static bool get isLinux => !kIsWeb && Platform.isLinux; - static bool get isWindows => !kIsWeb && Platform.isWindows; - static bool get isMacOS => !kIsWeb && Platform.isMacOS; - static bool get isFuchsia => !kIsWeb && Platform.isFuchsia; - static bool get isIOS => !kIsWeb && Platform.isIOS; - static bool get isAndroid => !kIsWeb && Platform.isAndroid; + static bool get isLinux => !kIsWeb && defaultTargetPlatform == TargetPlatform.linux; + static bool get isWindows => !kIsWeb && defaultTargetPlatform == TargetPlatform.windows; + static bool get isMacOS => !kIsWeb && defaultTargetPlatform == TargetPlatform.macOS; + static bool get isFuchsia => !kIsWeb && defaultTargetPlatform == TargetPlatform.fuchsia; + static bool get isIOS => !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS; + static bool get isAndroid => !kIsWeb && defaultTargetPlatform == TargetPlatform.android; static bool get isMobile => isAndroid || isIOS; static bool get isDesktop => isLinux || isWindows || isMacOS; static bool get isCanvasKit => isRendererCanvasKit; From fe7889054ed92213e55ee17b068aad322d9f7ade Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 3 Apr 2024 09:31:10 +0700 Subject: [PATCH 52/80] TF-2717 Write widget test for case recipient has expanded button --- .../widgets/recipient_composer_widget.dart | 36 +++--- .../widgets/recipient_tag_item_widget.dart | 8 +- .../recipient_composer_widget_test.dart | 104 +++++++++++++++++- 3 files changed, 126 insertions(+), 22 deletions(-) diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index 030ca52b52..aba026938e 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -63,6 +63,7 @@ class RecipientComposerWidget extends StatefulWidget { final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? margin; final OnEnableAllRecipientsInputAction? onEnableAllRecipientsInputAction; + final bool isTestingForWeb; const RecipientComposerWidget({ super.key, @@ -70,6 +71,7 @@ class RecipientComposerWidget extends StatefulWidget { required this.listEmailAddress, required this.imagePaths, required this.maxWidth, + @visibleForTesting this.isTestingForWeb = false, this.ccState = PrefixRecipientState.disabled, this.bccState = PrefixRecipientState.disabled, this.fromState = PrefixRecipientState.disabled, @@ -157,7 +159,7 @@ class _RecipientComposerWidgetState extends State { }, child: StatefulBuilder( builder: (context, stateSetter) { - if (PlatformInfo.isWeb) { + if (PlatformInfo.isWeb || widget.isTestingForWeb) { return DragTarget( builder: (context, candidateData, rejectedData) { return TagEditor( @@ -316,10 +318,11 @@ class _RecipientComposerWidgetState extends State { ), const SizedBox(width: RecipientComposerWidgetStyle.space), if (widget.prefix == PrefixEmailAddress.to) - if (PlatformInfo.isWeb) + if (PlatformInfo.isWeb || widget.isTestingForWeb) ...[ if (widget.fromState == PrefixRecipientState.disabled) TMailButtonWidget.fromText( + key: Key('prefix_${widget.prefix.name}_recipient_from_button'), text: AppLocalizations.of(context).from_email_address_prefix, textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, backgroundColor: Colors.transparent, @@ -329,6 +332,7 @@ class _RecipientComposerWidgetState extends State { ), if (widget.ccState == PrefixRecipientState.disabled) TMailButtonWidget.fromText( + key: Key('prefix_${widget.prefix.name}_recipient_cc_button'), text: AppLocalizations.of(context).cc_email_address_prefix, textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, backgroundColor: Colors.transparent, @@ -338,6 +342,7 @@ class _RecipientComposerWidgetState extends State { ), if (widget.bccState == PrefixRecipientState.disabled) TMailButtonWidget.fromText( + key: Key('prefix_${widget.prefix.name}_recipient_bcc_button'), text: AppLocalizations.of(context).bcc_email_address_prefix, textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, backgroundColor: Colors.transparent, @@ -347,20 +352,19 @@ class _RecipientComposerWidgetState extends State { ), ] else if (PlatformInfo.isMobile) - ...[ - TMailButtonWidget.fromIcon( - icon: _isAllRecipientInputEnabled - ? widget.imagePaths.icChevronUp - : widget.imagePaths.icChevronDownOutline, - backgroundColor: Colors.transparent, - iconSize: 24, - padding: const EdgeInsets.all(5), - iconColor: AppColor.colorLabelComposer, - margin: RecipientComposerWidgetStyle.enableRecipientButtonMargin, - onTapActionCallback: () => widget.onEnableAllRecipientsInputAction?.call(_isAllRecipientInputEnabled), - ) - ] - else if (PlatformInfo.isWeb) + TMailButtonWidget.fromIcon( + key: Key('prefix_${widget.prefix.name}_recipient_expand_button'), + icon: _isAllRecipientInputEnabled + ? widget.imagePaths.icChevronUp + : widget.imagePaths.icChevronDownOutline, + backgroundColor: Colors.transparent, + iconSize: 24, + padding: const EdgeInsets.all(5), + iconColor: AppColor.colorLabelComposer, + margin: RecipientComposerWidgetStyle.enableRecipientButtonMargin, + onTapActionCallback: () => widget.onEnableAllRecipientsInputAction?.call(_isAllRecipientInputEnabled), + ) + else if (PlatformInfo.isWeb || widget.isTestingForWeb) TMailButtonWidget.fromIcon( icon: widget.imagePaths.icClose, backgroundColor: Colors.transparent, diff --git a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart index cc46781e2f..6512509013 100644 --- a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart @@ -31,6 +31,7 @@ class RecipientTagItemWidget extends StatelessWidget { final List collapsedListEmailAddress; final OnShowFullListEmailAddressAction? onShowFullAction; final OnDeleteTagAction? onDeleteTagAction; + final bool isTestingForWeb; const RecipientTagItemWidget({ super.key, @@ -40,6 +41,7 @@ class RecipientTagItemWidget extends StatelessWidget { required this.currentListEmailAddress, required this.collapsedListEmailAddress, required this.imagePaths, + @visibleForTesting this.isTestingForWeb = false, this.isCollapsed = false, this.isLatestTagFocused = false, this.isLatestEmail = false, @@ -56,7 +58,7 @@ class RecipientTagItemWidget extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - if (PlatformInfo.isWeb) + if (PlatformInfo.isWeb || isTestingForWeb) Flexible( child: Padding( padding: EdgeInsetsDirectional.only( @@ -163,7 +165,7 @@ class RecipientTagItemWidget extends StatelessWidget { onTapActionCallback: () => onShowFullAction?.call(prefix), borderRadius: RecipientTagItemWidgetStyle.radius, textStyle: RecipientTagItemWidgetStyle.labelTextStyle, - padding: PlatformInfo.isWeb + padding: PlatformInfo.isWeb || isTestingForWeb ? RecipientTagItemWidgetStyle.counterPadding : RecipientTagItemWidgetStyle.mobileCounterPadding, backgroundColor: AppColor.colorEmailAddressTag, @@ -174,7 +176,7 @@ class RecipientTagItemWidget extends StatelessWidget { } EdgeInsetsGeometry? get _counterMargin { - if (PlatformInfo.isWeb) { + if (PlatformInfo.isWeb || isTestingForWeb) { return PlatformInfo.isCanvasKit ? RecipientTagItemWidgetStyle.webCounterMargin : RecipientTagItemWidgetStyle.webMobileCounterMargin; diff --git a/test/features/features/composer/recipient_composer_widget_test.dart b/test/features/features/composer/recipient_composer_widget_test.dart index e97d063257..679b90c0c8 100644 --- a/test/features/features/composer/recipient_composer_widget_test.dart +++ b/test/features/features/composer/recipient_composer_widget_test.dart @@ -1,5 +1,6 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -149,7 +150,7 @@ void main() { ); }); - testWidgets('RecipientComponentWidget should have all the components (PrefixLabel, RecipientTagItemWidget)', (tester) async { + testWidgets('ToRecipientComponentWidget should have all the components (PrefixLabel, RecipientTagItemWidget)', (tester) async { final listEmailAddress = [ EmailAddress('test1', 'test1@example.com'), ]; @@ -446,7 +447,7 @@ void main() { expect(isExceededTextOverflow, equals(false)); }); - testWidgets('RecipientComponentWidget should display prefix To label correctly when the locale is fr-FR', (tester) async { + testWidgets('ToRecipientComponentWidget should display prefix To label correctly when the locale is fr-FR', (tester) async { final listEmailAddress = [ EmailAddress('test1', 'test1@example.com'), ]; @@ -475,7 +476,7 @@ void main() { expect(prefixRecipientComposerWidget.data, equals('À:')); }); - testWidgets('RecipientComponentWidget should display prefix To label correctly when the locale is vi-VN', (tester) async { + testWidgets('ToRecipientComponentWidget should display prefix To label correctly when the locale is vi-VN', (tester) async { final listEmailAddress = [ EmailAddress('test1', 'test1@example.com'), ]; @@ -503,5 +504,102 @@ void main() { expect(prefixRecipientComposerWidget.data, equals('Đến:')); }); + + testWidgets('ToRecipientComponentWidget should have all the components (PrefixLabel, RecipientTagItemWidget, ExpandButton) on mobile platform', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final prefixRecipientComposerWidgetFinder = find.byKey(Key('prefix_${prefix.name}_recipient_composer_widget')); + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + + final Size prefixRecipientComposerWidgetSize = tester.getSize(prefixRecipientComposerWidgetFinder); + final Size recipientTagItemWidgetSize = tester.getSize(recipientTagItemWidgetFinder); + + expect(prefixRecipientComposerWidgetFinder, findsOneWidget); + expect(recipientTagItemWidgetFinder, findsOneWidget); + + log('recipient_composer_widget_test::main: PrefixLabelSize = $prefixRecipientComposerWidgetSize | TagSize = $recipientTagItemWidgetSize'); + + final recipientExpandButtonFinder = find.byKey(Key('prefix_${prefix.name}_recipient_expand_button')); + + final Size recipientExpandButtonSize = tester.getSize(recipientExpandButtonFinder); + + log('recipient_composer_widget_test::main: ExpandButtonSize = $recipientExpandButtonSize'); + + expect(recipientExpandButtonFinder, findsOneWidget); + + final totalComponentsSize = prefixRecipientComposerWidgetSize.width + + recipientTagItemWidgetSize.width + + recipientExpandButtonSize.width; + + log('recipient_composer_widget_test::main: totalComponentsSize = $totalComponentsSize'); + + expect(totalComponentsSize, lessThan(360)); + + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets('ToRecipientComponentWidget should have all the components (PrefixLabel, RecipientTagItemWidget, FromButton, CCButton, BccButton) on web platform', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + isTestingForWeb: true, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final prefixRecipientComposerWidgetFinder = find.byKey(Key('prefix_${prefix.name}_recipient_composer_widget')); + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + + final Size prefixRecipientComposerWidgetSize = tester.getSize(prefixRecipientComposerWidgetFinder); + final Size recipientTagItemWidgetSize = tester.getSize(recipientTagItemWidgetFinder); + + expect(prefixRecipientComposerWidgetFinder, findsOneWidget); + expect(recipientTagItemWidgetFinder, findsOneWidget); + + log('recipient_composer_widget_test::main: PrefixLabelSize = $prefixRecipientComposerWidgetSize | TagSize = $recipientTagItemWidgetSize'); + + final recipientFromButtonFinder = find.byKey(Key('prefix_${prefix.name}_recipient_from_button')); + final recipientCcButtonFinder = find.byKey(Key('prefix_${prefix.name}_recipient_cc_button')); + final recipientBccButtonFinder = find.byKey(Key('prefix_${prefix.name}_recipient_bcc_button')); + + final Size recipientFromButtonSize = tester.getSize(recipientFromButtonFinder); + final Size recipientCcButtonSize = tester.getSize(recipientCcButtonFinder); + final Size recipientBccButtonSize = tester.getSize(recipientBccButtonFinder); + + log('recipient_composer_widget_test::main: FromButtonSize = $recipientFromButtonSize | CcButtonSize = $recipientCcButtonSize | BccButtonSize = $recipientBccButtonSize'); + + expect(recipientFromButtonFinder, findsOneWidget); + expect(recipientCcButtonFinder, findsOneWidget); + expect(recipientBccButtonFinder, findsOneWidget); + }); }); } From 3242418ad00f6f4cf87e8a16ab6f47112f883b95 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 3 Apr 2024 19:05:12 +0700 Subject: [PATCH 53/80] TF-2717 Show text input field when click tag recipient item --- .../presentation/composer_controller.dart | 3 + .../widgets/recipient_composer_widget.dart | 10 ++ .../widgets/recipient_tag_item_widget.dart | 156 +++++++----------- pubspec.lock | 2 +- 4 files changed, 70 insertions(+), 101 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 43851f3e93..4c9a68325d 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1472,12 +1472,15 @@ class ComposerController extends BaseController with DragDropFileMixin { switch(prefixEmailAddress) { case PrefixEmailAddress.to: toAddressExpandMode.value = ExpandMode.EXPAND; + toAddressFocusNode?.requestFocus(); break; case PrefixEmailAddress.cc: ccAddressExpandMode.value = ExpandMode.EXPAND; + ccAddressFocusNode?.requestFocus(); break; case PrefixEmailAddress.bcc: bccAddressExpandMode.value = ExpandMode.EXPAND; + bccAddressFocusNode?.requestFocus(); break; default: break; diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index aba026938e..9c3d0da467 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -192,6 +192,11 @@ class _RecipientComposerWidgetState extends State { onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), onTapOutside: (_) {}, + onFocusTextInput: () { + if (_isCollapse) { + widget.onShowFullListEmailAddressAction?.call(widget.prefix); + } + }, inputDecoration: const InputDecoration(border: InputBorder.none), tagBuilder: (context, index) { final currentEmailAddress = _currentListEmailAddress[index]; @@ -271,6 +276,11 @@ class _RecipientComposerWidgetState extends State { onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), onTapOutside: (_) {}, + onFocusTextInput: () { + if (_isCollapse) { + widget.onShowFullListEmailAddressAction?.call(widget.prefix); + } + }, inputDecoration: const InputDecoration(border: InputBorder.none), tagBuilder: (context, index) { final currentEmailAddress = _currentListEmailAddress[index]; diff --git a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart index 6512509013..e3ba3dc22c 100644 --- a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart @@ -1,7 +1,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/extensions/string_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; -import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/views/avatar/gradient_circle_avatar_icon.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/utils/direction_utils.dart'; @@ -52,111 +51,68 @@ class RecipientTagItemWidget extends StatelessWidget { @override Widget build(BuildContext context) { + Widget tagWidget = Chip( + labelPadding: EdgeInsetsDirectional.symmetric( + horizontal: 4, + vertical: DirectionUtils.isDirectionRTLByHasAnyRtl(currentEmailAddress.asString()) ? 0 : 2 + ), + padding: EdgeInsets.zero, + label: Text( + key: Key('label_recipient_tag_item_${prefix.name}_$index'), + currentEmailAddress.asString(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: true, + ), + deleteIcon: SvgPicture.asset( + imagePaths.icClose, + key: Key('delete_icon_recipient_tag_item_${prefix.name}_$index'), + fit: BoxFit.fill + ), + labelStyle: RecipientTagItemWidgetStyle.labelTextStyle, + backgroundColor: _getTagBackgroundColor(), + side: _getTagBorderSide(), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(RecipientTagItemWidgetStyle.radius)), + ), + avatar: currentEmailAddress.displayName.isNotEmpty + ? GradientCircleAvatarIcon( + key: Key('avatar_icon_recipient_tag_item_${prefix.name}_$index'), + colors: currentEmailAddress.avatarColors, + label: currentEmailAddress.displayName.firstLetterToUpperCase, + labelFontSize: RecipientTagItemWidgetStyle.avatarLabelFontSize, + iconSize: RecipientTagItemWidgetStyle.avatarIconSize, + ) + : null, + onDeleted: () => onDeleteTagAction?.call(currentEmailAddress), + ); + + if (PlatformInfo.isWeb || isTestingForWeb) { + tagWidget = Draggable( + data: DraggableEmailAddress(emailAddress: currentEmailAddress, prefix: prefix), + feedback: DraggableRecipientTagWidget(emailAddress: currentEmailAddress), + childWhenDragging: DraggableRecipientTagWidget(emailAddress: currentEmailAddress), + child: MouseRegion( + cursor: SystemMouseCursors.grab, + child: tagWidget, + ), + ); + } + + if ((PlatformInfo.isWeb || isTestingForWeb) && PlatformInfo.isCanvasKit) { + tagWidget = Padding( + padding: const EdgeInsetsDirectional.only(top: 8), + child: tagWidget, + ); + } + return Container( key: Key('recipient_tag_item_${prefix.name}_$index'), constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), child: Row( mainAxisSize: MainAxisSize.min, children: [ - if (PlatformInfo.isWeb || isTestingForWeb) - Flexible( - child: Padding( - padding: EdgeInsetsDirectional.only( - top: !PlatformInfo.isCanvasKit ? 0 : 8 - ), - child: InkWell( - onTap: () => isCollapsed - ? onShowFullAction?.call(prefix) - : null, - child: Draggable( - data: DraggableEmailAddress(emailAddress: currentEmailAddress, prefix: prefix), - feedback: DraggableRecipientTagWidget(emailAddress: currentEmailAddress), - childWhenDragging: DraggableRecipientTagWidget(emailAddress: currentEmailAddress), - child: MouseRegion( - cursor: SystemMouseCursors.grab, - child: Chip( - labelPadding: EdgeInsetsDirectional.symmetric( - horizontal: 4, - vertical: DirectionUtils.isDirectionRTLByHasAnyRtl(currentEmailAddress.asString()) ? 0 : 2 - ), - padding: EdgeInsets.zero, - label: Text( - key: Key('label_recipient_tag_item_${prefix.name}_$index'), - currentEmailAddress.asString(), - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - ), - deleteIcon: SvgPicture.asset( - imagePaths.icClose, - key: Key('delete_icon_recipient_tag_item_${prefix.name}_$index'), - fit: BoxFit.fill - ), - labelStyle: RecipientTagItemWidgetStyle.labelTextStyle, - backgroundColor: _getTagBackgroundColor(), - side: _getTagBorderSide(), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(RecipientTagItemWidgetStyle.radius)), - ), - avatar: currentEmailAddress.displayName.isNotEmpty - ? GradientCircleAvatarIcon( - key: Key('avatar_icon_recipient_tag_item_${prefix.name}_$index'), - colors: currentEmailAddress.avatarColors, - label: currentEmailAddress.displayName.firstLetterToUpperCase, - labelFontSize: RecipientTagItemWidgetStyle.avatarLabelFontSize, - iconSize: RecipientTagItemWidgetStyle.avatarIconSize, - ) - : null, - onDeleted: () => onDeleteTagAction?.call(currentEmailAddress), - ), - ), - ) - ), - ), - ) - else - Flexible( - child: InkWell( - onTap: () => isCollapsed - ? onShowFullAction?.call(prefix) - : null, - child: Chip( - labelPadding: EdgeInsetsDirectional.symmetric( - horizontal: 4, - vertical: DirectionUtils.isDirectionRTLByHasAnyRtl(currentEmailAddress.asString()) ? 0 : 2 - ), - label: Text( - key: Key('label_recipient_tag_item_${prefix.name}_$index'), - currentEmailAddress.asString(), - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - ), - padding: EdgeInsets.zero, - deleteIcon: SvgPicture.asset( - imagePaths.icClose, - key: Key('delete_icon_recipient_tag_item_${prefix.name}_$index'), - fit: BoxFit.fill - ), - labelStyle: RecipientTagItemWidgetStyle.labelTextStyle, - backgroundColor: _getTagBackgroundColor(), - side: _getTagBorderSide(), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(RecipientTagItemWidgetStyle.radius)), - ), - avatar: currentEmailAddress.displayName.isNotEmpty - ? GradientCircleAvatarIcon( - key: Key('avatar_icon_recipient_tag_item_${prefix.name}_$index'), - colors: currentEmailAddress.avatarColors, - label: currentEmailAddress.displayName.firstLetterToUpperCase, - labelFontSize: RecipientTagItemWidgetStyle.avatarLabelFontSize, - iconSize: RecipientTagItemWidgetStyle.avatarIconSize, - ) - : null, - onDeleted: () => onDeleteTagAction?.call(currentEmailAddress), - ) - ), - ), + Flexible(child: tagWidget), if (isCollapsed) TMailButtonWidget.fromText( key: Key('counter_recipient_tag_item_${prefix.name}_$index'), diff --git a/pubspec.lock b/pubspec.lock index d5b834d63b..0850dc19ea 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1738,7 +1738,7 @@ packages: description: path: "." ref: master - resolved-ref: f8f6470b9cf7d1d9b9b07752d9e09c558e9819a1 + resolved-ref: "1954bb41a3a12899c426d17a7b1c9e561cccce06" url: "https://github.com/dab246/super_tag_editor.git" source: git version: "0.2.0" From b34411cf5ef0a5fb26525057cac3b8aeda4cea9f Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 16 Apr 2024 11:52:34 +0700 Subject: [PATCH 54/80] Update `html_editor_enhanced` dependency --- .../mailbox_dashboard_controller_test.dart | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart index d8b85c58a7..73ec7ad79a 100644 --- a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart +++ b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart @@ -2,6 +2,7 @@ import 'package:core/data/network/config/dynamic_url_interceptors.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/application_manager.dart'; import 'package:flutter/widgets.dart' hide State; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; @@ -16,13 +17,10 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; -import 'package:model/user/user_profile.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:rxdart/subjects.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_as_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/update_email_drafts_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/delete_email_permanently_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_restored_deleted_message_interactor.dart'; @@ -54,6 +52,7 @@ import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_na import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_all_recent_search_latest_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/quick_search_email_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_recent_search_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart'; @@ -68,6 +67,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/sear import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; import 'package:tmail_ui_user/features/sending_queue/domain/usecases/delete_sending_email_interactor.dart'; import 'package:tmail_ui_user/features/sending_queue/domain/usecases/get_all_sending_email_interactor.dart'; @@ -123,8 +123,6 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), - MockSpec(), - MockSpec(), MockSpec(), MockSpec(), MockSpec(), @@ -169,6 +167,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() { // mock mailbox dashboard controller direct dependencies @@ -195,8 +194,6 @@ void main() { final getAllSendingEmailInteractor = MockGetAllSendingEmailInteractor(); final storeSessionInteractor = MockStoreSessionInteractor(); final emptySpamFolderInteractor = MockEmptySpamFolderInteractor(); - final saveEmailAsDraftsInteractor = MockSaveEmailAsDraftsInteractor(); - final updateEmailDraftsInteractor = MockUpdateEmailDraftsInteractor(); final deleteSendingEmailInteractor = MockDeleteSendingEmailInteractor(); final unsubscribeEmailInteractor = MockUnsubscribeEmailInteractor(); final restoreDeletedMessageInteractor = @@ -231,6 +228,7 @@ void main() { final imagePaths = MockImagePaths(); final responsiveUtils = MockResponsiveUtils(); final uuid = MockUuid(); + final applicationManager = MockApplicationManager(); // mock reloadable controller Get dependencies final getSessionInteractor = MockGetSessionInteractor(); @@ -249,6 +247,8 @@ void main() { final verifyNameInteractor = MockVerifyNameInteractor(); final getAllMailboxInteractor = MockGetAllMailboxInteractor(); final refreshAllMailboxInteractor = MockRefreshAllMailboxInteractor(); + final removeComposerCacheOnWebInteractor = MockRemoveComposerCacheOnWebInteractor(); + final getAllIdentitiesInteractor = MockGetAllIdentitiesInteractor(); late MailboxController mailboxController; // mock thread controller direct dependencies @@ -295,9 +295,12 @@ void main() { Get.put(imagePaths); Get.put(responsiveUtils); Get.put(uuid); + Get.put(applicationManager); Get.put(getSessionInteractor); Get.put(getAuthenticatedAccountInteractor); Get.put(updateAuthenticationAccountInteractor); + Get.put(getAllIdentitiesInteractor); + Get.put(removeComposerCacheOnWebInteractor); Get.testMode = true; PackageInfo.setMockInitialValues( @@ -336,12 +339,13 @@ void main() { getAllSendingEmailInteractor, storeSessionInteractor, emptySpamFolderInteractor, - saveEmailAsDraftsInteractor, - updateEmailDraftsInteractor, deleteSendingEmailInteractor, unsubscribeEmailInteractor, restoreDeletedMessageInteractor, - getRestoredDeletedMessageInteractor); + getRestoredDeletedMessageInteractor, + removeComposerCacheOnWebInteractor, + getAllIdentitiesInteractor, + ); Get.put(mailboxDashboardController); mailboxDashboardController.onReady(); @@ -372,7 +376,6 @@ void main() { mailboxDashboardController.sessionCurrent = testSession; mailboxDashboardController.filterMessageOption.value = FilterMessageOption.all; - mailboxDashboardController.userProfile.value = UserProfile('test@gmail.com'); mailboxDashboardController.accountId.value = testAccountId; }); From 59dc51fd34786ef23de2b7584a9db1aa59532a6b Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 19 Apr 2024 10:33:48 +0700 Subject: [PATCH 55/80] Set default button padding in confirm dialog --- .../views/dialog/confirmation_dialog_builder.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart index 72b63ececf..b2b06456ad 100644 --- a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart +++ b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart @@ -230,7 +230,7 @@ class ConfirmDialogBuilder { ), ), Padding( - padding: _paddingButton ?? const EdgeInsetsDirectional.only(bottom: 16, start: 24, end: 24), + padding: _paddingButton ?? const EdgeInsetsDirectional.only(bottom: 16, start: 16, end: 16), child: Row( children: [ if (_cancelText.isNotEmpty) @@ -240,7 +240,7 @@ class ConfirmDialogBuilder { radius: _radiusButton, textStyle: _styleTextCancelButton, action: _onCancelButtonAction)), - if (_confirmText.isNotEmpty && _cancelText.isNotEmpty) const SizedBox(width: 16), + if (_confirmText.isNotEmpty && _cancelText.isNotEmpty) const SizedBox(width: 8), if (_confirmText.isNotEmpty) Expanded(child: _buildButton( name: _confirmText, From d08f41cc7186eac3deac47cbe3b25bc1a7b51cb8 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 19 Apr 2024 10:52:05 +0700 Subject: [PATCH 56/80] Disable outside dismissible confirm dialog in composer --- .../mixin/message_dialog_action_mixin.dart | 4 +++ .../presentation/composer_controller.dart | 31 ++++++++----------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/features/base/mixin/message_dialog_action_mixin.dart b/lib/features/base/mixin/message_dialog_action_mixin.dart index 0e542d15fc..9bada57620 100644 --- a/lib/features/base/mixin/message_dialog_action_mixin.dart +++ b/lib/features/base/mixin/message_dialog_action_mixin.dart @@ -21,6 +21,7 @@ mixin MessageDialogActionMixin { bool hasCancelButton = true, bool showAsBottomSheet = false, bool alignCenter = false, + bool outsideDismissible = true, List? listTextSpan, Widget? icon, TextStyle? titleStyle, @@ -72,6 +73,7 @@ mixin MessageDialogActionMixin { ).build() ), barrierColor: AppColor.colorDefaultCupertinoActionSheet, + barrierDismissible: outsideDismissible ); } else { if (responsiveUtils.isMobile(context)) { @@ -120,6 +122,7 @@ mixin MessageDialogActionMixin { isScrollControlled: true, barrierColor: AppColor.colorDefaultCupertinoActionSheet, backgroundColor: Colors.transparent, + isDismissible: outsideDismissible, enableDrag: true, ignoreSafeArea: false, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(18))), @@ -178,6 +181,7 @@ mixin MessageDialogActionMixin { .build() ), barrierColor: AppColor.colorDefaultCupertinoActionSheet, + barrierDismissible: outsideDismissible ); } } diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 4c9a68325d..30b9f6fb7b 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1178,7 +1178,7 @@ class ComposerController extends BaseController with DragDropFileMixin { resultState is GenerateEmailFailure) && context.mounted ) { - _showConfirmDialogWhenSaveMessageToDraftsFailure( + await _showConfirmDialogWhenSaveMessageToDraftsFailure( context: context, failure: resultState, onConfirmAction: () { @@ -1875,8 +1875,9 @@ class ComposerController extends BaseController with DragDropFileMixin { _closeComposerButtonState = ButtonState.disabled; - if (composerArguments.value == null) { - log('ComposerController::handleClickCloseComposer: ARGUMENTS is NULL'); + if (composerArguments.value == null || !_isEmailBodyLoaded) { + log('ComposerController::handleClickCloseComposer: ARGUMENTS is NULL or EMAIL NOT LOADED'); + _closeComposerButtonState = ButtonState.enabled; clearFocus(context); _closeComposerAction(); return; @@ -1891,15 +1892,7 @@ class ComposerController extends BaseController with DragDropFileMixin { if (isChanged && context.mounted) { clearFocus(context); - _showConfirmDialogSaveMessage(context); - return; - } - - if (!_isEmailBodyLoaded && context.mounted) { - log('ComposerController::handleClickCloseComposer: EDITOR NOT LOADED'); - _closeComposerButtonState = ButtonState.enabled; - clearFocus(context); - _closeComposerAction(); + await _showConfirmDialogSaveMessage(context); return; } @@ -1910,14 +1903,15 @@ class ComposerController extends BaseController with DragDropFileMixin { } } - void _showConfirmDialogSaveMessage(BuildContext context) { - showConfirmDialogAction( + Future _showConfirmDialogSaveMessage(BuildContext context) async { + await showConfirmDialogAction( context, title: AppLocalizations.of(context).saveMessage.capitalizeFirstEach, AppLocalizations.of(context).warningMessageWhenClickCloseComposer, AppLocalizations.of(context).save, cancelTitle: AppLocalizations.of(context).discardChanges, alignCenter: true, + outsideDismissible: false, onConfirmAction: () async => await Future.delayed( const Duration(milliseconds: 100), () => _handleSaveMessageToDraft(context) @@ -2038,7 +2032,7 @@ class ComposerController extends BaseController with DragDropFileMixin { resultState is GenerateEmailFailure) && context.mounted ) { - _showConfirmDialogWhenSaveMessageToDraftsFailure( + await _showConfirmDialogWhenSaveMessageToDraftsFailure( context: context, failure: resultState ); @@ -2106,19 +2100,20 @@ class ComposerController extends BaseController with DragDropFileMixin { cancelToken?.cancel([SavingEmailToDraftsCanceledException()]); } - void _showConfirmDialogWhenSaveMessageToDraftsFailure({ + Future _showConfirmDialogWhenSaveMessageToDraftsFailure({ required BuildContext context, required FeatureFailure failure, VoidCallback? onConfirmAction, VoidCallback? onCancelAction, - }) { - showConfirmDialogAction( + }) async { + await showConfirmDialogAction( context, title: '', AppLocalizations.of(context).warningMessageWhenSaveEmailToDraftsFailure, AppLocalizations.of(context).edit, cancelTitle: AppLocalizations.of(context).closeAnyway, alignCenter: true, + outsideDismissible: false, onConfirmAction: onConfirmAction ?? () { _closeComposerButtonState = ButtonState.enabled; _autoFocusFieldWhenLauncher(); From a79d96a6c9ec0668db03d35d0dda301c357514d5 Mon Sep 17 00:00:00 2001 From: DatDang Date: Tue, 26 Mar 2024 15:42:30 +0700 Subject: [PATCH 57/80] TF-2671 Fix scroll jumping when typing on composer (cherry picked from commit 2ab217d8b6193f61dfde0451ed49486cf51d664b) --- .../composer/presentation/composer_controller.dart | 11 +++++++---- lib/features/composer/presentation/composer_view.dart | 2 +- .../composer/presentation/styles/composer_style.dart | 1 - pubspec.lock | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 30b9f6fb7b..44fb52d8e7 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1651,7 +1651,7 @@ class ComposerController extends BaseController with DragDropFileMixin { 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 - ComposerStyle.keyboardMaxHeight - ComposerStyle.keyboardToolBarHeight; + final outsideHeight = Get.height - MediaQuery.viewInsetsOf(context).bottom - ComposerStyle.keyboardToolBarHeight; final webViewEditorClientY = max(outsideHeight, 0) + scrollController.position.pixels; if (scrollController.position.pixels >= realCoordinateY) { _scrollToCursorEditor( @@ -1675,9 +1675,12 @@ class ComposerController extends BaseController with DragDropFileMixin { double headerEditorMobileHeight, BuildContext context, ) { - scrollController.jumpTo( - realCoordinateY - (responsiveUtils.isLandscapeMobile(context) ? 0 : headerEditorMobileHeight / 2), - ); + final scrollTarget = realCoordinateY - + (responsiveUtils.isLandscapeMobile(context) + ? 0 + : headerEditorMobileHeight / 2); + final maxScrollExtend = scrollController.position.maxScrollExtent; + scrollController.jumpTo(min(scrollTarget, maxScrollExtend)); } void _onEnterKeyDown() { diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index 765f9c5f07..064acd3b3e 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -217,7 +217,7 @@ class ComposerView extends GetWidget { onLoadCompletedEditorAction: controller.onLoadCompletedMobileEditorAction, ), )), - const SizedBox(height: ComposerStyle.keyboardMaxHeight), + SizedBox(height: MediaQuery.viewInsetsOf(context).bottom), ], ), ), diff --git a/lib/features/composer/presentation/styles/composer_style.dart b/lib/features/composer/presentation/styles/composer_style.dart index d88eca6c80..ca246c3c0e 100644 --- a/lib/features/composer/presentation/styles/composer_style.dart +++ b/lib/features/composer/presentation/styles/composer_style.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; class ComposerStyle { static const double radius = 28; - static const double keyboardMaxHeight = 500; static const double keyboardToolBarHeight = 200; static const double popupMenuRadius = 8; diff --git a/pubspec.lock b/pubspec.lock index 0850dc19ea..bce43d73a0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -429,7 +429,7 @@ packages: description: path: "." ref: cnb_supported - resolved-ref: "669cfd989498cf398c88a6618eebb089967b9e28" + resolved-ref: "69996e32a708a62b8a613f2524c5635f5c556617" url: "https://github.com/linagora/enough_html_editor.git" source: git version: "0.0.5" From c24a21aac5c4fcfa2858df97aed0bc3185244f1b Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 15 Jan 2024 14:36:39 +0700 Subject: [PATCH 58/80] TF-2215 Remove LocaleInterceptor --- lib/main/bindings/network/network_bindings.dart | 3 --- .../network/network_isolate_binding.dart | 2 -- lib/main/localizations/locale_interceptor.dart | 17 ----------------- 3 files changed, 22 deletions(-) delete mode 100644 lib/main/localizations/locale_interceptor.dart diff --git a/lib/main/bindings/network/network_bindings.dart b/lib/main/bindings/network/network_bindings.dart index 2a015626b8..208c47854d 100644 --- a/lib/main/bindings/network/network_bindings.dart +++ b/lib/main/bindings/network/network_bindings.dart @@ -36,7 +36,6 @@ import 'package:tmail_ui_user/features/server_settings/data/network/server_setti import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/send_email_exception_thrower.dart'; -import 'package:tmail_ui_user/main/localizations/locale_interceptor.dart'; import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart'; import 'package:uuid/uuid.dart'; @@ -90,13 +89,11 @@ class NetworkBindings extends Bindings { Get.find(), Get.find(), )); - Get.put(LocaleInterceptor()); Get.find().interceptors.add(Get.find()); Get.find().interceptors.add(Get.find()); if (kDebugMode) { Get.find().interceptors.add(LogInterceptor(requestBody: true)); } - Get.find().interceptors.add(Get.find()); } void _bindingApi() { diff --git a/lib/main/bindings/network/network_isolate_binding.dart b/lib/main/bindings/network/network_isolate_binding.dart index 91f8862b5e..a7b81d0fdb 100644 --- a/lib/main/bindings/network/network_isolate_binding.dart +++ b/lib/main/bindings/network/network_isolate_binding.dart @@ -15,7 +15,6 @@ import 'package:tmail_ui_user/features/mailbox/data/network/mailbox_isolate_work import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_isolate_worker.dart'; import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; -import 'package:tmail_ui_user/main/localizations/locale_interceptor.dart'; import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart'; import 'package:uuid/uuid.dart'; import 'package:worker_manager/worker_manager.dart'; @@ -51,7 +50,6 @@ class NetworkIsolateBindings extends Bindings { if (kDebugMode) { dio.interceptors.add(LogInterceptor(requestBody: true)); } - dio.interceptors.add(Get.find()); } void _bindingApi() { diff --git a/lib/main/localizations/locale_interceptor.dart b/lib/main/localizations/locale_interceptor.dart deleted file mode 100644 index 82285d00b2..0000000000 --- a/lib/main/localizations/locale_interceptor.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:io'; - -import 'package:core/utils/app_logger.dart'; -import 'package:dio/dio.dart'; -import 'package:tmail_ui_user/main/localizations/localization_service.dart'; - -class LocaleInterceptor extends InterceptorsWrapper { - - @override - void onRequest(RequestOptions options, RequestInterceptorHandler handler) { - final currentLocale = LocalizationService.getLocaleFromLanguage(); - log('LocaleInterceptor::onRequest:currentLocale: $currentLocale'); - options.headers[HttpHeaders.acceptLanguageHeader] = LocalizationService.supportedLocalesToLanguageTags(); - options.headers[HttpHeaders.contentLanguageHeader] = currentLocale.toLanguageTag(); - super.onRequest(options, handler); - } -} \ No newline at end of file From acc8201c116def50055501a59bbf0a7487643a96 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 15 Jan 2024 14:37:17 +0700 Subject: [PATCH 59/80] TF-2215 Add `Accept-Language` and `Content-Language` when sent email --- .../extensions/create_email_request_extension.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/features/composer/presentation/extensions/create_email_request_extension.dart b/lib/features/composer/presentation/extensions/create_email_request_extension.dart index cc3b18b342..f112bc0e60 100644 --- a/lib/features/composer/presentation/extensions/create_email_request_extension.dart +++ b/lib/features/composer/presentation/extensions/create_email_request_extension.dart @@ -18,6 +18,7 @@ import 'package:tmail_ui_user/features/email/domain/extensions/list_attachments_ import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/sending_queue/domain/extensions/sending_email_extension.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; +import 'package:tmail_ui_user/main/localizations/localization_service.dart'; extension CreateEmailRequestExtension on CreateEmailRequest { @@ -124,9 +125,15 @@ extension CreateEmailRequestExtension on CreateEmailRequest { }, bodyValues: { partId: EmailBodyValue( - newEmailContent, - false, - false + value: newEmailContent, + isEncodingProblem: false, + isTruncated: false, + acceptLanguageHeader: { + IndividualHeaderIdentifier.acceptLanguageHeader: LocalizationService.supportedLocalesToLanguageTags() + }, + contentLanguageHeader: { + IndividualHeaderIdentifier.contentLanguageHeader: LocalizationService.getLocaleFromLanguage().toLanguageTag() + }, ) }, headerUserAgent: { From da3bf99cdff240cbf6892922840a6ff5c418deb1 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 23 Apr 2024 16:11:40 +0700 Subject: [PATCH 60/80] Fix identity fetching again when open composer --- .../presentation/controller/mailbox_dashboard_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 8af9ff2a2a..cbed929473 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -1402,7 +1402,7 @@ class MailboxDashBoardController extends ReloadableController { } void goToComposer(ComposerArguments arguments) async { - final argumentsWithIdentity = arguments.withIdentity(identities: _identities); + final argumentsWithIdentity = arguments.withIdentity(identities: List.from(_identities ?? [])); if (PlatformInfo.isWeb) { if (composerOverlayState.value == ComposerOverlayState.inActive) { From a59470e47cea86cde6ca46062edcb272aca10ce3 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 23 Apr 2024 16:19:39 +0700 Subject: [PATCH 61/80] Release the variable on memory when the controller is closed --- .../controller/mailbox_dashboard_controller.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index cbed929473..1245377259 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -2535,6 +2535,12 @@ class MailboxDashBoardController extends ReloadableController { _fcmService.closeStream(); applicationManager.releaseUserAgent(); BackButtonInterceptor.removeByName(AppRoutes.dashboard); + _identities = null; + composerArguments = null; + outboxMailbox = null; + sessionCurrent = null; + mapMailboxById = {}; + mapDefaultMailboxIdByRole = {}; super.onClose(); } } \ No newline at end of file From dfbd71af578ece537ff71c44ef545773031c1c25 Mon Sep 17 00:00:00 2001 From: Benoit TELLIER Date: Fri, 26 Apr 2024 15:50:13 +0200 Subject: [PATCH 62/80] Nettoyer -> Vider --- lib/l10n/intl_fr.arb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index a5c8dad9b2..dbca897408 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -3310,7 +3310,7 @@ "placeholders_order": [], "placeholders": {} }, - "clean": "Nettoyer", + "clean": "Vider", "@clean": { "type": "text", "placeholders_order": [], @@ -3448,7 +3448,7 @@ "placeholders_order": [], "placeholders": {} }, - "clearFolder": "Nettoyer le dossier", + "clearFolder": "Vider le dossier", "@clearFolder": { "type": "text", "placeholders_order": [], From fa40a823b0024631eaa059a4a92c9f3eff456b58 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 12 Apr 2024 14:38:16 +0700 Subject: [PATCH 63/80] TF-2122 Fix highlight and font search result incorrect --- .../extensions/string_extension.dart | 8 -- .../views/text/rich_text_builder.dart | 82 +++++++++--------- .../views/text/text_overflow_builder.dart | 28 +++---- .../mixin/base_email_item_tile.dart | 84 +++++++++---------- 4 files changed, 93 insertions(+), 109 deletions(-) diff --git a/core/lib/presentation/extensions/string_extension.dart b/core/lib/presentation/extensions/string_extension.dart index 544db2b584..cd4cd1502d 100644 --- a/core/lib/presentation/extensions/string_extension.dart +++ b/core/lib/presentation/extensions/string_extension.dart @@ -1,5 +1,4 @@ import 'package:core/utils/app_logger.dart'; -import 'package:flutter/material.dart'; extension StringExtension on String { @@ -39,11 +38,4 @@ extension StringExtension on String { return ''; } } - - String get overflow { - return characters - .replaceAll(Characters(''), Characters('\u{200B}')) - .replaceAll(Characters('-'), Characters('\u{2011}')) - .toString(); - } } \ No newline at end of file diff --git a/core/lib/presentation/views/text/rich_text_builder.dart b/core/lib/presentation/views/text/rich_text_builder.dart index 7c4c2f2a1c..9e1c368793 100644 --- a/core/lib/presentation/views/text/rich_text_builder.dart +++ b/core/lib/presentation/views/text/rich_text_builder.dart @@ -1,66 +1,62 @@ -import 'package:core/presentation/utils/style_utils.dart'; import 'package:flutter/material.dart'; -class RichTextBuilder { +class RichTextBuilder extends StatelessWidget { - final String _textOrigin; - final String _wordToStyle; - final TextStyle _styleOrigin; - final TextStyle _styleWord; + final String textOrigin; + final String wordToStyle; + final TextStyle styleOrigin; + final TextStyle styleWord; - Key? _key; - int? _maxLines; - TextOverflow? _overflow; + const RichTextBuilder({ + super.key, + required this.textOrigin, + required this.wordToStyle, + required this.styleOrigin, + required this.styleWord, + }); - RichTextBuilder( - this._textOrigin, - this._wordToStyle, - this._styleOrigin, - this._styleWord, - ); - - void key(Key key) { - _key = key; - } - - void maxLines(int maxLines) { - _maxLines = maxLines; - } - - void setOverflow(TextOverflow textOverflow) { - _overflow = textOverflow; - } - - RichText build() { - return RichText( - key: _key, - maxLines: _maxLines ?? 1, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: _overflow ?? CommonTextStyle.defaultTextOverFlow, - text: TextSpan( - style: _styleOrigin, - children: _getSpans(_textOrigin, _wordToStyle, _styleWord))); + @override + Widget build(BuildContext context) { + return Text.rich( + TextSpan( + style: styleOrigin, + children: _getSpans( + text: textOrigin, + word: wordToStyle, + styleOrigin: styleOrigin, + styleWord: styleWord, + ) + ), + style: styleOrigin, + maxLines: 1, + overflow: TextOverflow.ellipsis + ); } - List _getSpans(String text, String matchWord, TextStyle style) { + List _getSpans({ + required String text, + required String word, + required TextStyle styleOrigin, + required TextStyle styleWord, + }) { List spans = []; int spanBoundary = 0; do { // look for the next match - final startIndex = text.toLowerCase().indexOf(matchWord.toLowerCase(), spanBoundary); + final startIndex = text.toLowerCase().indexOf(word.toLowerCase(), spanBoundary); // if no more matches then add the rest of the string without style if (startIndex == -1) { - spans.add(TextSpan(text: text.substring(spanBoundary))); + spans.add(TextSpan(text: text.substring(spanBoundary), style: styleOrigin)); return spans; } // add any unStyled text before the next match if (startIndex > spanBoundary) { - spans.add(TextSpan(text: text.substring(spanBoundary, startIndex))); + spans.add(TextSpan(text: text.substring(spanBoundary, startIndex), style: styleOrigin)); } // style the matched text - final endIndex = startIndex + matchWord.length; + final endIndex = startIndex + word.length; final spanText = text.substring(startIndex, endIndex); - spans.add(TextSpan(text: spanText, style: style)); + spans.add(TextSpan(text: spanText, style: styleWord)); // mark the boundary to start the next search from spanBoundary = endIndex; // continue until there are no more matches diff --git a/core/lib/presentation/views/text/text_overflow_builder.dart b/core/lib/presentation/views/text/text_overflow_builder.dart index d10030f0dc..46669ef449 100644 --- a/core/lib/presentation/views/text/text_overflow_builder.dart +++ b/core/lib/presentation/views/text/text_overflow_builder.dart @@ -1,7 +1,4 @@ -import 'package:core/presentation/extensions/string_extension.dart'; -import 'package:core/presentation/utils/style_utils.dart'; -import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; class TextOverflowBuilder extends StatelessWidget { @@ -14,22 +11,23 @@ class TextOverflowBuilder extends StatelessWidget { final TextDirection? textDirection; final TextOverflow? overflow; - const TextOverflowBuilder(this.data, { - super.key, - this.style, - this.textAlign, - this.softWrap = CommonTextStyle.defaultSoftWrap, - this.maxLines = 1, - this.textDirection, - this.overflow = CommonTextStyle.defaultTextOverFlow, - }); + const TextOverflowBuilder( + this.data, + { + super.key, + this.style, + this.textAlign, + this.softWrap = true, + this.maxLines = 1, + this.textDirection, + this.overflow = TextOverflow.ellipsis, + } + ); @override Widget build(BuildContext context) { return Text( - DirectionUtils.isDirectionRTLByLanguage(context) - ? data - : data.overflow, + data, style: style, textAlign: textAlign, softWrap: softWrap, diff --git a/lib/features/thread/presentation/mixin/base_email_item_tile.dart b/lib/features/thread/presentation/mixin/base_email_item_tile.dart index cf37c63d30..493bac458d 100644 --- a/lib/features/thread/presentation/mixin/base_email_item_tile.dart +++ b/lib/features/thread/presentation/mixin/base_email_item_tile.dart @@ -1,12 +1,10 @@ import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/extensions/string_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/views/text/rich_text_builder.dart'; import 'package:core/presentation/views/text/text_overflow_builder.dart'; -import 'package:core/utils/direction_utils.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -88,20 +86,20 @@ mixin BaseEmailItemTile { ) { if (isSearchEnabled(isSearchEmailRunning, query)) { return RichTextBuilder( - DirectionUtils.isDirectionRTLByLanguage(context) - ? informationSender(email, mailbox) - : informationSender(email, mailbox).overflow, - query?.value ?? '', - TextStyle( - fontSize: 15, - color: buildTextColorForReadEmail(email), - fontWeight: buildFontForReadEmail(email)), - TextStyle( - fontSize: 15, - color: buildTextColorForReadEmail(email), - backgroundColor: AppColor.bgWordSearch, - fontWeight: buildFontForReadEmail(email)) - ).build(); + textOrigin: informationSender(email, mailbox), + wordToStyle: query?.value ?? '', + styleOrigin: TextStyle( + fontSize: 15, + color: buildTextColorForReadEmail(email), + fontWeight: buildFontForReadEmail(email) + ), + styleWord: TextStyle( + fontSize: 15, + color: buildTextColorForReadEmail(email), + backgroundColor: AppColor.bgWordSearch, + fontWeight: buildFontForReadEmail(email) + ) + ); } else { return TextOverflowBuilder( informationSender(email, mailbox), @@ -122,20 +120,20 @@ mixin BaseEmailItemTile { ) { if (isSearchEnabled(isSearchEmailRunning, query)) { return RichTextBuilder( - DirectionUtils.isDirectionRTLByLanguage(context) - ? email.getEmailTitle() - : email.getEmailTitle().overflow, - query?.value ?? '', - TextStyle( - fontSize: 13, - color: buildTextColorForReadEmail(email), - fontWeight: buildFontForReadEmail(email)), - TextStyle( - fontSize: 13, - backgroundColor: AppColor.bgWordSearch, - color: buildTextColorForReadEmail(email), - fontWeight: buildFontForReadEmail(email)) - ).build(); + textOrigin: email.getEmailTitle(), + wordToStyle: query?.value ?? '', + styleOrigin: TextStyle( + fontSize: 13, + color: buildTextColorForReadEmail(email), + fontWeight: buildFontForReadEmail(email) + ), + styleWord: TextStyle( + fontSize: 13, + backgroundColor: AppColor.bgWordSearch, + color: buildTextColorForReadEmail(email), + fontWeight: buildFontForReadEmail(email) + ) + ); } else { return TextOverflowBuilder( email.getEmailTitle(), @@ -155,19 +153,19 @@ mixin BaseEmailItemTile { ) { if (isSearchEnabled(isSearchEmailRunning, query)) { return RichTextBuilder( - DirectionUtils.isDirectionRTLByLanguage(context) - ? email.getPartialContent() - : email.getPartialContent().overflow, - query?.value ?? '', - const TextStyle( - fontSize: 13, - color: AppColor.colorContentEmail, - fontWeight: FontWeight.normal), - const TextStyle( - fontSize: 13, - color: AppColor.colorContentEmail, - backgroundColor: AppColor.bgWordSearch) - ).build(); + textOrigin: email.getPartialContent(), + wordToStyle: query?.value ?? '', + styleOrigin: const TextStyle( + fontSize: 13, + color: AppColor.colorContentEmail, + fontWeight: FontWeight.normal + ), + styleWord: const TextStyle( + fontSize: 13, + color: AppColor.colorContentEmail, + backgroundColor: AppColor.bgWordSearch + ) + ); } else { return TextOverflowBuilder( email.getPartialContent(), From 92d0f30eecf3e3273123e46d09a8445a1309415d Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 23 Apr 2024 23:26:17 +0700 Subject: [PATCH 64/80] Only create and use the correct rich text controller for each platform --- .../presentation/composer_bindings.dart | 15 ++- .../presentation/composer_controller.dart | 92 ++++++++++--------- .../composer/presentation/composer_view.dart | 4 +- .../presentation/composer_view_web.dart | 56 +++++------ .../rich_text_mobile_tablet_controller.dart | 9 ++ .../widgets/recipient_composer_widget.dart | 18 ++-- .../identity_creator_controller.dart | 40 +++++--- .../presentation/identity_creator_view.dart | 16 ++-- 8 files changed, 139 insertions(+), 111 deletions(-) diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index 65d5f5b900..b600963b6e 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -214,9 +214,12 @@ class ComposerBindings extends BaseBindings { @override void bindingsController() { - Get.lazyPut(() => RichTextMobileTabletController()); + if (PlatformInfo.isWeb) { + Get.lazyPut(() => RichTextWebController()); + } else { + Get.lazyPut(() => RichTextMobileTabletController()); + } Get.lazyPut(() => UploadController(Get.find())); - Get.lazyPut(() => RichTextWebController()); Get.lazyPut(() => ComposerController( Get.find(), Get.find(), @@ -225,7 +228,6 @@ class ComposerBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), - Get.find(), Get.find(), Get.find(), Get.find(), @@ -235,9 +237,12 @@ class ComposerBindings extends BaseBindings { } void dispose() { + if (PlatformInfo.isWeb) { + Get.delete(); + } else { + Get.delete(); + } Get.delete(); - Get.delete(); - Get.delete(); Get.delete(); } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 44fb52d8e7..2177ed5024 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -94,7 +94,6 @@ import 'package:universal_html/html.dart' as html; class ComposerController extends BaseController with DragDropFileMixin { final mailboxDashBoardController = Get.find(); - final richTextMobileTabletController = Get.find(); final networkConnectionController = Get.find(); final _dynamicUrlInterceptors = Get.find(); @@ -121,7 +120,6 @@ class ComposerController extends BaseController with DragDropFileMixin { final UploadController uploadController; final RemoveComposerCacheOnWebInteractor _removeComposerCacheOnWebInteractor; final SaveComposerCacheOnWebInteractor _saveComposerCacheOnWebInteractor; - final RichTextWebController richTextWebController; final DownloadImageAsBase64Interactor _downloadImageAsBase64Interactor; final TransformHtmlEmailContentInteractor _transformHtmlEmailContentInteractor; final GetAlwaysReadReceiptSettingInteractor _getAlwaysReadReceiptSettingInteractor; @@ -165,7 +163,8 @@ class ComposerController extends BaseController with DragDropFileMixin { StreamSubscription? _subscriptionOnDragLeave; StreamSubscription? _subscriptionOnDrop; - final RichTextController keyboardRichTextController = RichTextController(); + RichTextMobileTabletController? richTextMobileTabletController; + RichTextWebController? richTextWebController; final ScrollController scrollController = ScrollController(); final ScrollController scrollControllerEmailAddress = ScrollController(); @@ -194,7 +193,6 @@ class ComposerController extends BaseController with DragDropFileMixin { this.uploadController, this._removeComposerCacheOnWebInteractor, this._saveComposerCacheOnWebInteractor, - this.richTextWebController, this._downloadImageAsBase64Interactor, this._transformHtmlEmailContentInteractor, this._getAlwaysReadReceiptSettingInteractor, @@ -205,14 +203,17 @@ class ComposerController extends BaseController with DragDropFileMixin { @override void onInit() { super.onInit(); - createFocusNodeInput(); - scrollControllerEmailAddress.addListener(_scrollControllerEmailAddressListener); - _listenStreamEvent(); if (PlatformInfo.isWeb) { WidgetsBinding.instance.addPostFrameCallback((_) { _listenBrowserTabRefresh(); }); + richTextWebController = getBinding(); + } else { + richTextMobileTabletController = getBinding(); } + createFocusNodeInput(); + scrollControllerEmailAddress.addListener(_scrollControllerEmailAddressListener); + _listenStreamEvent(); _getAlwaysReadReceiptSetting(); } @@ -239,6 +240,7 @@ class ComposerController extends BaseController with DragDropFileMixin { _subscriptionOnDragOver?.cancel(); _subscriptionOnDragLeave?.cancel(); _subscriptionOnDrop?.cancel(); + subjectEmailInputFocusNode?.removeListener(_subjectEmailInputFocusListener); super.onClose(); } @@ -266,7 +268,6 @@ class ComposerController extends BaseController with DragDropFileMixin { bccEmailAddressController.dispose(); uploadInlineImageWorker.dispose(); dashboardViewStateWorker.dispose(); - keyboardRichTextController.dispose(); scrollController.dispose(); scrollControllerEmailAddress.removeListener(_scrollControllerEmailAddressListener); scrollControllerEmailAddress.dispose(); @@ -295,9 +296,9 @@ class ComposerController extends BaseController with DragDropFileMixin { } else if (success is DownloadImageAsBase64Success) { final inlineImage = InlineImage(fileInfo: success.fileInfo, base64Uri: success.base64Uri); if (PlatformInfo.isWeb) { - richTextWebController.insertImage(inlineImage); + richTextWebController?.insertImage(inlineImage); } else { - richTextMobileTabletController.insertImage(inlineImage); + richTextMobileTabletController?.insertImage(inlineImage); } maxWithEditor = null; } else if (success is GetAlwaysReadReceiptSettingSuccess) { @@ -414,15 +415,6 @@ class ComposerController extends BaseController with DragDropFileMixin { void createFocusNodeInput() { toAddressFocusNode = FocusNode(); - subjectEmailInputFocusNode = FocusNode( - onKey: (focus, event) { - if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) { - richTextWebController.editorController.setFocus(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - } - ); ccAddressFocusNode = FocusNode(); bccAddressFocusNode = FocusNode(); searchIdentitiesFocusNode = FocusNode(); @@ -430,24 +422,36 @@ class ComposerController extends BaseController with DragDropFileMixin { ccAddressFocusNodeKeyboard = FocusNode(); bccAddressFocusNodeKeyboard = FocusNode(); - subjectEmailInputFocusNode?.addListener(() { - log('ComposerController::createFocusNodeInput():subjectEmailInputFocusNode: ${subjectEmailInputFocusNode?.hasFocus}'); - if (subjectEmailInputFocusNode?.hasFocus == true) { - if (PlatformInfo.isMobile) { - htmlEditorApi?.unfocus(); - } - _collapseAllRecipient(); - _autoCreateEmailTag(); + subjectEmailInputFocusNode = FocusNode( + onKey: PlatformInfo.isWeb ? _subjectEmailInputOnKeyListener : null + ); + subjectEmailInputFocusNode?.addListener(_subjectEmailInputFocusListener); + } + + void _subjectEmailInputFocusListener() { + if (subjectEmailInputFocusNode?.hasFocus == true) { + if (PlatformInfo.isMobile) { + htmlEditorApi?.unfocus(); } - }); + _collapseAllRecipient(); + _autoCreateEmailTag(); + } + } + + KeyEventResult _subjectEmailInputOnKeyListener(FocusNode node, RawKeyEvent event) { + if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) { + richTextWebController?.editorController.setFocus(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; } void onCreatedMobileEditorAction(BuildContext context, HtmlEditorApi editorApi, String? content) { if (identitySelected.value != null) { initTextEditor(content); } - richTextMobileTabletController.htmlEditorApi = editorApi; - keyboardRichTextController.onCreateHTMLEditor( + richTextMobileTabletController?.htmlEditorApi = editorApi; + richTextMobileTabletController?.richTextController.onCreateHTMLEditor( editorApi, onEnterKeyDown: _onEnterKeyDown, context: context, @@ -1325,7 +1329,10 @@ class ComposerController extends BaseController with DragDropFileMixin { } void displayScreenTypeComposerAction(ScreenDisplayMode displayMode) async { - _updateTextForEditor(); + if (richTextWebController != null && screenDisplayMode.value != ScreenDisplayMode.minimize) { + final textCurrent = await richTextWebController!.editorController.getText(); + richTextWebController!.editorController.setText(textCurrent); + } screenDisplayMode.value = displayMode; await Future.delayed( @@ -1333,11 +1340,6 @@ class ComposerController extends BaseController with DragDropFileMixin { _autoFocusFieldWhenLauncher); } - void _updateTextForEditor() async { - final textCurrent = await richTextWebController.editorController.getText(); - richTextWebController.editorController.setText(textCurrent); - } - void deleteComposer() { FocusManager.instance.primaryFocus?.unfocus(); mailboxDashBoardController.closeComposerOverlay(); @@ -1589,7 +1591,7 @@ class ComposerController extends BaseController with DragDropFileMixin { Future _applySignature(String signature) async { if (PlatformInfo.isWeb) { - richTextWebController.editorController.insertSignature(signature); + richTextWebController?.editorController.insertSignature(signature); } else { await htmlEditorApi?.insertSignature(signature, allowCollapsed: false); } @@ -1598,7 +1600,7 @@ class ComposerController extends BaseController with DragDropFileMixin { Future _removeSignature() async { log('ComposerController::_removeSignature():'); if (PlatformInfo.isWeb) { - richTextWebController.editorController.removeSignature(); + richTextWebController?.editorController.removeSignature(); } else { await htmlEditorApi?.removeSignature(); } @@ -1712,7 +1714,7 @@ class ComposerController extends BaseController with DragDropFileMixin { } else if (subjectEmailInputController.text.isEmpty) { subjectEmailInputFocusNode?.requestFocus(); } else if (PlatformInfo.isWeb) { - richTextWebController.editorController.setFocus(); + richTextWebController?.editorController.setFocus(); } } @@ -1725,10 +1727,10 @@ class ComposerController extends BaseController with DragDropFileMixin { void handleInitHtmlEditorWeb(String initContent) async { log('ComposerController::handleInitHtmlEditorWeb:'); _isEmailBodyLoaded = true; - richTextWebController.editorController.setFullScreen(); - richTextWebController.editorController.setOnDragDropEvent(); + richTextWebController?.editorController.setFullScreen(); + richTextWebController?.editorController.setOnDragDropEvent(); onChangeTextEditorWeb(initContent); - richTextWebController.setEnableCodeView(); + richTextWebController?.setEnableCodeView(); if (identitySelected.value == null) { _getAllIdentities(); } else { @@ -1739,8 +1741,8 @@ class ComposerController extends BaseController with DragDropFileMixin { void handleOnFocusHtmlEditorWeb() { FocusManager.instance.primaryFocus?.unfocus(); - richTextWebController.editorController.setFocus(); - richTextWebController.closeAllMenuPopup(); + richTextWebController?.editorController.setFocus(); + richTextWebController?.closeAllMenuPopup(); } void handleOnUnFocusHtmlEditorWeb() { @@ -1779,7 +1781,7 @@ class ComposerController extends BaseController with DragDropFileMixin { String? get textEditorWeb => _textEditorWeb; - HtmlEditorApi? get htmlEditorApi => richTextMobileTabletController.htmlEditorApi; + HtmlEditorApi? get htmlEditorApi => richTextMobileTabletController?.htmlEditorApi; void onChangeTextEditorWeb(String? text) { if (identitySelected.value != null) { diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index 064acd3b3e..0da0604c82 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -36,7 +36,7 @@ class ComposerView extends GetWidget { return ResponsiveWidget( responsiveUtils: controller.responsiveUtils, mobile: MobileContainerView( - keyboardRichTextController: controller.keyboardRichTextController, + keyboardRichTextController: controller.richTextMobileTabletController!.richTextController, onCloseViewAction: () => controller.handleClickCloseComposer(context), onClearFocusAction: () => controller.clearFocus(context), onAttachFileAction: () => controller.isNetworkConnectionAvailable @@ -228,7 +228,7 @@ class ComposerView extends GetWidget { ), ), tablet: TabletContainerView( - keyboardRichTextController: controller.keyboardRichTextController, + keyboardRichTextController: controller.richTextMobileTabletController!.richTextController, onCloseViewAction: () => controller.handleClickCloseComposer(context), onClearFocusAction: () => controller.clearFocus(context), onAttachFileAction: () => controller.isNetworkConnectionAvailable diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index 30d990171f..efe72a1680 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -43,9 +43,9 @@ class ComposerView extends GetWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Obx(() => MobileResponsiveAppBarComposerWidget( - isCodeViewEnabled: controller.richTextWebController.codeViewEnabled, - isFormattingOptionsEnabled: controller.richTextWebController.isFormattingOptionsEnabled, - openRichToolbarAction: controller.richTextWebController.toggleFormattingOptions, + isCodeViewEnabled: controller.richTextWebController!.codeViewEnabled, + isFormattingOptionsEnabled: controller.richTextWebController!.isFormattingOptionsEnabled, + openRichToolbarAction: controller.richTextWebController!.toggleFormattingOptions, isSendButtonEnabled: controller.isEnableEmailSendButton.value, onCloseViewAction: () => controller.handleClickCloseComposer(context), attachFileAction: () => controller.openFilePickerByType(context, FileType.any), @@ -177,7 +177,7 @@ class ComposerView extends GetWidget { child: Padding( padding: ComposerStyle.mobileEditorPadding, child: Obx(() => WebEditorView( - editorController: controller.richTextWebController.editorController, + editorController: controller.richTextWebController!.editorController, arguments: controller.composerArguments.value, contentViewState: controller.emailContentsViewState.value, currentWebContent: controller.textEditorWeb, @@ -186,8 +186,8 @@ class ComposerView extends GetWidget { onFocus: controller.handleOnFocusHtmlEditorWeb, onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, - onEditorSettings: controller.richTextWebController.onEditorSettingsChange, - onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, + onEditorSettings: controller.richTextWebController!.onEditorSettingsChange, + onEditorTextSizeChanged: controller.richTextWebController!.onEditorTextSizeChanged, width: constraints.maxWidth, height: constraints.maxHeight, onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, @@ -207,9 +207,9 @@ class ComposerView extends GetWidget { } }), Obx(() { - if (controller.richTextWebController.isFormattingOptionsEnabled) { + if (controller.richTextWebController!.isFormattingOptionsEnabled) { return ToolbarRichTextWebBuilder( - richTextWebController: controller.richTextWebController, + richTextWebController: controller.richTextWebController!, padding: ComposerStyle.richToolbarPadding, decoration: const BoxDecoration( color: ComposerStyle.richToolbarColor, @@ -418,7 +418,7 @@ class ComposerView extends GetWidget { padding: ComposerStyle.desktopEditorPadding, child: Obx(() { return WebEditorView( - editorController: controller.richTextWebController.editorController, + editorController: controller.richTextWebController!.editorController, arguments: controller.composerArguments.value, contentViewState: controller.emailContentsViewState.value, currentWebContent: controller.textEditorWeb, @@ -427,8 +427,8 @@ class ComposerView extends GetWidget { onFocus: controller.handleOnFocusHtmlEditorWeb, onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, - onEditorSettings: controller.richTextWebController.onEditorSettingsChange, - onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, + onEditorSettings: controller.richTextWebController?.onEditorSettingsChange, + onEditorTextSizeChanged: controller.richTextWebController?.onEditorTextSizeChanged, width: constraints.maxWidth, height: constraints.maxHeight, onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, @@ -449,9 +449,9 @@ class ComposerView extends GetWidget { } }), Obx(() { - if (controller.richTextWebController.isFormattingOptionsEnabled) { + if (controller.richTextWebController!.isFormattingOptionsEnabled) { return ToolbarRichTextWebBuilder( - richTextWebController: controller.richTextWebController, + richTextWebController: controller.richTextWebController!, padding: ComposerStyle.richToolbarPadding, decoration: const BoxDecoration( color: ComposerStyle.richToolbarColor, @@ -467,12 +467,12 @@ class ComposerView extends GetWidget { ), ), Obx(() => BottomBarComposerWidget( - isCodeViewEnabled: controller.richTextWebController.codeViewEnabled, - isFormattingOptionsEnabled: controller.richTextWebController.isFormattingOptionsEnabled, - openRichToolbarAction: controller.richTextWebController.toggleFormattingOptions, + isCodeViewEnabled: controller.richTextWebController!.codeViewEnabled, + isFormattingOptionsEnabled: controller.richTextWebController!.isFormattingOptionsEnabled, + openRichToolbarAction: controller.richTextWebController!.toggleFormattingOptions, attachFileAction: () => controller.openFilePickerByType(context, FileType.any), insertImageAction: () => controller.insertImage(context, constraints.maxWidth), - showCodeViewAction: controller.richTextWebController.toggleCodeView, + showCodeViewAction: controller.richTextWebController!.toggleCodeView, deleteComposerAction: () => controller.handleClickDeleteComposer(context), saveToDraftAction: () => controller.handleClickSaveAsDraftsButton(context), sendMessageAction: () => controller.handleClickSendButton(context), @@ -680,7 +680,7 @@ class ComposerView extends GetWidget { child: Padding( padding: ComposerStyle.tabletEditorPadding, child: Obx(() => WebEditorView( - editorController: controller.richTextWebController.editorController, + editorController: controller.richTextWebController!.editorController, arguments: controller.composerArguments.value, contentViewState: controller.emailContentsViewState.value, currentWebContent: controller.textEditorWeb, @@ -689,8 +689,8 @@ class ComposerView extends GetWidget { onFocus: controller.handleOnFocusHtmlEditorWeb, onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, - onEditorSettings: controller.richTextWebController.onEditorSettingsChange, - onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, + onEditorSettings: controller.richTextWebController!.onEditorSettingsChange, + onEditorTextSizeChanged: controller.richTextWebController!.onEditorTextSizeChanged, width: constraints.maxWidth, height: constraints.maxHeight, onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, @@ -710,9 +710,9 @@ class ComposerView extends GetWidget { } }), Obx(() { - if (controller.richTextWebController.isFormattingOptionsEnabled) { + if (controller.richTextWebController!.isFormattingOptionsEnabled) { return ToolbarRichTextWebBuilder( - richTextWebController: controller.richTextWebController, + richTextWebController: controller.richTextWebController!, padding: ComposerStyle.richToolbarPadding, decoration: const BoxDecoration( color: ComposerStyle.richToolbarColor, @@ -777,12 +777,12 @@ class ComposerView extends GetWidget { ), ), Obx(() => BottomBarComposerWidget( - isCodeViewEnabled: controller.richTextWebController.codeViewEnabled, - isFormattingOptionsEnabled: controller.richTextWebController.isFormattingOptionsEnabled, - openRichToolbarAction: controller.richTextWebController.toggleFormattingOptions, + isCodeViewEnabled: controller.richTextWebController!.codeViewEnabled, + isFormattingOptionsEnabled: controller.richTextWebController!.isFormattingOptionsEnabled, + openRichToolbarAction: controller.richTextWebController!.toggleFormattingOptions, attachFileAction: () => controller.openFilePickerByType(context, FileType.any), insertImageAction: () => controller.insertImage(context, constraints.maxWidth), - showCodeViewAction: controller.richTextWebController.toggleCodeView, + showCodeViewAction: controller.richTextWebController!.toggleCodeView, deleteComposerAction: () => controller.handleClickDeleteComposer(context), saveToDraftAction: () => controller.handleClickSaveAsDraftsButton(context), sendMessageAction: () => controller.handleClickSendButton(context), @@ -833,10 +833,10 @@ class ComposerView extends GetWidget { colorIcon: ComposerStyle.popupItemIconColor, padding: ComposerStyle.popupItemPadding, selectedIcon: controller.imagePaths.icFilterSelected, - isSelected: controller.richTextWebController.codeViewEnabled, + isSelected: controller.richTextWebController?.codeViewEnabled, onCallbackAction: () { popBack(); - controller.richTextWebController.toggleCodeView(); + controller.richTextWebController?.toggleCodeView(); } ) ), diff --git a/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart b/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart index 269c5503eb..ec887b5f1c 100644 --- a/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart +++ b/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart @@ -11,6 +11,8 @@ import 'package:tmail_ui_user/features/composer/presentation/model/inline_image. class RichTextMobileTabletController extends BaseRichTextController { HtmlEditorApi? htmlEditorApi; + final RichTextController richTextController = RichTextController(); + void insertImage(InlineImage inlineImage) async { if (inlineImage.fileInfo.isShared == true) { await htmlEditorApi?.moveCursorAtLastNode(); @@ -41,4 +43,11 @@ class RichTextMobileTabletController extends BaseRichTextController { logError('RichTextMobileTabletController::insertImageData:Exception: $e'); } } + + @override + void onClose() { + richTextController.dispose(); + htmlEditorApi = null; + super.onClose(); + } } diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index 9c3d0da467..45a5ba332a 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -149,14 +149,7 @@ class _RecipientComposerWidgetState extends State { child: FocusScope( child: Focus( onFocusChange: (focus) => widget.onFocusEmailAddressChangeAction?.call(widget.prefix, focus), - onKey: (focusNode, event) { - if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) { - widget.nextFocusNode?.requestFocus(); - widget.onFocusNextAddressAction?.call(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - }, + onKey: PlatformInfo.isWeb ? _recipientInputOnKeyListener : null, child: StatefulBuilder( builder: (context, stateSetter) { if (PlatformInfo.isWeb || widget.isTestingForWeb) { @@ -389,6 +382,15 @@ class _RecipientComposerWidgetState extends State { ); } + KeyEventResult _recipientInputOnKeyListener(FocusNode node, RawKeyEvent event) { + if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) { + widget.nextFocusNode?.requestFocus(); + widget.onFocusNextAddressAction?.call(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } + bool get _isCollapse => _currentListEmailAddress.length > 1 && widget.expandMode == ExpandMode.COLLAPSE; bool get _isAllRecipientInputEnabled => widget.fromState == PrefixRecipientState.enabled diff --git a/lib/features/identity_creator/presentation/identity_creator_controller.dart b/lib/features/identity_creator/presentation/identity_creator_controller.dart index 11e0d8ca27..4266539e20 100644 --- a/lib/features/identity_creator/presentation/identity_creator_controller.dart +++ b/lib/features/identity_creator/presentation/identity_creator_controller.dart @@ -67,15 +67,15 @@ class IdentityCreatorController extends BaseController { final isMobileEditorFocus = RxBool(false); final isCompressingInlineImage = RxBool(false); - final RichTextController keyboardRichTextController = RichTextController(); - final RichTextMobileTabletController richTextMobileTabletController = RichTextMobileTabletController(); - final RichTextWebController richTextWebController = RichTextWebController(); final TextEditingController inputNameIdentityController = TextEditingController(); final TextEditingController inputBccIdentityController = TextEditingController(); final FocusNode inputNameIdentityFocusNode = FocusNode(); final FocusNode inputBccIdentityFocusNode = FocusNode(); final ScrollController scrollController = ScrollController(); + RichTextMobileTabletController? richTextMobileTabletController; + RichTextWebController? richTextWebController; + String? _nameIdentity; String? _contentHtmlEditor; AccountId? accountId; @@ -110,6 +110,11 @@ class IdentityCreatorController extends BaseController { @override void onInit() { super.onInit(); + if (PlatformInfo.isWeb) { + richTextWebController = RichTextWebController(); + } else { + richTextMobileTabletController = RichTextMobileTabletController(); + } log('IdentityCreatorController::onInit():arguments: ${Get.arguments}'); arguments = Get.arguments; } @@ -132,13 +137,18 @@ class IdentityCreatorController extends BaseController { @override void onClose() { log('IdentityCreatorController::onClose():'); - keyboardRichTextController.dispose(); inputNameIdentityFocusNode.dispose(); inputBccIdentityFocusNode.dispose(); inputNameIdentityController.dispose(); inputBccIdentityController.dispose(); scrollController.dispose(); - richTextWebController.onClose(); + if (PlatformInfo.isWeb) { + richTextWebController?.onClose(); + richTextWebController = null; + } else { + richTextMobileTabletController?.onClose(); + richTextMobileTabletController = null; + } super.onClose(); } @@ -171,7 +181,7 @@ class IdentityCreatorController extends BaseController { if (identity?.signatureAsString.isNotEmpty == true) { updateContentHtmlEditor(arguments?.identity?.signatureAsString ?? ''); if (PlatformInfo.isWeb) { - richTextWebController.editorController.setText(arguments?.identity?.signatureAsString ?? ''); + richTextWebController?.editorController.setText(arguments?.identity?.signatureAsString ?? ''); } } } @@ -299,9 +309,9 @@ class IdentityCreatorController extends BaseController { Future _getSignatureHtmlText() async { if (PlatformInfo.isWeb) { - return richTextWebController.editorController.getText(); + return richTextWebController?.editorController.getText(); } else { - return keyboardRichTextController.htmlEditorApi?.getText(); + return richTextMobileTabletController?.richTextController.htmlEditorApi?.getText(); } } @@ -421,7 +431,7 @@ class IdentityCreatorController extends BaseController { void clearFocusEditor(BuildContext context) { if (PlatformInfo.isMobile) { - keyboardRichTextController.htmlEditorApi?.unfocus(); + richTextMobileTabletController?.htmlEditorApi?.unfocus(); KeyboardUtils.hideSystemKeyboardMobile(); } KeyboardUtils.hideKeyboard(context); @@ -434,15 +444,15 @@ class IdentityCreatorController extends BaseController { } void initRichTextForMobile(BuildContext context, HtmlEditorApi editorApi) { - richTextMobileTabletController.htmlEditorApi = editorApi; - keyboardRichTextController.onCreateHTMLEditor( + richTextMobileTabletController?.htmlEditorApi = editorApi; + richTextMobileTabletController?.richTextController.onCreateHTMLEditor( editorApi, onEnterKeyDown: _onEnterKeyDownOnMobile, onFocus: _onFocusHTMLEditorOnMobile, context: context ); - keyboardRichTextController.htmlEditorApi?.onFocusOut = () { - keyboardRichTextController.hideRichTextView(); + richTextMobileTabletController?.htmlEditorApi?.onFocusOut = () { + richTextMobileTabletController?.richTextController.hideRichTextView(); isMobileEditorFocus.value = false; }; } @@ -546,9 +556,9 @@ class IdentityCreatorController extends BaseController { } } else { if (PlatformInfo.isWeb) { - richTextWebController.insertImageAsBase64(platformFile: file, maxWidth: maxWidth); + richTextWebController?.insertImageAsBase64(platformFile: file, maxWidth: maxWidth); } else if (PlatformInfo.isMobile) { - richTextMobileTabletController.insertImageData(platformFile: file, maxWidth: maxWidth); + richTextMobileTabletController?.insertImageData(platformFile: file, maxWidth: maxWidth); if (file.path != null) { _deleteCompressedFileOnMobile(file.path!); } diff --git a/lib/features/identity_creator/presentation/identity_creator_view.dart b/lib/features/identity_creator/presentation/identity_creator_view.dart index e4668d184a..042d754e4f 100644 --- a/lib/features/identity_creator/presentation/identity_creator_view.dart +++ b/lib/features/identity_creator/presentation/identity_creator_view.dart @@ -147,14 +147,14 @@ class IdentityCreatorView extends GetWidget ? AppColor.colorBackgroundKeyboard : AppColor.colorBackgroundKeyboardAndroid, isLandScapeMode: controller.responsiveUtils.isLandscapeMobile(context), - richTextController: controller.keyboardRichTextController, + richTextController: controller.richTextMobileTabletController!.richTextController, titleQuickStyleBottomSheet: AppLocalizations.of(context).titleQuickStyles, titleBackgroundBottomSheet: AppLocalizations.of(context).titleBackground, titleForegroundBottomSheet: AppLocalizations.of(context).titleForeground, titleFormatBottomSheet: AppLocalizations.of(context).titleFormat, insertImage: () => controller.pickImage(context), ), - richTextController: controller.keyboardRichTextController, + richTextController: controller.richTextMobileTabletController!.richTextController, paddingChild: EdgeInsets.zero, child: responsiveWidget ); @@ -311,7 +311,7 @@ class IdentityCreatorView extends GetWidget children: [ if (PlatformInfo.isWeb) ToolbarRichTextWebBuilder( - richTextWebController: controller.richTextWebController, + richTextWebController: controller.richTextWebController!, padding: const EdgeInsets.only(bottom: 12), extendedOption: [ Padding( @@ -344,7 +344,7 @@ class IdentityCreatorView extends GetWidget Widget _buildHtmlEditorWeb(BuildContext context, String initContent) { return html_editor_browser.HtmlEditor( key: const Key('identity_create_editor_web'), - controller: controller.richTextWebController.editorController, + controller: controller.richTextWebController!.editorController, htmlEditorOptions: html_editor_browser.HtmlEditorOptions( shouldEnsureVisible: true, hint: '', @@ -361,16 +361,16 @@ class IdentityCreatorView extends GetWidget onBeforeCommand: controller.updateContentHtmlEditor, onChangeContent: controller.updateContentHtmlEditor, onInit: () { - controller.richTextWebController.editorController.setFullScreen(); + controller.richTextWebController?.editorController.setFullScreen(); controller.updateContentHtmlEditor(initContent); }, onFocus: () { FocusManager.instance.primaryFocus?.unfocus(); Future.delayed(const Duration(milliseconds: 500), () { - controller.richTextWebController.editorController.setFocus(); + controller.richTextWebController?.editorController.setFocus(); }); - controller.richTextWebController.closeAllMenuPopup(); + controller.richTextWebController?.closeAllMenuPopup(); }, - onChangeSelection: controller.richTextWebController.onEditorSettingsChange, + onChangeSelection: controller.richTextWebController?.onEditorSettingsChange, onChangeCodeview: controller.updateContentHtmlEditor ), ); From 914d903f800b16fe115548a4985cce9a9f881634 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 23 Apr 2024 23:31:57 +0700 Subject: [PATCH 65/80] Fix overflow height delete identity dialog on mobile --- .../views/dialog/confirmation_dialog_builder.dart | 6 ------ .../identities/widgets/delete_identity_dialog_builder.dart | 5 ++--- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart index b2b06456ad..f0d169c504 100644 --- a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart +++ b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart @@ -29,7 +29,6 @@ class ConfirmDialogBuilder { EdgeInsetsGeometry? _marginIcon; EdgeInsets? _margin; double? _widthDialog; - double? _heightDialog; double maxWith; Alignment? _alignment; Color? _backgroundColor; @@ -117,10 +116,6 @@ class ConfirmDialogBuilder { _widthDialog = value; } - void heightDialog(double? value) { - _heightDialog = value; - } - void alignment(Alignment? alignment) { _alignment = alignment; } @@ -168,7 +163,6 @@ class ConfirmDialogBuilder { Widget _bodyContent() { return Container( width: _widthDialog ?? 400, - height: _heightDialog, constraints: BoxConstraints(maxWidth: maxWith), decoration: const BoxDecoration( color: Colors.white, diff --git a/lib/features/manage_account/presentation/profiles/identities/widgets/delete_identity_dialog_builder.dart b/lib/features/manage_account/presentation/profiles/identities/widgets/delete_identity_dialog_builder.dart index a6e6e9fe34..a6e307f763 100644 --- a/lib/features/manage_account/presentation/profiles/identities/widgets/delete_identity_dialog_builder.dart +++ b/lib/features/manage_account/presentation/profiles/identities/widgets/delete_identity_dialog_builder.dart @@ -26,9 +26,8 @@ class DeleteIdentityDialogBuilder extends StatelessWidget { responsiveUtils: responsiveUtils, mobile: (_buildDeleteDialog(context) ..alignment(Alignment.bottomCenter) - ..outsideDialogPadding(const EdgeInsets.only(left: 0, right: 0, bottom: PlatformInfo.isWeb ? 42 : 16)) - ..widthDialog(MediaQuery.of(context).size.width - 16) - ..heightDialog(280)) + ..outsideDialogPadding(const EdgeInsets.only(bottom: PlatformInfo.isWeb ? 42 : 16)) + ..widthDialog(MediaQuery.of(context).size.width - 16)) .build(), landscapeMobile: _buildDeleteDialog(context).build(), tablet: _buildDeleteDialog(context).build(), From 5df55a03b2529c4b2804990ccb9e4f72286899fe Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 23 Apr 2024 23:53:58 +0700 Subject: [PATCH 66/80] Avoid fetching again identity lists in SingleEmailController --- .../controller/single_email_controller.dart | 26 ++++++++++++------- .../mailbox_dashboard_controller.dart | 2 ++ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index f7516f01ba..e4d843a99a 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -327,8 +327,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin { markAsEmailRead(selectedEmail, ReadActions.markAsRead, MarkReadAction.tap); } - if (_identitySelected == null) { + if (mailboxDashBoardController.listIdentities.isEmpty) { _getAllIdentities(); + } else { + _initializeSelectedIdentity(mailboxDashBoardController.listIdentities); } } @@ -418,17 +420,20 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _getAllIdentitiesSuccess(GetAllIdentitiesSuccess success) { if (success.identities?.isNotEmpty == true) { - if (currentEmail != null) { - final currentMailbox = getMailboxContain(currentEmail!); - log('SingleEmailController::_getAllIdentitiesSuccess():currentMailbox: $currentMailbox'); - if (_isBelongToTeamMailboxes(currentMailbox)) { - _setUpDefaultIdentityForTeamMailbox(success.identities!, currentMailbox!); - } else { - _setUpDefaultIdentity(success.identities!); - } + _initializeSelectedIdentity(success.identities!); + } + } + + void _initializeSelectedIdentity(List identities) { + if (currentEmail != null) { + final currentMailbox = getMailboxContain(currentEmail!); + if (_isBelongToTeamMailboxes(currentMailbox)) { + _setUpDefaultIdentityForTeamMailbox(identities, currentMailbox!); } else { - _setUpDefaultIdentity(success.identities!); + _setUpDefaultIdentity(identities); } + } else { + _setUpDefaultIdentity(identities); } } @@ -621,6 +626,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { blobCalendarEvent.value = null; emailUnsubscribe.value = null; _printEmailAction = null; + _identitySelected = null; if (isEmailClosing) { emailLoadedViewState.value = Right(UIState.idle); viewState.value = Right(UIState.idle); diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 1245377259..20af0cc918 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -2523,6 +2523,8 @@ class MailboxDashBoardController extends ReloadableController { log('MailboxDashBoardController::_handleGetAllIdentitiesSuccess: IDENTITIES_SIZE = ${_identities?.length}'); } + List get listIdentities => _identities ?? []; + @override void onClose() { _emailReceiveManager.closeEmailReceiveManagerStream(); From a8f566d762e6f5af4d934a8b5c7d842a90005f3b Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 24 Apr 2024 08:31:14 +0700 Subject: [PATCH 67/80] Fix part of the content is lost when replying or forwarding emails --- lib/features/composer/presentation/composer_controller.dart | 4 ++++ pubspec.lock | 6 +++--- pubspec.yaml | 5 +++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 2177ed5024..df60f50194 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1563,6 +1563,10 @@ class ComposerController extends BaseController with DragDropFileMixin { if (newIdentity.signatureAsString.isNotEmpty == true) { await _applySignature(newIdentity.signatureAsString.asSignatureHtml()); } + + if (PlatformInfo.isMobile) { + await htmlEditorApi?.onDocumentChanged(); + } } void _applyBccEmailAddressFromIdentity(Set listEmailAddress) { diff --git a/pubspec.lock b/pubspec.lock index bce43d73a0..6ab8f5d3bf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -425,11 +425,11 @@ packages: source: path version: "1.0.0+1" enough_html_editor: - dependency: transitive + dependency: "direct overridden" description: path: "." - ref: cnb_supported - resolved-ref: "69996e32a708a62b8a613f2524c5635f5c556617" + ref: "bugfix/fix-lost-part-of-content-with-long-text" + resolved-ref: "273f3817c1f93a5fbdebbc09580a303338237f21" url: "https://github.com/linagora/enough_html_editor.git" source: git version: "0.0.5" diff --git a/pubspec.yaml b/pubspec.yaml index 3b82ed9479..348146e915 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -274,6 +274,11 @@ dependency_overrides: url: https://github.com/linagora/flutter_file_picker ref: email_supported_5.3.1 + enough_html_editor: + git: + url: https://github.com/linagora/enough_html_editor.git + ref: bugfix/fix-lost-part-of-content-with-long-text + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From 56ab1f20c139202a61b2bc3b369d69672e496678 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 25 Apr 2024 00:27:52 +0700 Subject: [PATCH 68/80] Fix tThe editor background appears gray when the waning dialog appears --- .../presentation/action/email_ui_action.dart | 6 ++++- .../controller/single_email_controller.dart | 7 +++++ .../email/presentation/email_view.dart | 24 +++++++++++++++++ .../mailbox_dashboard_controller.dart | 27 ++++++++++++++++++- lib/main/pages/app_pages.dart | 1 - 5 files changed, 62 insertions(+), 3 deletions(-) diff --git a/lib/features/email/presentation/action/email_ui_action.dart b/lib/features/email/presentation/action/email_ui_action.dart index e7a5445902..3a276779ba 100644 --- a/lib/features/email/presentation/action/email_ui_action.dart +++ b/lib/features/email/presentation/action/email_ui_action.dart @@ -40,4 +40,8 @@ class PrintEmailAction extends EmailUIAction { @override List get props => [context, userEmail, email]; -} \ No newline at end of file +} + +class HideEmailContentViewAction extends EmailUIAction {} + +class ShowEmailContentViewAction extends EmailUIAction {} \ No newline at end of file diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index e4d843a99a..5911c02d5c 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -144,6 +144,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final emailLoadedViewState = Rx>(Right(UIState.idle)); final emailUnsubscribe = Rxn(); final attachmentsViewState = RxMap>(); + final isEmailContentHidden = RxBool(false); EmailId? _currentEmailId; Identity? _identitySelected; @@ -289,6 +290,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } else if (action is CloseEmailDetailedViewAction) { closeEmailView(context: currentContext); mailboxDashBoardController.clearEmailUIAction(); + } else if (action is HideEmailContentViewAction) { + isEmailContentHidden.value = true; + mailboxDashBoardController.clearEmailUIAction(); + } else if (action is ShowEmailContentViewAction) { + isEmailContentHidden.value = false; + mailboxDashBoardController.clearEmailUIAction(); } }); diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 092853e036..49d80ba001 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -427,6 +427,30 @@ class EmailView extends GetWidget { }), ), ); + } else if (PlatformInfo.isIOS + && !controller.responsiveUtils.isScreenWithShortestSide(context)) { + return Obx(() { + if (controller.isEmailContentHidden.isTrue) { + return const SizedBox.shrink(); + } else { + return Padding( + padding: const EdgeInsetsDirectional.symmetric( + vertical: EmailViewStyles.mobileContentVerticalMargin, + horizontal: EmailViewStyles.mobileContentHorizontalMargin + ), + child: LayoutBuilder(builder: (context, constraints) { + return HtmlContentViewer( + contentHtml: allEmailContents, + initialWidth: constraints.maxWidth, + direction: AppUtils.getCurrentDirection(context), + onMailtoDelegateAction: controller.openMailToLink, + onScrollHorizontalEnd: controller.toggleScrollPhysicsPagerView, + onLoadWidthHtmlViewer: controller.emailSupervisorController.updateScrollPhysicPageView, + ); + }) + ); + } + }); } else { return Padding( padding: const EdgeInsetsDirectional.symmetric( diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 20af0cc918..e6002a7af4 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -38,6 +38,7 @@ import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_bindings.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/compose_action_mode.dart'; @@ -1411,7 +1412,31 @@ class MailboxDashBoardController extends ReloadableController { } else { BackButtonInterceptor.removeByName(AppRoutes.dashboard); - final result = await push(AppRoutes.composer, arguments: argumentsWithIdentity); + bool isTabletPlatform = currentContext != null + && !responsiveUtils.isScreenWithShortestSide(currentContext!); + dynamic result; + + if (isTabletPlatform) { + if (PlatformInfo.isIOS) { + dispatchEmailUIAction(HideEmailContentViewAction()); + } + + result = await Get.to( + () => const ComposerView(), + binding: ComposerBindings(), + opaque: false, + arguments: argumentsWithIdentity); + + if (PlatformInfo.isIOS) { + await Future.delayed( + const Duration(milliseconds: 200), + () => dispatchEmailUIAction(ShowEmailContentViewAction())); + } + } else { + result = await push( + AppRoutes.composer, + arguments: argumentsWithIdentity); + } BackButtonInterceptor.add(_onBackButtonInterceptor, name: AppRoutes.dashboard); diff --git a/lib/main/pages/app_pages.dart b/lib/main/pages/app_pages.dart index e13239bc97..83e23838f0 100644 --- a/lib/main/pages/app_pages.dart +++ b/lib/main/pages/app_pages.dart @@ -85,7 +85,6 @@ class AppPages { ...[ GetPage( name: AppRoutes.composer, - opaque: false, page: () => DeferredWidget( composer.loadLibrary, () => composer.ComposerView()), From 44a1dddb94417d26a68e117910674e6885c69be2 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 25 Apr 2024 00:36:20 +0700 Subject: [PATCH 69/80] Remove functions that are not called on some platforms --- lib/features/composer/presentation/composer_controller.dart | 5 ----- .../controller/mailbox_dashboard_controller.dart | 4 ---- 2 files changed, 9 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index df60f50194..ac60bb50f6 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1340,11 +1340,6 @@ class ComposerController extends BaseController with DragDropFileMixin { _autoFocusFieldWhenLauncher); } - void deleteComposer() { - FocusManager.instance.primaryFocus?.unfocus(); - mailboxDashBoardController.closeComposerOverlay(); - } - void addEmailAddressType(PrefixEmailAddress prefixEmailAddress) { switch(prefixEmailAddress) { case PrefixEmailAddress.from: diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index e6002a7af4..6cbc94eb79 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -1276,8 +1276,6 @@ class MailboxDashBoardController extends ReloadableController { result is SaveEmailAsDraftsSuccess || result is UpdateEmailDraftsSuccess) { consumeState(Stream.value(Right(result))); - } else if (validateSendingEmailFailedWhenNetworkIsLostOnMobile(result)) { - _storeSendingEmailInCaseOfSendingFailureInMobile(result); } await _removeComposerCacheOnWeb(); @@ -1449,8 +1447,6 @@ class MailboxDashBoardController extends ReloadableController { } else if (validateSendingEmailFailedWhenNetworkIsLostOnMobile(result)) { _storeSendingEmailInCaseOfSendingFailureInMobile(result); } - - await _removeComposerCacheOnWeb(); } } From 86952a45c7bd8eed04208f71be42ebc953ffe836 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 25 Apr 2024 01:18:45 +0700 Subject: [PATCH 70/80] Fix composer controller not deleted from GetX when click `Discard Changes` button --- .../mixin/message_dialog_action_mixin.dart | 33 +++++++--- .../presentation/composer_controller.dart | 64 +++++++++---------- lib/main/routes/route_navigation.dart | 4 +- 3 files changed, 59 insertions(+), 42 deletions(-) diff --git a/lib/features/base/mixin/message_dialog_action_mixin.dart b/lib/features/base/mixin/message_dialog_action_mixin.dart index 9bada57620..dd72e48567 100644 --- a/lib/features/base/mixin/message_dialog_action_mixin.dart +++ b/lib/features/base/mixin/message_dialog_action_mixin.dart @@ -22,6 +22,7 @@ mixin MessageDialogActionMixin { bool showAsBottomSheet = false, bool alignCenter = false, bool outsideDismissible = true, + bool autoPerformPopBack = true, List? listTextSpan, Widget? icon, TextStyle? titleStyle, @@ -59,13 +60,17 @@ mixin MessageDialogActionMixin { ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) ..onConfirmButtonAction(actionName, () { - popBack(); + if (autoPerformPopBack) { + popBack(); + } onConfirmAction?.call(); }) ..onCancelButtonAction( hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', () { - popBack(); + if (autoPerformPopBack) { + popBack(); + } onCancelAction?.call(); } ) @@ -106,13 +111,17 @@ mixin MessageDialogActionMixin { ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) ..onConfirmButtonAction(actionName, () { - popBack(); + if (autoPerformPopBack) { + popBack(); + } onConfirmAction?.call(); }) ..onCancelButtonAction( hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', () { - popBack(); + if (autoPerformPopBack) { + popBack(); + } onCancelAction?.call(); } ) @@ -136,12 +145,16 @@ mixin MessageDialogActionMixin { ..onCancelAction( cancelTitle ?? AppLocalizations.of(context).cancel, () { - popBack(); + if (autoPerformPopBack) { + popBack(); + } onCancelAction?.call(); } ) ..onConfirmAction(actionName, () { - popBack(); + if (autoPerformPopBack) { + popBack(); + } onConfirmAction?.call(); })).show(); } @@ -167,13 +180,17 @@ mixin MessageDialogActionMixin { ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) ..onConfirmButtonAction(actionName, () { - popBack(); + if (autoPerformPopBack) { + popBack(); + } onConfirmAction?.call(); }) ..onCancelButtonAction( hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', () { - popBack(); + if (autoPerformPopBack) { + popBack(); + } onCancelAction?.call(); } ) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index ac60bb50f6..5925970e65 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -770,6 +770,7 @@ class ComposerController extends BaseController with DragDropFileMixin { AppLocalizations.of(context).message_dialog_send_email_without_a_subject, AppLocalizations.of(context).send_anyway, onConfirmAction: () => _handleSendMessages(context), + autoPerformPopBack: false, title: AppLocalizations.of(context).empty_subject, showAsBottomSheet: true, icon: SvgPicture.asset(imagePaths.icEmpty, fit: BoxFit.fill), @@ -824,10 +825,12 @@ class ComposerController extends BaseController with DragDropFileMixin { ) { log('ComposerController::_handleSendMessages: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); _sendButtonState = ButtonState.enabled; - _closeComposerAction(); + _closeComposerAction(closeOverlays: true); return; } + popBack(); + final emailContent = await _getContentInEditor(); final cancelToken = CancelToken(); final resultState = await _showSendingMessageDialog( @@ -841,7 +844,7 @@ class ComposerController extends BaseController with DragDropFileMixin { } else if (resultState is SendEmailFailure && resultState.exception is SendingEmailCanceledException) { _sendButtonState = ButtonState.enabled; } else if ((resultState is SendEmailFailure || resultState is GenerateEmailFailure) && context.mounted) { - _showConfirmDialogWhenSendMessageFailure( + await _showConfirmDialogWhenSendMessageFailure( context: context, failure: resultState ); @@ -900,27 +903,27 @@ class ComposerController extends BaseController with DragDropFileMixin { cancelToken?.cancel([SendingEmailCanceledException()]); } - void _showConfirmDialogWhenSendMessageFailure({ + Future _showConfirmDialogWhenSendMessageFailure({ required BuildContext context, required FeatureFailure failure - }) { - showConfirmDialogAction( + }) async { + await showConfirmDialogAction( context, title: '', AppLocalizations.of(context).warningMessageWhenSendEmailFailure, AppLocalizations.of(context).edit, cancelTitle: AppLocalizations.of(context).closeAnyway, alignCenter: true, + outsideDismissible: false, + autoPerformPopBack: false, onConfirmAction: () { _sendButtonState = ButtonState.enabled; + popBack(); _autoFocusFieldWhenLauncher(); }, - onCancelAction: () async { + onCancelAction: () { _sendButtonState = ButtonState.enabled; - await Future.delayed( - const Duration(milliseconds: 100), - _closeComposerAction - ); + _closeComposerAction(closeOverlays: true); }, icon: SvgPicture.asset( imagePaths.icQuotasWarning, @@ -1187,14 +1190,12 @@ class ComposerController extends BaseController with DragDropFileMixin { failure: resultState, onConfirmAction: () { _saveToDraftButtonState = ButtonState.enabled; + popBack(); _autoFocusFieldWhenLauncher(); }, - onCancelAction: () async { + onCancelAction: () { _saveToDraftButtonState = ButtonState.enabled; - await Future.delayed( - const Duration(milliseconds: 100), - _closeComposerAction - ); + _closeComposerAction(closeOverlays: true); } ); } else { @@ -1320,11 +1321,14 @@ class ComposerController extends BaseController with DragDropFileMixin { FocusScope.of(context).unfocus(); } - void _closeComposerAction({dynamic result}) { + void _closeComposerAction({dynamic result, bool closeOverlays = false}) { if (PlatformInfo.isWeb) { + if (closeOverlays) { + popBack(); + } mailboxDashBoardController.closeComposerOverlay(result: result); } else { - popBack(result: result); + popBack(result: result, closeOverlays: closeOverlays); } } @@ -1916,16 +1920,11 @@ class ComposerController extends BaseController with DragDropFileMixin { cancelTitle: AppLocalizations.of(context).discardChanges, alignCenter: true, outsideDismissible: false, - onConfirmAction: () async => await Future.delayed( - const Duration(milliseconds: 100), - () => _handleSaveMessageToDraft(context) - ), - onCancelAction: () async { + autoPerformPopBack: false, + onConfirmAction: () => _handleSaveMessageToDraft(context), + onCancelAction: () { _closeComposerButtonState = ButtonState.enabled; - await Future.delayed( - const Duration(milliseconds: 100), - _closeComposerAction - ); + _closeComposerAction(closeOverlays: true); }, onCloseButtonAction: () { _closeComposerButtonState = ButtonState.enabled; @@ -2010,10 +2009,12 @@ class ComposerController extends BaseController with DragDropFileMixin { ) { log('ComposerController::_handleSaveMessageToDraft: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); _closeComposerButtonState = ButtonState.enabled; - _closeComposerAction(); + _closeComposerAction(closeOverlays: true); return; } + popBack(); + final emailContent = await _getContentInEditor(); final draftEmailId = _getDraftEmailId(); log('ComposerController::_handleSaveMessageToDraft: draftEmailId = $draftEmailId'); @@ -2118,16 +2119,15 @@ class ComposerController extends BaseController with DragDropFileMixin { cancelTitle: AppLocalizations.of(context).closeAnyway, alignCenter: true, outsideDismissible: false, + autoPerformPopBack: false, onConfirmAction: onConfirmAction ?? () { _closeComposerButtonState = ButtonState.enabled; + popBack(); _autoFocusFieldWhenLauncher(); }, - onCancelAction: onCancelAction ?? () async { + onCancelAction: onCancelAction ?? () { _closeComposerButtonState = ButtonState.enabled; - await Future.delayed( - const Duration(milliseconds: 100), - _closeComposerAction - ); + _closeComposerAction(closeOverlays: true); }, icon: SvgPicture.asset( imagePaths.icQuotasWarning, diff --git a/lib/main/routes/route_navigation.dart b/lib/main/routes/route_navigation.dart index 3130fd4525..cfd6cbca53 100644 --- a/lib/main/routes/route_navigation.dart +++ b/lib/main/routes/route_navigation.dart @@ -18,8 +18,8 @@ Future pushAndPopAll(String routeName, {dynamic arguments}) async { return Get.offAllNamed(routeName, arguments: arguments); } -void popBack({dynamic result}) { - Get.back(result: result); +void popBack({dynamic result, bool closeOverlays = false}) { + Get.back(closeOverlays: closeOverlays, result: result); } bool canBack(BuildContext context) { From 4ea80eef8a6624676bcf6f1674229cca8bf99052 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 25 Apr 2024 02:21:42 +0700 Subject: [PATCH 71/80] Remove duplicated KeyboardVisibilityBuilder --- lib/features/composer/presentation/composer_view.dart | 3 ++- .../styles/mobile/mobile_container_view_style.dart | 2 -- .../styles/mobile/tablet_container_view_style.dart | 2 -- .../presentation/view/mobile/mobile_container_view.dart | 6 +----- .../presentation/view/mobile/tablet_container_view.dart | 6 +----- 5 files changed, 4 insertions(+), 15 deletions(-) diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index 0da0604c82..18743df6e7 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -217,7 +217,7 @@ class ComposerView extends GetWidget { onLoadCompletedEditorAction: controller.onLoadCompletedMobileEditorAction, ), )), - SizedBox(height: MediaQuery.viewInsetsOf(context).bottom), + SizedBox(height: MediaQuery.viewInsetsOf(context).bottom + 64), ], ), ), @@ -369,6 +369,7 @@ class ComposerView extends GetWidget { onLoadCompletedEditorAction: controller.onLoadCompletedMobileEditorAction, ), )), + SizedBox(height: MediaQuery.viewInsetsOf(context).bottom + 64), ], ), ) diff --git a/lib/features/composer/presentation/styles/mobile/mobile_container_view_style.dart b/lib/features/composer/presentation/styles/mobile/mobile_container_view_style.dart index 7012513aee..39bc5164a6 100644 --- a/lib/features/composer/presentation/styles/mobile/mobile_container_view_style.dart +++ b/lib/features/composer/presentation/styles/mobile/mobile_container_view_style.dart @@ -8,6 +8,4 @@ class MobileContainerViewStyle { static final Color keyboardToolbarBackgroundColor = PlatformInfo.isIOS ? AppColor.colorBackgroundKeyboard : AppColor.colorBackgroundKeyboardAndroid; - - static const EdgeInsets keyboardToolbarPadding = EdgeInsets.only(bottom: 64); } \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/mobile/tablet_container_view_style.dart b/lib/features/composer/presentation/styles/mobile/tablet_container_view_style.dart index 19c480ab8b..a6fac6b200 100644 --- a/lib/features/composer/presentation/styles/mobile/tablet_container_view_style.dart +++ b/lib/features/composer/presentation/styles/mobile/tablet_container_view_style.dart @@ -14,8 +14,6 @@ class TabletContainerViewStyle { ? AppColor.colorBackgroundKeyboard : AppColor.colorBackgroundKeyboardAndroid; - static const EdgeInsets keyboardToolbarPadding = EdgeInsets.only(bottom: 64); - static EdgeInsetsGeometry getMargin( BuildContext context, ResponsiveUtils responsiveUtils diff --git a/lib/features/composer/presentation/view/mobile/mobile_container_view.dart b/lib/features/composer/presentation/view/mobile/mobile_container_view.dart index 2c6c23f27d..fe0517fa17 100644 --- a/lib/features/composer/presentation/view/mobile/mobile_container_view.dart +++ b/lib/features/composer/presentation/view/mobile/mobile_container_view.dart @@ -49,7 +49,6 @@ class MobileContainerView extends StatelessWidget { backgroundColor: backgroundColor ?? MobileContainerViewStyle.outSideBackgroundColor, resizeToAvoidBottomInset: false, body: LayoutBuilder(builder: (context, constraints) { - return KeyboardVisibilityBuilder(builder: (context, isKeyboardVisible) { return rich_composer.KeyboardRichText( richTextController: keyboardRichTextController, keyBroadToolbar: RichTextKeyboardToolBar( @@ -66,12 +65,9 @@ class MobileContainerView extends StatelessWidget { titleFormatBottomSheet: AppLocalizations.of(context).titleFormat, titleBack: AppLocalizations.of(context).format, ), - paddingChild: isKeyboardVisible - ? MobileContainerViewStyle.keyboardToolbarPadding - : EdgeInsets.zero, + paddingChild: EdgeInsets.zero, child: childBuilder(context, constraints), ); - }); }) ), ) diff --git a/lib/features/composer/presentation/view/mobile/tablet_container_view.dart b/lib/features/composer/presentation/view/mobile/tablet_container_view.dart index 2274ab08e1..7fdb869f2a 100644 --- a/lib/features/composer/presentation/view/mobile/tablet_container_view.dart +++ b/lib/features/composer/presentation/view/mobile/tablet_container_view.dart @@ -45,7 +45,6 @@ class TabletContainerView extends StatelessWidget { child: Scaffold( backgroundColor: TabletContainerViewStyle.outSideBackgroundColor, body: LayoutBuilder(builder: (context, constraints) { - return KeyboardVisibilityBuilder(builder: (context, isKeyboardVisible) { return KeyboardRichText( richTextController: keyboardRichTextController, keyBroadToolbar: RichTextKeyboardToolBar( @@ -62,9 +61,7 @@ class TabletContainerView extends StatelessWidget { titleFormatBottomSheet: AppLocalizations.of(context).titleFormat, titleBack: AppLocalizations.of(context).format, ), - paddingChild: isKeyboardVisible - ? TabletContainerViewStyle.keyboardToolbarPadding - : EdgeInsets.zero, + paddingChild: EdgeInsets.zero, child: Center( child: Card( elevation: TabletContainerViewStyle.elevation, @@ -81,7 +78,6 @@ class TabletContainerView extends StatelessWidget { ), ), ); - }); }) ), ) From edc5c7268e274626d9aa510d8b793653f4ceaabc Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 25 Apr 2024 02:22:48 +0700 Subject: [PATCH 72/80] Use `MediaQuery.sizeOf` to avoid rebuilding multiple times widget --- .../presentation/utils/responsive_utils.dart | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/core/lib/presentation/utils/responsive_utils.dart b/core/lib/presentation/utils/responsive_utils.dart index e19414b05f..6b989d9b68 100644 --- a/core/lib/presentation/utils/responsive_utils.dart +++ b/core/lib/presentation/utils/responsive_utils.dart @@ -19,15 +19,15 @@ class ResponsiveUtils { static const double desktopVerticalMargin = 120.0; static const double desktopHorizontalMargin = 200.0; - bool isScreenWithShortestSide(BuildContext context) => context.mediaQueryShortestSide < minTabletWidth; + bool isScreenWithShortestSide(BuildContext context) => getSizeScreenShortestSide(context) < minTabletWidth; - double getSizeScreenWidth(BuildContext context) => context.width; + double getSizeScreenWidth(BuildContext context) => MediaQuery.sizeOf(context).width; - double getSizeScreenHeight(BuildContext context) => context.height; + double getSizeScreenHeight(BuildContext context) => MediaQuery.sizeOf(context).height; - double getSizeScreenShortestSide(BuildContext context) => context.mediaQueryShortestSide; + double getSizeScreenShortestSide(BuildContext context) => MediaQuery.sizeOf(context).shortestSide; - double getDeviceWidth(BuildContext context) => context.width; + double getDeviceWidth(BuildContext context) => MediaQuery.sizeOf(context).width; bool isMobile(BuildContext context) => getDeviceWidth(context) < minTabletWidth; @@ -46,21 +46,21 @@ class ResponsiveUtils { bool isLandscapeMobile(BuildContext context) => isScreenWithShortestSide(context) && isLandscape(context); bool isLandscapeTablet(BuildContext context) { - return context.mediaQueryShortestSide >= minTabletWidth && - context.mediaQueryShortestSide < minDesktopWidth && + return getSizeScreenShortestSide(context) >= minTabletWidth && + getSizeScreenShortestSide(context) < minDesktopWidth && isLandscape(context); } bool isPortraitMobile(BuildContext context) => isScreenWithShortestSide(context) && isPortrait(context); bool isPortraitTablet(BuildContext context) { - return context.mediaQueryShortestSide >= minTabletWidth && - context.mediaQueryShortestSide < minDesktopWidth && + return getSizeScreenShortestSide(context) >= minTabletWidth && + getSizeScreenShortestSide(context) < minDesktopWidth && isPortrait(context); } bool isHeightShortest(BuildContext context) { - return MediaQuery.of(context).size.shortestSide < heightShortest; + return getSizeScreenShortestSide(context) < heightShortest; } double getMaxWidthToast(BuildContext context) { From 2c509eecf98d1e0df668e3422b9805bdc184d77f Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 25 Apr 2024 09:24:51 +0700 Subject: [PATCH 73/80] Remove import unnecessary --- .../composer/presentation/view/mobile/mobile_container_view.dart | 1 - .../composer/presentation/view/mobile/tablet_container_view.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/features/composer/presentation/view/mobile/mobile_container_view.dart b/lib/features/composer/presentation/view/mobile/mobile_container_view.dart index fe0517fa17..de1ffcd81c 100644 --- a/lib/features/composer/presentation/view/mobile/mobile_container_view.dart +++ b/lib/features/composer/presentation/view/mobile/mobile_container_view.dart @@ -1,7 +1,6 @@ import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/utils/app_logger.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:get/get.dart'; import 'package:rich_text_composer/rich_text_composer.dart' as rich_composer; import 'package:rich_text_composer/views/widgets/rich_text_keyboard_toolbar.dart'; diff --git a/lib/features/composer/presentation/view/mobile/tablet_container_view.dart b/lib/features/composer/presentation/view/mobile/tablet_container_view.dart index 7fdb869f2a..9af4547939 100644 --- a/lib/features/composer/presentation/view/mobile/tablet_container_view.dart +++ b/lib/features/composer/presentation/view/mobile/tablet_container_view.dart @@ -1,7 +1,6 @@ import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/utils/app_logger.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:get/get.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:rich_text_composer/views/widgets/rich_text_keyboard_toolbar.dart'; From 24d643d2ea9a34a8b3040d4f94dd1c6f7646d296 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 26 Apr 2024 16:55:22 +0700 Subject: [PATCH 74/80] Fix difficulty scrolling email content on Android --- .../views/html_viewer/html_content_viewer_widget.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart b/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart index 3b0fdacea8..964a841495 100644 --- a/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart +++ b/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart @@ -65,12 +65,12 @@ class _HtmlContentViewState extends State { super.initState(); if (PlatformInfo.isAndroid) { _gestureRecognizers = { - Factory(() => LongPressGestureRecognizer(duration: _longPressGestureDuration)), + Factory(() => LongPressGestureRecognizer()), Factory(() => ScaleGestureRecognizer()), }; } else { _gestureRecognizers = { - Factory(() => LongPressGestureRecognizer(duration: _longPressGestureDuration)), + Factory(() => LongPressGestureRecognizer(duration: _longPressGestureDurationIOS)), }; } if (PlatformInfo.isAndroid) { @@ -211,7 +211,7 @@ class _HtmlContentViewState extends State { if (!isScrollActivated && PlatformInfo.isIOS) { newGestureRecognizers = { - Factory(() => LongPressGestureRecognizer(duration: _longPressGestureDuration)), + Factory(() => LongPressGestureRecognizer(duration: _longPressGestureDurationIOS)), Factory(() => HorizontalDragGestureRecognizer()) }; } @@ -272,7 +272,7 @@ class _HtmlContentViewState extends State { return NavigationActionPolicy.CANCEL; } - Duration? get _longPressGestureDuration => const Duration(milliseconds: 100); + Duration? get _longPressGestureDurationIOS => const Duration(milliseconds: 100); @override void dispose() { From cd799b99cf7005349b2d97205e458e6cdd18cf2a Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 26 Apr 2024 17:09:22 +0700 Subject: [PATCH 75/80] Avoid close composer when cancel sending or saving draft --- lib/features/composer/presentation/composer_controller.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 5925970e65..c4ced79f18 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -829,7 +829,9 @@ class ComposerController extends BaseController with DragDropFileMixin { return; } - popBack(); + if (Get.isDialogOpen == true) { + popBack(); + } final emailContent = await _getContentInEditor(); final cancelToken = CancelToken(); @@ -2031,7 +2033,6 @@ class ComposerController extends BaseController with DragDropFileMixin { } else if ((resultState is SaveEmailAsDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException) || (resultState is UpdateEmailDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException)) { _closeComposerButtonState = ButtonState.enabled; - _closeComposerAction(); } else if ((resultState is SaveEmailAsDraftsFailure || resultState is UpdateEmailDraftsFailure || resultState is GenerateEmailFailure) && From 6e3ab42792afb3139237b2321b2380edab4d1fc7 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 2 May 2024 02:33:13 +0700 Subject: [PATCH 76/80] Fix can not show saving dialog anymore when click on back system button --- .../mixin/message_dialog_action_mixin.dart | 209 +++++++++--------- .../presentation/composer_controller.dart | 9 + 2 files changed, 119 insertions(+), 99 deletions(-) diff --git a/lib/features/base/mixin/message_dialog_action_mixin.dart b/lib/features/base/mixin/message_dialog_action_mixin.dart index dd72e48567..df503438e4 100644 --- a/lib/features/base/mixin/message_dialog_action_mixin.dart +++ b/lib/features/base/mixin/message_dialog_action_mixin.dart @@ -23,6 +23,7 @@ mixin MessageDialogActionMixin { bool alignCenter = false, bool outsideDismissible = true, bool autoPerformPopBack = true, + bool usePopScope = false, List? listTextSpan, Widget? icon, TextStyle? titleStyle, @@ -32,102 +33,109 @@ mixin MessageDialogActionMixin { Color? actionButtonColor, Color? cancelButtonColor, EdgeInsetsGeometry? marginIcon, + PopInvokedCallback? onPopInvoked, } ) async { final responsiveUtils = Get.find(); final imagePaths = Get.find(); if (alignCenter) { + final childWidget = PointerInterceptor( + child: (ConfirmDialogBuilder(imagePaths, listTextSpan: listTextSpan) + ..key(const Key('confirm_dialog_action')) + ..title(title ?? '') + ..content(message) + ..addIcon(icon) + ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) + ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) + ..marginIcon(icon != null ? (marginIcon ?? const EdgeInsets.only(top: 24)) : null) + ..paddingTitle(icon != null + ? const EdgeInsetsDirectional.only(top: 24, start: 24, end: 24) + : const EdgeInsetsDirectional.symmetric(horizontal: 24) + ) + ..radiusButton(12) + ..paddingContent(const EdgeInsets.only(left: 24, right: 24, bottom: 24, top: 12)) + ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 24, left: 24, right: 24)) + ..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)) + ..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) + ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) + ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) + ..onConfirmButtonAction(actionName, () { + if (autoPerformPopBack) { + popBack(); + } + onConfirmAction?.call(); + }) + ..onCancelButtonAction( + hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', + () { + if (autoPerformPopBack) { + popBack(); + } + onCancelAction?.call(); + } + ) + ..onCloseButtonAction(onCloseButtonAction) + ).build() + ); return await Get.dialog( - PointerInterceptor( - child: (ConfirmDialogBuilder(imagePaths, listTextSpan: listTextSpan) + usePopScope && PlatformInfo.isMobile + ? PopScope(onPopInvoked: onPopInvoked, canPop: false, child: childWidget) + : childWidget, + barrierColor: AppColor.colorDefaultCupertinoActionSheet, + barrierDismissible: outsideDismissible + ); + } else { + if (responsiveUtils.isMobile(context)) { + final childWidget = PointerInterceptor( + child: (ConfirmDialogBuilder( + imagePaths, + showAsBottomSheet: true, + listTextSpan: listTextSpan, + maxWith: responsiveUtils.getSizeScreenShortestSide(context) - 16 + ) ..key(const Key('confirm_dialog_action')) ..title(title ?? '') ..content(message) ..addIcon(icon) + ..margin(const EdgeInsets.only(bottom: 42)) + ..widthDialog(responsiveUtils.getSizeScreenWidth(context)) ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) - ..marginIcon(icon != null ? (marginIcon ?? const EdgeInsets.only(top: 24)) : null) ..paddingTitle(icon != null ? const EdgeInsetsDirectional.only(top: 24, start: 24, end: 24) : const EdgeInsetsDirectional.symmetric(horizontal: 24) ) - ..radiusButton(12) - ..paddingContent(const EdgeInsets.only(left: 24, right: 24, bottom: 24, top: 12)) - ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 24, left: 24, right: 24)) + ..marginIcon(EdgeInsets.zero) + ..paddingContent(const EdgeInsets.only(left: 44, right: 44, bottom: 24, top: 12)) + ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44)) ..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)) ..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) ..onConfirmButtonAction(actionName, () { - if (autoPerformPopBack) { - popBack(); - } - onConfirmAction?.call(); + if (autoPerformPopBack) { + popBack(); + } + onConfirmAction?.call(); }) ..onCancelButtonAction( hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', - () { + () { if (autoPerformPopBack) { popBack(); } onCancelAction?.call(); } ) - ..onCloseButtonAction(onCloseButtonAction) + ..onCloseButtonAction(onCloseButtonAction ?? () => popBack()) ).build() - ), - barrierColor: AppColor.colorDefaultCupertinoActionSheet, - barrierDismissible: outsideDismissible - ); - } else { - if (responsiveUtils.isMobile(context)) { + ); if (showAsBottomSheet) { return await Get.bottomSheet( - PointerInterceptor( - child: (ConfirmDialogBuilder( - imagePaths, - showAsBottomSheet: true, - listTextSpan: listTextSpan, - maxWith: responsiveUtils.getSizeScreenShortestSide(context) - 16 - ) - ..key(const Key('confirm_dialog_action')) - ..title(title ?? '') - ..content(message) - ..addIcon(icon) - ..margin(const EdgeInsets.only(bottom: 42)) - ..widthDialog(responsiveUtils.getSizeScreenWidth(context)) - ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) - ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) - ..paddingTitle(icon != null - ? const EdgeInsetsDirectional.only(top: 24, start: 24, end: 24) - : const EdgeInsetsDirectional.symmetric(horizontal: 24) - ) - ..marginIcon(EdgeInsets.zero) - ..paddingContent(const EdgeInsets.only(left: 44, right: 44, bottom: 24, top: 12)) - ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44)) - ..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)) - ..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) - ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) - ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) - ..onConfirmButtonAction(actionName, () { - if (autoPerformPopBack) { - popBack(); - } - onConfirmAction?.call(); - }) - ..onCancelButtonAction( - hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', - () { - if (autoPerformPopBack) { - popBack(); - } - onCancelAction?.call(); - } - ) - ..onCloseButtonAction(onCloseButtonAction ?? () => popBack())) - .build() - ), + usePopScope && PlatformInfo.isMobile + ? PopScope(onPopInvoked: onPopInvoked, canPop: false, child: childWidget) + : childWidget, isScrollControlled: true, barrierColor: AppColor.colorDefaultCupertinoActionSheet, backgroundColor: Colors.transparent, @@ -159,44 +167,47 @@ mixin MessageDialogActionMixin { })).show(); } } else { - return await Get.dialog( - PointerInterceptor( - child: (ConfirmDialogBuilder(imagePaths, listTextSpan: listTextSpan) - ..key(const Key('confirm_dialog_action')) - ..title(title ?? '') - ..content(message) - ..addIcon(icon) - ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) - ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) - ..marginIcon(icon != null ? const EdgeInsets.only(top: 24) : null) - ..paddingTitle(icon != null - ? const EdgeInsetsDirectional.only(top: 24, start: 24, end: 24) - : const EdgeInsetsDirectional.symmetric(horizontal: 24)) - ..marginIcon(EdgeInsets.zero) - ..paddingContent(const EdgeInsets.only(left: 44, right: 44, bottom: 24, top: 12)) - ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44)) - ..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)) - ..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) - ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) - ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) - ..onConfirmButtonAction(actionName, () { - if (autoPerformPopBack) { - popBack(); - } - onConfirmAction?.call(); - }) - ..onCancelButtonAction( - hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', - () { - if (autoPerformPopBack) { - popBack(); - } - onCancelAction?.call(); + final childWidget = PointerInterceptor( + child: (ConfirmDialogBuilder(imagePaths, listTextSpan: listTextSpan) + ..key(const Key('confirm_dialog_action')) + ..title(title ?? '') + ..content(message) + ..addIcon(icon) + ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) + ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) + ..marginIcon(icon != null ? const EdgeInsets.only(top: 24) : null) + ..paddingTitle(icon != null + ? const EdgeInsetsDirectional.only(top: 24, start: 24, end: 24) + : const EdgeInsetsDirectional.symmetric(horizontal: 24)) + ..marginIcon(EdgeInsets.zero) + ..paddingContent(const EdgeInsets.only(left: 44, right: 44, bottom: 24, top: 12)) + ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44)) + ..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)) + ..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) + ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) + ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) + ..onConfirmButtonAction(actionName, () { + if (autoPerformPopBack) { + popBack(); + } + onConfirmAction?.call(); + }) + ..onCancelButtonAction( + hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', + () { + if (autoPerformPopBack) { + popBack(); } - ) - ..onCloseButtonAction(onCloseButtonAction ?? () => popBack())) - .build() - ), + onCancelAction?.call(); + } + ) + ..onCloseButtonAction(onCloseButtonAction ?? () => popBack()) + ).build() + ); + return await Get.dialog( + usePopScope && PlatformInfo.isMobile + ? PopScope(onPopInvoked: onPopInvoked, canPop: false, child: childWidget) + : childWidget, barrierColor: AppColor.colorDefaultCupertinoActionSheet, barrierDismissible: outsideDismissible ); diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index c4ced79f18..c2023ea20b 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1923,6 +1923,7 @@ class ComposerController extends BaseController with DragDropFileMixin { alignCenter: true, outsideDismissible: false, autoPerformPopBack: false, + usePopScope: true, onConfirmAction: () => _handleSaveMessageToDraft(context), onCancelAction: () { _closeComposerButtonState = ButtonState.enabled; @@ -1933,6 +1934,14 @@ class ComposerController extends BaseController with DragDropFileMixin { popBack(); _autoFocusFieldWhenLauncher(); }, + onPopInvoked: (didPop) { + log('ComposerController::_showConfirmDialogSaveMessage: didPop = $didPop'); + if (!didPop) { + _closeComposerButtonState = ButtonState.enabled; + popBack(); + _autoFocusFieldWhenLauncher(); + } + }, marginIcon: EdgeInsets.zero, icon: SvgPicture.asset( imagePaths.icQuotasWarning, From 1b5e979f9ef7fbf980ee8d6c2b303346b0688024 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 21 May 2024 14:46:42 +0700 Subject: [PATCH 77/80] Fix mail is stuck in loading in composer. --- .../utils/html_transformer/transform_configuration.dart | 1 - pubspec.lock | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/lib/presentation/utils/html_transformer/transform_configuration.dart b/core/lib/presentation/utils/html_transformer/transform_configuration.dart index fecf928d13..6c8ad45207 100644 --- a/core/lib/presentation/utils/html_transformer/transform_configuration.dart +++ b/core/lib/presentation/utils/html_transformer/transform_configuration.dart @@ -43,7 +43,6 @@ class TransformConfiguration { if (PlatformInfo.isWeb) const RemoveTooltipLinkTransformer(), const SignatureTransformer(), - const RemoveLazyLoadingForBackgroundImageTransformer(), const RemoveCollapsedSignatureButtonTransformer(), ]); diff --git a/pubspec.lock b/pubspec.lock index 6ab8f5d3bf..7efae43a06 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -429,7 +429,7 @@ packages: description: path: "." ref: "bugfix/fix-lost-part-of-content-with-long-text" - resolved-ref: "273f3817c1f93a5fbdebbc09580a303338237f21" + resolved-ref: cb615723b8d086024e91ab72fb3777e775751708 url: "https://github.com/linagora/enough_html_editor.git" source: git version: "0.0.5" From d7a62b37039824a44d327bb377fe6923ae59e73f Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 24 May 2024 16:29:30 +0700 Subject: [PATCH 78/80] Fix size `Discard Changes` button in dialog composer --- .../dialog/confirmation_dialog_builder.dart | 71 ++++++++++++++----- .../mixin/message_dialog_action_mixin.dart | 20 +++++- .../presentation/composer_controller.dart | 2 + 3 files changed, 72 insertions(+), 21 deletions(-) diff --git a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart index f0d169c504..18dbcf2586 100644 --- a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart +++ b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart @@ -34,6 +34,8 @@ class ConfirmDialogBuilder { Color? _backgroundColor; bool showAsBottomSheet; List? listTextSpan; + int? titleActionButtonMaxLines; + bool isArrangeActionButtonsVertical; OnConfirmButtonAction? _onConfirmButtonAction; OnCancelButtonAction? _onCancelButtonAction; @@ -45,6 +47,8 @@ class ConfirmDialogBuilder { this.showAsBottomSheet = false, this.listTextSpan, this.maxWith = double.infinity, + this.titleActionButtonMaxLines, + this.isArrangeActionButtonsVertical = false, } ); @@ -223,7 +227,32 @@ class ConfirmDialogBuilder { ), ), ), - Padding( + if (isArrangeActionButtonsVertical) + ...[ + if (_cancelText.isNotEmpty) + Padding( + padding: const EdgeInsetsDirectional.only(top: 8, start: 16, end: 16), + child: _buildButton( + name: _cancelText, + bgColor: _colorCancelButton, + radius: _radiusButton, + textStyle: _styleTextCancelButton, + action: _onCancelButtonAction), + ), + if (_confirmText.isNotEmpty) + Padding( + padding: const EdgeInsetsDirectional.only(top: 8, start: 16, end: 16), + child: _buildButton( + name: _confirmText, + bgColor: _colorConfirmButton, + radius: _radiusButton, + textStyle: _styleTextConfirmButton, + action: _onConfirmButtonAction), + ), + const SizedBox(height: 16), + ] + else + Padding( padding: _paddingButton ?? const EdgeInsetsDirectional.only(bottom: 16, start: 16, end: 16), child: Row( children: [ @@ -254,27 +283,33 @@ class ConfirmDialogBuilder { TextStyle? textStyle, Color? bgColor, double? radius, - Function? action + Function()? action }) { return SizedBox( width: double.infinity, + height: titleActionButtonMaxLines == 1 ? 45 : null, child: ElevatedButton( - onPressed: () => action?.call(), - style: ButtonStyle( - foregroundColor: MaterialStateProperty.resolveWith( - (Set states) => bgColor ?? AppColor.colorTextButton), - backgroundColor: MaterialStateProperty.resolveWith( - (Set states) => bgColor ?? AppColor.colorTextButton), - shape: MaterialStateProperty.all(RoundedRectangleBorder( - borderRadius: BorderRadius.circular(radius ?? 8), - side: BorderSide(width: 0, color: bgColor ?? AppColor.colorTextButton), - )), - padding: MaterialStateProperty.resolveWith((Set states) => const EdgeInsets.all(8)), - elevation: MaterialStateProperty.resolveWith((Set states) => 0)), - child: Text(name ?? '', - textAlign: TextAlign.center, - style: textStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)), - ) + onPressed: action, + style: ElevatedButton.styleFrom( + foregroundColor: bgColor ?? AppColor.colorTextButton, + backgroundColor: bgColor ?? AppColor.colorTextButton, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(radius ?? 8), + side: BorderSide(width: 0, color: bgColor ?? AppColor.colorTextButton), + ), + padding: const EdgeInsets.all(8), + elevation: 0 + ), + child: Text( + name ?? '', + textAlign: TextAlign.center, + maxLines: titleActionButtonMaxLines, + style: textStyle ?? const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: Colors.white + )), + ), ); } } diff --git a/lib/features/base/mixin/message_dialog_action_mixin.dart b/lib/features/base/mixin/message_dialog_action_mixin.dart index df503438e4..69c2337c4d 100644 --- a/lib/features/base/mixin/message_dialog_action_mixin.dart +++ b/lib/features/base/mixin/message_dialog_action_mixin.dart @@ -34,6 +34,8 @@ mixin MessageDialogActionMixin { Color? cancelButtonColor, EdgeInsetsGeometry? marginIcon, PopInvokedCallback? onPopInvoked, + bool isArrangeActionButtonsVertical = false, + int? titleActionButtonMaxLines, } ) async { final responsiveUtils = Get.find(); @@ -41,7 +43,12 @@ mixin MessageDialogActionMixin { if (alignCenter) { final childWidget = PointerInterceptor( - child: (ConfirmDialogBuilder(imagePaths, listTextSpan: listTextSpan) + child: (ConfirmDialogBuilder( + imagePaths, + listTextSpan: listTextSpan, + titleActionButtonMaxLines: titleActionButtonMaxLines, + isArrangeActionButtonsVertical: isArrangeActionButtonsVertical + ) ..key(const Key('confirm_dialog_action')) ..title(title ?? '') ..content(message) @@ -92,7 +99,9 @@ mixin MessageDialogActionMixin { imagePaths, showAsBottomSheet: true, listTextSpan: listTextSpan, - maxWith: responsiveUtils.getSizeScreenShortestSide(context) - 16 + maxWith: responsiveUtils.getSizeScreenShortestSide(context) - 16, + titleActionButtonMaxLines: titleActionButtonMaxLines, + isArrangeActionButtonsVertical: isArrangeActionButtonsVertical ) ..key(const Key('confirm_dialog_action')) ..title(title ?? '') @@ -168,7 +177,12 @@ mixin MessageDialogActionMixin { } } else { final childWidget = PointerInterceptor( - child: (ConfirmDialogBuilder(imagePaths, listTextSpan: listTextSpan) + child: (ConfirmDialogBuilder( + imagePaths, + listTextSpan: listTextSpan, + titleActionButtonMaxLines: titleActionButtonMaxLines, + isArrangeActionButtonsVertical: isArrangeActionButtonsVertical + ) ..key(const Key('confirm_dialog_action')) ..title(title ?? '') ..content(message) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index c2023ea20b..ab5a517352 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1923,6 +1923,8 @@ class ComposerController extends BaseController with DragDropFileMixin { alignCenter: true, outsideDismissible: false, autoPerformPopBack: false, + titleActionButtonMaxLines: 1, + isArrangeActionButtonsVertical: true, usePopScope: true, onConfirmAction: () => _handleSaveMessageToDraft(context), onCancelAction: () { From 82235feba4ca077f174f5529ccce5cffe407d825 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 28 May 2024 09:10:53 +0700 Subject: [PATCH 79/80] Update `rich-text-composer` dependency --- pubspec.lock | 6 +++--- pubspec.yaml | 5 ----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 7efae43a06..1e131f8fe6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -425,11 +425,11 @@ packages: source: path version: "1.0.0+1" enough_html_editor: - dependency: "direct overridden" + dependency: transitive description: path: "." - ref: "bugfix/fix-lost-part-of-content-with-long-text" - resolved-ref: cb615723b8d086024e91ab72fb3777e775751708 + ref: cnb_supported + resolved-ref: "6409f47b01c5992906971f2d8fdb8be3278bf787" url: "https://github.com/linagora/enough_html_editor.git" source: git version: "0.0.5" diff --git a/pubspec.yaml b/pubspec.yaml index 348146e915..3b82ed9479 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -274,11 +274,6 @@ dependency_overrides: url: https://github.com/linagora/flutter_file_picker ref: email_supported_5.3.1 - enough_html_editor: - git: - url: https://github.com/linagora/enough_html_editor.git - ref: bugfix/fix-lost-part-of-content-with-long-text - # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From f873aa6741b5862f4542bd826d8927395e7e80b6 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 5 Jun 2024 16:19:03 +0700 Subject: [PATCH 80/80] Migrate `v0.11.3-patch4-dev` to `master` branch --- contact/pubspec.lock | 4 +- contact/pubspec.yaml | 4 +- email_recovery/pubspec.lock | 4 +- email_recovery/pubspec.yaml | 4 +- fcm/pubspec.lock | 4 +- fcm/pubspec.yaml | 4 +- forward/pubspec.lock | 4 +- forward/pubspec.yaml | 4 +- .../mixin/drag_drog_file_mixin.dart | 4 +- .../email/domain/model/event_action.dart | 4 ++ .../controller/single_email_controller.dart | 24 ++++++- .../email/presentation/email_view.dart | 67 +++++++++---------- .../extensions/attachment_extension.dart | 2 + .../calendar_event_action_button_widget.dart | 8 +++ .../calendar_event_detail_widget.dart | 10 +++ .../calendar_event_information_widget.dart | 10 +++ .../widgets/user_information_widget.dart | 2 - model/pubspec.lock | 4 +- model/pubspec.yaml | 4 +- pubspec.lock | 10 +-- pubspec.yaml | 10 ++- rule_filter/pubspec.lock | 4 +- rule_filter/pubspec.yaml | 4 +- server_settings/pubspec.yaml | 4 +- .../single_email_controller_test.dart | 15 +++++ .../presentation/home_controller_test.dart | 5 ++ .../presentation/login_controller_test.dart | 5 ++ .../mailbox_dashboard_controller_test.dart | 9 +-- 28 files changed, 161 insertions(+), 76 deletions(-) diff --git a/contact/pubspec.lock b/contact/pubspec.lock index afc4ca7524..9b5c6e164a 100644 --- a/contact/pubspec.lock +++ b/contact/pubspec.lock @@ -571,8 +571,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "21d15bc1a6a75e048ee1e3cd751dd0815492ce20" + ref: migrate_cnb_to_master_5Jun + resolved-ref: "49305a382ca77211a0668fd9fe196a3c057cdc8e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/contact/pubspec.yaml b/contact/pubspec.yaml index 97466ee95c..04932b60a9 100644 --- a/contact/pubspec.yaml +++ b/contact/pubspec.yaml @@ -15,10 +15,12 @@ dependencies: path: ../model ### Dependencies from git ### + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/linagora/jmap-dart-client/pull/87 jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: master + ref: migrate_cnb_to_master_5Jun ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/email_recovery/pubspec.lock b/email_recovery/pubspec.lock index 3924f67c6b..5047fd12e7 100644 --- a/email_recovery/pubspec.lock +++ b/email_recovery/pubspec.lock @@ -295,8 +295,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "21d15bc1a6a75e048ee1e3cd751dd0815492ce20" + ref: migrate_cnb_to_master_5Jun + resolved-ref: "49305a382ca77211a0668fd9fe196a3c057cdc8e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/email_recovery/pubspec.yaml b/email_recovery/pubspec.yaml index 1cb39b9c3c..c5bb9b1c8b 100644 --- a/email_recovery/pubspec.yaml +++ b/email_recovery/pubspec.yaml @@ -12,10 +12,12 @@ dependencies: sdk: flutter ### Dependencies from git ### + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/linagora/jmap-dart-client/pull/87 jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: master + ref: migrate_cnb_to_master_5Jun ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/fcm/pubspec.lock b/fcm/pubspec.lock index 54a8878c9c..f3f4194524 100644 --- a/fcm/pubspec.lock +++ b/fcm/pubspec.lock @@ -295,8 +295,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "21d15bc1a6a75e048ee1e3cd751dd0815492ce20" + ref: migrate_cnb_to_master_5Jun + resolved-ref: "49305a382ca77211a0668fd9fe196a3c057cdc8e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/fcm/pubspec.yaml b/fcm/pubspec.yaml index fc1464a52a..bc1e35188b 100644 --- a/fcm/pubspec.yaml +++ b/fcm/pubspec.yaml @@ -12,10 +12,12 @@ dependencies: sdk: flutter ### Dependencies from git ### + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/linagora/jmap-dart-client/pull/87 jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: master + ref: migrate_cnb_to_master_5Jun ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/forward/pubspec.lock b/forward/pubspec.lock index 54a8878c9c..f3f4194524 100644 --- a/forward/pubspec.lock +++ b/forward/pubspec.lock @@ -295,8 +295,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "21d15bc1a6a75e048ee1e3cd751dd0815492ce20" + ref: migrate_cnb_to_master_5Jun + resolved-ref: "49305a382ca77211a0668fd9fe196a3c057cdc8e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/forward/pubspec.yaml b/forward/pubspec.yaml index 94181ea0ca..351f8916cb 100644 --- a/forward/pubspec.yaml +++ b/forward/pubspec.yaml @@ -12,10 +12,12 @@ dependencies: sdk: flutter ### Dependencies from git ### + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/linagora/jmap-dart-client/pull/87 jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: master + ref: migrate_cnb_to_master_5Jun ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart b/lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart index 44b763109c..1d27758307 100644 --- a/lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart +++ b/lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart @@ -1,6 +1,6 @@ import 'dart:async' as async; import 'package:async/async.dart'; -import 'package:core/domain/extensions/media_type_extension.dart'; +import 'package:core/data/constants/constant.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/material.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; @@ -43,7 +43,7 @@ mixin DragDropFileMixin { fileName: details.files[i].name, type: details.files[i].mimeType, fileSize: bytesList.result![i].length, - isInline: details.files[i].mimeType?.startsWith(MediaTypeExtension.imageType) == true + isInline: details.files[i].mimeType?.startsWith(Constant.imageType) == true ), ); } diff --git a/lib/features/email/domain/model/event_action.dart b/lib/features/email/domain/model/event_action.dart index adf04ec476..a14a5ebdb3 100644 --- a/lib/features/email/domain/model/event_action.dart +++ b/lib/features/email/domain/model/event_action.dart @@ -44,6 +44,8 @@ enum EventActionType { KeyWordIdentifierExtension.tentativelyAcceptedEventAttendance.generatePath(): null, KeyWordIdentifierExtension.rejectedEventAttendance.generatePath(): true, }); + case EventActionType.mailToAttendees: + return PatchObject({}); } } @@ -55,6 +57,8 @@ enum EventActionType { return AppLocalizations.of(context).youMayAttendThisMeeting; case EventActionType.no: return AppLocalizations.of(context).youWillNotAttendThisMeeting; + case EventActionType.mailToAttendees: + return ''; } } } diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 5911c02d5c..68fdcf9e20 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -74,8 +74,8 @@ import 'package:tmail_ui_user/features/email/domain/usecases/store_opened_email_ import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/bindings/calendar_event_interactor_bindings.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/email_supervisor_controller.dart'; -import 'package:tmail_ui_user/features/email/presentation/model/blob_calendar_event.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/blob_calendar_event.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart'; @@ -1800,7 +1800,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void handleViewAttachmentAction(BuildContext context, Attachment attachment) { if (PlatformInfo.isWeb) { - if (PlatformInfo.isCanvasKit && attachment.isDisplayedPDFIcon) { + if (PlatformInfo.isCanvasKit && attachment.validatePDFIcon()) { previewPDFFileAction(context, attachment); } else { downloadAttachmentForWeb(attachment); @@ -1839,4 +1839,24 @@ class SingleEmailController extends BaseController with AppLoaderMixin { }, ); } + + void handleMailToAttendees(CalendarOrganizer? organizer, List? attendees) { + final listEmailAddressAttendees = attendees + ?.map((attendee) => EmailAddress(attendee.name?.name, attendee.mailto?.mailAddress.value)) + .toList() ?? []; + + if (organizer != null) { + listEmailAddressAttendees.add(EmailAddress(organizer.name, organizer.mailto?.value)); + } + + final listEmailAddressMailTo = listEmailAddressAttendees + .where((emailAddress) => emailAddress.emailAddress.isNotEmpty && emailAddress.emailAddress != mailboxDashBoardController.sessionCurrent?.username.value) + .toSet() + .toList(); + + log('SingleEmailController::handleMailToAttendees: listEmailAddressMailTo = $listEmailAddressMailTo'); + mailboxDashBoardController.goToComposer( + ComposerArguments.fromMailtoUri(listEmailAddress: listEmailAddressMailTo) + ); + } } \ No newline at end of file diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 49d80ba001..a64fa314aa 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -16,7 +16,6 @@ import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/features/base/widget/popup_item_widget.dart'; -import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_event_extension.dart'; @@ -148,33 +147,30 @@ class EmailView extends GetWidget { child: Obx(() => _buildEmailMessage( context: context, presentationEmail: currentEmail, - calendarEvent: controller.calendarEvent.value, + calendarEvent: controller.calendarEvent, maxBodyHeight: constraints.maxHeight )) ) ); } else { return Obx(() { - final calendarEvent = controller.calendarEvent.value; + final calendarEvent = controller.calendarEvent; if (currentEmail.hasCalendarEvent && calendarEvent != null) { return Padding( padding: const EdgeInsetsDirectional.symmetric(horizontal: 4), - child: ScrollbarListView( - scrollController: controller.emailContentScrollController, - child: SingleChildScrollView( - physics : const ClampingScrollPhysics(), - child: Container( - width: double.infinity, - alignment: Alignment.center, - color: Colors.white, - child: _buildEmailMessage( - context: context, - presentationEmail: currentEmail, - calendarEvent: calendarEvent, - emailAddressSender: currentEmail.listEmailAddressSender.getListAddress(), - ) + child: SingleChildScrollView( + physics : const ClampingScrollPhysics(), + child: Container( + width: double.infinity, + alignment: Alignment.center, + color: Colors.white, + child: _buildEmailMessage( + context: context, + presentationEmail: currentEmail, + calendarEvent: calendarEvent, + emailAddressSender: currentEmail.listEmailAddressSender.getListAddress(), ) - ), + ) ), ); } else { @@ -199,34 +195,31 @@ class EmailView extends GetWidget { child: Obx(() => _buildEmailMessage( context: context, presentationEmail: currentEmail, - calendarEvent: controller.calendarEvent.value, + calendarEvent: controller.calendarEvent, maxBodyHeight: constraints.maxHeight )) ) ); } else { return Obx(() { - final calendarEvent = controller.calendarEvent.value; + final calendarEvent = controller.calendarEvent; if (currentEmail.hasCalendarEvent && calendarEvent != null) { return Padding( padding: const EdgeInsetsDirectional.symmetric(horizontal: 4), - child: ScrollbarListView( - scrollController: controller.emailContentScrollController, - child: SingleChildScrollView( - physics : const ClampingScrollPhysics(), - child: Container( - width: double.infinity, - alignment: Alignment.center, - color: Colors.white, - child: _buildEmailMessage( - context: context, - presentationEmail: currentEmail, - calendarEvent: calendarEvent, - emailAddressSender: currentEmail.listEmailAddressSender.getListAddress(), - maxBodyHeight: constraints.maxHeight - ) + child: SingleChildScrollView( + physics : const ClampingScrollPhysics(), + child: Container( + width: double.infinity, + alignment: Alignment.center, + color: Colors.white, + child: _buildEmailMessage( + context: context, + presentationEmail: currentEmail, + calendarEvent: calendarEvent, + emailAddressSender: currentEmail.listEmailAddressSender.getListAddress(), + maxBodyHeight: constraints.maxHeight ) - ), + ) ), ); } else { @@ -367,6 +360,7 @@ class EmailView extends GetWidget { controller.onCalendarEventReplyAction(eventActionType, presentationEmail.id!), calendarEventReplying: controller.calendarEventProcessing, presentationEmail: controller.currentEmail, + onMailtoAttendeesAction: controller.handleMailToAttendees, )), if (calendarEvent.getTitleEventAction(context, emailAddressSender ?? []).isNotEmpty) CalendarEventActionBannerWidget( @@ -384,6 +378,7 @@ class EmailView extends GetWidget { controller.onCalendarEventReplyAction(eventActionType, presentationEmail.id!), calendarEventReplying: controller.calendarEventProcessing, presentationEmail: controller.currentEmail, + onMailtoAttendeesAction: controller.handleMailToAttendees, )), ], ) diff --git a/lib/features/email/presentation/extensions/attachment_extension.dart b/lib/features/email/presentation/extensions/attachment_extension.dart index 27087a60da..64fc09d289 100644 --- a/lib/features/email/presentation/extensions/attachment_extension.dart +++ b/lib/features/email/presentation/extensions/attachment_extension.dart @@ -4,4 +4,6 @@ import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_exten extension AttachmentExtension on Attachment { String getIcon(ImagePaths imagePaths) => type?.getIcon(imagePaths, fileName: name) ?? imagePaths.icFileEPup; + + bool validatePDFIcon() => type?.validatePDFIcon(fileName: name) ?? false; } \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart index eb3df766b7..73dd1e37c4 100644 --- a/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart @@ -97,6 +97,8 @@ class CalendarEventActionButtonWidget extends StatelessWidget { return CalendarEventActionButtonWidgetStyles.loadingBackgroundColor; } + return CalendarEventActionButtonWidgetStyles.backgroundColor; + case EventActionType.mailToAttendees: return CalendarEventActionButtonWidgetStyles.backgroundColor; } } @@ -120,6 +122,8 @@ class CalendarEventActionButtonWidget extends StatelessWidget { return CalendarEventActionButtonWidgetStyles.selectedTextColor; } + return CalendarEventActionButtonWidgetStyles.textColor; + case EventActionType.mailToAttendees: return CalendarEventActionButtonWidgetStyles.textColor; } } @@ -143,6 +147,8 @@ class CalendarEventActionButtonWidget extends StatelessWidget { return CalendarEventActionButtonWidgetStyles.selectedBackgroundColor; } + return CalendarEventActionButtonWidgetStyles.textColor; + case EventActionType.mailToAttendees: return CalendarEventActionButtonWidgetStyles.textColor; } } @@ -164,6 +170,8 @@ class CalendarEventActionButtonWidget extends StatelessWidget { return null; } return () => onCalendarEventReplyActionClick(eventActionType); + case EventActionType.mailToAttendees: + return onMailToAttendeesAction; } } } \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart index c571072b55..748bb307c3 100644 --- a/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart @@ -1,6 +1,8 @@ import 'package:core/presentation/views/html_viewer/html_content_viewer_widget.dart'; import 'package:flutter/material.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_organizer.dart'; import 'package:model/email/presentation_email.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_event_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/styles/calendar_event_detail_widget_styles.dart'; @@ -13,6 +15,8 @@ import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_title_widget.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; +typedef OnMailtoAttendeesAction = Function(CalendarOrganizer? organizer, List? participants); + class CalendarEventDetailWidget extends StatelessWidget { final CalendarEvent calendarEvent; @@ -24,6 +28,7 @@ class CalendarEventDetailWidget extends StatelessWidget { final OnCalendarEventReplyActionClick onCalendarEventReplyActionClick; final bool calendarEventReplying; final PresentationEmail? presentationEmail; + final OnMailtoAttendeesAction? onMailtoAttendeesAction; const CalendarEventDetailWidget({ super.key, @@ -36,6 +41,7 @@ class CalendarEventDetailWidget extends StatelessWidget { this.onOpenComposerAction, this.onMailtoDelegateAction, this.presentationEmail, + this.onMailtoAttendeesAction, }); @override @@ -99,6 +105,10 @@ class CalendarEventDetailWidget extends StatelessWidget { onCalendarEventReplyActionClick: onCalendarEventReplyActionClick, calendarEventReplying: calendarEventReplying, presentationEmail: presentationEmail, + onMailToAttendeesAction: () => onMailtoAttendeesAction?.call( + calendarEvent.organizer, + calendarEvent.participants, + ), ), ], ), diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart index f73562fd50..fb65d3f25f 100644 --- a/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart @@ -25,6 +25,7 @@ class CalendarEventInformationWidget extends StatelessWidget { final OnCalendarEventReplyActionClick onCalendarEventReplyActionClick; final bool calendarEventReplying; final PresentationEmail? presentationEmail; + final OnMailtoAttendeesAction? onMailtoAttendeesAction; final _responsiveUtils = Get.find(); @@ -36,6 +37,7 @@ class CalendarEventInformationWidget extends StatelessWidget { this.onOpenNewTabAction, this.onOpenComposerAction, this.presentationEmail, + this.onMailtoAttendeesAction, }); @override @@ -123,6 +125,10 @@ class CalendarEventInformationWidget extends StatelessWidget { onCalendarEventReplyActionClick: onCalendarEventReplyActionClick, calendarEventReplying: calendarEventReplying, presentationEmail: presentationEmail, + onMailToAttendeesAction: () => onMailtoAttendeesAction?.call( + calendarEvent.organizer, + calendarEvent.participants, + ), ), ], ), @@ -194,6 +200,10 @@ class CalendarEventInformationWidget extends StatelessWidget { onCalendarEventReplyActionClick: onCalendarEventReplyActionClick, calendarEventReplying: calendarEventReplying, presentationEmail: presentationEmail, + onMailToAttendeesAction: () => onMailtoAttendeesAction?.call( + calendarEvent.organizer, + calendarEvent.participants, + ), ), ], ), diff --git a/lib/features/mailbox/presentation/widgets/user_information_widget.dart b/lib/features/mailbox/presentation/widgets/user_information_widget.dart index 45d7122ecf..6f378e66cf 100644 --- a/lib/features/mailbox/presentation/widgets/user_information_widget.dart +++ b/lib/features/mailbox/presentation/widgets/user_information_widget.dart @@ -4,8 +4,6 @@ import 'package:core/presentation/views/image/avatar_builder.dart'; import 'package:core/presentation/views/text/text_overflow_builder.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/extensions/username_extension.dart'; import 'package:tmail_ui_user/features/base/widget/material_text_button.dart'; diff --git a/model/pubspec.lock b/model/pubspec.lock index aee232529b..fb784ca817 100644 --- a/model/pubspec.lock +++ b/model/pubspec.lock @@ -563,8 +563,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "21d15bc1a6a75e048ee1e3cd751dd0815492ce20" + ref: migrate_cnb_to_master_5Jun + resolved-ref: "49305a382ca77211a0668fd9fe196a3c057cdc8e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/model/pubspec.yaml b/model/pubspec.yaml index a37203b6ca..b8eef5797d 100644 --- a/model/pubspec.yaml +++ b/model/pubspec.yaml @@ -30,10 +30,12 @@ dependencies: path: ../core ### Dependencies from git ### + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/linagora/jmap-dart-client/pull/87 jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: master + ref: migrate_cnb_to_master_5Jun ### Dependencies from pub.dev ### cupertino_icons: 1.0.6 diff --git a/pubspec.lock b/pubspec.lock index 1e131f8fe6..942919c5a5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -521,7 +521,7 @@ packages: source: hosted version: "6.1.4" file_picker: - dependency: "direct overridden" + dependency: "direct main" description: path: "." ref: "email_supported_5.3.1" @@ -1058,8 +1058,8 @@ packages: dependency: "direct main" description: path: "." - ref: "bugfix/composer-reply-memory-leak" - resolved-ref: "7a20f8e924e403eb54f4a6177c3c8ecb0c7b3687" + ref: cnb_supported + resolved-ref: "978886d768e6540fc7dbe016dd83733c56ffb220" url: "https://github.com/linagora/html-editor-enhanced.git" source: git version: "2.5.1" @@ -1147,8 +1147,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "21d15bc1a6a75e048ee1e3cd751dd0815492ce20" + ref: migrate_cnb_to_master_5Jun + resolved-ref: "49305a382ca77211a0668fd9fe196a3c057cdc8e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index 3b82ed9479..dd8335bff2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,17 +55,19 @@ dependencies: rich_text_composer: git: url: https://github.com/linagora/rich-text-composer.git - ref: master + ref: cnb_supported html_editor_enhanced: git: url: https://github.com/linagora/html-editor-enhanced.git - ref: bugfix/composer-reply-memory-leak + ref: cnb_supported + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/linagora/jmap-dart-client/pull/87 jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: master + ref: migrate_cnb_to_master_5Jun contacts_service: git: @@ -240,6 +242,8 @@ dependencies: future_loading_dialog: 0.3.0 + file_picker: 5.3.1 + dev_dependencies: flutter_test: sdk: flutter diff --git a/rule_filter/pubspec.lock b/rule_filter/pubspec.lock index 54a8878c9c..f3f4194524 100644 --- a/rule_filter/pubspec.lock +++ b/rule_filter/pubspec.lock @@ -295,8 +295,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "21d15bc1a6a75e048ee1e3cd751dd0815492ce20" + ref: migrate_cnb_to_master_5Jun + resolved-ref: "49305a382ca77211a0668fd9fe196a3c057cdc8e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/rule_filter/pubspec.yaml b/rule_filter/pubspec.yaml index 3406395aad..ac8d6dd110 100644 --- a/rule_filter/pubspec.yaml +++ b/rule_filter/pubspec.yaml @@ -12,10 +12,12 @@ dependencies: sdk: flutter ### Dependencies from git ### + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/linagora/jmap-dart-client/pull/87 jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: master + ref: migrate_cnb_to_master_5Jun ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/server_settings/pubspec.yaml b/server_settings/pubspec.yaml index d4570b6a55..6d263ac4e1 100644 --- a/server_settings/pubspec.yaml +++ b/server_settings/pubspec.yaml @@ -11,10 +11,12 @@ dependencies: sdk: flutter ### Dependencies from git ### + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/linagora/jmap-dart-client/pull/87 jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: master + ref: migrate_cnb_to_master_5Jun ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/test/features/email/presentation/controller/single_email_controller_test.dart b/test/features/email/presentation/controller/single_email_controller_test.dart index 79491b7136..82ae55253c 100644 --- a/test/features/email/presentation/controller/single_email_controller_test.dart +++ b/test/features/email/presentation/controller/single_email_controller_test.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:core/core.dart'; import 'package:core/utils/application_manager.dart'; import 'package:dartz/dartz.dart' hide State; +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; @@ -185,6 +186,7 @@ void main() { setUp(() { Get.locale = locale; + debugDefaultTargetPlatformOverride = TargetPlatform.macOS; }); group('accept test:', () { @@ -270,9 +272,18 @@ void main() { verify(rejectCalendarEventInteractor.execute(testAccountId, {blobId}, emailId, locale.languageCode)).called(1); }); }); + + tearDown(() { + debugDefaultTargetPlatformOverride = null; + }); }); group('StoreEventAttendanceStatusInteractor test', () { + + setUp(() { + debugDefaultTargetPlatformOverride = TargetPlatform.macOS; + }); + group('calendarEventSuccess method test', () { test( 'SHOULD call execute on StoreEventAttendanceStatusInteractor\n' @@ -343,5 +354,9 @@ void main() { )).called(1); }); }); + + tearDown(() { + debugDefaultTargetPlatformOverride = null; + }); }); } diff --git a/test/features/home/presentation/home_controller_test.dart b/test/features/home/presentation/home_controller_test.dart index 77f089ec21..fff4bf85f7 100644 --- a/test/features/home/presentation/home_controller_test.dart +++ b/test/features/home/presentation/home_controller_test.dart @@ -4,6 +4,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/application_manager.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; @@ -51,6 +52,7 @@ import 'home_controller_test.mocks.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -77,6 +79,7 @@ void main() { late MockImagePaths mockImagePaths; late MockResponsiveUtils mockResponsiveUtils; late MockUuid mockUuid; + late MockApplicationManager mockApplicationManager; setUpAll(() { cleanupEmailCacheInteractor = MockCleanupEmailCacheInteractor(); @@ -102,6 +105,7 @@ void main() { mockImagePaths = MockImagePaths(); mockResponsiveUtils = MockResponsiveUtils(); mockUuid = MockUuid(); + mockApplicationManager = MockApplicationManager(); Get.put(mockGetSessionInteractor); Get.put(mockGetAuthenticatedAccountInteractor); @@ -122,6 +126,7 @@ void main() { Get.put(mockImagePaths); Get.put(mockResponsiveUtils); Get.put(mockUuid); + Get.put(mockApplicationManager); Get.testMode = true; homeController = HomeController( diff --git a/test/features/login/presentation/login_controller_test.dart b/test/features/login/presentation/login_controller_test.dart index 745551b988..0c9750e576 100644 --- a/test/features/login/presentation/login_controller_test.dart +++ b/test/features/login/presentation/login_controller_test.dart @@ -3,6 +3,7 @@ import 'package:core/data/network/config/dynamic_url_interceptors.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/application_manager.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; import 'package:mockito/annotations.dart'; @@ -65,6 +66,7 @@ import 'login_controller_test.mocks.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() { late MockAuthenticationInteractor mockAuthenticationInteractor; @@ -94,6 +96,7 @@ void main() { late MockImagePaths mockImagePaths; late MockResponsiveUtils mockResponsiveUtils; late MockUuid mockUuid; + late MockApplicationManager mockApplicationManager; late LoginController loginController; @@ -130,6 +133,7 @@ void main() { mockImagePaths = MockImagePaths(); mockResponsiveUtils = MockResponsiveUtils(); mockUuid = MockUuid(); + mockApplicationManager = MockApplicationManager(); Get.put(mockGetSessionInteractor); Get.put(mockGetAuthenticatedAccountInteractor); @@ -149,6 +153,7 @@ void main() { Get.put(mockImagePaths); Get.put(mockResponsiveUtils); Get.put(mockUuid); + Get.put(mockApplicationManager); Get.testMode = true; loginController = LoginController( diff --git a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart index 73ec7ad79a..dd1cc34f96 100644 --- a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart +++ b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart @@ -17,7 +17,6 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; -import 'package:package_info_plus/package_info_plus.dart'; import 'package:rxdart/subjects.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; @@ -168,6 +167,8 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), + MockSpec(), ]) void main() { // mock mailbox dashboard controller direct dependencies @@ -303,12 +304,6 @@ void main() { Get.put(removeComposerCacheOnWebInteractor); Get.testMode = true; - PackageInfo.setMockInitialValues( - appName: '', - packageName: '', - version: '', - buildNumber: '', - buildSignature: ''); when(emailReceiveManager.pendingEmailAddressInfo).thenAnswer((_) => BehaviorSubject.seeded(null)); when(emailReceiveManager.pendingEmailContentInfo).thenAnswer((_) => BehaviorSubject.seeded(null));