From 63894032413ea6645488945cd0210494d2119eb0 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 15 Oct 2024 13:18:50 +0700 Subject: [PATCH] Add suggestion for `From/To` dialog when input is new email address --- .../mixin/auto_complete_result_mixin.dart | 46 ++++++++++ .../presentation/composer_controller.dart | 88 ++++++------------- .../presentation/contact_controller.dart | 64 +++++++------- .../auto_complete_result_mixin_test.dart | 59 +++++++++++++ 4 files changed, 166 insertions(+), 91 deletions(-) create mode 100644 lib/features/base/mixin/auto_complete_result_mixin.dart create mode 100644 test/features/base/mixin/auto_complete_result_mixin_test.dart diff --git a/lib/features/base/mixin/auto_complete_result_mixin.dart b/lib/features/base/mixin/auto_complete_result_mixin.dart new file mode 100644 index 000000000..9119bd85a --- /dev/null +++ b/lib/features/base/mixin/auto_complete_result_mixin.dart @@ -0,0 +1,46 @@ + +import 'dart:async'; + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:get/get_utils/src/get_utils/get_utils.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/get_device_contact_suggestions_state.dart'; + +mixin AutoCompleteResultMixin { + + FutureOr> handleAutoCompleteResultState({ + required Either resultState, + required String queryString, + }) { + return resultState.fold( + (failure) => [], + (success) => handleAutoCompleteSuccess(success, queryString) + ); + } + + List handleAutoCompleteSuccess(Success success, String queryString) { + List listEmailAddress = []; + + if (success is GetAutoCompleteSuccess) { + listEmailAddress = success.listEmailAddress; + } else if (success is GetDeviceContactSuggestionsSuccess) { + listEmailAddress = success.listEmailAddress; + } + + if (listEmailAddress.isEmpty && GetUtils.isEmail(queryString)) { + return [EmailAddress(queryString, queryString)]; + } + + bool isEmailExist = listEmailAddress + .any((email) => email.emailAddress == queryString); + if (GetUtils.isEmail(queryString) && !isEmailExist) { + listEmailAddress.insert(0, EmailAddress(queryString, queryString)); + } + + return listEmailAddress; + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 62a30e80c..b3ec9a78d 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -34,14 +34,13 @@ import 'package:super_tag_editor/tag_editor.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/before_reconnect_handler.dart'; import 'package:tmail_ui_user/features/base/before_reconnect_manager.dart'; +import 'package:tmail_ui_user/features/base/mixin/auto_complete_result_mixin.dart'; import 'package:tmail_ui_user/features/base/state/base_ui_state.dart'; import 'package:tmail_ui_user/features/base/state/button_state.dart'; import 'package:tmail_ui_user/features/composer/domain/exceptions/compose_email_exception.dart'; import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.dart'; import 'package:tmail_ui_user/features/composer/domain/state/download_image_as_base64_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/get_device_contact_suggestions_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/restore_email_inline_images_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; @@ -106,7 +105,9 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/universal_import/html_stub.dart' as html; -class ComposerController extends BaseController with DragDropFileMixin implements BeforeReconnectHandler { +class ComposerController extends BaseController + with DragDropFileMixin, AutoCompleteResultMixin + implements BeforeReconnectHandler { final mailboxDashBoardController = Get.find(); final networkConnectionController = Get.find(); @@ -1105,75 +1106,44 @@ class ComposerController extends BaseController with DragDropFileMixin implement } } - Future> getAutoCompleteSuggestion(String word) async { - log('ComposerController::getAutoCompleteSuggestion(): $word | $_contactSuggestionSource'); + Future> getAutoCompleteSuggestion(String queryString) async { + log('ComposerController::getAutoCompleteSuggestion(): $queryString | $_contactSuggestionSource'); _getAllAutoCompleteInteractor = getBinding(); _getAutoCompleteInteractor = getBinding(); _getDeviceContactSuggestionsInteractor = getBinding(); + final autoCompletePattern = AutoCompletePattern( + word: queryString, + accountId: mailboxDashBoardController.accountId.value); + if (_contactSuggestionSource == ContactSuggestionSource.all) { if (_getAllAutoCompleteInteractor != null) { return await _getAllAutoCompleteInteractor! - .execute(AutoCompletePattern(word: word, accountId: mailboxDashBoardController.accountId.value)) - .then((value) => value.fold( - (failure) => [], - (success) => _getAutoCompleteSuccess(success, word) - )); + .execute(autoCompletePattern) + .then((value) => handleAutoCompleteResultState( + resultState: value, + queryString: queryString, + ) + ); } else if (_getDeviceContactSuggestionsInteractor != null) { return await _getDeviceContactSuggestionsInteractor! - .execute(AutoCompletePattern(word: word, accountId: mailboxDashBoardController.accountId.value)) - .then((value) => value.fold( - (failure) => [], - (success) => _getAutoCompleteSuccess(success, word) - )); + .execute(autoCompletePattern) + .then((value) => handleAutoCompleteResultState( + resultState: value, + queryString: queryString, + ) + ); } else { return []; } } else { - if (_getAutoCompleteInteractor == null) { - return []; - } else { - return await _getAutoCompleteInteractor! - .execute(AutoCompletePattern(word: word, accountId: mailboxDashBoardController.accountId.value)) - .then((value) => value.fold( - (failure) => [], - (success) => _getAutoCompleteSuccess(success, word) - )); - } - } - } - - List _getAutoCompleteSuccess(Success success, String word) { - if (success is GetAutoCompleteSuccess) { - if (success.listEmailAddress.isEmpty == true && GetUtils.isEmail(word)) { - final unknownEmailAddress = EmailAddress(word, word); - return [unknownEmailAddress]; - } - if (GetUtils.isEmail(word)) { - bool isContainsTypedEmail = success.listEmailAddress.any((emailAddress) => emailAddress.email == word); - if (!isContainsTypedEmail) { - final unknownEmailAddress = EmailAddress(word, word); - success.listEmailAddress.insert(0, unknownEmailAddress); - return success.listEmailAddress; - } - } - return success.listEmailAddress; - } else if (success is GetDeviceContactSuggestionsSuccess) { - if (success.listEmailAddress.isEmpty == true && GetUtils.isEmail(word)) { - final unknownEmailAddress = EmailAddress(word, word); - return [unknownEmailAddress]; - } - if (GetUtils.isEmail(word)) { - bool isContainsTypedEmail = success.listEmailAddress.any((emailAddress) => emailAddress.email == word); - if (!isContainsTypedEmail) { - final unknownEmailAddress = EmailAddress(word, word); - success.listEmailAddress.insert(0, unknownEmailAddress); - return success.listEmailAddress; - } - } - return success.listEmailAddress; - } else { - return []; + return await _getAutoCompleteInteractor + ?.execute(autoCompletePattern) + .then((value) => handleAutoCompleteResultState( + resultState: value, + queryString: queryString, + ) + ) ?? []; } } diff --git a/lib/features/contact/presentation/contact_controller.dart b/lib/features/contact/presentation/contact_controller.dart index 283285585..cf4467667 100644 --- a/lib/features/contact/presentation/contact_controller.dart +++ b/lib/features/contact/presentation/contact_controller.dart @@ -16,9 +16,8 @@ import 'package:model/autocomplete/auto_complete_pattern.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; +import 'package:tmail_ui_user/features/base/mixin/auto_complete_result_mixin.dart'; import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/get_device_contact_suggestions_state.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_all_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_device_contact_suggestions_interactor.dart'; @@ -28,8 +27,9 @@ import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/search_status.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; +import 'package:tmail_ui_user/main/utils/app_config.dart'; -class ContactController extends BaseController { +class ContactController extends BaseController with AutoCompleteResultMixin { final TextEditingController textInputSearchController = TextEditingController(); final FocusNode textInputSearchFocus = FocusNode(); @@ -113,14 +113,16 @@ class ContactController extends BaseController { } Future _handleDeBounceTimeSearchContact(String value) async { - if (value.trim().isEmpty) { + final queryStringTrimmed = value.trim(); + + if (queryStringTrimmed.length < AppConfig.limitCharToStartSearch) { searchStatus.value = SearchStatus.INACTIVE; return; } searchStatus.value = SearchStatus.ACTIVE; searchViewState.value = Right(SearchingState()); - searchQuery.value = SearchQuery(value); + searchQuery.value = SearchQuery(queryStringTrimmed); await _searchContactByNameOrEmail(searchQuery.value.value); searchViewState.value = Right(UIState.idle); } @@ -150,46 +152,44 @@ class ContactController extends BaseController { searchedContactList.value = listContact; } - Future> _getAutoCompleteSuggestion(String query) async { + Future> _getAutoCompleteSuggestion(String queryString) async { _getAllAutoCompleteInteractor = getBinding(); _getAutoCompleteInteractor = getBinding(); _getDeviceContactSuggestionsInteractor = getBinding(); + final autoCompletePattern = AutoCompletePattern( + word: queryString, + limit: 30, + accountId: _accountId); + if (_contactSuggestionSource == ContactSuggestionSource.all) { if (_getAllAutoCompleteInteractor != null) { return await _getAllAutoCompleteInteractor! - .execute(AutoCompletePattern(word: query, limit: 30, accountId: _accountId)) - .then((value) => value.fold( - (failure) => [], - (success) => success is GetAutoCompleteSuccess - ? success.listEmailAddress - : [] - )); + .execute(autoCompletePattern) + .then((value) => handleAutoCompleteResultState( + resultState: value, + queryString: queryString, + ) + ); } else if (_getDeviceContactSuggestionsInteractor != null) { return await _getDeviceContactSuggestionsInteractor! - .execute(AutoCompletePattern(word: query, limit: 30, accountId: _accountId)) - .then((value) => value.fold( - (failure) => [], - (success) => success is GetDeviceContactSuggestionsSuccess - ? success.listEmailAddress - : [] - )); + .execute(autoCompletePattern) + .then((value) => handleAutoCompleteResultState( + resultState: value, + queryString: queryString, + ) + ); } else { return []; } } else { - if (_getAutoCompleteInteractor == null) { - return []; - } else { - return await _getAutoCompleteInteractor! - .execute(AutoCompletePattern(word: query, limit: 30, accountId: _accountId)) - .then((value) => value.fold( - (failure) => [], - (success) => success is GetAutoCompleteSuccess - ? success.listEmailAddress - : [] - )); - } + return await _getAutoCompleteInteractor + ?.execute(autoCompletePattern) + .then((value) => handleAutoCompleteResultState( + resultState: value, + queryString: queryString, + ) + ) ?? []; } } diff --git a/test/features/base/mixin/auto_complete_result_mixin_test.dart b/test/features/base/mixin/auto_complete_result_mixin_test.dart new file mode 100644 index 000000000..ee8954cb3 --- /dev/null +++ b/test/features/base/mixin/auto_complete_result_mixin_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:tmail_ui_user/features/base/mixin/auto_complete_result_mixin.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/get_device_contact_suggestions_state.dart'; + +class AutoCompleteResult with AutoCompleteResultMixin {} + +void main() { + late AutoCompleteResult autoCompleteResult; + + setUp(() { + autoCompleteResult = AutoCompleteResult(); + }); + + group('handleAutoCompleteSuccess::test', () { + test('SHOULD return list of EmailAddress from GetAutoCompleteSuccess', () { + final mockEmailAddress = EmailAddress('Test User', 'test@example.com'); + final success = GetAutoCompleteSuccess([mockEmailAddress]); + final result = autoCompleteResult.handleAutoCompleteSuccess(success, 'query'); + + expect(result, [mockEmailAddress]); + }); + + test('SHOULD return list of EmailAddress from GetDeviceContactSuggestionsSuccess', () { + final mockEmailAddress = EmailAddress('Contact User', 'contact@example.com'); + final success = GetDeviceContactSuggestionsSuccess([mockEmailAddress]); + final result = autoCompleteResult.handleAutoCompleteSuccess(success, 'query'); + + expect(result, [mockEmailAddress]); + }); + + test('SHOULD return query email address if list is empty and query is a valid email', () { + final success = GetAutoCompleteSuccess([]); + final result = autoCompleteResult.handleAutoCompleteSuccess(success, 'newemail@example.com'); + + expect(result, [EmailAddress('newemail@example.com', 'newemail@example.com')]); + }); + + test('SHOULD add query email address to the start if not present and is a valid email', () { + final mockEmailAddress = EmailAddress('Existing User', 'existing@example.com'); + final success = GetAutoCompleteSuccess([mockEmailAddress]); + final result = autoCompleteResult.handleAutoCompleteSuccess(success, 'newemail@example.com'); + + expect(result, [ + EmailAddress('newemail@example.com', 'newemail@example.com'), + mockEmailAddress + ]); + }); + + test('SHOULD not add query email address if already present', () { + final mockEmailAddress = EmailAddress('Existing User', 'existing@example.com'); + final success = GetAutoCompleteSuccess([mockEmailAddress]); + final result = autoCompleteResult.handleAutoCompleteSuccess(success, 'existing@example.com'); + + expect(result, [mockEmailAddress]); + }); + }); +}