diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index d29f3fb88a..d9cf7ae556 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -118,7 +118,6 @@ class ComposerView extends GetWidget { expandMode: controller.toAddressExpandMode.value, controller: controller.toEmailAddressController, focusNode: controller.toAddressFocusNode, - autoDisposeFocusNode: false, keyTagEditor: controller.keyToEmailTagEditor, isInitial: controller.isInitialRecipient.value, padding: ComposerStyle.mobileRecipientPadding, @@ -139,7 +138,6 @@ class ComposerView extends GetWidget { expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, - autoDisposeFocusNode: false, keyTagEditor: controller.keyCcEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.getNextFocusOfCcEmailAddress(), @@ -160,7 +158,6 @@ class ComposerView extends GetWidget { expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, - autoDisposeFocusNode: false, keyTagEditor: controller.keyBccEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, @@ -269,7 +266,6 @@ class ComposerView extends GetWidget { expandMode: controller.toAddressExpandMode.value, controller: controller.toEmailAddressController, focusNode: controller.toAddressFocusNode, - autoDisposeFocusNode: false, keyTagEditor: controller.keyToEmailTagEditor, isInitial: controller.isInitialRecipient.value, padding: ComposerStyle.mobileRecipientPadding, @@ -290,7 +286,6 @@ class ComposerView extends GetWidget { expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, - autoDisposeFocusNode: false, keyTagEditor: controller.keyCcEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.getNextFocusOfCcEmailAddress(), @@ -311,7 +306,6 @@ class ComposerView extends GetWidget { expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, - autoDisposeFocusNode: false, keyTagEditor: controller.keyBccEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index e724cc6748..4e105a8d92 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -95,7 +95,6 @@ class ComposerView extends GetWidget { expandMode: controller.toAddressExpandMode.value, controller: controller.toEmailAddressController, focusNode: controller.toAddressFocusNode, - autoDisposeFocusNode: false, keyTagEditor: controller.keyToEmailTagEditor, isInitial: controller.isInitialRecipient.value, padding: ComposerStyle.mobileRecipientPadding, @@ -116,7 +115,6 @@ class ComposerView extends GetWidget { expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, - autoDisposeFocusNode: false, keyTagEditor: controller.keyCcEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.getNextFocusOfCcEmailAddress(), @@ -137,7 +135,6 @@ class ComposerView extends GetWidget { expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, - autoDisposeFocusNode: false, keyTagEditor: controller.keyBccEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, @@ -276,7 +273,6 @@ class ComposerView extends GetWidget { expandMode: controller.toAddressExpandMode.value, controller: controller.toEmailAddressController, focusNode: controller.toAddressFocusNode, - autoDisposeFocusNode: false, keyTagEditor: controller.keyToEmailTagEditor, isInitial: controller.isInitialRecipient.value, padding: ComposerStyle.desktopRecipientPadding, @@ -297,7 +293,6 @@ class ComposerView extends GetWidget { expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, - autoDisposeFocusNode: false, keyTagEditor: controller.keyCcEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.getNextFocusOfCcEmailAddress(), @@ -318,7 +313,6 @@ class ComposerView extends GetWidget { expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, - autoDisposeFocusNode: false, keyTagEditor: controller.keyBccEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, @@ -509,7 +503,6 @@ class ComposerView extends GetWidget { expandMode: controller.toAddressExpandMode.value, controller: controller.toEmailAddressController, focusNode: controller.toAddressFocusNode, - autoDisposeFocusNode: false, keyTagEditor: controller.keyToEmailTagEditor, isInitial: controller.isInitialRecipient.value, padding: ComposerStyle.tabletRecipientPadding, @@ -530,7 +523,6 @@ class ComposerView extends GetWidget { expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, - autoDisposeFocusNode: false, keyTagEditor: controller.keyCcEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.getNextFocusOfCcEmailAddress(), @@ -551,7 +543,6 @@ class ComposerView extends GetWidget { expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, - autoDisposeFocusNode: false, keyTagEditor: controller.keyBccEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index ad77746a20..deb0932c65 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -45,7 +45,6 @@ class RecipientComposerWidget extends StatefulWidget { final PrefixRecipientState bccState; final bool? isInitial; final FocusNode? focusNode; - final bool autoDisposeFocusNode; final GlobalKey? keyTagEditor; final FocusNode? nextFocusNode; final TextEditingController? controller; @@ -71,7 +70,6 @@ class RecipientComposerWidget extends StatefulWidget { this.isInitial, this.controller, this.focusNode, - this.autoDisposeFocusNode = true, this.expandMode = ExpandMode.EXPAND, this.keyTagEditor, this.nextFocusNode, @@ -165,7 +163,6 @@ class _RecipientComposerWidgetState extends State { enableBorder: _isDragging, borderRadius: RecipientComposerWidgetStyle.enableBorderRadius, enableBorderColor: RecipientComposerWidgetStyle.enableBorderColor, - autoDisposeFocusNode: widget.autoDisposeFocusNode, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.done, debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, @@ -240,7 +237,6 @@ class _RecipientComposerWidgetState extends State { length: _collapsedListEmailAddress.length, controller: widget.controller, focusNode: widget.focusNode, - autoDisposeFocusNode: widget.autoDisposeFocusNode, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.done, debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, diff --git a/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart b/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart index db96fa245b..a2d6bd4abe 100644 --- a/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart @@ -50,7 +50,7 @@ class RecipientSuggestionItemWidget extends StatelessWidget { textOrigin: emailAddress.asString(), wordSearched: suggestionValid ?? '' ), - subtitle: emailAddress.emailAddress.isNotEmpty + subtitle: emailAddress.displayName.isNotEmpty ? RichTextWidget( textOrigin: emailAddress.emailAddress, wordSearched: suggestionValid ?? '', @@ -79,7 +79,7 @@ class RecipientSuggestionItemWidget extends StatelessWidget { textOrigin: emailAddress.asString(), wordSearched: suggestionValid ?? '' ), - subtitle: emailAddress.emailAddress.isNotEmpty + subtitle: emailAddress.displayName.isNotEmpty ? RichTextWidget( textOrigin: emailAddress.emailAddress, wordSearched: suggestionValid ?? '', diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index e06c46c451..6d5169970d 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -268,13 +268,12 @@ class EmailView extends GetWidget { ); } - EdgeInsets _getMarginEmailView(BuildContext context) { + EdgeInsetsGeometry _getMarginEmailView(BuildContext context) { if (PlatformInfo.isWeb) { if (responsiveUtils.isDesktop(context)) { - return EdgeInsets.only( - left: AppUtils.isDirectionRTL(context) ? 16 : 0, - right: AppUtils.isDirectionRTL(context) ? 0 : 16, - top: 16, + return const EdgeInsetsDirectional.only( + end: 16, + top: 8, bottom: 16 ); } else { diff --git a/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart b/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart index b65c8f55a1..1a0aa73dd5 100644 --- a/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart +++ b/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart @@ -4,6 +4,7 @@ import 'package:model/email/email_action_type.dart'; import 'package:model/email/presentation_email.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/main/routes/navigation_router.dart'; @@ -30,7 +31,7 @@ class FilterMessageAction extends DashBoardAction { FilterMessageAction(this.context, this.option); @override - List get props => [option]; + List get props => [context, option]; } class HandleEmailActionTypeAction extends DashBoardAction { @@ -42,7 +43,7 @@ class HandleEmailActionTypeAction extends DashBoardAction { HandleEmailActionTypeAction(this.context, this.listEmailSelected, this.emailAction); @override - List get props => [listEmailSelected, emailAction]; + List get props => [context, listEmailSelected, emailAction]; } class OpenEmailDetailedFromSuggestionQuickSearchAction extends DashBoardAction { @@ -53,10 +54,17 @@ class OpenEmailDetailedFromSuggestionQuickSearchAction extends DashBoardAction { OpenEmailDetailedFromSuggestionQuickSearchAction(this.context, this.presentationEmail); @override - List get props => [presentationEmail]; + List get props => [context, presentationEmail]; } -class StartSearchEmailAction extends DashBoardAction {} +class StartSearchEmailAction extends DashBoardAction { + final QuickSearchFilter? filter; + + StartSearchEmailAction({this.filter}); + + @override + List get props => [filter]; +} class EmptyTrashAction extends DashBoardAction { @@ -65,7 +73,7 @@ class EmptyTrashAction extends DashBoardAction { EmptyTrashAction(this.context); @override - List get props => []; + List get props => [context]; } class ClearSearchEmailAction extends DashBoardAction {} diff --git a/lib/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart index 36fe088c78..844c90cf04 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart @@ -8,6 +8,7 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/model.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:super_tag_editor/tag_editor.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart'; @@ -20,7 +21,9 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/das import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/input_field_focus_manager.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/advanced_search_filter.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/datetime_extension.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; @@ -35,11 +38,19 @@ class AdvancedFilterController extends BaseController { final dateFilterSelectedFormAdvancedSearch = EmailReceiveTimeType.allTime.obs; final hasAttachment = false.obs; - final lastTextForm = ''.obs; - final lastTextTo = ''.obs; final startDate = Rxn(); final endDate = Rxn(); + final GlobalKey keyFromEmailTagEditor = GlobalKey(); + final GlobalKey keyToEmailTagEditor = GlobalKey(); + final fromAddressExpandMode = ExpandMode.EXPAND.obs; + final toAddressExpandMode = ExpandMode.EXPAND.obs; + + List listFromEmailAddress = []; + List listToEmailAddress = []; + + TextEditingController fromEmailAddressController = TextEditingController(); + TextEditingController toEmailAddressController = TextEditingController(); TextEditingController subjectFilterInputController = TextEditingController(); TextEditingController hasKeyWordFilterInputController = TextEditingController(); TextEditingController notKeyWordFilterInputController = TextEditingController(); @@ -61,6 +72,7 @@ class AdvancedFilterController extends BaseController { @override void onInit() { _registerWorkerListener(); + _registerFocusListener(); super.onInit(); } @@ -77,17 +89,13 @@ class AdvancedFilterController extends BaseController { super.onReady(); } - void cleanSearchFilter(BuildContext context) { + void clearSearchFilter(BuildContext context) { searchController.clearSearchFilter(); - _updateDateRangeTime(EmailReceiveTimeType.allTime); - subjectFilterInputController.text = ''; - hasKeyWordFilterInputController.text = ''; - notKeyWordFilterInputController.text = ''; - hasAttachment.value = false; - _destinationMailboxSelected = null; + _resetAllToOriginalValue(); + _clearAllTextFieldInput(); searchController.searchInputController.clear(); searchController.deactivateAdvancedSearch(); - searchController.isAdvancedSearchViewOpen.toggle(); + searchController.isAdvancedSearchViewOpen.value = false; _mailboxDashBoardController.searchEmail(context); } @@ -105,17 +113,17 @@ class AdvancedFilterController extends BaseController { searchController.updateFilterEmail(notKeyword: {}); } - if (lastTextForm.isNotEmpty && !searchController.searchEmailFilter.value.from.contains(lastTextForm.value)){ - searchController.updateFilterEmail(fromOption: Some(searchController.searchEmailFilter.value.from..add(lastTextForm.value))); - lastTextForm.value = ''; + if (listFromEmailAddress.isNotEmpty) { + final listAddress = listFromEmailAddress.map((emailAddress) => emailAddress.emailAddress).toSet(); + searchController.updateFilterEmail(fromOption: Some(listAddress)); + } else { + searchController.updateFilterEmail(fromOption: const None()); } - - if (lastTextTo.isNotEmpty && !searchController.searchEmailFilter.value.to.contains(lastTextTo.value)){ - searchController.updateFilterEmail( - to: searchController.searchEmailFilter.value.to..add(lastTextTo.value), - ); - - lastTextTo.value = ''; + if (listToEmailAddress.isNotEmpty) { + final listAddress = listToEmailAddress.map((emailAddress) => emailAddress.emailAddress).toSet(); + searchController.updateFilterEmail(toOption: Some(listAddress)); + } else { + searchController.updateFilterEmail(toOption: const None()); } searchController.updateFilterEmail( @@ -166,7 +174,7 @@ class AdvancedFilterController extends BaseController { if (!isAdvancedSearchHasApplied) { searchController.updateFilterEmail(beforeOption: const None()); } - searchController.isAdvancedSearchViewOpen.toggle(); + searchController.isAdvancedSearchViewOpen.value = false; _mailboxDashBoardController.searchEmail(context); } @@ -256,6 +264,16 @@ class AdvancedFilterController extends BaseController { mailBoxFilterInputController.text = StringConvert.writeNullToEmpty(searchEmailFilter.mailbox?.getDisplayName(context)); } hasAttachment.value = searchEmailFilter.hasAttachment; + if (searchEmailFilter.from.isEmpty) { + listFromEmailAddress.clear(); + } else { + fromAddressExpandMode.value = ExpandMode.COLLAPSE; + } + if (searchEmailFilter.to.isEmpty) { + listToEmailAddress.clear(); + } else { + toAddressExpandMode.value = ExpandMode.COLLAPSE; + } } void selectDateRange(BuildContext context) { @@ -296,13 +314,114 @@ class AdvancedFilterController extends BaseController { } } + void _onFromFieldFocusChange() { + if (focusManager.fromFieldFocusNode.hasFocus) { + fromAddressExpandMode.value = ExpandMode.EXPAND; + toAddressExpandMode.value = ExpandMode.COLLAPSE; + _closeSuggestionBox(); + } else { + fromAddressExpandMode.value = ExpandMode.COLLAPSE; + _autoCreateTagFromField(); + } + } + + void _onToFieldFocusChange() { + if (focusManager.toFieldFocusNode.hasFocus) { + toAddressExpandMode.value = ExpandMode.EXPAND; + fromAddressExpandMode.value = ExpandMode.COLLAPSE; + _closeSuggestionBox(); + } else { + toAddressExpandMode.value = ExpandMode.COLLAPSE; + _autoCreateTagToField(); + } + } + + void _closeSuggestionBox() { + if (fromEmailAddressController.text.isNotEmpty) { + keyFromEmailTagEditor.currentState?.closeSuggestionBox(); + } + + if (toEmailAddressController.text.isNotEmpty) { + keyToEmailTagEditor.currentState?.closeSuggestionBox(); + } + } + + void showFullEmailAddress(AdvancedSearchFilterField field) { + FocusManager.instance.primaryFocus?.unfocus(); + + switch(field) { + case AdvancedSearchFilterField.from: + fromAddressExpandMode.value = ExpandMode.EXPAND; + break; + case AdvancedSearchFilterField.to: + toAddressExpandMode.value = ExpandMode.EXPAND; + break; + default: + break; + } + } + + void updateListEmailAddress( + AdvancedSearchFilterField field, + List listEmailAddress, + ) { + switch(field) { + case AdvancedSearchFilterField.from: + listFromEmailAddress = List.from(listEmailAddress); + break; + case AdvancedSearchFilterField.to: + listToEmailAddress = List.from(listEmailAddress); + break; + default: + break; + } + } + + bool _isDuplicatedEmailAddress(String inputEmail, List listEmailAddress) { + return listEmailAddress + .map((emailAddress) => emailAddress.email) + .whereNotNull() + .contains(inputEmail); + } + + void _autoCreateTagFromField() { + final inputEmail = fromEmailAddressController.text; + if (inputEmail.isEmpty) { + return; + } + + if (!_isDuplicatedEmailAddress(inputEmail, listFromEmailAddress)) { + final emailAddress = EmailAddress(null, inputEmail); + listFromEmailAddress.add(emailAddress); + keyFromEmailTagEditor.currentState?.resetTextField(); + Future.delayed(const Duration(milliseconds: 300), () { + keyFromEmailTagEditor.currentState?.closeSuggestionBox(); + }); + } + } + + void _autoCreateTagToField() { + final inputEmail = toEmailAddressController.text; + if (inputEmail.isEmpty) { + return; + } + + if (!_isDuplicatedEmailAddress(inputEmail, listToEmailAddress)) { + final emailAddress = EmailAddress(null, inputEmail); + listToEmailAddress.add(emailAddress); + keyToEmailTagEditor.currentState?.resetTextField(); + Future.delayed(const Duration(milliseconds: 300), () { + keyToEmailTagEditor.currentState?.closeSuggestionBox(); + }); + } + } + void _resetAllToOriginalValue() { - dateFilterSelectedFormAdvancedSearch.value = EmailReceiveTimeType.allTime; + _updateDateRangeTime(EmailReceiveTimeType.allTime); hasAttachment.value = false; - lastTextForm.value = ''; - lastTextTo.value = ''; - startDate.value = null; - endDate.value = null; + listFromEmailAddress.clear(); + listToEmailAddress.clear(); + _destinationMailboxSelected = null; } void _clearAllTextFieldInput() { @@ -310,6 +429,8 @@ class AdvancedFilterController extends BaseController { hasKeyWordFilterInputController.clear(); notKeyWordFilterInputController.clear(); mailBoxFilterInputController.clear(); + fromEmailAddressController.clear(); + toEmailAddressController.clear(); } void _registerWorkerListener() { @@ -326,27 +447,54 @@ class AdvancedFilterController extends BaseController { ); } else if (action is ClearDateRangeToAdvancedSearch) { _updateDateRangeTime(action.receiveTime); + } else if (action is StartSearchEmailAction) { + if (action.filter == QuickSearchFilter.fromMe) { + _updateFromField(); + } } } ); } + void _registerFocusListener() { + focusManager.fromFieldFocusNode.addListener(_onFromFieldFocusChange); + focusManager.toFieldFocusNode.addListener(_onToFieldFocusChange); + } + void _unregisterWorkerListener() { _dashboardActionWorker.dispose(); } + void _removeFocusListener() { + focusManager.fromFieldFocusNode.removeListener(_onFromFieldFocusChange); + focusManager.toFieldFocusNode.removeListener(_onToFieldFocusChange); + } + void _handleClearAllFieldOfAdvancedSearch() { _resetAllToOriginalValue(); _clearAllTextFieldInput(); } + void _updateFromField() { + final listEmailAddress = searchEmailFilter.from.map((address) => EmailAddress(null, address)).toList(); + listFromEmailAddress = List.from(listEmailAddress); + } + + void onSearchAction(BuildContext context) { + FocusScope.of(context).unfocus(); + applyAdvancedSearchFilter(context); + } + @override void onClose() { + _removeFocusListener(); focusManager.dispose(); subjectFilterInputController.dispose(); hasKeyWordFilterInputController.dispose(); notKeyWordFilterInputController.dispose(); mailBoxFilterInputController.dispose(); + toEmailAddressController.dispose(); + fromEmailAddressController.dispose(); _unregisterWorkerListener(); super.onClose(); } diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index e9628e3513..cd8406d122 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -1505,7 +1505,7 @@ class MailboxDashBoardController extends ReloadableController { void selectQuickSearchFilterAction(QuickSearchFilter filter) { log('MailboxDashBoardController::selectQuickSearchFilterAction(): filter: $filter'); selectQuickSearchFilter(filter); - dispatchAction(StartSearchEmailAction()); + dispatchAction(StartSearchEmailAction(filter: filter)); } void selectReceiveTimeQuickSearchFilter(BuildContext context, EmailReceiveTimeType receiveTime) { diff --git a/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart index e09a493574..7eb06f468d 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; @@ -29,7 +28,6 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_re import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_bottom_sheet.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/search_state.dart'; @@ -40,8 +38,6 @@ class SearchController extends BaseController with DateRangePickerMixin { final SaveRecentSearchInteractor _saveRecentSearchInteractor; final GetAllRecentSearchLatestInteractor _getAllRecentSearchLatestInteractor; - final ResponsiveUtils _responsiveUtils = Get.find(); - final searchInputController = TextEditingController(); final searchEmailFilter = SearchEmailFilter.initial().obs; final searchState = SearchState.initial().obs; @@ -60,8 +56,12 @@ class SearchController extends BaseController with DateRangePickerMixin { this._getAllRecentSearchLatestInteractor, ); - void selectOpenAdvanceSearch() { - isAdvancedSearchViewOpen.toggle(); + void openAdvanceSearch() { + isAdvancedSearchViewOpen.value = true; + } + + void closeAdvanceSearch() { + isAdvancedSearchViewOpen.value = false; } void clearSearchFilter() { @@ -79,10 +79,8 @@ class SearchController extends BaseController with DateRangePickerMixin { updateFilterEmail(emailReceiveTimeType: EmailReceiveTimeType.allTime); return; case QuickSearchFilter.fromMe: - isFilterSelected - ? searchEmailFilter.value.from.removeWhere((e) => e == userProfile.email) - : searchEmailFilter.value.from.add(userProfile.email); - updateFilterEmail(fromOption: Some(searchEmailFilter.value.from)); + final newListEmailAddress = isFilterSelected ? {} : {userProfile.email}; + updateFilterEmail(fromOption: Some(newListEmailAddress)); return; } } @@ -172,7 +170,7 @@ class SearchController extends BaseController with DateRangePickerMixin { void updateFilterEmail({ Option>? fromOption, - Set? to, + Option>? toOption, SearchQuery? text, Option? subjectOption, Set? notKeyword, @@ -185,7 +183,7 @@ class SearchController extends BaseController with DateRangePickerMixin { }) { searchEmailFilter.value = searchEmailFilter.value.copyWith( fromOption: fromOption, - to: to, + toOption: toOption, text: text, subjectOption: subjectOption, notKeyword: notKeyword, @@ -244,14 +242,6 @@ class SearchController extends BaseController with DateRangePickerMixin { : [])); } - void showAdvancedFilterView(BuildContext context) async { - selectOpenAdvanceSearch(); - if (_responsiveUtils.isMobile(context)) { - await showAdvancedSearchFilterBottomSheet(context); - selectOpenAdvanceSearch(); - } - } - void activateSimpleSearch() { simpleSearchIsActivated.value = true; } diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index 948cd53069..605b880d9d 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -535,11 +535,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { return Obx(() { if (supportListButtonQuickSearchFilter(context)) { return Padding( - padding: EdgeInsets.only( - right: AppUtils.isDirectionRTL(context) ? 0 : 16, - left: AppUtils.isDirectionRTL(context) ? 16 : 0, - top: 16 - ), + padding: const EdgeInsetsDirectional.only(end: 16, top: 8), child: Row(children: QuickSearchFilter.values .map((filter) => _buildQuickSearchFilterButton(context, filter)) .toList() diff --git a/lib/features/mailbox_dashboard/presentation/model/search/advanced_search_filter.dart b/lib/features/mailbox_dashboard/presentation/model/search/advanced_search_filter.dart index dfdbebc524..d6d4b1b846 100644 --- a/lib/features/mailbox_dashboard/presentation/model/search/advanced_search_filter.dart +++ b/lib/features/mailbox_dashboard/presentation/model/search/advanced_search_filter.dart @@ -3,7 +3,7 @@ import 'package:flutter/cupertino.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; enum AdvancedSearchFilterField { - form, + from, to, subject, hasKeyword, @@ -14,7 +14,7 @@ enum AdvancedSearchFilterField { String getTitle(BuildContext context) { switch (this) { - case AdvancedSearchFilterField.form: + case AdvancedSearchFilterField.from: return AppLocalizations.of(context).form; case AdvancedSearchFilterField.to: return AppLocalizations.of(context).to; @@ -35,7 +35,7 @@ enum AdvancedSearchFilterField { String getHintText(BuildContext context) { switch (this) { - case AdvancedSearchFilterField.form: + case AdvancedSearchFilterField.from: case AdvancedSearchFilterField.to: return AppLocalizations.of(context).nameOrEmailAddress; case AdvancedSearchFilterField.subject: diff --git a/lib/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart b/lib/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart index 9789f8ad46..8a3e90bdaa 100644 --- a/lib/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart +++ b/lib/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart @@ -47,7 +47,7 @@ class SearchEmailFilter with EquatableMixin { SearchEmailFilter copyWith({ Option>? fromOption, - Set? to, + Option>? toOption, SearchQuery? text, Option? subjectOption, Set? notKeyword, @@ -60,7 +60,7 @@ class SearchEmailFilter with EquatableMixin { }) { return SearchEmailFilter( from: _getOptionParam(fromOption, from), - to: to ?? this.to, + to: _getOptionParam(toOption, to), text: text ?? this.text, subject: _getOptionParam(subjectOption, subject), notKeyword: notKeyword ?? this.notKeyword, diff --git a/lib/features/mailbox_dashboard/presentation/model/search/suggesstion_email_address.dart b/lib/features/mailbox_dashboard/presentation/model/search/suggesstion_email_address.dart new file mode 100644 index 0000000000..c46348b61a --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/model/search/suggesstion_email_address.dart @@ -0,0 +1,17 @@ +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; + +class SuggestionEmailAddress with EquatableMixin { + final EmailAddress emailAddress; + final SuggestionEmailState state; + + SuggestionEmailAddress(this.emailAddress, {this.state = SuggestionEmailState.valid}); + + @override + List get props => [emailAddress, state]; +} + +enum SuggestionEmailState { + valid, + duplicated, +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/styles/autocomplete_suggestion_item_web_style.dart b/lib/features/mailbox_dashboard/presentation/styles/autocomplete_suggestion_item_web_style.dart new file mode 100644 index 0000000000..7887066608 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/styles/autocomplete_suggestion_item_web_style.dart @@ -0,0 +1,27 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class AutoCompleteSuggestionItemWebStyle { + static const double iconSize = 24.0; + + static const EdgeInsetsGeometry margin = EdgeInsets.all(8.0); + + static const EdgeInsetsGeometry contentPaddingDuplicated = EdgeInsets.symmetric(horizontal: 8.0); + static const EdgeInsetsGeometry contentPaddingValid = EdgeInsets.symmetric(horizontal: 16.0); + + static const Decoration decoration = BoxDecoration( + color: AppColor.colorBgMenuItemDropDownSelected, + borderRadius: BorderRadius.all(Radius.circular(20)) + ); + + static const TextStyle subTitleTextOriginStyle = TextStyle( + color: AppColor.colorHintSearchBar, + fontSize: 13, + fontWeight: FontWeight.normal + ); + static const TextStyle subTitleWordSearchStyle = TextStyle( + color: Colors.black, + fontSize: 13, + fontWeight: FontWeight.bold + ); +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/styles/autocomplete_tag_item_web_style.dart b/lib/features/mailbox_dashboard/presentation/styles/autocomplete_tag_item_web_style.dart new file mode 100644 index 0000000000..806762794d --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/styles/autocomplete_tag_item_web_style.dart @@ -0,0 +1,26 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class AutoCompleteTagItemWebStyle { + static const double paddingTop = 4.5; + static const double paddingEnd = 40.0; + static const double labelPaddingHorizontal = 4.0; + static const EdgeInsetsGeometry collapsedPadding = EdgeInsetsDirectional.symmetric(vertical: 5, horizontal: 8); + + static const EdgeInsetsGeometry marginCollapsed = EdgeInsets.only(top: 5.5); + + static const BorderRadius shapeBorderRadius = BorderRadius.all(Radius.circular(10.0)); + + static const Color collapsedBackgroundColor = AppColor.colorEmailAddressTag; + + static const TextStyle labelTextStyle = TextStyle( + color: Colors.black, + fontSize: 17, + fontWeight: FontWeight.w400 + ); + static const TextStyle collapsedTextStyle = TextStyle( + color: Colors.black, + fontSize: 17, + fontWeight: FontWeight.w400 + ); +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/styles/text_field_autocomplete_email_address_web_style.dart b/lib/features/mailbox_dashboard/presentation/styles/text_field_autocomplete_email_address_web_style.dart new file mode 100644 index 0000000000..afe00e5bad --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/styles/text_field_autocomplete_email_address_web_style.dart @@ -0,0 +1,47 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class TextFieldAutoCompleteEmailAddressWebStyles { + static const EdgeInsetsGeometry padding = EdgeInsets.symmetric(vertical: 8); + static const EdgeInsetsGeometry textInputContentPadding = EdgeInsetsDirectional.only(top: 16, bottom: 16, start: 12); + static const EdgeInsetsGeometry textInputContentPaddingWithSomeTag = EdgeInsetsDirectional.symmetric(vertical: 16); + static const EdgeInsets tagEditorPadding = EdgeInsets.symmetric(horizontal: 12); + + static const double borderRadius = 10.0; + static const double suggestionBoxRadius = 20.0; + + static const double borderWidth = 1.0; + static const double minTextFieldWidth = 40.0; + + static const double suggestionBoxElevation = 20.0; + static const double suggestionBoxMaxHeight = 350.0; + + static const double fieldTitleWidth = 112.0; + static const double space = 8.0; + + static const InputBorder textInputBorder = OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + borderSide: BorderSide.none, + ); + + static const Color textInputFillColor = Colors.white; + static const Color focusedBorderColor = AppColor.primaryColor; + static const Color enableBorderColor = AppColor.colorInputBorderCreateMailbox; + static const Color suggestionBoxBackgroundColor = Colors.white; + + static const TextStyle fieldTitleTextStyle = TextStyle( + color: AppColor.colorContentEmail, + fontSize: 14, + ); + static const TextStyle textInputHintStyle = TextStyle( + fontSize: 16, + color: AppColor.colorHintSearchBar, + ); + static const TextStyle textInputTextStyle = TextStyle( + fontSize: 16, + color: Colors.black, + fontWeight: FontWeight.w500, + ); + + static const Duration debounceDuration = Duration(milliseconds: 150); +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_bottom_sheet.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_bottom_sheet.dart deleted file mode 100644 index cd7c5d7b23..0000000000 --- a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_bottom_sheet.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:core/core.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_input_form.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -Future showAdvancedSearchFilterBottomSheet(BuildContext context) async { - final ImagePaths imagePaths = Get.find(); - - await FullScreenActionSheetBuilder( - context: context, - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 12), - child: AdvancedSearchInputForm(), - ), - ), - cancelWidget: Padding( - padding: const EdgeInsets.only(right: 16), - child: SvgPicture.asset( - imagePaths.icCircleClose, - colorFilter: AppColor.colorHintSearchBar.asFilter(), - width: 24, - height: 24, - ), - ), - titleWidget: Text( - AppLocalizations.of(context).advancedSearch, - style: const TextStyle( - fontSize: 20, - color: AppColor.colorNameEmail, - ), - ), - ).show(); -} diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form_bottom_view.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form_bottom_view.dart index 3626a3504f..f170a5607c 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form_bottom_view.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form_bottom_view.dart @@ -47,7 +47,7 @@ class AdvancedSearchFilterFormBottomView extends GetWidget return FocusTraversalGroup( child: Column( children: [ - _buildSuggestionFilterField( - listTagSelected: controller.searchEmailFilter.from, - context: context, - advancedSearchFilterField: AdvancedSearchFilterField.form, - listTagInitial: controller.searchEmailFilter.from, - currentFocusNode: controller.focusManager.fromFieldFocusNode, - nextFocusNode: controller.focusManager.toFieldFocusNode - ), - _buildSuggestionFilterField( - listTagSelected: controller.searchEmailFilter.to, - context: context, - advancedSearchFilterField: AdvancedSearchFilterField.to, - listTagInitial: controller.searchEmailFilter.to, - currentFocusNode: controller.focusManager.toFieldFocusNode, - nextFocusNode: controller.focusManager.subjectFieldFocusNode - ), + Obx(() => TextFieldAutocompleteEmailAddressWeb( + field: AdvancedSearchFilterField.from, + listEmailAddress: controller.listFromEmailAddress, + expandMode: controller.fromAddressExpandMode.value, + controller: controller.fromEmailAddressController, + focusNode: controller.focusManager.fromFieldFocusNode, + nextFocusNode: controller.focusManager.toFieldFocusNode, + keyTagEditor: controller.keyFromEmailTagEditor, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onSearchAction: () => controller.onSearchAction.call(context), + )), + Obx(() => TextFieldAutocompleteEmailAddressWeb( + field: AdvancedSearchFilterField.to, + listEmailAddress: controller.listToEmailAddress, + expandMode: controller.toAddressExpandMode.value, + controller: controller.toEmailAddressController, + focusNode: controller.focusManager.toFieldFocusNode, + nextFocusNode: controller.focusManager.subjectFieldFocusNode, + keyTagEditor: controller.keyToEmailTagEditor, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onSearchAction: () => controller.onSearchAction.call(context), + )), _buildFilterField( textEditingController: controller.subjectFilterInputController, context: context, @@ -233,114 +243,6 @@ class AdvancedSearchInputForm extends GetWidget } } - Widget _buildSuggestionFilterField({ - required AdvancedSearchFilterField advancedSearchFilterField, - required BuildContext context, - required Set listTagSelected, - required Set listTagInitial, - FocusNode? currentFocusNode, - FocusNode? nextFocusNode, - }) { - final child = [ - SizedBox( - width: _responsiveUtils.isMobile(context) || _responsiveUtils.landscapeTabletSupported(context) - ? null : 112, - child: Text( - advancedSearchFilterField.getTitle(context), - style: const TextStyle( - fontSize: 14, - color: AppColor.colorContentEmail, - ), - ), - ), - const Padding(padding: EdgeInsets.all(4)), - _responsiveUtils.isMobile(context) || _responsiveUtils.landscapeTabletSupported(context) - ? TextFieldAutocompleteEmailAddress( - optionsBuilder: controller.getAutoCompleteSuggestion, - advancedSearchFilterField: advancedSearchFilterField, - initialTags: listTagInitial, - currentFocusNode: currentFocusNode, - nextFocusNode: nextFocusNode, - onAddTag: (value) { - if (advancedSearchFilterField == AdvancedSearchFilterField.form) { - controller.searchEmailFilter.from.add(value.trim()); - controller.lastTextForm.value = ''; - } - if (advancedSearchFilterField == AdvancedSearchFilterField.to) { - controller.searchEmailFilter.to.add(value.trim()); - controller.lastTextTo.value = ''; - } - }, - onDeleteTag: (tag) { - if (advancedSearchFilterField == AdvancedSearchFilterField.form) { - controller.searchEmailFilter.from.remove(tag); - controller.lastTextForm.value = ''; - } - if (advancedSearchFilterField == AdvancedSearchFilterField.to) { - controller.searchEmailFilter.to.remove(tag); - controller.lastTextTo.value = ''; - } - }, - onChange: (value) { - if (advancedSearchFilterField == AdvancedSearchFilterField.form) { - controller.lastTextForm.value = value.trim(); - } - if (advancedSearchFilterField == AdvancedSearchFilterField.to) { - controller.lastTextTo.value = value.trim(); - } - }, - ) - : Expanded( - child: TextFieldAutocompleteEmailAddress( - optionsBuilder: controller.getAutoCompleteSuggestion, - advancedSearchFilterField: advancedSearchFilterField, - initialTags: listTagInitial, - currentFocusNode: currentFocusNode, - nextFocusNode: nextFocusNode, - onAddTag: (value) { - if (advancedSearchFilterField == AdvancedSearchFilterField.form) { - controller.searchEmailFilter.from.add(value.trim()); - controller.lastTextForm.value = ''; - } - if (advancedSearchFilterField == AdvancedSearchFilterField.to) { - controller.searchEmailFilter.to.add(value.trim()); - controller.lastTextTo.value = ''; - } - }, - onChange: (value) { - if (advancedSearchFilterField == AdvancedSearchFilterField.form) { - controller.lastTextForm.value = value.trim(); - } - if (advancedSearchFilterField == AdvancedSearchFilterField.to) { - controller.lastTextTo.value = value.trim(); - } - }, - onDeleteTag: (tag) { - if (advancedSearchFilterField == AdvancedSearchFilterField.form) { - controller.searchEmailFilter.from.remove(tag); - controller.lastTextForm.value = ''; - } - if (advancedSearchFilterField == AdvancedSearchFilterField.to) { - controller.searchEmailFilter.to.remove(tag); - controller.lastTextTo.value = ''; - } - }, - ), - ) - ]; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: _responsiveUtils.isMobile(context) || _responsiveUtils.landscapeTabletSupported(context) - ? Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: child, - ) - : SizedBox(height: 44, child: Row(children: child)), - ); - } - Widget _buildTextField({ required BuildContext context, required AdvancedSearchFilterField advancedSearchFilterField, @@ -371,18 +273,14 @@ class AdvancedSearchInputForm extends GetWidget if (isSelectFormList) { onTap?.call(); } else { - FocusScope.of(context).unfocus(); - controller.applyAdvancedSearchFilter(context); + controller.onSearchAction.call(context); popBack(); } }, decoration: InputDecoration( filled: true, fillColor: isSelectFormList ? AppColor.colorItemSelected : Colors.white, - contentPadding: const EdgeInsets.only( - right: 8, - left: 12, - ), + contentPadding: const EdgeInsetsDirectional.symmetric(horizontal: 12), enabledBorder: OutlineInputBorder( borderRadius: const BorderRadius.all( Radius.circular(10), @@ -401,6 +299,15 @@ class AdvancedSearchInputForm extends GetWidget color: AppColor.colorInputBorderCreateMailbox, ), ), + focusedBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all( + Radius.circular(10), + ), + borderSide: BorderSide( + width: isSelectFormList ? 0.5 : 1, + color: AppColor.primaryColor, + ), + ), hintText: advancedSearchFilterField.getHintText(context), hintStyle: TextStyle( fontSize: 16, diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_suggestion_item_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_suggestion_item_widget.dart deleted file mode 100644 index 263dcde2cd..0000000000 --- a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_suggestion_item_widget.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:core/presentation/utils/style_utils.dart'; -import 'package:flutter/material.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:model/extensions/email_address_extension.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/autocomplete_suggestion_item_style.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/avatar_suggestion_item_widget.dart'; - -typedef OnSelectSuggestionItemCallback = Function(String emailAddress); - -class AutocompleteSuggestionItemWidget extends StatelessWidget { - - final EmailAddress emailAddress; - final OnSelectSuggestionItemCallback onSelectCallback; - - const AutocompleteSuggestionItemWidget({ - super.key, - required this.emailAddress, - required this.onSelectCallback, - }); - - @override - Widget build(BuildContext context) { - return Material( - color: Colors.transparent, - child: InkWell( - onTap: () => onSelectCallback(emailAddress.emailAddress), - child: Container( - padding: AutocompleteSuggestionItemStyle.padding, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AvatarSuggestionItemWidget(emailAddress: emailAddress), - const SizedBox(width: AutocompleteSuggestionItemStyle.space), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (emailAddress.displayName.isNotEmpty) - Text( - emailAddress.displayName, - maxLines: 1, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, - style: AutocompleteSuggestionItemStyle.displayNameTextStyle - ), - if (emailAddress.emailAddress.isNotEmpty) - Text( - emailAddress.emailAddress, - maxLines: 1, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, - style: AutocompleteSuggestionItemStyle.emailAddressNameTextStyle - ) - ] - ) - ), - ] - ), - ), - ), - ); - } -} diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_suggestion_item_widget_web.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_suggestion_item_widget_web.dart new file mode 100644 index 0000000000..669894fd8c --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_suggestion_item_widget_web.dart @@ -0,0 +1,91 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/extensions/email_address_extension.dart'; +import 'package:super_tag_editor/widgets/rich_text_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/suggesstion_email_address.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/autocomplete_suggestion_item_web_style.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/avatar_suggestion_item_widget.dart'; + +typedef OnSelectedRecipientSuggestionAction = Function(EmailAddress emailAddress); + +class AutoCompleteSuggestionItemWidgetWeb extends StatelessWidget { + + final SuggestionEmailState suggestionState; + final EmailAddress emailAddress; + final String? suggestionValid; + final bool highlight; + final OnSelectedRecipientSuggestionAction? onSelectedAction; + + final _imagePaths = Get.find(); + + AutoCompleteSuggestionItemWidgetWeb({ + Key? key, + required this.suggestionState, + required this.emailAddress, + this.highlight = false, + this.suggestionValid, + this.onSelectedAction, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (suggestionState == SuggestionEmailState.duplicated) { + return Container( + margin: AutoCompleteSuggestionItemWebStyle.margin, + decoration: AutoCompleteSuggestionItemWebStyle.decoration, + child: Material( + type: MaterialType.transparency, + child: ListTile( + contentPadding: AutoCompleteSuggestionItemWebStyle.contentPaddingDuplicated, + leading: AvatarSuggestionItemWidget(emailAddress: emailAddress), + title: RichTextWidget( + textOrigin: emailAddress.asString(), + wordSearched: suggestionValid ?? '' + ), + subtitle: emailAddress.displayName.isNotEmpty + ? RichTextWidget( + textOrigin: emailAddress.emailAddress, + wordSearched: suggestionValid ?? '', + styleTextOrigin: AutoCompleteSuggestionItemWebStyle.subTitleTextOriginStyle, + styleWordSearched: AutoCompleteSuggestionItemWebStyle.subTitleWordSearchStyle, + ) + : null, + trailing: SvgPicture.asset( + _imagePaths.icFilterSelected, + width: AutoCompleteSuggestionItemWebStyle.iconSize, + height: AutoCompleteSuggestionItemWebStyle.iconSize, + fit: BoxFit.fill, + ), + ), + ), + ); + } else { + return Container( + color: highlight ? AppColor.colorItemSelected : Colors.white, + child: Material( + type: MaterialType.transparency, + child: ListTile( + contentPadding: AutoCompleteSuggestionItemWebStyle.contentPaddingValid, + leading: AvatarSuggestionItemWidget(emailAddress: emailAddress), + title: RichTextWidget( + textOrigin: emailAddress.asString(), + wordSearched: suggestionValid ?? '' + ), + subtitle: emailAddress.displayName.isNotEmpty + ? RichTextWidget( + textOrigin: emailAddress.emailAddress, + wordSearched: suggestionValid ?? '', + styleTextOrigin: AutoCompleteSuggestionItemWebStyle.subTitleTextOriginStyle, + styleWordSearched: AutoCompleteSuggestionItemWebStyle.subTitleWordSearchStyle, + ) + : null, + onTap: () => onSelectedAction?.call(emailAddress), + ), + ), + ); + } + } +} diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_tag_item_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_tag_item_widget.dart deleted file mode 100644 index 18d5800876..0000000000 --- a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_tag_item_widget.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:core/presentation/resources/image_paths.dart'; -import 'package:core/presentation/utils/style_utils.dart'; -import 'package:core/presentation/views/button/tmail_button_widget.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/autocomplete_tag_item_style.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/avatar_tag_item_widget.dart'; - -typedef OnDeleteTagItemCallback = Function(String tagName); - -class AutocompleteTagItemWidget extends StatelessWidget { - - final String tagName; - final OnDeleteTagItemCallback onDeleteCallback; - - const AutocompleteTagItemWidget({ - super.key, - required this.tagName, - required this.onDeleteCallback, - }); - - @override - Widget build(BuildContext context) { - final imagePaths = Get.find(); - return Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(AutocompleteTagItemStyle.radius), - ), - color: AutocompleteTagItemStyle.backgroundColor, - ), - margin: AutocompleteTagItemStyle.margin, - padding: AutocompleteTagItemStyle.padding, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - AvatarTagItemWidget(tagName: tagName), - const SizedBox(width: AutocompleteTagItemStyle.space), - Text( - tagName, - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - style: AutocompleteTagItemStyle.labelTextStyle - ), - const SizedBox(width: AutocompleteTagItemStyle.space), - TMailButtonWidget.fromIcon( - icon: imagePaths.icClose, - iconSize: AutocompleteTagItemStyle.deleteIconSize, - backgroundColor: Colors.transparent, - padding: EdgeInsets.zero, - onTapActionCallback: () => onDeleteCallback(tagName), - ) - ], - ), - ); - } -} diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_tag_item_widget_web.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_tag_item_widget_web.dart new file mode 100644 index 0000000000..70d34e53e9 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_tag_item_widget_web.dart @@ -0,0 +1,117 @@ + +import 'package:core/core.dart'; +import 'package:core/utils/direction_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/extensions/email_address_extension.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/advanced_search_filter.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/autocomplete_tag_item_web_style.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/avatar_tag_item_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_autocomplete_email_address_web.dart'; + +class AutoCompleteTagItemWidgetWeb extends StatelessWidget { + + final bool isCollapsed; + final bool isLatestTagFocused; + final bool isLatestEmail; + final AdvancedSearchFilterField field; + final EmailAddress currentEmailAddress; + final List currentListEmailAddress; + final List collapsedListEmailAddress; + final OnDeleteTagAction? onDeleteTagAction; + final OnShowFullListEmailAddressAction? onShowFullAction; + + final _imagePaths = Get.find(); + + AutoCompleteTagItemWidgetWeb({ + Key? key, + required this.field, + required this.currentEmailAddress, + required this.currentListEmailAddress, + required this.collapsedListEmailAddress, + this.isCollapsed = false, + this.isLatestTagFocused = false, + this.isLatestEmail = false, + this.onDeleteTagAction, + this.onShowFullAction, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Stack( + alignment: AlignmentDirectional.centerEnd, + children: [ + Padding( + padding: EdgeInsetsDirectional.only( + top: AutoCompleteTagItemWebStyle.paddingTop, + end: isCollapsed ? AutoCompleteTagItemWebStyle.paddingEnd : 0, + ), + child: TextFieldTapRegion( + child: InkWell( + onTap: () => isCollapsed + ? onShowFullAction?.call(field) + : null, + child: MouseRegion( + cursor: SystemMouseCursors.grab, + child: Chip( + labelPadding: EdgeInsetsDirectional.symmetric( + horizontal: AutoCompleteTagItemWebStyle.labelPaddingHorizontal, + vertical: DirectionUtils.isDirectionRTLByHasAnyRtl(currentEmailAddress.asString()) ? 0 : 2 + ), + label: Text( + currentEmailAddress.asString(), + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + ), + deleteIcon: SvgPicture.asset( + _imagePaths.icClose, + fit: BoxFit.fill, + ), + labelStyle: AutoCompleteTagItemWebStyle.labelTextStyle, + backgroundColor: _getTagBackgroundColor(), + shape: RoundedRectangleBorder( + borderRadius: AutoCompleteTagItemWebStyle.shapeBorderRadius, + side: _getTagBorderSide(), + ), + avatar: currentEmailAddress.emailAddress.isNotEmpty + ? AvatarTagItemWidget(tagName: currentEmailAddress.emailAddress) + : null, + onDeleted: () => onDeleteTagAction?.call(currentEmailAddress), + ), + ), + ), + ), + ), + if (isCollapsed) + TMailButtonWidget.fromText( + margin: AutoCompleteTagItemWebStyle.marginCollapsed, + text: '+${currentListEmailAddress.length - collapsedListEmailAddress.length}', + onTapActionCallback: () => onShowFullAction?.call(field), + borderRadius: 10, + textStyle: AutoCompleteTagItemWebStyle.collapsedTextStyle, + padding: AutoCompleteTagItemWebStyle.collapsedPadding, + backgroundColor: AutoCompleteTagItemWebStyle.collapsedBackgroundColor, + ) + ], + ); + } + + Color _getTagBackgroundColor() { + if (isLatestTagFocused && isLatestEmail) { + return AppColor.colorItemRecipientSelected; + } else { + return AppColor.colorEmailAddressTag; + } + } + + BorderSide _getTagBorderSide() { + if (isLatestTagFocused && isLatestEmail) { + return const BorderSide(width: 1, color: AppColor.primaryColor); + } else { + return BorderSide.none; + } + } +} diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/icon_open_advanced_search_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/icon_open_advanced_search_widget.dart index 3b08d0fc20..40318b6d18 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/icon_open_advanced_search_widget.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/icon_open_advanced_search_widget.dart @@ -10,15 +10,11 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller import 'package:tmail_ui_user/main/utils/app_utils.dart'; class IconOpenAdvancedSearchWidget extends StatelessWidget { - IconOpenAdvancedSearchWidget( - this._parentContext, { - Key? key, - }) : super(key: key); + IconOpenAdvancedSearchWidget({Key? key}) : super(key: key); final _imagePaths = Get.find(); final search.SearchController searchController = Get.find(); final AdvancedFilterController advancedFilterController = Get.find(); - final BuildContext _parentContext; @override Widget build(BuildContext context) { @@ -44,7 +40,7 @@ class IconOpenAdvancedSearchWidget extends StatelessWidget { onTap: () { log('IconOpenAdvancedSearchWidget::build(): clicked'); advancedFilterController.initSearchFilterField(context); - searchController.showAdvancedFilterView(_parentContext); + searchController.openAdvanceSearch(); }), ), ); diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_autocomplete_email_address_web.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_autocomplete_email_address_web.dart new file mode 100644 index 0000000000..d8c11ced2f --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_autocomplete_email_address_web.dart @@ -0,0 +1,341 @@ +import 'dart:async'; +import 'dart:math'; +import 'package:collection/collection.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/extensions/email_address_extension.dart'; +import 'package:model/mailbox/expand_mode.dart'; +import 'package:super_tag_editor/tag_editor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/advanced_search_filter.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/suggesstion_email_address.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/advanced_search_input_form_style.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/text_field_autocomplete_email_address_web_style.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_suggestion_item_widget_web.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_tag_item_widget_web.dart'; +import 'package:tmail_ui_user/main/utils/app_constants.dart'; + +typedef OnSuggestionEmailAddress = Future> Function(String word); +typedef OnUpdateListEmailAddressAction = void Function(AdvancedSearchFilterField field, List newData); +typedef OnDeleteEmailAddressTypeAction = void Function(AdvancedSearchFilterField field); +typedef OnShowFullListEmailAddressAction = void Function(AdvancedSearchFilterField field); +typedef OnDeleteTagAction = void Function(EmailAddress emailAddress); + +class TextFieldAutocompleteEmailAddressWeb extends StatefulWidget { + + final AdvancedSearchFilterField field; + final List listEmailAddress; + final ExpandMode expandMode; + final FocusNode? focusNode; + final GlobalKey? keyTagEditor; + final FocusNode? nextFocusNode; + final EdgeInsetsGeometry? padding; + final EdgeInsetsGeometry? margin; + final OnSuggestionEmailAddress? onSuggestionEmailAddress; + final OnDeleteTagAction? onDeleteTag; + final OnUpdateListEmailAddressAction? onUpdateListEmailAddressAction; + final OnDeleteEmailAddressTypeAction? onDeleteEmailAddressTypeAction; + final OnShowFullListEmailAddressAction? onShowFullListEmailAddressAction; + final TextEditingController? controller; + final VoidCallback? onSearchAction; + + const TextFieldAutocompleteEmailAddressWeb({ + Key? key, + required this.field, + required this.listEmailAddress, + this.expandMode = ExpandMode.EXPAND, + this.focusNode, + this.keyTagEditor, + this.nextFocusNode, + this.padding, + this.margin, + this.onSuggestionEmailAddress, + this.onDeleteTag, + this.onUpdateListEmailAddressAction, + this.onDeleteEmailAddressTypeAction, + this.onShowFullListEmailAddressAction, + this.controller, + this.onSearchAction, + }) : super(key: key); + + @override + State createState() => _TextFieldAutocompleteEmailAddressWebState(); +} + +class _TextFieldAutocompleteEmailAddressWebState extends State { + bool _lastTagFocused = false; + late List _currentListEmailAddress; + Timer? _gapBetweenTagChangedAndFindSuggestion; + + @override + void initState() { + super.initState(); + _currentListEmailAddress = widget.listEmailAddress; + } + + @override + void didUpdateWidget(covariant TextFieldAutocompleteEmailAddressWeb oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.listEmailAddress != oldWidget.listEmailAddress) { + _currentListEmailAddress = widget.listEmailAddress; + } + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: TextFieldAutoCompleteEmailAddressWebStyles.padding, + child: LayoutBuilder( + builder: ((context, constraints) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: TextFieldAutoCompleteEmailAddressWebStyles.fieldTitleWidth, + child: Text( + widget.field.getTitle(context), + style: TextFieldAutoCompleteEmailAddressWebStyles.fieldTitleTextStyle, + ), + ), + const SizedBox(width: TextFieldAutoCompleteEmailAddressWebStyles.space), + Expanded( + child: StatefulBuilder( + builder: ((context, setState) { + return TagEditor( + key: widget.keyTagEditor, + length: _collapsedListEmailAddress.length, + controller: widget.controller, + focusNodeKeyboard: widget.focusNode, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.done, + debounceDuration: TextFieldAutoCompleteEmailAddressWebStyles.debounceDuration, + hasAddButton: false, + inputDecoration: InputDecoration( + filled: true, + fillColor: TextFieldAutoCompleteEmailAddressWebStyles.textInputFillColor, + border: TextFieldAutoCompleteEmailAddressWebStyles.textInputBorder, + hintText: widget.field.getHintText(context), + hintStyle: TextFieldAutoCompleteEmailAddressWebStyles.textInputHintStyle, + isDense: true, + contentPadding: _currentListEmailAddress.isNotEmpty + ? TextFieldAutoCompleteEmailAddressWebStyles.textInputContentPaddingWithSomeTag + : TextFieldAutoCompleteEmailAddressWebStyles.textInputContentPadding + ), + padding: _currentListEmailAddress.isNotEmpty + ? TextFieldAutoCompleteEmailAddressWebStyles.tagEditorPadding + : EdgeInsets.zero, + borderRadius: TextFieldAutoCompleteEmailAddressWebStyles.borderRadius, + borderSize: TextFieldAutoCompleteEmailAddressWebStyles.borderWidth, + focusedBorderColor: TextFieldAutoCompleteEmailAddressWebStyles.focusedBorderColor, + enableBorder: true, + enableBorderColor: AppColor.colorInputBorderCreateMailbox, + minTextFieldWidth: TextFieldAutoCompleteEmailAddressWebStyles.minTextFieldWidth, + resetTextOnSubmitted: true, + suggestionsBoxElevation: TextFieldAutoCompleteEmailAddressWebStyles.suggestionBoxElevation, + suggestionsBoxBackgroundColor: TextFieldAutoCompleteEmailAddressWebStyles.suggestionBoxBackgroundColor, + suggestionsBoxRadius: TextFieldAutoCompleteEmailAddressWebStyles.suggestionBoxRadius, + suggestionsBoxMaxHeight: TextFieldAutoCompleteEmailAddressWebStyles.suggestionBoxMaxHeight, + suggestionBoxWidth: _getSuggestionBoxWidth(constraints.maxWidth), + textStyle: AdvancedSearchInputFormStyle.inputTextStyle, + onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, setState), + onDeleteTagAction: () => _handleDeleteLatestTagAction.call(setState), + onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, setState), + onSubmitted: (value) => _handleSubmitTagAction.call(value, setState), + tagBuilder: (context, index) { + final currentEmailAddress = _currentListEmailAddress.elementAt(index); + final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; + return AutoCompleteTagItemWidgetWeb( + field: widget.field, + currentEmailAddress: currentEmailAddress, + currentListEmailAddress: _currentListEmailAddress, + collapsedListEmailAddress: _collapsedListEmailAddress, + isLatestEmail: isLatestEmail, + isCollapsed: _isCollapse, + isLatestTagFocused: _lastTagFocused, + onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, setState), + onShowFullAction: widget.onShowFullListEmailAddressAction, + ); + }, + onTagChanged: (tag) => _handleOnTagChangeAction.call(tag, setState), + findSuggestions: _findSuggestions, + suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { + return AutoCompleteSuggestionItemWidgetWeb( + suggestionState: suggestionEmailAddress.state, + emailAddress: suggestionEmailAddress.emailAddress, + suggestionValid: suggestionValid, + highlight: highlight, + onSelectedAction: (emailAddress) { + setState(() => _currentListEmailAddress.add(emailAddress)); + _updateListEmailAddressAction(); + tagEditorState.resetTextField(); + tagEditorState.closeSuggestionBox(); + }, + ); + }, + onHandleKeyEventAction: (event) { + if (event is RawKeyDownEvent) { + switch (event.logicalKey) { + case LogicalKeyboardKey.tab: + widget.nextFocusNode?.requestFocus(); + break; + default: + break; + } + } + }, + ); + }) + ), + ), + ], + ); + }) + ), + ); + } + + bool get _isCollapse => _currentListEmailAddress.length > 1 && widget.expandMode == ExpandMode.COLLAPSE; + + List get _collapsedListEmailAddress => _isCollapse + ? _currentListEmailAddress.sublist(0, 1) + : _currentListEmailAddress; + + FutureOr> _findSuggestions(String query) async { + if (_gapBetweenTagChangedAndFindSuggestion?.isActive ?? false) { + return []; + } + + final processedQuery = query.trim(); + if (processedQuery.isEmpty) { + return []; + } + + final tmailSuggestion = List.empty(growable: true); + if (processedQuery.length >= AppConstants.limitCharToStartSearch && + widget.onSuggestionEmailAddress != null) { + final listEmailAddress = await widget.onSuggestionEmailAddress!(processedQuery); + final listSuggestionEmailAddress = listEmailAddress.map((emailAddress) => _toSuggestionEmailAddress(emailAddress, _currentListEmailAddress)); + tmailSuggestion.addAll(listSuggestionEmailAddress); + } + + tmailSuggestion.addAll(_matchedSuggestionEmailAddress(processedQuery, _currentListEmailAddress)); + + final currentTextOnTextField = widget.controller?.text ?? ''; + if (currentTextOnTextField.isEmpty) { + return []; + } + + return tmailSuggestion.toSet().toList(); + } + + bool _isDuplicated(String inputEmail) { + if (inputEmail.isEmpty) { + return false; + } + return _currentListEmailAddress + .map((emailAddress) => emailAddress.email) + .whereNotNull() + .contains(inputEmail); + } + + SuggestionEmailAddress _toSuggestionEmailAddress(EmailAddress item, List addedEmailAddresses) { + if (addedEmailAddresses.contains(item)) { + return SuggestionEmailAddress(item, state: SuggestionEmailState.duplicated); + } else { + return SuggestionEmailAddress(item); + } + } + + Iterable _matchedSuggestionEmailAddress(String query, List addedEmailAddress) { + return addedEmailAddress + .where((addedMail) => addedMail.emailAddress.contains(query)) + .map((emailAddress) => SuggestionEmailAddress( + emailAddress, + state: SuggestionEmailState.duplicated + )); + } + + void _handleGapBetweenTagChangedAndFindSuggestion() { + log('_TextFieldAutocompleteEmailAddressWebState::_handleGapBetweenTagChangedAndFindSuggestion:Timeout'); + } + + void _updateListEmailAddressAction() { + widget.onUpdateListEmailAddressAction?.call( + widget.field, + _currentListEmailAddress + ); + } + + void _handleFocusTagAction(bool focused, StateSetter stateSetter) { + if (mounted) { + stateSetter(() => _lastTagFocused = focused); + } + } + + void _handleDeleteLatestTagAction(StateSetter stateSetter) { + if (_currentListEmailAddress.isNotEmpty) { + stateSetter(_currentListEmailAddress.removeLast); + _updateListEmailAddressAction(); + } + } + + void _handleDeleteTagAction(EmailAddress emailAddress, StateSetter stateSetter) { + if (_currentListEmailAddress.isNotEmpty) { + stateSetter(() => _currentListEmailAddress.remove(emailAddress)); + _updateListEmailAddressAction(); + } + } + + void _handleSelectOptionAction( + SuggestionEmailAddress suggestionEmailAddress, + StateSetter stateSetter + ) { + if (!_isDuplicated(suggestionEmailAddress.emailAddress.emailAddress)) { + stateSetter(() => _currentListEmailAddress.add(suggestionEmailAddress.emailAddress)); + _updateListEmailAddressAction(); + } + } + + void _handleSubmitTagAction( + String value, + StateSetter stateSetter + ) { + final textTrim = value.trim(); + if (textTrim.isEmpty) { + widget.onSearchAction?.call(); + return; + } + + if (!_isDuplicated(textTrim)) { + stateSetter(() => _currentListEmailAddress.add(EmailAddress(null, textTrim))); + _updateListEmailAddressAction(); + } + } + + void _handleOnTagChangeAction( + String value, + StateSetter stateSetter + ) { + final textTrim = value.trim(); + if (!_isDuplicated(textTrim)) { + stateSetter(() => _currentListEmailAddress.add(EmailAddress(null, textTrim))); + _updateListEmailAddressAction(); + } + _gapBetweenTagChangedAndFindSuggestion = Timer( + const Duration(seconds: 1), + _handleGapBetweenTagChangedAndFindSuggestion + ); + } + + double? _getSuggestionBoxWidth(double maxWidth) { + if (maxWidth < ResponsiveUtils.minTabletWidth) { + final newWidth = min(maxWidth, 300.0); + return newWidth; + } else { + return null; + } + } +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_autocomplete_email_adress.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_autocomplete_email_adress.dart deleted file mode 100644 index 891cd897e1..0000000000 --- a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_autocomplete_email_adress.dart +++ /dev/null @@ -1,250 +0,0 @@ -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/presentation/views/text/text_field_builder.dart'; -import 'package:core/utils/platform_info.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:model/extensions/email_address_extension.dart'; -import 'package:textfield_tags/textfield_tags.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/advanced_search_filter.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/custom_tf_tag_controller.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/text_field_autocomplete_email_address_style.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_suggestion_item_widget.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_tag_item_widget.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -class TextFieldAutocompleteEmailAddress extends StatefulWidget { - const TextFieldAutocompleteEmailAddress({ - Key? key, - required this.advancedSearchFilterField, - required this.initialTags, - required this.optionsBuilder, - required this.onChange, - required this.onDeleteTag, - required this.onAddTag, - this.currentFocusNode, - this.nextFocusNode, - }) : super(key: key); - final AdvancedSearchFilterField advancedSearchFilterField; - final Set initialTags; - final Future> Function(String) optionsBuilder; - final Function(String) onChange; - final Function(String) onDeleteTag; - final Function(String) onAddTag; - final FocusNode? currentFocusNode; - final FocusNode? nextFocusNode; - - @override - State createState() => - _TextFieldAutocompleteEmailAddressState(); -} - -class _TextFieldAutocompleteEmailAddressState - extends State { - final double _distanceToField = 380; - final _responsiveUtils = Get.find(); - late CustomController _controller; - - @override - void initState() { - _controller = CustomController(); - _controller.setActionRemoveTag((tag) { - widget.onDeleteTag.call(tag); - }); - _controller.setActionAddTag((tag) { - widget.onAddTag.call(tag); - }); - _controller.setActionChangeText((tag) { - widget.onChange.call(tag); - }); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Autocomplete( - optionsViewBuilder: (context, onSelected, listEmailAddress) { - return Container( - margin: const EdgeInsets.only( - top: PlatformInfo.isWeb ? 5 : 8, - bottom: 16), - height: _getHeightSuggestionBox(listEmailAddress.length, 65), - width: maxWidthSuggestionBox, - alignment: Alignment.topLeft, - child: Card( - elevation: 20, - color: Colors.transparent, - child: ClipRRect( - borderRadius: BorderRadius.circular(20.0), - child: Container( - alignment: Alignment.topCenter, - height: _getHeightSuggestionBox(listEmailAddress.length, 65), - width: maxWidthSuggestionBox, - color: Colors.white, - child: ListView.builder( - shrinkWrap: true, - padding: EdgeInsets.zero, - itemExtent: 65, - itemCount: listEmailAddress.length, - itemBuilder: (BuildContext context, int index) { - final emailAddress = listEmailAddress.elementAt(index); - return AutocompleteSuggestionItemWidget( - emailAddress: emailAddress, - onSelectCallback: _controller.onSubmitted, - ); - }, - ), - ), - ), - ), - ); - }, - optionsBuilder: (TextEditingValue textEditingValue) { - if (textEditingValue.text == '') { - return const Iterable.empty(); - } - return widget.optionsBuilder.call(textEditingValue.text.toLowerCase()); - }, - onSelected: (EmailAddress selectedTag) { - _controller.addTag = selectedTag.emailAddress; - }, - fieldViewBuilder: (context, ttec, tfn, onFieldSubmitted) { - return TextFieldTags( - initialTags: widget.initialTags.toList(), - textEditingController: ttec, - focusNode: tfn, - textfieldTagsController: _controller, - textSeparators: const ['\n', ','], - letterCase: LetterCase.normal, - validator: (String tag) { - if (_controller.getTags!.contains(tag)) { - return AppLocalizations.of(context).messageDuplicateTagFilterMail; - } - return null; - }, - inputfieldBuilder: (context, tec, fn, error, onChanged, onSubmitted) { - return ((context, sc, tags, onTagDelete) { - return RawKeyboardListener( - focusNode: widget.currentFocusNode ?? FocusNode(), - onKey: (event) { - if (event is RawKeyDownEvent && - event.logicalKey == LogicalKeyboardKey.tab) { - widget.nextFocusNode?.requestFocus(); - } - }, - child: TextFieldBuilder( - controller: tec, - focusNode: fn, - textInputAction: TextInputAction.next, - maxLines: 1, - textStyle: TextFieldAutocompleteEmailAddressStyle.inputTextStyle, - decoration: InputDecoration( - filled: true, - fillColor: Colors.white, - contentPadding: const EdgeInsets.only( - right: 8, - left: 12, - ), - enabledBorder: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10), - ), - borderSide: BorderSide( - width: 1, - color: AppColor.colorInputBorderCreateMailbox, - ), - ), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10), - ), - borderSide: BorderSide( - width: 1, - color: AppColor.colorInputBorderCreateMailbox, - ), - ), - hintText: widget.advancedSearchFilterField.getHintText(context), - hintStyle: const TextStyle( - fontSize: 16, - color: AppColor.colorHintSearchBar, - ), - prefixIconConstraints: BoxConstraints(maxWidth: _distanceToField * 0.74), - prefixIcon: tags.isNotEmpty - ? SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 10), - controller: sc, - scrollDirection: Axis.horizontal, - child: Row( - children: tags - .map((tagName) => AutocompleteTagItemWidget( - tagName: tagName, - onDeleteCallback: onTagDelete - )) - .toList() - ), - ) - : null, - ), - onTextChange: (value) { - if (value.trim().isNotEmpty) { - onChanged?.call(value); - } - }, - onTextSubmitted: (tag) { - if (tag.trim().isNotEmpty) { - onSubmitted?.call(tag); - fn.requestFocus(); - } else { - FocusScope.of(context).unfocus(); - } - }, - ), - ); - }); - }, - ); - }, - ); - } - - double _getHeightSuggestionBox(int countItem, double heightItem) { - final maxHeightList = countItem * heightItem; - - if (PlatformInfo.isWeb) { - return maxHeightList > 250 ? 250 : maxHeightList; - } else { - if (_responsiveUtils.isLandscapeMobile(context)) { - return maxHeightList > 180 ? 180 : maxHeightList; - } else { - return maxHeightList > 250 ? 250 : maxHeightList; - } - } - } - - double get maxWidthSuggestionBox { - if (PlatformInfo.isWeb) { - if (_responsiveUtils.isTabletLarge(context)) { - return 300; - } else { - return 400; - } - } else { - if (_responsiveUtils.isLandscapeMobile(context)) { - return 400; - } else if (_responsiveUtils.isLandscapeTablet(context) || - _responsiveUtils.isTabletLarge(context) || - _responsiveUtils.isDesktop(context)) { - return 300; - } else { - return 350; - } - } - } - - @override - void dispose() { - super.dispose(); - } -} diff --git a/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart index c9f0b51bb0..7a712bd4e3 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart @@ -39,7 +39,7 @@ class SearchInputFormWidget extends StatelessWidget with AppLoaderMixin { portalFollower: PointerInterceptor( child: GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => _searchController.selectOpenAdvanceSearch() + onTap: _searchController.closeAdvanceSearch ), ), child: PortalTarget( @@ -210,7 +210,7 @@ class SearchInputFormWidget extends StatelessWidget with AppLoaderMixin { ), onTap: _searchController.clearTextSearch ), - rightButton: IconOpenAdvancedSearchWidget(context) + rightButton: IconOpenAdvancedSearchWidget() ); } diff --git a/pubspec.lock b/pubspec.lock index 6705e217bd..1e27514802 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1553,7 +1553,7 @@ packages: description: path: "." ref: master - resolved-ref: "796d679e3aaf34d0107819d1f12bb3cf45a59d64" + resolved-ref: aad6fb53c832fbe3b86f2b9eaefaab7a83392529 url: "https://github.com/dab246/super_tag_editor.git" source: git version: "0.2.0"