From 74fc8d5d72548bb6c6106afaeeaf64ee3f1b18ce Mon Sep 17 00:00:00 2001 From: Florent Azavant Date: Wed, 30 Oct 2024 16:02:00 +0100 Subject: [PATCH] TF-3189 new `reply to` field in the mail composer --- .../utils/scenario_utils_mixin.dart | 1 + .../presentation/composer_controller.dart | 103 +++++++++++++++++- .../composer/presentation/composer_view.dart | 52 ++++++++- .../presentation/composer_view_web.dart | 72 ++++++++++++ .../create_email_request_extension.dart | 4 +- .../email_action_type_extension.dart | 15 ++- .../prefix_email_address_extension.dart | 4 + .../model/create_email_request.dart | 3 + .../presentation/model/saved_email_draft.dart | 4 + .../presentation/styles/composer_style.dart | 6 +- .../widgets/recipient_composer_widget.dart | 15 ++- lib/l10n/intl_messages.arb | 6 + lib/main/localizations/app_localizations.dart | 7 ++ model/lib/email/prefix_email_address.dart | 3 +- ..._save_email_to_drafts_interactor_test.dart | 2 + ...te_new_and_send_email_interactor_test.dart | 1 + .../composer_controller_test.dart | 37 +++++++ .../create_email_request_extension_test.dart | 1 + .../model/saved_email_draft_test.dart | 33 +++++- .../presentation_email_extension_test.dart | 16 +-- 20 files changed, 354 insertions(+), 31 deletions(-) diff --git a/integration_test/utils/scenario_utils_mixin.dart b/integration_test/utils/scenario_utils_mixin.dart index f9292937c..487476d73 100644 --- a/integration_test/utils/scenario_utils_mixin.dart +++ b/integration_test/utils/scenario_utils_mixin.dart @@ -58,6 +58,7 @@ mixin ScenarioUtilsMixin { toRecipients: {EmailAddress(null, provisioningEmail.toEmail)}, ccRecipients: {}, bccRecipients: {}, + replyToRecipients: {}, outboxMailboxId: mailboxDashBoardController.outboxMailbox?.mailboxId, sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], identity: identity, diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 5671e3be6..f88e7f224 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -126,11 +126,13 @@ class ComposerController extends BaseController final toAddressExpandMode = ExpandMode.EXPAND.obs; final ccAddressExpandMode = ExpandMode.EXPAND.obs; final bccAddressExpandMode = ExpandMode.EXPAND.obs; + final replyToAddressExpandMode = ExpandMode.EXPAND.obs; final emailContentsViewState = Rxn>(); final hasRequestReadReceipt = false.obs; final fromRecipientState = PrefixRecipientState.disabled.obs; final ccRecipientState = PrefixRecipientState.disabled.obs; final bccRecipientState = PrefixRecipientState.disabled.obs; + final replyToRecipientState = PrefixRecipientState.disabled.obs; final identitySelected = Rxn(); final listFromIdentities = RxList(); final isEmailChanged = Rx(false); @@ -157,17 +159,20 @@ class ComposerController extends BaseController List listToEmailAddress = []; List listCcEmailAddress = []; List listBccEmailAddress = []; + List listReplyToEmailAddress = []; ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.tMailContact; final subjectEmailInputController = TextEditingController(); final toEmailAddressController = TextEditingController(); final ccEmailAddressController = TextEditingController(); final bccEmailAddressController = TextEditingController(); + final replyToEmailAddressController = TextEditingController(); final searchIdentitiesInputController = TextEditingController(); final GlobalKey keyToEmailTagEditor = GlobalKey(); final GlobalKey keyCcEmailTagEditor = GlobalKey(); final GlobalKey keyBccEmailTagEditor = GlobalKey(); + final GlobalKey keyReplyToEmailTagEditor = GlobalKey(); final GlobalKey headerEditorMobileWidgetKey = GlobalKey(); final GlobalKey identityDropdownKey = GlobalKey(); final double defaultPaddingCoordinateYCursorEditor = 8; @@ -176,10 +181,12 @@ class ComposerController extends BaseController FocusNode? toAddressFocusNode; FocusNode? ccAddressFocusNode; FocusNode? bccAddressFocusNode; + FocusNode? replyToAddressFocusNode; FocusNode? searchIdentitiesFocusNode; FocusNode? toAddressFocusNodeKeyboard; FocusNode? ccAddressFocusNodeKeyboard; FocusNode? bccAddressFocusNodeKeyboard; + FocusNode? replyToAddressFocusNodeKeyboard; StreamSubscription? _subscriptionOnDragEnter; StreamSubscription? _subscriptionOnDragOver; @@ -305,18 +312,23 @@ class ComposerController extends BaseController ccAddressFocusNode = null; bccAddressFocusNode?.dispose(); bccAddressFocusNode = null; + replyToAddressFocusNode?.dispose(); + replyToAddressFocusNode = null; toAddressFocusNodeKeyboard?.dispose(); toAddressFocusNodeKeyboard = null; ccAddressFocusNodeKeyboard?.dispose(); ccAddressFocusNodeKeyboard = null; bccAddressFocusNodeKeyboard?.dispose(); bccAddressFocusNodeKeyboard = null; + replyToAddressFocusNodeKeyboard?.dispose(); + replyToAddressFocusNodeKeyboard = null; searchIdentitiesFocusNode?.dispose(); searchIdentitiesFocusNode = null; subjectEmailInputController.dispose(); toEmailAddressController.dispose(); ccEmailAddressController.dispose(); bccEmailAddressController.dispose(); + replyToEmailAddressController.dispose(); uploadInlineImageWorker.dispose(); dashboardViewStateWorker.dispose(); scrollController.dispose(); @@ -480,6 +492,7 @@ class ComposerController extends BaseController toRecipients: listToEmailAddress.toSet(), ccRecipients: listCcEmailAddress.toSet(), bccRecipients: listBccEmailAddress.toSet(), + replyToRecipients: listReplyToEmailAddress.toSet(), hasRequestReadReceipt: hasRequestReadReceipt.value, identity: identitySelected.value, attachments: uploadController.attachmentsUploaded, @@ -507,16 +520,21 @@ class ComposerController extends BaseController if (bccEmailAddressController.text.isNotEmpty) { keyBccEmailTagEditor.currentState?.closeSuggestionBox(); } + if (replyToEmailAddressController.text.isNotEmpty) { + keyReplyToEmailTagEditor.currentState?.closeSuggestionBox(); + } } void createFocusNodeInput() { toAddressFocusNode = FocusNode(); ccAddressFocusNode = FocusNode(); bccAddressFocusNode = FocusNode(); + replyToAddressFocusNode = FocusNode(); searchIdentitiesFocusNode = FocusNode(); toAddressFocusNodeKeyboard = FocusNode(); ccAddressFocusNodeKeyboard = FocusNode(); bccAddressFocusNodeKeyboard = FocusNode(); + replyToAddressFocusNodeKeyboard = FocusNode(); subjectEmailInputFocusNode = FocusNode( onKeyEvent: PlatformInfo.isWeb ? _subjectEmailInputOnKeyListener : null, @@ -851,7 +869,7 @@ class ComposerController extends BaseController listCcEmailAddress = List.from(recipients.value2); listBccEmailAddress = List.from(recipients.value3); - if (listToEmailAddress.isNotEmpty || listCcEmailAddress.isNotEmpty || listBccEmailAddress.isNotEmpty) { + if (listToEmailAddress.isNotEmpty || listCcEmailAddress.isNotEmpty || listBccEmailAddress.isNotEmpty || listReplyToEmailAddress.isNotEmpty) { isInitialRecipient.value = true; toAddressExpandMode.value = ExpandMode.COLLAPSE; } @@ -866,6 +884,11 @@ class ComposerController extends BaseController bccAddressExpandMode.value = ExpandMode.COLLAPSE; } + if (listReplyToEmailAddress.isNotEmpty) { + replyToRecipientState.value = PrefixRecipientState.enabled; + replyToAddressExpandMode.value = ExpandMode.COLLAPSE; + } + _updateStatusEmailSendButton(); } @@ -883,6 +906,9 @@ class ComposerController extends BaseController case PrefixEmailAddress.bcc: listBccEmailAddress = List.from(newListEmailAddress); break; + case PrefixEmailAddress.replyTo: + listReplyToEmailAddress = List.from(newListEmailAddress); + break; default: break; } @@ -891,8 +917,9 @@ class ComposerController extends BaseController void _updateStatusEmailSendButton() { if (listToEmailAddress.isNotEmpty + || listCcEmailAddress.isNotEmpty || listBccEmailAddress.isNotEmpty - || listCcEmailAddress.isNotEmpty) { + || listReplyToEmailAddress.isNotEmpty) { isEnableEmailSendButton.value = true; } else { isEnableEmailSendButton.value = false; @@ -910,7 +937,8 @@ class ComposerController extends BaseController if (toEmailAddressController.text.isNotEmpty || ccEmailAddressController.text.isNotEmpty - || bccEmailAddressController.text.isNotEmpty) { + || bccEmailAddressController.text.isNotEmpty + || replyToEmailAddressController.text.isNotEmpty) { _collapseAllRecipient(); _autoCreateEmailTag(); } @@ -927,7 +955,7 @@ class ComposerController extends BaseController return; } - final allListEmailAddress = listToEmailAddress + listCcEmailAddress + listBccEmailAddress; + final allListEmailAddress = listToEmailAddress + listCcEmailAddress + listBccEmailAddress + listReplyToEmailAddress; final listEmailAddressInvalid = allListEmailAddress .where((emailAddress) => !EmailUtils.isEmailAddressValid(emailAddress.emailAddress)) .toList(); @@ -939,6 +967,7 @@ class ComposerController extends BaseController toAddressExpandMode.value = ExpandMode.EXPAND; ccAddressExpandMode.value = ExpandMode.EXPAND; bccAddressExpandMode.value = ExpandMode.EXPAND; + replyToAddressExpandMode.value = ExpandMode.EXPAND; }, showAsBottomSheet: true, title: AppLocalizations.of(context).sending_failed, @@ -1058,6 +1087,7 @@ class ComposerController extends BaseController toRecipients: listToEmailAddress.toSet(), ccRecipients: listCcEmailAddress.toSet(), bccRecipients: listBccEmailAddress.toSet(), + replyToRecipients: listReplyToEmailAddress.toSet(), hasRequestReadReceipt: hasRequestReadReceipt.value, identity: identitySelected.value, attachments: uploadController.attachmentsUploaded, @@ -1276,6 +1306,7 @@ class ComposerController extends BaseController toRecipients: listToEmailAddress.toSet(), ccRecipients: listCcEmailAddress.toSet(), bccRecipients: listBccEmailAddress.toSet(), + replyToRecipients: listReplyToEmailAddress.toSet(), identity: identitySelected.value, attachments: uploadController.attachmentsUploaded, hasReadReceipt: hasRequestReadReceipt.value, @@ -1560,6 +1591,9 @@ class ComposerController extends BaseController case PrefixEmailAddress.bcc: bccRecipientState.value = PrefixRecipientState.enabled; break; + case PrefixEmailAddress.replyTo: + replyToRecipientState.value = PrefixRecipientState.enabled; + break; default: break; } @@ -1578,6 +1612,11 @@ class ComposerController extends BaseController bccAddressFocusNode = FocusNode(); bccEmailAddressController.clear(); break; + case PrefixEmailAddress.replyTo: + replyToRecipientState.value = PrefixRecipientState.disabled; + replyToAddressFocusNode = FocusNode(); + replyToEmailAddressController.clear(); + break; default: break; } @@ -1587,12 +1626,14 @@ class ComposerController extends BaseController toAddressExpandMode.value = ExpandMode.COLLAPSE; ccAddressExpandMode.value = ExpandMode.COLLAPSE; bccAddressExpandMode.value = ExpandMode.COLLAPSE; + replyToAddressExpandMode.value = ExpandMode.COLLAPSE; } void _autoCreateEmailTag() { final inputToEmail = toEmailAddressController.text; final inputCcEmail = ccEmailAddressController.text; final inputBccEmail = bccEmailAddressController.text; + final inputReplyToEmail = replyToEmailAddressController.text; log('ComposerController::_autoCreateEmailTag:inputToEmail = $inputToEmail | inputCcEmail = $inputCcEmail | inputBccEmail = $inputBccEmail'); if (inputToEmail.trim().isNotEmpty) { _autoCreateEmailTagForRecipientField( @@ -1618,6 +1659,9 @@ class ComposerController extends BaseController keyEmailTagEditor: keyBccEmailTagEditor, ); } + if (inputReplyToEmail.isNotEmpty) { + _autoCreateReplyToEmailTag(inputReplyToEmail); + } } bool _isDuplicatedRecipient(String inputEmail, List listEmailAddress) { @@ -1676,6 +1720,20 @@ class ComposerController extends BaseController } } + void _autoCreateReplyToEmailTag(String inputEmail) { + if (!_isDuplicatedRecipient(inputEmail, listReplyToEmailAddress)) { + final emailAddress = EmailAddress(null, inputEmail); + listReplyToEmailAddress.add(emailAddress); + 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(); @@ -1686,6 +1744,9 @@ class ComposerController extends BaseController if (bccEmailAddressController.text.isEmpty) { keyBccEmailTagEditor.currentState?.closeSuggestionBox(); } + if (replyToEmailAddressController.text.isEmpty) { + keyReplyToEmailTagEditor.currentState?.closeSuggestionBox(); + } } void showFullEmailAddress(PrefixEmailAddress prefixEmailAddress) { @@ -1702,6 +1763,10 @@ class ComposerController extends BaseController bccAddressExpandMode.value = ExpandMode.EXPAND; bccAddressFocusNode?.requestFocus(); break; + case PrefixEmailAddress.replyTo: + replyToAddressExpandMode.value = ExpandMode.EXPAND; + replyToAddressFocusNode?.requestFocus(); + break; default: break; } @@ -1719,6 +1784,9 @@ class ComposerController extends BaseController case PrefixEmailAddress.bcc: bccAddressExpandMode.value = ExpandMode.EXPAND; break; + case PrefixEmailAddress.replyTo: + replyToAddressExpandMode.value = ExpandMode.EXPAND; + break; default: break; } @@ -1766,6 +1834,13 @@ class ComposerController extends BaseController ); } break; + case PrefixEmailAddress.replyTo: + replyToAddressExpandMode.value = ExpandMode.COLLAPSE; + final inputReplyToEmail = replyToEmailAddressController.text; + if (inputReplyToEmail.isNotEmpty) { + _autoCreateReplyToEmailTag(inputReplyToEmail); + } + break; default: break; } @@ -2003,12 +2078,14 @@ class ComposerController extends BaseController return toAddressFocusNode?.hasFocus == true || ccAddressFocusNode?.hasFocus == true || bccAddressFocusNode?.hasFocus == true || + replyToAddressFocusNode?.hasFocus == true || subjectEmailInputFocusNode?.hasFocus == true; } else if (PlatformInfo.isMobile) { final isEditorFocused = (await richTextMobileTabletController?.isEditorFocused) ?? false; return toAddressFocusNode?.hasFocus == true || ccAddressFocusNode?.hasFocus == true || bccAddressFocusNode?.hasFocus == true || + replyToAddressFocusNode?.hasFocus == true || subjectEmailInputFocusNode?.hasFocus == true || isEditorFocused; } @@ -2052,6 +2129,8 @@ class ComposerController extends BaseController return ccAddressFocusNode; } else if (bccRecipientState.value == PrefixRecipientState.enabled) { return bccAddressFocusNode; + } else if (replyToRecipientState.value == PrefixRecipientState.enabled) { + return replyToAddressFocusNode; } else { return subjectEmailInputFocusNode; } @@ -2060,6 +2139,16 @@ class ComposerController extends BaseController FocusNode? getNextFocusOfCcEmailAddress() { if (bccRecipientState.value == PrefixRecipientState.enabled) { return bccAddressFocusNode; + } else if (replyToRecipientState.value == PrefixRecipientState.enabled) { + return replyToAddressFocusNode; + } else { + return subjectEmailInputFocusNode; + } + } + + FocusNode? getNextFocusOfBccEmailAddress() { + if (replyToRecipientState.value == PrefixRecipientState.enabled) { + return replyToAddressFocusNode; } else { return subjectEmailInputFocusNode; } @@ -2115,6 +2204,10 @@ class ComposerController extends BaseController listBccEmailAddress.remove(draggableEmailAddress.emailAddress); bccAddressExpandMode.value = ExpandMode.EXPAND; break; + case PrefixEmailAddress.replyTo: + listReplyToEmailAddress.remove(draggableEmailAddress.emailAddress); + replyToAddressExpandMode.value = ExpandMode.EXPAND; + break; default: break; } @@ -2380,6 +2473,7 @@ class ComposerController extends BaseController toRecipients: listToEmailAddress.toSet(), ccRecipients: listCcEmailAddress.toSet(), bccRecipients: listBccEmailAddress.toSet(), + replyToRecipients: listReplyToEmailAddress.toSet(), hasRequestReadReceipt: hasRequestReadReceipt.value, identity: identitySelected.value, attachments: uploadController.attachmentsUploaded, @@ -2461,6 +2555,7 @@ class ComposerController extends BaseController fromRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; ccRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; bccRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; + replyToRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; } void _handleGetEmailContentFailure(GetEmailContentFailure failure) { diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index 0a48ac8f7..7b02dac14 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -132,6 +132,7 @@ class ComposerView extends GetWidget { fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, + replyToState: controller.replyToRecipientState.value, expandMode: controller.toAddressExpandMode.value, controller: controller.toEmailAddressController, focusNode: controller.toAddressFocusNode, @@ -192,9 +193,9 @@ class ComposerView extends GetWidget { focusNode: controller.bccAddressFocusNode, keyTagEditor: controller.keyBccEmailTagEditor, isInitial: controller.isInitialRecipient.value, - nextFocusNode: controller.subjectEmailInputFocusNode, padding: ComposerStyle.mobileRecipientPadding, margin: ComposerStyle.mobileRecipientMargin, + nextFocusNode: controller.getNextFocusOfBccEmailAddress(), onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, onShowFullListEmailAddressAction: controller.showFullEmailAddress, onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, @@ -206,6 +207,33 @@ class ComposerView extends GetWidget { return const SizedBox.shrink(); } }), + Obx(() { + if (controller.replyToRecipientState.value == PrefixRecipientState.enabled) { + return RecipientComposerWidget( + prefix: PrefixEmailAddress.replyTo, + listEmailAddress: controller.listReplyToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, + expandMode: controller.replyToAddressExpandMode.value, + controller: controller.replyToEmailAddressController, + focusNode: controller.replyToAddressFocusNode, + keyTagEditor: controller.keyReplyToEmailTagEditor, + isInitial: controller.isInitialRecipient.value, + padding: ComposerStyle.mobileRecipientPadding, + margin: ComposerStyle.mobileRecipientMargin, + nextFocusNode: controller.subjectEmailInputFocusNode, + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onAddEmailAddressTypeAction: controller.addEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputAction, + ); + } else { + return const SizedBox.shrink(); + } + }), SubjectComposerWidget( focusNode: controller.subjectEmailInputFocusNode, textController: controller.subjectEmailInputController, @@ -297,6 +325,7 @@ class ComposerView extends GetWidget { fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, + replyToState: controller.replyToRecipientState.value, expandMode: controller.toAddressExpandMode.value, controller: controller.toEmailAddressController, focusNode: controller.toAddressFocusNode, @@ -351,6 +380,27 @@ class ComposerView extends GetWidget { focusNode: controller.bccAddressFocusNode, keyTagEditor: controller.keyBccEmailTagEditor, isInitial: controller.isInitialRecipient.value, + nextFocusNode: controller.getNextFocusOfBccEmailAddress(), + padding: ComposerStyle.mobileRecipientPadding, + margin: ComposerStyle.mobileRecipientMargin, + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + ), + if (controller.replyToRecipientState.value == PrefixRecipientState.enabled) + RecipientComposerWidget( + prefix: PrefixEmailAddress.replyTo, + listEmailAddress: controller.listReplyToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, + expandMode: controller.replyToAddressExpandMode.value, + controller: controller.replyToEmailAddressController, + focusNode: controller.replyToAddressFocusNode, + keyTagEditor: controller.keyReplyToEmailTagEditor, + isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, padding: ComposerStyle.mobileRecipientPadding, margin: ComposerStyle.mobileRecipientMargin, diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index a6be47119..21cb1eaeb 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -94,6 +94,7 @@ class ComposerView extends GetWidget { fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, + replyToState: controller.replyToRecipientState.value, expandMode: controller.toAddressExpandMode.value, controller: controller.toEmailAddressController, focusNode: controller.toAddressFocusNode, @@ -152,6 +153,29 @@ class ComposerView extends GetWidget { focusNodeKeyboard: controller.bccAddressFocusNodeKeyboard, keyTagEditor: controller.keyBccEmailTagEditor, isInitial: controller.isInitialRecipient.value, + nextFocusNode: controller.getNextFocusOfCcEmailAddress(), + padding: ComposerStyle.mobileRecipientPadding, + margin: ComposerStyle.mobileRecipientMargin, + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, + ), + if (controller.replyToRecipientState.value == PrefixRecipientState.enabled) + RecipientComposerWidget( + prefix: PrefixEmailAddress.replyTo, + listEmailAddress: controller.listReplyToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, + expandMode: controller.replyToAddressExpandMode.value, + controller: controller.replyToEmailAddressController, + focusNode: controller.replyToAddressFocusNode, + focusNodeKeyboard: controller.replyToAddressFocusNodeKeyboard, + keyTagEditor: controller.keyReplyToEmailTagEditor, + isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, padding: ComposerStyle.mobileRecipientPadding, margin: ComposerStyle.mobileRecipientMargin, @@ -343,6 +367,7 @@ class ComposerView extends GetWidget { fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, + replyToState: controller.replyToRecipientState.value, expandMode: controller.toAddressExpandMode.value, controller: controller.toEmailAddressController, focusNode: controller.toAddressFocusNode, @@ -401,6 +426,29 @@ class ComposerView extends GetWidget { focusNodeKeyboard: controller.bccAddressFocusNodeKeyboard, keyTagEditor: controller.keyBccEmailTagEditor, isInitial: controller.isInitialRecipient.value, + nextFocusNode: controller.getNextFocusOfBccEmailAddress(), + padding: ComposerStyle.desktopRecipientPadding, + margin: ComposerStyle.desktopRecipientMargin, + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, + ), + if (controller.replyToRecipientState.value == PrefixRecipientState.enabled) + RecipientComposerWidget( + prefix: PrefixEmailAddress.replyTo, + listEmailAddress: controller.listReplyToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, + expandMode: controller.replyToAddressExpandMode.value, + controller: controller.replyToEmailAddressController, + focusNode: controller.replyToAddressFocusNode, + focusNodeKeyboard: controller.replyToAddressFocusNodeKeyboard, + keyTagEditor: controller.keyReplyToEmailTagEditor, + isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, padding: ComposerStyle.desktopRecipientPadding, margin: ComposerStyle.desktopRecipientMargin, @@ -629,6 +677,7 @@ class ComposerView extends GetWidget { fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, + replyToState: controller.replyToRecipientState.value, expandMode: controller.toAddressExpandMode.value, controller: controller.toEmailAddressController, focusNode: controller.toAddressFocusNode, @@ -687,6 +736,29 @@ class ComposerView extends GetWidget { focusNodeKeyboard: controller.bccAddressFocusNodeKeyboard, keyTagEditor: controller.keyBccEmailTagEditor, isInitial: controller.isInitialRecipient.value, + nextFocusNode: controller.getNextFocusOfBccEmailAddress(), + padding: ComposerStyle.tabletRecipientPadding, + margin: ComposerStyle.tabletRecipientMargin, + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, + ), + if (controller.replyToRecipientState.value == PrefixRecipientState.enabled) + RecipientComposerWidget( + prefix: PrefixEmailAddress.replyTo, + listEmailAddress: controller.listReplyToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, + expandMode: controller.replyToAddressExpandMode.value, + controller: controller.replyToEmailAddressController, + focusNode: controller.replyToAddressFocusNode, + focusNodeKeyboard: controller.replyToAddressFocusNodeKeyboard, + keyTagEditor: controller.keyReplyToEmailTagEditor, + isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, padding: ComposerStyle.tabletRecipientPadding, margin: ComposerStyle.tabletRecipientMargin, 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 25ad3abb1..c2f0e3d5a 100644 --- a/lib/features/composer/presentation/extensions/create_email_request_extension.dart +++ b/lib/features/composer/presentation/extensions/create_email_request_extension.dart @@ -39,7 +39,9 @@ extension CreateEmailRequestExtension on CreateEmailRequest { } Set createReplyToRecipients() { - if (identity?.replyTo?.isNotEmpty == true) { + if (replyToRecipients.isNotEmpty) { + return replyToRecipients.toSet(); + } else if (identity?.replyTo?.isNotEmpty == true) { return identity!.replyTo!.toSet(); } else { return { EmailAddress(null, session.getOwnEmailAddress()) }; diff --git a/lib/features/composer/presentation/extensions/email_action_type_extension.dart b/lib/features/composer/presentation/extensions/email_action_type_extension.dart index cb89aa4d3..bb773b8d1 100644 --- a/lib/features/composer/presentation/extensions/email_action_type_extension.dart +++ b/lib/features/composer/presentation/extensions/email_action_type_extension.dart @@ -75,10 +75,11 @@ extension EmailActionTypeExtension on EmailActionType { final subject = presentationEmail.subject?.escapeLtGtHtmlString() ?? ''; final receivedAt = presentationEmail.receivedAt; - final fromEmailAddress = presentationEmail.from.toEscapeHtmlStringUseCommaSeparator(); - final toEmailAddress = presentationEmail.to.toEscapeHtmlStringUseCommaSeparator(); - final ccEmailAddress = presentationEmail.cc.toEscapeHtmlStringUseCommaSeparator(); - final bccEmailAddress = presentationEmail.bcc.toEscapeHtmlStringUseCommaSeparator(); + final fromEmailAddress = presentationEmail.from.listEmailAddressToString(isFullEmailAddress: true); + final toEmailAddress = presentationEmail.to.listEmailAddressToString(isFullEmailAddress: true); + final ccEmailAddress = presentationEmail.cc.listEmailAddressToString(isFullEmailAddress: true); + final bccEmailAddress = presentationEmail.bcc.listEmailAddressToString(isFullEmailAddress: true); + final replyToEmailAddress = presentationEmail.replyTo.listEmailAddressToString(isFullEmailAddress: true); if (subject.isNotEmpty) { headerQuoted = headerQuoted @@ -116,6 +117,12 @@ extension EmailActionTypeExtension on EmailActionType { .append(bccEmailAddress) .addNewLineTag(); } + if (replyToEmailAddress.isNotEmpty) { + headerQuoted = headerQuoted + .append('${appLocalizations.reply_to_email_address_prefix}: ') + .append(replyToEmailAddress) + .addNewLineTag(); + } return headerQuoted; default: diff --git a/lib/features/composer/presentation/extensions/prefix_email_address_extension.dart b/lib/features/composer/presentation/extensions/prefix_email_address_extension.dart index b9780fb10..a425c6a62 100644 --- a/lib/features/composer/presentation/extensions/prefix_email_address_extension.dart +++ b/lib/features/composer/presentation/extensions/prefix_email_address_extension.dart @@ -14,6 +14,8 @@ extension PrefixEmailAddressExtension on PrefixEmailAddress { return AppLocalizations.of(context).cc_email_address_prefix; case PrefixEmailAddress.bcc: return AppLocalizations.of(context).bcc_email_address_prefix; + case PrefixEmailAddress.replyTo: + return AppLocalizations.of(context).reply_to_email_address_prefix; case PrefixEmailAddress.from: return AppLocalizations.of(context).from_email_address_prefix; } @@ -27,6 +29,8 @@ extension PrefixEmailAddressExtension on PrefixEmailAddress { return email.cc?.toList() ?? List.empty(); case PrefixEmailAddress.bcc: return email.bcc?.toList() ?? List.empty(); + case PrefixEmailAddress.replyTo: + return email.replyTo?.toList() ?? List.empty(); case PrefixEmailAddress.from: return email.from?.toList() ?? List.empty(); } diff --git a/lib/features/composer/presentation/model/create_email_request.dart b/lib/features/composer/presentation/model/create_email_request.dart index b3f104cfa..d6c82e44b 100644 --- a/lib/features/composer/presentation/model/create_email_request.dart +++ b/lib/features/composer/presentation/model/create_email_request.dart @@ -23,6 +23,7 @@ class CreateEmailRequest with EquatableMixin { final Set toRecipients; final Set ccRecipients; final Set bccRecipients; + final Set replyToRecipients; final Identity? identity; final List? attachments; final Map? inlineAttachments; @@ -47,6 +48,7 @@ class CreateEmailRequest with EquatableMixin { required this.toRecipients, required this.ccRecipients, required this.bccRecipients, + required this.replyToRecipients, this.hasRequestReadReceipt = true, this.identity, this.attachments, @@ -74,6 +76,7 @@ class CreateEmailRequest with EquatableMixin { toRecipients, ccRecipients, bccRecipients, + replyToRecipients, identity, hasRequestReadReceipt, attachments, diff --git a/lib/features/composer/presentation/model/saved_email_draft.dart b/lib/features/composer/presentation/model/saved_email_draft.dart index 47bd1ca4e..536750ede 100644 --- a/lib/features/composer/presentation/model/saved_email_draft.dart +++ b/lib/features/composer/presentation/model/saved_email_draft.dart @@ -9,6 +9,7 @@ class SavedEmailDraft with EquatableMixin { final Set toRecipients; final Set ccRecipients; final Set bccRecipients; + final Set replyToRecipients; final List attachments; final Identity? identity; final bool hasReadReceipt; @@ -19,6 +20,7 @@ class SavedEmailDraft with EquatableMixin { required this.toRecipients, required this.ccRecipients, required this.bccRecipients, + required this.replyToRecipients, required this.attachments, required this.identity, required this.hasReadReceipt, @@ -31,6 +33,7 @@ class SavedEmailDraft with EquatableMixin { toRecipients: {}, ccRecipients: {}, bccRecipients: {}, + replyToRecipients: {}, attachments: [], identity: null, hasReadReceipt: false, @@ -45,6 +48,7 @@ class SavedEmailDraft with EquatableMixin { {0: toRecipients}, {1: ccRecipients}, {2: bccRecipients}, + {3: replyToRecipients}, attachments, identity, hasReadReceipt diff --git a/lib/features/composer/presentation/styles/composer_style.dart b/lib/features/composer/presentation/styles/composer_style.dart index 6c0a88d7f..f7f2a9970 100644 --- a/lib/features/composer/presentation/styles/composer_style.dart +++ b/lib/features/composer/presentation/styles/composer_style.dart @@ -156,10 +156,6 @@ class ComposerStyle { } static double getMaxHeightEmailAddressWidget(BuildContext context, BoxConstraints constraints, ResponsiveUtils responsiveUtils) { - if (responsiveUtils.isDesktop(context)) { - return constraints.maxHeight > 0 ? constraints.maxHeight * 0.3 : 150.0; - } else { - return constraints.maxHeight > 0 ? constraints.maxHeight * 0.4 : 150.0; - } + return constraints.maxHeight > 0 ? constraints.maxHeight * 0.4 : 150.0; } } \ 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 b2c3ce6a5..27b25d3ee 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -47,6 +47,7 @@ class RecipientComposerWidget extends StatefulWidget { final PrefixRecipientState fromState; final PrefixRecipientState ccState; final PrefixRecipientState bccState; + final PrefixRecipientState replyToState; final bool? isInitial; final FocusNode? focusNode; final FocusNode? focusNodeKeyboard; @@ -77,6 +78,7 @@ class RecipientComposerWidget extends StatefulWidget { @visibleForTesting this.isTestingForWeb = false, this.ccState = PrefixRecipientState.disabled, this.bccState = PrefixRecipientState.disabled, + this.replyToState = PrefixRecipientState.disabled, this.fromState = PrefixRecipientState.disabled, this.isInitial, this.controller, @@ -368,6 +370,16 @@ class _RecipientComposerWidgetState extends State { margin: RecipientComposerWidgetStyle.recipientMargin, onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.bcc), ), + if (widget.replyToState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + key: Key('prefix_${widget.prefix.name}_recipient_reply_to_button'), + text: AppLocalizations.of(context).reply_to_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, + backgroundColor: Colors.transparent, + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.replyTo), + ), ] else if (PlatformInfo.isMobile) TMailButtonWidget.fromIcon( @@ -410,7 +422,8 @@ class _RecipientComposerWidgetState extends State { bool get _isAllRecipientInputEnabled => widget.fromState == PrefixRecipientState.enabled && widget.ccState == PrefixRecipientState.enabled - && widget.bccState == PrefixRecipientState.enabled; + && widget.bccState == PrefixRecipientState.enabled + && widget.replyToState == PrefixRecipientState.enabled; List get _collapsedListEmailAddress => _isCollapse ? _currentListEmailAddress.sublist(0, 1) diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 160dd15e9..182e763ca 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -206,6 +206,12 @@ "placeholders_order": [], "placeholders": {} }, + "reply_to_email_address_prefix": "Reply to", + "@reply_to_email_address_prefix": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, "cc_email_address_prefix": "Cc", "@cc_email_address_prefix": { "type": "text", diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 132e90012..fb3d9d39e 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -204,6 +204,13 @@ class AppLocalizations { ); } + String get reply_to_email_address_prefix { + return Intl.message( + 'Reply to', + name: 'reply_to_email_address_prefix', + ); + } + String get cc_email_address_prefix { return Intl.message( 'Cc', diff --git a/model/lib/email/prefix_email_address.dart b/model/lib/email/prefix_email_address.dart index 9b93fbec1..13a99d4aa 100644 --- a/model/lib/email/prefix_email_address.dart +++ b/model/lib/email/prefix_email_address.dart @@ -3,5 +3,6 @@ enum PrefixEmailAddress { from, to, cc, - bcc + bcc, + replyTo } \ No newline at end of file diff --git a/test/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor_test.dart b/test/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor_test.dart index 1b0f1805c..f59149069 100644 --- a/test/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor_test.dart +++ b/test/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor_test.dart @@ -47,6 +47,7 @@ void main() { toRecipients: {}, ccRecipients: {}, bccRecipients: {}, + replyToRecipients: {}, draftsEmailId: EmailId(Id('some-id')) ); when(composerRepository.generateEmail(any, withIdentityHeader: anyNamed('withIdentityHeader'))) @@ -85,6 +86,7 @@ void main() { toRecipients: {}, ccRecipients: {}, bccRecipients: {}, + replyToRecipients: {}, ); when(composerRepository.generateEmail(any, withIdentityHeader: anyNamed('withIdentityHeader'))) .thenAnswer((_) async => Email()); diff --git a/test/features/composer/domain/usecases/create_new_and_send_email_interactor_test.dart b/test/features/composer/domain/usecases/create_new_and_send_email_interactor_test.dart index f5bac5362..8cc95f22e 100644 --- a/test/features/composer/domain/usecases/create_new_and_send_email_interactor_test.dart +++ b/test/features/composer/domain/usecases/create_new_and_send_email_interactor_test.dart @@ -38,6 +38,7 @@ void main() { toRecipients: {}, ccRecipients: {}, bccRecipients: {}, + replyToRecipients: {}, ); when(composerRepository.generateEmail(any, withIdentityHeader: anyNamed('withIdentityHeader'))) .thenAnswer((_) async => Email()); diff --git a/test/features/composer/presentation/composer_controller_test.dart b/test/features/composer/presentation/composer_controller_test.dart index a2c8fee21..21aba3859 100644 --- a/test/features/composer/presentation/composer_controller_test.dart +++ b/test/features/composer/presentation/composer_controller_test.dart @@ -306,6 +306,7 @@ void main() { final toRecipient = EmailAddress('to', 'to@linagora.com'); final ccRecipient = EmailAddress('cc', 'cc@linagora.com'); final bccRecipient = EmailAddress('bcc', 'bcc@linagora.com'); + final replyToRecipient = EmailAddress('replyTo', 'replyTo@linagora.com'); final identity = Identity(); final attachment = Attachment(); const alwaysReadReceiptEnabled = true; @@ -331,6 +332,7 @@ void main() { composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.listReplyToEmailAddress = [replyToRecipient]; composerController?.identitySelected.value = identity; when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); final state = GetAlwaysReadReceiptSettingSuccess( @@ -342,6 +344,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: identity, attachments: [attachment], hasReadReceipt: alwaysReadReceiptEnabled @@ -370,6 +373,7 @@ void main() { composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.listReplyToEmailAddress = [replyToRecipient]; composerController?.identitySelected.value = identity; when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); final state = GetAlwaysReadReceiptSettingFailure(Exception()); @@ -380,6 +384,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: identity, attachments: [attachment], hasReadReceipt: false @@ -407,6 +412,7 @@ void main() { composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.listReplyToEmailAddress = [replyToRecipient]; final selectedIdentity = Identity(id: IdentityId(Id('alice'))); when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); @@ -421,6 +427,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: selectedIdentity, attachments: [attachment], hasReadReceipt: false @@ -451,6 +458,7 @@ void main() { composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.listReplyToEmailAddress = [replyToRecipient]; when(mockMailboxDashBoardController.composerArguments).thenReturn( ComposerArguments(identities: [identity])); when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); @@ -461,6 +469,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: identity, attachments: [attachment], hasReadReceipt: false @@ -491,6 +500,7 @@ void main() { composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.listReplyToEmailAddress = [replyToRecipient]; when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); final selectedIdentity = Identity( @@ -508,6 +518,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: selectedIdentity, attachments: [attachment], hasReadReceipt: false @@ -538,6 +549,7 @@ void main() { composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.listReplyToEmailAddress = [replyToRecipient]; when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); final identity = Identity( @@ -555,6 +567,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: identity, attachments: [attachment], hasReadReceipt: false @@ -594,6 +607,7 @@ void main() { composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.listReplyToEmailAddress = [replyToRecipient]; when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); final selectedIdentity = Identity(id: IdentityId(Id('alice'))); @@ -612,6 +626,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: selectedIdentity, attachments: [attachment], hasReadReceipt: false @@ -665,6 +680,7 @@ void main() { composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.listReplyToEmailAddress = [replyToRecipient]; when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); final selectedIdentity = Identity(id: IdentityId(Id('alice'))); @@ -683,6 +699,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: selectedIdentity, attachments: [attachment], hasReadReceipt: false @@ -735,6 +752,7 @@ void main() { composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.listReplyToEmailAddress = [replyToRecipient]; composerController?.identitySelected.value = identity; when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); @@ -748,6 +766,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: identity, attachments: [attachment], hasReadReceipt: alwaysReadReceiptEnabled @@ -776,6 +795,7 @@ void main() { composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.listReplyToEmailAddress = [replyToRecipient]; composerController?.identitySelected.value = identity; when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); @@ -787,6 +807,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: identity, attachments: [attachment], hasReadReceipt: false @@ -823,6 +844,7 @@ void main() { to: {toRecipient}, cc: {ccRecipient}, bcc: {bccRecipient}, + replyTo: {replyToRecipient}, mailboxContain: PresentationMailbox( MailboxId(Id('some-mailbox-id')), role: PresentationMailbox.roleJunk)), @@ -837,6 +859,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: selectedIdentity, attachments: [attachment], hasReadReceipt: false @@ -876,6 +899,7 @@ void main() { to: {toRecipient}, cc: {ccRecipient}, bcc: {bccRecipient}, + replyTo: {replyToRecipient}, mailboxContain: PresentationMailbox( MailboxId(Id('some-mailbox-id')), role: PresentationMailbox.roleJunk)), @@ -889,6 +913,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: identity, attachments: [attachment], hasReadReceipt: false @@ -930,6 +955,7 @@ void main() { to: {toRecipient}, cc: {ccRecipient}, bcc: {bccRecipient}, + replyTo: {replyToRecipient}, mailboxContain: PresentationMailbox( MailboxId(Id('some-mailbox-id')), role: PresentationMailbox.roleJunk)), @@ -946,6 +972,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: selectedIdentity, attachments: [attachment], hasReadReceipt: false @@ -987,6 +1014,7 @@ void main() { to: {toRecipient}, cc: {ccRecipient}, bcc: {bccRecipient}, + replyTo: {replyToRecipient}, mailboxContain: PresentationMailbox( MailboxId(Id('some-mailbox-id')), role: PresentationMailbox.roleJunk)),)); @@ -1002,6 +1030,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: identity, attachments: [attachment], hasReadReceipt: false @@ -1041,6 +1070,7 @@ void main() { composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.listReplyToEmailAddress = [replyToRecipient]; final selectedIdentity = Identity(id: IdentityId(Id('alice'))); composerController?.identitySelected.value = selectedIdentity; when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); @@ -1060,6 +1090,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: selectedIdentity, attachments: [attachment], hasReadReceipt: false @@ -1113,6 +1144,7 @@ void main() { composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.listReplyToEmailAddress = [replyToRecipient]; final selectedIdentity = Identity(id: IdentityId(Id('alice'))); composerController?.identitySelected.value = selectedIdentity; when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); @@ -1132,6 +1164,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: selectedIdentity, attachments: [attachment], hasReadReceipt: false @@ -1178,6 +1211,7 @@ void main() { composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.listReplyToEmailAddress = [replyToRecipient]; composerController?.hasRequestReadReceipt.value = alwaysReadReceiptEnabled; const idenityId = 'some-identity-id'; @@ -1198,6 +1232,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: identity, attachments: [attachment], hasReadReceipt: alwaysReadReceiptEnabled @@ -1227,6 +1262,7 @@ void main() { composerController?.listToEmailAddress = [toRecipient]; composerController?.listCcEmailAddress = [ccRecipient]; composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.listReplyToEmailAddress = [replyToRecipient]; composerController?.identitySelected.value = identity; when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); composerController?.hasRequestReadReceipt.value = alwaysReadReceiptEnabled; @@ -1237,6 +1273,7 @@ void main() { toRecipients: {toRecipient}, ccRecipients: {ccRecipient}, bccRecipients: {bccRecipient}, + replyToRecipients: {replyToRecipient}, identity: identity, attachments: [attachment], hasReadReceipt: alwaysReadReceiptEnabled diff --git a/test/features/composer/presentation/extensions/create_email_request_extension_test.dart b/test/features/composer/presentation/extensions/create_email_request_extension_test.dart index 84aa86019..956c807a2 100644 --- a/test/features/composer/presentation/extensions/create_email_request_extension_test.dart +++ b/test/features/composer/presentation/extensions/create_email_request_extension_test.dart @@ -19,6 +19,7 @@ void main() { toRecipients: {}, ccRecipients: {}, bccRecipients: {}, + replyToRecipients: {}, ); group('create email request extension test:', () { diff --git a/test/features/composer/presentation/model/saved_email_draft_test.dart b/test/features/composer/presentation/model/saved_email_draft_test.dart index 1544ded79..cbc1dcf8a 100644 --- a/test/features/composer/presentation/model/saved_email_draft_test.dart +++ b/test/features/composer/presentation/model/saved_email_draft_test.dart @@ -16,6 +16,7 @@ void main() { toRecipients: {EmailAddress('to name', 'to email')}, ccRecipients: {EmailAddress('cc name', 'cc email')}, bccRecipients: {EmailAddress('bcc name', 'bcc email')}, + replyToRecipients: {EmailAddress('replyTo name', 'replyTo email')}, identity: null, attachments: [], hasReadReceipt: false @@ -37,7 +38,7 @@ void main() { // arrange const subject = 'subject'; const content = 'content'; - final recipent = EmailAddress('recipent name', 'recipent email'); + final recipient = EmailAddress('recipient name', 'recipient email'); final identity = Identity(); final attachments = []; const hasReadReceipt = false; @@ -45,9 +46,10 @@ void main() { final toSavedEmailDraft = SavedEmailDraft( subject: subject, content: content, - toRecipients: {recipent}, + toRecipients: {recipient}, ccRecipients: {}, bccRecipients: {}, + replyToRecipients: {}, identity: identity, attachments: attachments, hasReadReceipt: hasReadReceipt @@ -57,8 +59,9 @@ void main() { subject: subject, content: content, toRecipients: {}, - ccRecipients: {recipent}, + ccRecipients: {recipient}, bccRecipients: {}, + replyToRecipients: {}, identity: identity, attachments: attachments, hasReadReceipt: hasReadReceipt @@ -69,21 +72,36 @@ void main() { content: content, toRecipients: {}, ccRecipients: {}, - bccRecipients: {recipent}, + bccRecipients: {recipient}, + replyToRecipients: {}, identity: identity, attachments: attachments, hasReadReceipt: hasReadReceipt ); + + final replyToSavedEmailDraft = SavedEmailDraft( + subject: subject, + content: content, + toRecipients: {}, + ccRecipients: {}, + bccRecipients: {}, + replyToRecipients: {recipient}, + identity: identity, + attachments: attachments, + hasReadReceipt: hasReadReceipt + ); // act final toProps = toSavedEmailDraft.props; final ccProps = ccSavedEmailDraft.props; final bccProps = bccSavedEmailDraft.props; - + final replyToProps = replyToSavedEmailDraft.props; + // assert expect(toProps.hashCode, isNot(ccProps.hashCode)); expect(ccProps.hashCode, isNot(bccProps.hashCode)); - expect(bccProps.hashCode, isNot(toProps.hashCode)); + expect(bccProps.hashCode, isNot(replyToProps.hashCode)); + expect(replyToProps.hashCode, isNot(toProps.hashCode)); }); test( @@ -100,6 +118,7 @@ void main() { toRecipients: listToRecipients, ccRecipients: {EmailAddress('cc name', 'cc email')}, bccRecipients: {EmailAddress('bcc name', 'bcc email')}, + replyToRecipients: {EmailAddress('replyTo name', 'replyTo email')}, identity: null, attachments: [], hasReadReceipt: false @@ -125,6 +144,7 @@ void main() { toRecipients: {EmailAddress('to name', 'to email')}, ccRecipients: {EmailAddress('cc name', 'cc email')}, bccRecipients: {EmailAddress('bcc name', 'bcc email')}, + replyToRecipients: {EmailAddress('replyTo name', 'replyTo email')}, identity: null, attachments: [], hasReadReceipt: false @@ -136,6 +156,7 @@ void main() { toRecipients: {EmailAddress('to name', 'to email')}, ccRecipients: {EmailAddress('cc name', 'cc email')}, bccRecipients: {EmailAddress('bcc name', 'bcc email')}, + replyToRecipients: {EmailAddress('replyTo name', 'replyTo email')}, identity: null, attachments: [], hasReadReceipt: false diff --git a/test/model/lib/extensions/presentation_email_extension_test.dart b/test/model/lib/extensions/presentation_email_extension_test.dart index f0fde8440..d8cd039e1 100644 --- a/test/model/lib/extensions/presentation_email_extension_test.dart +++ b/test/model/lib/extensions/presentation_email_extension_test.dart @@ -16,8 +16,8 @@ void main() { final replyToEmailAddress = EmailAddress('Reply To', 'replyToThis@domain.com'); final replyToListEmailAddress = EmailAddress(null, 'replyToList@domain.com'); - group('GIVEN user A is the sender AND send an email to user B and user E, cc to user C, bcc to user D', () { - test('THEN user A click reply, generateRecipientsEmailAddressForComposer SHOULD return user B email + user E email to reply', () { + 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], [], []); final emailToReply = PresentationEmail( @@ -38,7 +38,7 @@ void main() { expect(result.value3, containsAll(expectedResult.value3)); }); - test('THEN user A click 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', () { + 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]); final emailToReply = PresentationEmail( @@ -82,7 +82,7 @@ void main() { expect(result.value3, containsAll(expectedResult.value3)); }); - test('THEN user A click 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', () { + 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]); final emailToReply = PresentationEmail( @@ -104,8 +104,8 @@ void main() { }); }); - group('GIVEN user B is the sender, SENDER does not have 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 user B email to reply', () { + 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], [], []); final emailToReply = PresentationEmail( @@ -125,7 +125,7 @@ void main() { expect(result.value3, containsAll(expectedResult.value3)); }); - test('THEN user A click 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', () { + 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]); final emailToReply = PresentationEmail( @@ -171,7 +171,7 @@ void main() { }); }); - group('Given user A is the sender AND send an email to user B + user E, cc to user C, bcc to user D THEN user B click forward', () { + 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]);