diff --git a/lib/features/identity_creator/presentation/identity_creator_controller.dart b/lib/features/identity_creator/presentation/identity_creator_controller.dart index e829b1d4e..1eaea7f5b 100644 --- a/lib/features/identity_creator/presentation/identity_creator_controller.dart +++ b/lib/features/identity_creator/presentation/identity_creator_controller.dart @@ -12,6 +12,7 @@ import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/model/identity_creator_arguments.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/model/signature_type.dart'; +import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/email_address_validator.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/empty_name_validator.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/state/verify_name_view_state.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart'; @@ -31,6 +32,7 @@ class IdentityCreatorController extends BaseController { final listEmailAddressDefault = [].obs; final listEmailAddressOfReplyTo = [].obs; final errorNameIdentity = Rxn(); + final errorBccIdentity = Rxn(); final emailOfIdentity = Rxn(); final replyToOfIdentity = Rxn(); final bccOfIdentity = Rxn(); @@ -39,6 +41,7 @@ class IdentityCreatorController extends BaseController { final HtmlEditorController signatureHtmlEditorController = HtmlEditorController(processNewLineAsBr: true); final TextEditingController signaturePlainEditorController = TextEditingController(); final TextEditingController inputNameIdentityController = TextEditingController(); + final TextEditingController inputBccIdentityController = TextEditingController(); final FocusNode inputNameIdentityFocusNode = FocusNode(); HtmlEditorApi? signatureHtmlEditorMobileController; @@ -77,6 +80,7 @@ class IdentityCreatorController extends BaseController { signaturePlainEditorController.dispose(); inputNameIdentityFocusNode.dispose(); inputNameIdentityController.dispose(); + inputBccIdentityController.dispose(); super.onClose(); } @@ -141,6 +145,8 @@ class IdentityCreatorController extends BaseController { listEmailAddressOfReplyTo.add(noneEmailAddress); listEmailAddressOfReplyTo.addAll(listEmailAddressDefault); + log('IdentityCreatorController::_getALlIdentitiesSuccess(): listEmailAddressOfReplyTo: ${listEmailAddressOfReplyTo.toJson()}'); + _setUpAllFieldEmailAddress(); } else { _setDefaultEmailAddressList(); @@ -165,24 +171,48 @@ class IdentityCreatorController extends BaseController { void _setUpAllFieldEmailAddress() { if (actionType.value == IdentityActionType.edit && identity != null) { - replyToOfIdentity.value = identity?.replyTo?.isNotEmpty == true - ? identity!.replyTo!.first - : noneEmailAddress; + if (identity?.replyTo?.isNotEmpty == true) { + try { + replyToOfIdentity.value = listEmailAddressOfReplyTo + .firstWhere((emailAddress) => emailAddress == identity?.replyTo!.first); + } catch(e) { + replyToOfIdentity.value = null; + } + } else { + replyToOfIdentity.value = null; + } + log('IdentityCreatorController::_setUpAllFieldEmailAddress(): replyToOfIdentity: ${replyToOfIdentity.value}'); - bccOfIdentity.value = identity?.bcc?.isNotEmpty == true - ? identity!.bcc!.first - : noneEmailAddress; + if (identity?.bcc?.isNotEmpty == true) { + try { + bccOfIdentity.value = listEmailAddressOfReplyTo + .firstWhere((emailAddress) => emailAddress == identity?.bcc!.first); + + inputBccIdentityController.text = bccOfIdentity.value?.email ?? ''; + } catch(e) { + bccOfIdentity.value = null; + } + } else { + bccOfIdentity.value = null; + } + + log('IdentityCreatorController::_setUpAllFieldEmailAddress(): bccOfIdentity: ${bccOfIdentity.value}'); if (identity?.email?.isNotEmpty == true) { - emailOfIdentity.value = EmailAddress(null, identity?.email!); + try { + emailOfIdentity.value = listEmailAddressDefault + .firstWhere((emailAddress) => emailAddress.email == identity?.email); + } catch(e) { + emailOfIdentity.value = null; + } } else { emailOfIdentity.value = listEmailAddressDefault.isNotEmpty ? listEmailAddressDefault.first : null; } } else { - replyToOfIdentity.value = noneEmailAddress; - bccOfIdentity.value = noneEmailAddress; + replyToOfIdentity.value = null; + bccOfIdentity.value = null; emailOfIdentity.value = listEmailAddressDefault.isNotEmpty ? listEmailAddressDefault.first : null; @@ -220,6 +250,8 @@ class IdentityCreatorController extends BaseController { } void createNewIdentity(BuildContext context) async { + clearFocusEditor(context); + final error = _getErrorInputNameString(context); if (error?.isNotEmpty == true) { log('IdentityCreatorController::createNewIdentity(): error: $error'); @@ -227,6 +259,13 @@ class IdentityCreatorController extends BaseController { return; } + final errorBcc = _getErrorInputAddressString(context); + if (errorBcc?.isNotEmpty == true) { + log('IdentityCreatorController::createNewIdentity(): errorBcc: $errorBcc'); + errorBccIdentity.value = errorBcc; + return; + } + final signatureHtmlText = await _getSignatureHtmlText(); log('IdentityCreatorController::createNewIdentity(): signatureHtmlText: $signatureHtmlText'); @@ -246,7 +285,6 @@ class IdentityCreatorController extends BaseController { log('IdentityCreatorController::createNewIdentity(): $newIdentity'); _clearAll(); - clearFocusEditor(context); popBack(result: newIdentity); } @@ -266,9 +304,47 @@ class IdentityCreatorController extends BaseController { ); } + String? _getErrorInputAddressString(BuildContext context, {String? value}) { + final emailAddress = value ?? inputBccIdentityController.text; + if (emailAddress.trim().isEmpty) { + return null; + } + return _verifyNameInteractor.execute( + emailAddress, + [EmailAddressValidator()] + ).fold( + (failure) { + if (failure is VerifyNameFailure) { + return failure.getMessageIdentity(context); + } else { + return null; + } + }, + (success) => null + ); + } + + void validateInputBccAddress(BuildContext context, String? value) { + final errorBcc = _getErrorInputAddressString(context, value: value); + if (errorBccIdentity.value?.isNotEmpty == true + && (errorBcc == null || errorBcc.isEmpty)) { + errorBccIdentity.value = null; + } + } + void _clearAll() { signaturePlainEditorController.clear(); inputNameIdentityController.clear(); + inputBccIdentityController.clear(); + } + + List getSuggestionEmailAddress(String? pattern) { + if (pattern == null || pattern.isEmpty) { + return List.empty(); + } + return listEmailAddressOfReplyTo + .where((emailAddress) => emailAddress.email?.contains(pattern) == true) + .toList(); } void clearFocusEditor(BuildContext context) { diff --git a/lib/features/identity_creator/presentation/identity_creator_view.dart b/lib/features/identity_creator/presentation/identity_creator_view.dart index 9a3c13452..a26f77e5f 100644 --- a/lib/features/identity_creator/presentation/identity_creator_view.dart +++ b/lib/features/identity_creator/presentation/identity_creator_view.dart @@ -4,11 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:html_editor_enhanced/html_editor.dart' as html_editor_browser; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_controller.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/model/signature_type.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_drop_list_field_builder.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_field_no_editable_builder.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_input_field_builder.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/identity_action_type.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -175,14 +177,22 @@ class IdentityCreatorView extends GetWidget { controller.updaterReplyToOfIdentity(emailAddress))) .build())), const SizedBox(width: 24), - Expanded(child: Obx(() => (IdentityDropListFieldBuilder( - _imagePaths, + Expanded(child: Obx(() => (IdentityInputWithDropListFieldBuilder( AppLocalizations.of(context).bcc_to_address, - controller.bccOfIdentity.value, - controller.listEmailAddressOfReplyTo) - ..addOnSelectEmailAddressDropListAction((emailAddress) => - controller.updateBccOfIdentity(emailAddress))) - .build())), + controller.errorBccIdentity.value, + controller.inputBccIdentityController) + ..addOnSelectedSuggestionAction((newEmailAddress) { + controller.inputBccIdentityController.text = newEmailAddress?.email ?? ''; + controller.updateBccOfIdentity(newEmailAddress); + }) + ..addOnChangeInputSuggestionAction((pattern) { + controller.validateInputBccAddress(context, pattern); + controller.updateBccOfIdentity(EmailAddress(null, pattern)); + }) + ..addOnSuggestionCallbackAction((pattern) => + controller.getSuggestionEmailAddress(pattern))) + .build() + )), ]), const SizedBox(height: 32), Row(children: [ @@ -308,14 +318,22 @@ class IdentityCreatorView extends GetWidget { controller.updaterReplyToOfIdentity(newEmailAddress))) .build()), const SizedBox(height: 24), - Obx(() => (IdentityDropListFieldBuilder( - _imagePaths, + Obx(() => (IdentityInputWithDropListFieldBuilder( AppLocalizations.of(context).bcc_to_address, - controller.bccOfIdentity.value, - controller.listEmailAddressOfReplyTo) - ..addOnSelectEmailAddressDropListAction((newEmailAddress) => - controller.updateBccOfIdentity(newEmailAddress))) - .build()), + controller.errorBccIdentity.value, + controller.inputBccIdentityController) + ..addOnSelectedSuggestionAction((newEmailAddress) { + controller.inputBccIdentityController.text = newEmailAddress?.email ?? ''; + controller.updateBccOfIdentity(newEmailAddress); + }) + ..addOnChangeInputSuggestionAction((pattern) { + controller.validateInputBccAddress(context, pattern); + controller.updateBccOfIdentity(EmailAddress(null, pattern)); + }) + ..addOnSuggestionCallbackAction((pattern) => + controller.getSuggestionEmailAddress(pattern))) + .build() + ), const SizedBox(height: 32), Text(AppLocalizations.of(context).signature, style: const TextStyle( diff --git a/lib/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart b/lib/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart new file mode 100644 index 000000000..e52a0240c --- /dev/null +++ b/lib/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart @@ -0,0 +1,94 @@ + +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_input_decoration_builder.dart'; + +typedef OnSelectedSuggestionAction = Function(EmailAddress? emailAddress); +typedef OnSuggestionCallbackAction = Function(String? pattern); +typedef OnChangeInputSuggestionAction = Function(String? pattern); + +class IdentityInputWithDropListFieldBuilder { + + final String _label; + final String? _error; + final TextEditingController editingController; + + OnSelectedSuggestionAction? _onSelectedSuggestionAction; + OnSuggestionCallbackAction? _onSuggestionCallbackAction; + OnChangeInputSuggestionAction? _onChangeInputSuggestionAction; + + IdentityInputWithDropListFieldBuilder( + this._label, + this._error, + this.editingController, + ); + + void addOnSelectedSuggestionAction(OnSelectedSuggestionAction action) { + _onSelectedSuggestionAction = action; + } + + void addOnSuggestionCallbackAction(OnSuggestionCallbackAction action) { + _onSuggestionCallbackAction = action; + } + + void addOnChangeInputSuggestionAction(OnChangeInputSuggestionAction action) { + _onChangeInputSuggestionAction = action; + } + + Widget build() { + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(_label, style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: AppColor.colorContentEmail)), + const SizedBox(height: 8), + TypeAheadFormField( + textFieldConfiguration: TextFieldConfiguration( + controller: editingController, + textInputAction: TextInputAction.done, + decoration: (IdentityInputDecorationBuilder() + ..setContentPadding(const EdgeInsets.symmetric( + vertical: BuildUtils.isWeb ? 16 : 12, + horizontal: 12)) + ..setErrorText(_error)) + .build() + ), + debounceDuration: const Duration(milliseconds: 500), + suggestionsCallback: (pattern) async { + if (_onChangeInputSuggestionAction != null) { + _onChangeInputSuggestionAction!(pattern); + } + if (_onSuggestionCallbackAction != null) { + return _onSuggestionCallbackAction!(pattern); + } else { + return []; + } + }, + itemBuilder: (BuildContext context, emailAddress) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Text(emailAddress.email ?? '', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black)), + ); + }, + onSuggestionSelected: (emailSelected) { + if (_onSelectedSuggestionAction != null) { + _onSelectedSuggestionAction!(emailSelected); + } + }, + suggestionsBoxDecoration: SuggestionsBoxDecoration( + borderRadius: BorderRadius.circular(14) + ), + noItemsFoundBuilder: (context) => const SizedBox(), + hideOnEmpty: true, + hideOnError: true, + hideOnLoading: true, + ) + ]); + } +} \ No newline at end of file diff --git a/lib/features/mailbox_creator/domain/exceptions/verify_name_exception.dart b/lib/features/mailbox_creator/domain/exceptions/verify_name_exception.dart index 11a5d059e..77eb70412 100644 --- a/lib/features/mailbox_creator/domain/exceptions/verify_name_exception.dart +++ b/lib/features/mailbox_creator/domain/exceptions/verify_name_exception.dart @@ -5,6 +5,7 @@ abstract class VerifyNameException extends Equatable implements Exception { static const emptyName = 'The name cannot be empty!'; static const duplicatedName = 'The name already exists!'; static const nameContainSpecialCharacter = 'The name cannot contain special characters'; + static const emailAddressInvalid = 'The email address invalid'; final String? message; @@ -28,6 +29,13 @@ class DuplicatedNameException extends VerifyNameException { class SpecialCharacterException extends VerifyNameException { const SpecialCharacterException() : super(VerifyNameException.nameContainSpecialCharacter); + @override + List get props => []; +} + +class EmailAddressInvalidException extends VerifyNameException { + const EmailAddressInvalidException() : super(VerifyNameException.emailAddressInvalid); + @override List get props => []; } \ No newline at end of file diff --git a/lib/features/mailbox_creator/domain/model/verification/email_address_validator.dart b/lib/features/mailbox_creator/domain/model/verification/email_address_validator.dart new file mode 100644 index 000000000..2785e23b3 --- /dev/null +++ b/lib/features/mailbox_creator/domain/model/verification/email_address_validator.dart @@ -0,0 +1,20 @@ + +import 'package:core/core.dart'; +import 'package:dartz/dartz.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/mailbox_creator/domain/exceptions/verify_name_exception.dart'; +import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/new_name_request.dart'; +import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/validator.dart'; +import 'package:tmail_ui_user/features/mailbox_creator/domain/state/verify_name_view_state.dart'; + +class EmailAddressValidator extends Validator { + + @override + Either validate(NewNameRequest newNameRequest) { + if (newNameRequest.value != null && GetUtils.isEmail(newNameRequest.value!)) { + return Right(VerifyNameViewState()); + } else { + return Left(VerifyNameFailure(const EmailAddressInvalidException())); + } + } +} \ No newline at end of file diff --git a/lib/features/mailbox_creator/presentation/extensions/validator_failure_extension.dart b/lib/features/mailbox_creator/presentation/extensions/validator_failure_extension.dart index fb8469425..ff79e2b21 100644 --- a/lib/features/mailbox_creator/presentation/extensions/validator_failure_extension.dart +++ b/lib/features/mailbox_creator/presentation/extensions/validator_failure_extension.dart @@ -28,6 +28,8 @@ extension ValicatorFailureExtension on VerifyNameFailure { String getMessageIdentity(BuildContext context) { if (exception is EmptyNameException) { return AppLocalizations.of(context).this_field_cannot_be_blank; + } else if (exception is EmailAddressInvalidException) { + return AppLocalizations.of(context).thisEmailAddressInvalid; } else { return ''; } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index fab2658c6..d28eb2e09 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-05-24T09:43:00.761124", + "@@last_modified": "2022-05-26T12:07:26.941919", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -1347,5 +1347,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "thisEmailAddressInvalid": "This email address invalid", + "@thisEmailAddressInvalid": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 336ae7964..cb3d04fe7 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -1380,4 +1380,11 @@ class AppLocalizations { 'Last years', name: 'lastYears'); } + + String get thisEmailAddressInvalid { + return Intl.message( + 'This email address invalid', + name: 'thisEmailAddressInvalid', + ); + } } \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index ae5a5c1eb..23f3575a4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,9 @@ dependencies: # flutter_dotenv flutter_dotenv: 5.0.2 + # flutter_typeahead + flutter_typeahead: 3.2.7 + built_collection: 5.1.1 # jmap_dart_client