From bcad1e0cd9615781bd00308607352c923ddf8ffc Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 18 Feb 2025 11:50:27 +0700 Subject: [PATCH] Handle the behaviour of Reply/ReplyToList/ReplyAll button Signed-off-by: dab246 --- .../presentation/composer_controller.dart | 128 ++---- ...o_create_tag_for_recipients_extension.dart | 74 ++++ .../widgets/recipient_composer_widget.dart | 63 +-- .../presentation_email_extension.dart | 143 ++++--- .../email/presentation/utils/email_utils.dart | 7 + .../widgets/email_view_bottom_bar_widget.dart | 8 +- model/lib/extensions/email_extension.dart | 4 +- .../list_email_address_extension.dart | 13 + .../presentation_email_extension_test.dart | 388 +++++++++++++++++- 9 files changed, 601 insertions(+), 227 deletions(-) create mode 100644 lib/features/composer/presentation/extensions/auto_create_tag_for_recipients_extension.dart diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index a7c608306..f9780b057 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -55,13 +55,13 @@ import 'package:tmail_ui_user/features/composer/domain/usecases/restore_email_in 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/auto_create_tag_for_recipients_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/get_draft_mailbox_id_for_composer_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/get_outbox_mailbox_id_for_composer_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/get_sent_mailbox_id_for_composer_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/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/extensions/mail_address_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'; @@ -461,7 +461,7 @@ class ComposerController extends BaseController } Future _saveComposerCacheOnWebAction() async { - _autoCreateEmailTag(); + autoCreateEmailTag(); final createEmailRequest = await _generateCreateEmailRequest(); if (createEmailRequest == null) return; @@ -551,7 +551,7 @@ class ComposerController extends BaseController richTextMobileTabletController?.richTextController.hideRichTextView(); } _collapseAllRecipient(); - _autoCreateEmailTag(); + autoCreateEmailTag(); } } @@ -660,7 +660,7 @@ class ComposerController extends BaseController listToEmailAddress.addAll(arguments.listEmailAddress ?? []); isInitialRecipient.value = true; toAddressExpandMode.value = ExpandMode.COLLAPSE; - _updateStatusEmailSendButton(); + updateStatusEmailSendButton(); break; case EmailActionType.composeFromMailtoUri: if (arguments.subject != null) { @@ -683,7 +683,7 @@ class ComposerController extends BaseController listBccEmailAddress = arguments.bcc!; } _getEmailContentFromMailtoUri(arguments.body ?? ''); - _updateStatusEmailSendButton(); + updateStatusEmailSendButton(); break; case EmailActionType.reply: case EmailActionType.replyToList: @@ -760,7 +760,7 @@ class ComposerController extends BaseController toAddressExpandMode.value = ExpandMode.COLLAPSE; } _getEmailContentFromUnsubscribeMailtoLink(arguments.body ?? ''); - _updateStatusEmailSendButton(); + updateStatusEmailSendButton(); break; default: break; @@ -891,7 +891,7 @@ class ComposerController extends BaseController replyToAddressExpandMode.value = ExpandMode.COLLAPSE; } - _updateStatusEmailSendButton(); + updateStatusEmailSendButton(); } void updateListEmailAddress( @@ -914,10 +914,10 @@ class ComposerController extends BaseController default: break; } - _updateStatusEmailSendButton(); + updateStatusEmailSendButton(); } - void _updateStatusEmailSendButton() { + void updateStatusEmailSendButton() { if (listToEmailAddress.isNotEmpty || listCcEmailAddress.isNotEmpty || listBccEmailAddress.isNotEmpty @@ -942,7 +942,7 @@ class ComposerController extends BaseController || bccEmailAddressController.text.isNotEmpty || replyToEmailAddressController.text.isNotEmpty) { _collapseAllRecipient(); - _autoCreateEmailTag(); + autoCreateEmailTag(); } if (!isEnableEmailSendButton.value) { @@ -1631,86 +1631,6 @@ class ComposerController extends BaseController replyToAddressExpandMode.value = ExpandMode.COLLAPSE; } - void _autoCreateEmailTag() { - final inputToEmail = toEmailAddressController.text; - final inputCcEmail = ccEmailAddressController.text; - final inputBccEmail = bccEmailAddressController.text; - final inputReplyToEmail = replyToEmailAddressController.text; - - if (inputToEmail.isNotEmpty) { - _autoCreateToEmailTag(MailAddress.validateAddress(inputToEmail)); - } - if (inputCcEmail.isNotEmpty) { - _autoCreateCcEmailTag(MailAddress.validateAddress(inputCcEmail)); - } - if (inputBccEmail.isNotEmpty) { - _autoCreateBccEmailTag(MailAddress.validateAddress(inputBccEmail)); - } - if (inputReplyToEmail.isNotEmpty) { - _autoCreateReplyToEmailTag(MailAddress.validateAddress(inputReplyToEmail)); - } - } - - bool _isDuplicatedRecipient(String inputEmail, List listEmailAddress) { - return listEmailAddress - .map((emailAddress) => emailAddress.email) - .whereNotNull() - .contains(inputEmail); - } - - void _autoCreateToEmailTag(MailAddress inputMailAddress) { - if (!_isDuplicatedRecipient(inputMailAddress.asEncodedString(), listToEmailAddress)) { - listToEmailAddress.add(inputMailAddress.asEmailAddress()); - isInitialRecipient.value = true; - isInitialRecipient.refresh(); - _updateStatusEmailSendButton(); - } - log('ComposerController::_autoCreateToEmailTag(): STATE: ${keyToEmailTagEditor.currentState}'); - keyToEmailTagEditor.currentState?.resetTextField(); - Future.delayed(const Duration(milliseconds: 300), () { - keyToEmailTagEditor.currentState?.closeSuggestionBox(); - }); - } - - void _autoCreateCcEmailTag(MailAddress inputMailAddress) { - if (!_isDuplicatedRecipient(inputMailAddress.asEncodedString(), listCcEmailAddress)) { - listCcEmailAddress.add(inputMailAddress.asEmailAddress()); - isInitialRecipient.value = true; - isInitialRecipient.refresh(); - _updateStatusEmailSendButton(); - } - keyCcEmailTagEditor.currentState?.resetTextField(); - Future.delayed(const Duration(milliseconds: 300), () { - keyCcEmailTagEditor.currentState?.closeSuggestionBox(); - }); - } - - void _autoCreateBccEmailTag(MailAddress inputMailAddress) { - if (!_isDuplicatedRecipient(inputMailAddress.asEncodedString(), listBccEmailAddress)) { - listBccEmailAddress.add(inputMailAddress.asEmailAddress()); - isInitialRecipient.value = true; - isInitialRecipient.refresh(); - _updateStatusEmailSendButton(); - } - keyBccEmailTagEditor.currentState?.resetTextField(); - Future.delayed(const Duration(milliseconds: 300), () { - keyBccEmailTagEditor.currentState?.closeSuggestionBox(); - }); - } - - void _autoCreateReplyToEmailTag(MailAddress inputMailAddress) { - if (!_isDuplicatedRecipient(inputMailAddress.asEncodedString(), listReplyToEmailAddress)) { - listReplyToEmailAddress.add(inputMailAddress.asEmailAddress()); - isInitialRecipient.value = true; - isInitialRecipient.refresh(); - _updateStatusEmailSendButton(); - } - keyReplyToEmailTagEditor.currentState?.resetTextField(); - Future.delayed(const Duration(milliseconds: 300), () { - keyReplyToEmailTagEditor.currentState?.closeSuggestionBox(); - }); - } - void _closeSuggestionBox() { if (toEmailAddressController.text.isEmpty) { keyToEmailTagEditor.currentState?.closeSuggestionBox(); @@ -1778,29 +1698,29 @@ class ComposerController extends BaseController case PrefixEmailAddress.to: toAddressExpandMode.value = ExpandMode.COLLAPSE; final inputToEmail = toEmailAddressController.text; - if (inputToEmail.isNotEmpty) { - _autoCreateToEmailTag(MailAddress.validateAddress(inputToEmail)); + if (inputToEmail.trim().isNotEmpty) { + autoCreateEmailTagForType(PrefixEmailAddress.to, inputToEmail); } break; case PrefixEmailAddress.cc: ccAddressExpandMode.value = ExpandMode.COLLAPSE; final inputCcEmail = ccEmailAddressController.text; - if (inputCcEmail.isNotEmpty) { - _autoCreateCcEmailTag(MailAddress.validateAddress(inputCcEmail)); + if (inputCcEmail.trim().isNotEmpty) { + autoCreateEmailTagForType(PrefixEmailAddress.cc, inputCcEmail); } break; case PrefixEmailAddress.bcc: bccAddressExpandMode.value = ExpandMode.COLLAPSE; final inputBccEmail = bccEmailAddressController.text; - if (inputBccEmail.isNotEmpty) { - _autoCreateBccEmailTag(MailAddress.validateAddress(inputBccEmail)); + if (inputBccEmail.trim().isNotEmpty) { + autoCreateEmailTagForType(PrefixEmailAddress.bcc, inputBccEmail); } break; case PrefixEmailAddress.replyTo: replyToAddressExpandMode.value = ExpandMode.COLLAPSE; final inputReplyToEmail = replyToEmailAddressController.text; - if (inputReplyToEmail.isNotEmpty) { - _autoCreateReplyToEmailTag(MailAddress.validateAddress(inputReplyToEmail)); + if (inputReplyToEmail.trim().isNotEmpty) { + autoCreateEmailTagForType(PrefixEmailAddress.replyTo, inputReplyToEmail); } break; default: @@ -1866,7 +1786,7 @@ class ComposerController extends BaseController ccAddressExpandMode.value = ExpandMode.COLLAPSE; bccAddressExpandMode.value = ExpandMode.COLLAPSE; bccAddressExpandMode.refresh(); - _updateStatusEmailSendButton(); + updateStatusEmailSendButton(); } void _removeBccEmailAddressFromFormerIdentity(Set listEmailAddress) { @@ -1879,7 +1799,7 @@ class ComposerController extends BaseController toAddressExpandMode.value = ExpandMode.COLLAPSE; ccAddressExpandMode.value = ExpandMode.COLLAPSE; bccAddressExpandMode.value = ExpandMode.COLLAPSE; - _updateStatusEmailSendButton(); + updateStatusEmailSendButton(); } Future _applySignature(String signature) async { @@ -1952,7 +1872,7 @@ class ComposerController extends BaseController richTextMobileTabletController?.richTextController.showDeviceKeyboard); } _collapseAllRecipient(); - _autoCreateEmailTag(); + autoCreateEmailTag(); } void _onChangeCursorOnMobile(List? coordinates, BuildContext context) { @@ -2083,7 +2003,7 @@ class ComposerController extends BaseController void handleOnMouseDownHtmlEditorWeb() { _collapseAllRecipient(); - _autoCreateEmailTag(); + autoCreateEmailTag(); } FocusNode? getNextFocusOfToEmailAddress() { @@ -2117,7 +2037,7 @@ class ComposerController extends BaseController } void handleFocusNextAddressAction() { - _autoCreateEmailTag(); + autoCreateEmailTag(); } bool get isNetworkConnectionAvailable => networkConnectionController.isNetworkConnectionAvailable(); @@ -2176,7 +2096,7 @@ class ComposerController extends BaseController isInitialRecipient.value = true; isInitialRecipient.refresh(); - _updateStatusEmailSendButton(); + updateStatusEmailSendButton(); } void onAttachmentDropZoneListener(Attachment attachment) { diff --git a/lib/features/composer/presentation/extensions/auto_create_tag_for_recipients_extension.dart b/lib/features/composer/presentation/extensions/auto_create_tag_for_recipients_extension.dart new file mode 100644 index 000000000..db8690d97 --- /dev/null +++ b/lib/features/composer/presentation/extensions/auto_create_tag_for_recipients_extension.dart @@ -0,0 +1,74 @@ + +import 'package:core/utils/mail/mail_address.dart'; +import 'package:flutter/cupertino.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/composer_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/mail_address_extension.dart'; + +extension AutoCreateTagForRecipientsExtension on ComposerController { + + void autoCreateEmailTag() { + final emailInputs = { + PrefixEmailAddress.to: toEmailAddressController.text, + PrefixEmailAddress.cc: ccEmailAddressController.text, + PrefixEmailAddress.bcc: bccEmailAddressController.text, + PrefixEmailAddress.replyTo: replyToEmailAddressController.text, + }; + + emailInputs.forEach((prefix, input) { + if (input.trim().isNotEmpty) { + autoCreateEmailTagForType(prefix, input); + } + }); + } + + Map> get _emailLists => { + PrefixEmailAddress.to: listToEmailAddress, + PrefixEmailAddress.cc: listCcEmailAddress, + PrefixEmailAddress.bcc: listBccEmailAddress, + PrefixEmailAddress.replyTo: listReplyToEmailAddress, + }; + + Map> get _emailEditors => { + PrefixEmailAddress.to: keyToEmailTagEditor, + PrefixEmailAddress.cc: keyCcEmailTagEditor, + PrefixEmailAddress.bcc: keyBccEmailTagEditor, + PrefixEmailAddress.replyTo: keyReplyToEmailTagEditor, + }; + + void autoCreateEmailTagForType(PrefixEmailAddress type, String input) { + final emailAddressRecord = _generateEmailAddressFromString(input); + + final emailList = _emailLists[type]!; + final keyEditor = _emailEditors[type]!; + + final emailSet = emailList.toSet(); + if (emailSet.any((email) => email.email == emailAddressRecord.$1)) return; + + emailSet.add(emailAddressRecord.$2); + emailList + ..clear() + ..addAll(emailSet); + + isInitialRecipient.value = true; + isInitialRecipient.refresh(); + updateStatusEmailSendButton(); + + keyEditor.currentState?.resetTextField(); + Future.delayed( + const Duration(milliseconds: 300), + keyEditor.currentState?.closeSuggestionBox, + ); + } + + (String email, EmailAddress emailAddress) _generateEmailAddressFromString(String input) { + try { + final mailAddress = MailAddress.validateAddress(input); + return (mailAddress.asEncodedString(), mailAddress.asEmailAddress()); + } catch (e) { + return (input, EmailAddress(null, input)); + } + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index a3114e008..5abe4f6e5 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -2,7 +2,6 @@ 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'; @@ -230,7 +229,7 @@ class _RecipientComposerWidgetState extends State { return RecipientSuggestionItemWidget( imagePaths: widget.imagePaths, suggestionState: suggestionEmailAddress.state, - emailAddress: MailAddress.validateAddress(suggestionEmailAddress.emailAddress.emailAddress).asEmailAddress(), + emailAddress: _subAddressingValidatedEmailAddress(suggestionEmailAddress.emailAddress), suggestionValid: suggestionValid, highlight: highlight, onSelectedAction: (emailAddress) { @@ -321,7 +320,7 @@ class _RecipientComposerWidgetState extends State { return RecipientSuggestionItemWidget( imagePaths: widget.imagePaths, suggestionState: suggestionEmailAddress.state, - emailAddress: MailAddress.validateAddress(suggestionEmailAddress.emailAddress.emailAddress).asEmailAddress(), + emailAddress: _subAddressingValidatedEmailAddress(suggestionEmailAddress.emailAddress), suggestionValid: suggestionValid, highlight: highlight, onSelectedAction: (emailAddress) { @@ -466,13 +465,10 @@ class _RecipientComposerWidgetState extends State { } bool _isDuplicatedRecipient(String inputEmail) { - if (inputEmail.isEmpty) { - return false; - } + if (inputEmail.trim().isEmpty) return false; + return _currentListEmailAddress - .map((emailAddress) => emailAddress.email) - .whereNotNull() - .contains(inputEmail); + .any((emailAddress) => emailAddress.email?.trim() == inputEmail.trim()); } SuggestionEmailAddress _toSuggestionEmailAddress(EmailAddress item, List addedEmailAddresses) { @@ -524,13 +520,10 @@ class _RecipientComposerWidgetState extends State { void _handleSelectOptionAction( SuggestionEmailAddress suggestionEmailAddress, StateSetter stateSetter - ) { - MailAddress mailAddress = MailAddress.validateAddress(suggestionEmailAddress.emailAddress.emailAddress); - if (!_isDuplicatedRecipient(mailAddress.asEncodedString())) { - stateSetter(() => _currentListEmailAddress.add(mailAddress.asEmailAddress())); - _updateListEmailAddressAction(); - } - } + ) => _onEmailAddressReceived( + suggestionEmailAddress.emailAddress.emailAddress, + stateSetter, + ); void _handleSubmitTagAction( String value, @@ -539,11 +532,9 @@ class _RecipientComposerWidgetState extends State { void _createMailTag(String value, StateSetter stateSetter) { final listString = StringConvert.extractStrings(value.trim()).toSet(); - MailAddress mailAddress = MailAddress.validateAddress(value.trim()); - if (listString.isEmpty && !_isDuplicatedRecipient(mailAddress.asEncodedString())) { - stateSetter(() => _currentListEmailAddress.add(mailAddress.asEmailAddress())); - _updateListEmailAddressAction(); + if (listString.isEmpty && !_isDuplicatedRecipient(value)) { + _onEmailAddressReceived(value, stateSetter); } else if (listString.isNotEmpty) { final listStringNotExist = listString .where((text) => !_isDuplicatedRecipient(text)) @@ -564,11 +555,7 @@ class _RecipientComposerWidgetState extends State { String value, StateSetter stateSetter ) { - MailAddress mailAddress = MailAddress.validateAddress(value.trim()); - if (!_isDuplicatedRecipient(mailAddress.asEncodedString())) { - stateSetter(() => _currentListEmailAddress.add(mailAddress.asEmailAddress())); - _updateListEmailAddressAction(); - } + _onEmailAddressReceived(value, stateSetter); _gapBetweenTagChangedAndFindSuggestion = Timer( const Duration(seconds: 1), _handleGapBetweenTagChangedAndFindSuggestion @@ -608,4 +595,30 @@ class _RecipientComposerWidgetState extends State { return null; } } + + void _onEmailAddressReceived(String input, StateSetter stateSetter) { + final emailAddressRecord = _generateEmailAddressFromString(input); + if (!_isDuplicatedRecipient(emailAddressRecord.$1)) { + stateSetter(() => _currentListEmailAddress.add(emailAddressRecord.$2)); + _updateListEmailAddressAction(); + } + } + + EmailAddress _subAddressingValidatedEmailAddress(EmailAddress emailAddress) { + try { + final mailAddress = MailAddress.validateAddress(emailAddress.emailAddress); + return mailAddress.asEmailAddress(); + } catch (e) { + return emailAddress; + } + } + + (String email, EmailAddress emailAddress) _generateEmailAddressFromString(String input) { + try { + final mailAddress = MailAddress.validateAddress(input); + return (mailAddress.asEncodedString(), mailAddress.asEmailAddress()); + } catch (e) { + return (input, EmailAddress(null, input)); + } + } } \ No newline at end of file diff --git a/lib/features/email/presentation/extensions/presentation_email_extension.dart b/lib/features/email/presentation/extensions/presentation_email_extension.dart index 2d8a1ea98..60bc242e4 100644 --- a/lib/features/email/presentation/extensions/presentation_email_extension.dart +++ b/lib/features/email/presentation/extensions/presentation_email_extension.dart @@ -21,85 +21,84 @@ extension PresentationEmailExtension on PresentationEmail { switch (emailActionType) { case EmailActionType.reply: - List listReplyAddress; - if (newReplyToAddress.isNotEmpty) { - listReplyAddress = newReplyToAddress; - } else if (isSender) { - listReplyAddress = newToAddress; - } else { - listReplyAddress = newFromAddress; - } - final listReplyAddressWithoutUsername = listReplyAddress.withoutMe(userName); + return _handleReply( + isSender: isSender, + newToAddress: newToAddress, + newFromAddress: newFromAddress, + newReplyToAddress: newReplyToAddress, + userName: userName, + ); - return Tuple4(listReplyAddressWithoutUsername, [], [], []); case EmailActionType.replyToList: - final recipientRecord = EmailUtils.extractRecipientsFromListPost(listPost ?? ''); + return _handleReplyToList(listPost, userName); - final listToAddressWithoutUsername = recipientRecord.toMailAddresses - .toSet() - .removeDuplicateEmails() - .withoutMe(userName); - - final listCcAddressWithoutUsername = recipientRecord.ccMailAddresses - .toSet() - .removeDuplicateEmails() - .withoutMe(userName); - - final listBccAddressWithoutUsername = recipientRecord.bccMailAddresses - .toSet() - .removeDuplicateEmails() - .withoutMe(userName); - - return Tuple4( - listToAddressWithoutUsername, - listCcAddressWithoutUsername, - listBccAddressWithoutUsername, - [] - ); case EmailActionType.replyAll: - final recipientRecord = EmailUtils.extractRecipientsFromListPost(listPost ?? ''); - - final listToAddress = recipientRecord.toMailAddresses - + newReplyToAddress - + newFromAddress - + newToAddress; - final listCcAddress = recipientRecord.ccMailAddresses + newCcAddress; - final listBccAddress = recipientRecord.bccMailAddresses + newBccAddress; - - final listToAddressWithoutUsername = listToAddress - .toSet() - .removeDuplicateEmails() - .withoutMe(userName); - final listCcAddressWithoutUsername = listCcAddress - .toSet() - .removeDuplicateEmails() - .withoutMe(userName); - final listBccAddressWithoutUsername = listBccAddress - .toSet() - .removeDuplicateEmails() - .withoutMe(userName); - - return Tuple4( - listToAddressWithoutUsername, - listCcAddressWithoutUsername, - listBccAddressWithoutUsername, - [] + return _handleReplyAll( + isSender: isSender, + newToAddress: newToAddress, + newCcAddress: newCcAddress, + newBccAddress: newBccAddress, + newReplyToAddress: newReplyToAddress, + newFromAddress: newFromAddress, + userName: userName, ); - case EmailActionType.editDraft: - return Tuple4(newToAddress, newCcAddress, newBccAddress, newReplyToAddress); - default: - final listToAddressWithoutUsername = newToAddress.withoutMe(userName); - final listCcAddressWithoutUsername = newCcAddress.withoutMe(userName); - final listBccAddressWithoutUsername = newBccAddress.withoutMe(userName); - - return Tuple4( - listToAddressWithoutUsername, - listCcAddressWithoutUsername, - listBccAddressWithoutUsername, - [] - ); + return Tuple4(newToAddress, newCcAddress, newBccAddress, newReplyToAddress); } } + + Tuple4, List, List, List> _handleReply({ + required bool isSender, + required List newToAddress, + required List newFromAddress, + required List newReplyToAddress, + String? userName, + }) { + if (isSender) { + return Tuple4(newToAddress, [], [], newReplyToAddress); + } + final listToAddress = newReplyToAddress.isNotEmpty + ? newReplyToAddress.withoutMe(userName) + : newFromAddress.withoutMe(userName); + return Tuple4(listToAddress, [], [], []); + } + + Tuple4, List, List, List> _handleReplyToList(String? listPost, String? userName) { + final recipientRecord = EmailUtils.extractRecipientsFromListPost(listPost ?? ''); + + return Tuple4( + recipientRecord.toMailAddresses.removeDuplicateEmails().withoutMe(userName), + recipientRecord.ccMailAddresses.removeDuplicateEmails().withoutMe(userName), + recipientRecord.bccMailAddresses.removeDuplicateEmails().withoutMe(userName), + [], + ); + } + + Tuple4, List, List, List> _handleReplyAll({ + required bool isSender, + required List newToAddress, + required List newCcAddress, + required List newBccAddress, + required List newReplyToAddress, + required List newFromAddress, + String? userName, + }) { + if (isSender) { + return Tuple4(newToAddress, newCcAddress, newBccAddress, newReplyToAddress); + } + + final listToAddress = { + ...(newReplyToAddress.isNotEmpty ? newReplyToAddress : newFromAddress), + ...newToAddress, + }.removeDuplicateEmails().withoutMe(userName); + + return Tuple4( + listToAddress, + newCcAddress.withoutMe(userName), + newBccAddress.withoutMe(userName), + [], + ); + } + } \ 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 5062a87f2..7b67cbcbb 100644 --- a/lib/features/email/presentation/utils/email_utils.dart +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -218,4 +218,11 @@ class EmailUtils { return null; } } + + static bool isReplyToListEnabled(String listPost) { + final recipientRecord = EmailUtils.extractRecipientsFromListPost(listPost); + return recipientRecord.toMailAddresses.isNotEmpty || + recipientRecord.ccMailAddresses.isNotEmpty || + recipientRecord.bccMailAddresses.isNotEmpty; + } } \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/email_view_bottom_bar_widget.dart b/lib/features/email/presentation/widgets/email_view_bottom_bar_widget.dart index 4343695aa..b7e761eed 100644 --- a/lib/features/email/presentation/widgets/email_view_bottom_bar_widget.dart +++ b/lib/features/email/presentation/widgets/email_view_bottom_bar_widget.dart @@ -9,6 +9,7 @@ import 'package:model/extensions/email_extension.dart'; import 'package:model/extensions/presentation_email_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/styles/email_view_bottom_bar_widget_styles.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; typedef OnEmailActionCallback = void Function(EmailActionType, PresentationEmail); @@ -76,9 +77,10 @@ class EmailViewBottomBarWidget extends StatelessWidget { }), Obx(() { final currentEmailLoaded = _singleEmailController.currentEmailLoaded.value; - - if (currentEmailLoaded != null && - currentEmailLoaded.emailCurrent?.hasListPost == true) { + final isReplyToListEnabled = EmailUtils.isReplyToListEnabled( + currentEmailLoaded?.emailCurrent?.listPost ?? '', + ); + if (currentEmailLoaded != null && isReplyToListEnabled) { return Expanded( child: TMailButtonWidget( key: const Key('reply_to_list_email_button'), diff --git a/model/lib/extensions/email_extension.dart b/model/lib/extensions/email_extension.dart index 70e0294aa..fefdee347 100644 --- a/model/lib/extensions/email_extension.dart +++ b/model/lib/extensions/email_extension.dart @@ -36,9 +36,7 @@ extension EmailExtension on Email { String get sMimeStatusHeaderParsed => sMimeStatusHeader?[IndividualHeaderIdentifier.sMimeStatusHeader]?.trim() ?? ''; - String get listPost => headers.listPost; - - bool get hasListPost => listPost.isNotEmpty; + String get listPost => headers.listPost.trim(); IdentityId? get identityIdFromHeader { final rawIdentityId = identityHeader?[IndividualHeaderIdentifier.identityHeader]; diff --git a/model/lib/extensions/list_email_address_extension.dart b/model/lib/extensions/list_email_address_extension.dart index 66ffef861..79d28a076 100644 --- a/model/lib/extensions/list_email_address_extension.dart +++ b/model/lib/extensions/list_email_address_extension.dart @@ -83,4 +83,17 @@ extension ListEmailAddressExtension on List { return where((emailAddress) => emailAddress.emailAddress != userName).toList(); } + + List removeDuplicateEmails() { + final seenEmails = {}; + return where((emailAddress) { + if (emailAddress.emailAddress.isEmpty || + seenEmails.contains(emailAddress.emailAddress)) { + return false; + } else { + seenEmails.add(emailAddress.emailAddress); + return true; + } + }).toList(); + } } \ No newline at end of file diff --git a/test/model/lib/extensions/presentation_email_extension_test.dart b/test/model/lib/extensions/presentation_email_extension_test.dart index 885d6e248..b476c8224 100644 --- a/test/model/lib/extensions/presentation_email_extension_test.dart +++ b/test/model/lib/extensions/presentation_email_extension_test.dart @@ -15,10 +15,22 @@ void main() { final userEEmailAddress = EmailAddress('User E', 'userE@domain.com'); final replyToEmailAddress = EmailAddress('Reply To', 'replyToThis@domain.com'); final replyToListEmailAddress = EmailAddress(null, 'replyToList@domain.com'); + final listPost = ''; - group('GIVEN user A is the sender AND sends an email to user B and user E, cc to user C, bcc to user D', () { - test('THEN user A clicks reply, generateRecipientsEmailAddressForComposer SHOULD return user B email + user E email to reply', () { - final expectedResult = Tuple3([userBEmailAddress, userEEmailAddress], [], []); + group( + 'GIVEN user A is the sender\n' + 'AND sends an email to user B and user E, cc to user C, bcc to user D', + () { + test( + 'THEN user A clicks reply, generateRecipientsEmailAddressForComposer\n' + 'SHOULD return user B email + user E email to reply', + () { + final expectedResult = Tuple4( + [userBEmailAddress, userEEmailAddress], + [], + [], + [], + ); final emailToReply = PresentationEmail( from: {userAEmailAddress}, @@ -36,10 +48,19 @@ void main() { expect(result.value1, containsAll(expectedResult.value1)); expect(result.value2, containsAll(expectedResult.value2)); expect(result.value3, containsAll(expectedResult.value3)); + expect(result.value4, containsAll(expectedResult.value4)); }); - test('THEN user A clicks reply all, generateRecipientsEmailAddressForComposer SHOULD return user B email + user E email to reply, user C email address to cc, user D email address to bcc', () { - final expectedResult = Tuple3([userBEmailAddress, userEEmailAddress], [userCEmailAddress], [userDEmailAddress]); + test( + 'THEN user A clicks reply all, generateRecipientsEmailAddressForComposer\n' + 'SHOULD return user B email + user E email to reply, user C email address to cc, user D email address to bcc', + () { + final expectedResult = Tuple4( + [userBEmailAddress, userEEmailAddress], + [userCEmailAddress], + [userDEmailAddress], + [], + ); final emailToReply = PresentationEmail( from: {userAEmailAddress}, @@ -57,12 +78,294 @@ void main() { expect(result.value1, containsAll(expectedResult.value1)); expect(result.value2, containsAll(expectedResult.value2)); expect(result.value3, containsAll(expectedResult.value3)); + expect(result.value4, containsAll(expectedResult.value4)); }); }); - group('GIVEN user B is the sender, SENDER configured the replyTo email AND send an email to user A and user E, cc to user C, bcc to user D', () { - test('THEN user A click reply, generateRecipientsEmailAddressForComposer SHOULD return only replyToEmailAddress email to reply' , () { - final expectedResult = Tuple3([replyToEmailAddress], [], []); + group( + 'GIVEN user A is the sender\n' + 'AND sends an email to user B and user E, cc to user C, bcc to user D', + () { + test( + 'Email has Reply To and List-Post' + 'THEN user A clicks reply, generateRecipientsEmailAddressForComposer\n' + 'SHOULD return user B email + user E email to reply', + () { + final expectedResult = Tuple4( + [userBEmailAddress, userEEmailAddress], + [], + [], + [replyToEmailAddress], + ); + + final emailToReply = PresentationEmail( + from: {userAEmailAddress}, + to: {userBEmailAddress, userEEmailAddress}, + cc: {userCEmailAddress}, + bcc: {userDEmailAddress}, + replyTo: {replyToEmailAddress}, + ); + + final result = emailToReply.generateRecipientsEmailAddressForComposer( + emailActionType: EmailActionType.reply, + isSender: true, + userName: userAEmailAddress.emailAddress, + listPost: listPost, + ); + + expect(result.value1, containsAll(expectedResult.value1)); + expect(result.value2, containsAll(expectedResult.value2)); + expect(result.value3, containsAll(expectedResult.value3)); + expect(result.value4, containsAll(expectedResult.value4)); + }); + + test( + 'Email has Reply To and not List-Post' + 'THEN user A clicks reply, generateRecipientsEmailAddressForComposer\n' + 'SHOULD return user B email + user E email to reply', + () { + final expectedResult = Tuple4( + [userBEmailAddress, userEEmailAddress], + [], + [], + [replyToEmailAddress], + ); + + final emailToReply = PresentationEmail( + from: {userAEmailAddress}, + to: {userBEmailAddress, userEEmailAddress}, + cc: {userCEmailAddress}, + bcc: {userDEmailAddress}, + replyTo: {replyToEmailAddress}, + ); + + final result = emailToReply.generateRecipientsEmailAddressForComposer( + emailActionType: EmailActionType.reply, + isSender: true, + userName: userAEmailAddress.emailAddress, + ); + + expect(result.value1, containsAll(expectedResult.value1)); + expect(result.value2, containsAll(expectedResult.value2)); + expect(result.value3, containsAll(expectedResult.value3)); + expect(result.value4, containsAll(expectedResult.value4)); + }); + + test( + 'Email has List-Post and not Reply To' + 'THEN user A clicks reply, generateRecipientsEmailAddressForComposer\n' + 'SHOULD return user B email + user E email to reply', + () { + final expectedResult = Tuple4( + [userBEmailAddress, userEEmailAddress], + [], + [], + [], + ); + + final emailToReply = PresentationEmail( + from: {userAEmailAddress}, + to: {userBEmailAddress, userEEmailAddress}, + cc: {userCEmailAddress}, + bcc: {userDEmailAddress}, + replyTo: {replyToEmailAddress}, + ); + + final result = emailToReply.generateRecipientsEmailAddressForComposer( + emailActionType: EmailActionType.reply, + isSender: true, + userName: userAEmailAddress.emailAddress, + listPost: listPost, + ); + + expect(result.value1, containsAll(expectedResult.value1)); + expect(result.value2, containsAll(expectedResult.value2)); + expect(result.value3, containsAll(expectedResult.value3)); + expect(result.value4, containsAll(expectedResult.value4)); + }); + + test( + 'Email has not Reply To and List-Post' + 'THEN user A clicks reply, generateRecipientsEmailAddressForComposer\n' + 'SHOULD return user B email + user E email to reply', + () { + final expectedResult = Tuple4( + [userBEmailAddress, userEEmailAddress], + [], + [], + [], + ); + + final emailToReply = PresentationEmail( + from: {userAEmailAddress}, + to: {userBEmailAddress, userEEmailAddress}, + cc: {userCEmailAddress}, + bcc: {userDEmailAddress}, + replyTo: {}, + ); + + final result = emailToReply.generateRecipientsEmailAddressForComposer( + emailActionType: EmailActionType.reply, + isSender: true, + userName: userAEmailAddress.emailAddress, + ); + + expect(result.value1, containsAll(expectedResult.value1)); + expect(result.value2, containsAll(expectedResult.value2)); + expect(result.value3, containsAll(expectedResult.value3)); + expect(result.value4, containsAll(expectedResult.value4)); + }); + }); + + group( + 'GIVEN user A is the sender\n' + 'AND sends an email to user A and user B, cc to user C, bcc to user D', + () { + test( + 'Email has Reply To and List-Post' + 'THEN user A clicks reply, generateRecipientsEmailAddressForComposer\n' + 'SHOULD return user A email + user B email to reply', + () { + final expectedResult = Tuple4( + [userBEmailAddress, userAEmailAddress], + [], + [], + [replyToEmailAddress], + ); + + final emailToReply = PresentationEmail( + from: {userAEmailAddress}, + to: {userBEmailAddress, userAEmailAddress}, + cc: {userCEmailAddress}, + bcc: {userDEmailAddress}, + replyTo: {replyToEmailAddress}, + ); + + final result = emailToReply.generateRecipientsEmailAddressForComposer( + emailActionType: EmailActionType.reply, + isSender: true, + userName: userAEmailAddress.emailAddress, + listPost: listPost, + ); + + expect(result.value1, containsAll(expectedResult.value1)); + expect(result.value2, containsAll(expectedResult.value2)); + expect(result.value3, containsAll(expectedResult.value3)); + expect(result.value4, containsAll(expectedResult.value4)); + }); + + test( + 'Email has Reply To and not List-Post' + 'THEN user A clicks reply, generateRecipientsEmailAddressForComposer\n' + 'SHOULD return user A email + user B email to reply', + () { + final expectedResult = Tuple4( + [userBEmailAddress, userAEmailAddress], + [], + [], + [replyToEmailAddress], + ); + + final emailToReply = PresentationEmail( + from: {userAEmailAddress}, + to: {userBEmailAddress, userAEmailAddress}, + cc: {userCEmailAddress}, + bcc: {userDEmailAddress}, + replyTo: {replyToEmailAddress}, + ); + + final result = emailToReply.generateRecipientsEmailAddressForComposer( + emailActionType: EmailActionType.reply, + isSender: true, + userName: userAEmailAddress.emailAddress, + ); + + expect(result.value1, containsAll(expectedResult.value1)); + expect(result.value2, containsAll(expectedResult.value2)); + expect(result.value3, containsAll(expectedResult.value3)); + expect(result.value4, containsAll(expectedResult.value4)); + }); + + test( + 'Email has List-Post and not Reply To' + 'THEN user A clicks reply, generateRecipientsEmailAddressForComposer\n' + 'SHOULD return user A email + user B email to reply', + () { + final expectedResult = Tuple4( + [userBEmailAddress, userAEmailAddress], + [], + [], + [], + ); + + final emailToReply = PresentationEmail( + from: {userAEmailAddress}, + to: {userBEmailAddress, userAEmailAddress}, + cc: {userCEmailAddress}, + bcc: {userDEmailAddress}, + replyTo: {replyToEmailAddress}, + ); + + final result = emailToReply.generateRecipientsEmailAddressForComposer( + emailActionType: EmailActionType.reply, + isSender: true, + userName: userAEmailAddress.emailAddress, + listPost: listPost, + ); + + expect(result.value1, containsAll(expectedResult.value1)); + expect(result.value2, containsAll(expectedResult.value2)); + expect(result.value3, containsAll(expectedResult.value3)); + expect(result.value4, containsAll(expectedResult.value4)); + }); + + test( + 'Email has not Reply To and List-Post' + 'THEN user A clicks reply, generateRecipientsEmailAddressForComposer\n' + 'SHOULD return user A email + user B email to reply', + () { + final expectedResult = Tuple4( + [userBEmailAddress, userAEmailAddress], + [], + [], + [], + ); + + final emailToReply = PresentationEmail( + from: {userAEmailAddress}, + to: {userBEmailAddress, userAEmailAddress}, + cc: {userCEmailAddress}, + bcc: {userDEmailAddress}, + replyTo: {}, + ); + + final result = emailToReply.generateRecipientsEmailAddressForComposer( + emailActionType: EmailActionType.reply, + isSender: true, + userName: userAEmailAddress.emailAddress, + ); + + expect(result.value1, containsAll(expectedResult.value1)); + expect(result.value2, containsAll(expectedResult.value2)); + expect(result.value3, containsAll(expectedResult.value3)); + expect(result.value4, containsAll(expectedResult.value4)); + }); + }); + + group( + 'GIVEN user B is the sender, SENDER configured the replyTo email\n' + 'AND send an email to user A and user E, cc to user C, bcc to user D', + () { + test( + 'THEN user A click reply, generateRecipientsEmailAddressForComposer\n' + 'SHOULD return only replyToEmailAddress email to reply' , + () { + final expectedResult = Tuple4( + [replyToEmailAddress], + [], + [], + [], + ); final emailToReply = PresentationEmail( from: {userBEmailAddress}, @@ -80,10 +383,19 @@ void main() { expect(result.value1, containsAll(expectedResult.value1)); expect(result.value2, containsAll(expectedResult.value2)); expect(result.value3, containsAll(expectedResult.value3)); + expect(result.value4, containsAll(expectedResult.value4)); }); - test('THEN user A clicks reply all, generateRecipientsEmailAddressForComposer SHOULD return replyToEmailAddress + user A email + user E email to reply, user C email address to cc, user D email address to bcc', () { - final expectedResult = Tuple3([userAEmailAddress, userEEmailAddress, replyToEmailAddress], [userCEmailAddress], [userDEmailAddress]); + test( + 'THEN user A clicks reply all, generateRecipientsEmailAddressForComposer\n' + 'SHOULD return replyToEmailAddress + user A email + user E email to reply, user C email address to cc, user D email address to bcc', + () { + final expectedResult = Tuple4( + [userAEmailAddress, userEEmailAddress, replyToEmailAddress], + [userCEmailAddress], + [userDEmailAddress], + [], + ); final emailToReply = PresentationEmail( from: {userBEmailAddress}, @@ -101,12 +413,24 @@ void main() { expect(result.value1, containsAll(expectedResult.value1)); expect(result.value2, containsAll(expectedResult.value2)); expect(result.value3, containsAll(expectedResult.value3)); + expect(result.value4, containsAll(expectedResult.value4)); }); }); - group('GIVEN user B is the sender, SENDER does not have the replyTo email AND sends an email to user A and user E, cc to user C, bcc to user D', () { - test('THEN user A clicks reply, generateRecipientsEmailAddressForComposer SHOULD return only user B email to reply', () { - final expectedResult = Tuple3([userBEmailAddress], [], []); + group( + 'GIVEN user B is the sender, SENDER does not have the replyTo email\n' + 'AND sends an email to user A and user E, cc to user C, bcc to user D', + () { + test( + 'THEN user A clicks reply, generateRecipientsEmailAddressForComposer\n' + 'SHOULD return only user B email to reply', + () { + final expectedResult = Tuple4( + [userBEmailAddress], + [], + [], + [], + ); final emailToReply = PresentationEmail( from: {userBEmailAddress}, @@ -123,10 +447,19 @@ void main() { expect(result.value1, containsAll(expectedResult.value1)); expect(result.value2, containsAll(expectedResult.value2)); expect(result.value3, containsAll(expectedResult.value3)); + expect(result.value4, containsAll(expectedResult.value4)); }); - test('THEN user A clicks reply all, generateRecipientsEmailAddressForComposer SHOULD return user A email + user E email + user B email to reply, user C email to cc, user D email to bcc', () { - final expectedResult = Tuple3([userAEmailAddress, userEEmailAddress, userBEmailAddress], [userCEmailAddress], [userDEmailAddress]); + test( + 'THEN user A clicks reply all, generateRecipientsEmailAddressForComposer\n' + 'SHOULD return user A email + user E email + user B email to reply, user C email to cc, user D email to bcc', + () { + final expectedResult = Tuple4( + [userAEmailAddress, userEEmailAddress, userBEmailAddress], + [userCEmailAddress], + [userDEmailAddress], + [], + ); final emailToReply = PresentationEmail( from: {userBEmailAddress}, @@ -143,13 +476,14 @@ void main() { expect(result.value1, containsAll(expectedResult.value1)); expect(result.value2, containsAll(expectedResult.value2)); expect(result.value3, containsAll(expectedResult.value3)); + expect(result.value4, containsAll(expectedResult.value4)); }); test( 'THEN user A click reply to list, generateRecipientsEmailAddressForComposer\n' 'SHOULD return email address in mailto of List-Post to reply\n', () { - final expectedResult = Tuple3([replyToListEmailAddress], [], []); + final expectedResult = Tuple4([replyToListEmailAddress], [], [], []); final emailToReplyToList = PresentationEmail( from: {userBEmailAddress}, @@ -162,18 +496,31 @@ void main() { final result = emailToReplyToList.generateRecipientsEmailAddressForComposer( emailActionType: EmailActionType.replyToList, isSender: false, - listPost: '', + listPost: listPost, ); expect(result.value1, containsAll(expectedResult.value1)); expect(result.value2, containsAll(expectedResult.value2)); expect(result.value3, containsAll(expectedResult.value3)); + expect(result.value4, containsAll(expectedResult.value4)); }); }); - group('Given user A is the sender AND sends an email to user B + user E, cc to user C, bcc to user D THEN user B clicks forward', () { - test('generateRecipientsEmailAddressForComposer SHOULD return user user B email + user E email to reply, user C email to cc, user D email to bcc', () { - final expectedResult = Tuple3([userBEmailAddress, userEEmailAddress], [userCEmailAddress], [userDEmailAddress]); + group( + 'Given user A is the sender\n' + 'AND sends an email to user B + user E, cc to user C, bcc to user D\n' + 'THEN user B clicks forward', + () { + test( + 'generateRecipientsEmailAddressForComposer\n' + 'SHOULD return user user B email + user E email to reply, user C email to cc, user D email to bcc', + () { + final expectedResult = Tuple4( + [userBEmailAddress, userEEmailAddress], + [userCEmailAddress], + [userDEmailAddress], + [], + ); final emailToReply = PresentationEmail( from: {userAEmailAddress}, @@ -190,6 +537,7 @@ void main() { expect(result.value1, containsAll(expectedResult.value1)); expect(result.value2, containsAll(expectedResult.value2)); expect(result.value3, containsAll(expectedResult.value3)); + expect(result.value4, containsAll(expectedResult.value4)); }); }); });