diff --git a/core/lib/presentation/views/quick_search/quick_search_input_form.dart b/core/lib/presentation/views/quick_search/quick_search_input_form.dart index 4d0a00f8b1..95c7af2732 100644 --- a/core/lib/presentation/views/quick_search/quick_search_input_form.dart +++ b/core/lib/presentation/views/quick_search/quick_search_input_form.dart @@ -35,7 +35,7 @@ final supportedPlatform = (kIsWeb || Platform.isAndroid || Platform.isIOS); /// /// * [TypeAheadFieldQuickSearch], A [TextField](https://docs.flutter.io/flutter/material/TextField-class.html) /// that displays a list of suggestions as the user types -class QuickSearchInputForm extends FormField { +class QuickSearchInputForm extends FormField { /// The configuration of the [TextField](https://docs.flutter.io/flutter/material/TextField-class.html) /// that the TypeAhead widget displays final QuickSearchTextFieldConfiguration textFieldConfiguration; @@ -87,6 +87,9 @@ class QuickSearchInputForm extends FormField { BoxDecoration? decoration, double? maxHeight, bool isDirectionRTL = false, + ItemBuilder

? contactItemBuilder, + SuggestionsCallback

? contactSuggestionsCallback, + SuggestionSelectionCallback

? onContactSuggestionSelected, }) : assert( initialValue == null || textFieldConfiguration.controller == null), assert(minCharsForSuggestions >= 0), @@ -101,7 +104,7 @@ class QuickSearchInputForm extends FormField { autovalidateMode: autovalidateMode, builder: (FormFieldState field) { final _TypeAheadFormFieldState state = - field as _TypeAheadFormFieldState; + field as _TypeAheadFormFieldState; return TypeAheadFieldQuickSearch( getImmediateSuggestions: getImmediateSuggestions, @@ -151,21 +154,24 @@ class QuickSearchInputForm extends FormField { decoration: decoration, maxHeight: maxHeight, isDirectionRTL: isDirectionRTL, + contactItemBuilder: contactItemBuilder, + contactSuggestionsCallback: contactSuggestionsCallback, + onContactSuggestionSelected: onContactSuggestionSelected, ); }); @override - FormFieldState createState() => _TypeAheadFormFieldState(); + FormFieldState createState() => _TypeAheadFormFieldState(); } -class _TypeAheadFormFieldState extends FormFieldState { +class _TypeAheadFormFieldState extends FormFieldState { TextEditingController? _controller; TextEditingController? get _effectiveController => widget.textFieldConfiguration.controller ?? _controller; @override - QuickSearchInputForm get widget => super.widget as QuickSearchInputForm; + QuickSearchInputForm get widget => super.widget as QuickSearchInputForm; @override void initState() { @@ -239,7 +245,7 @@ class _TypeAheadFormFieldState extends FormFieldState { /// * [QuickSearchInputForm], a [FormField](https://docs.flutter.io/flutter/widgets/FormField-class.html) /// implementation of [TypeAheadFieldQuickSearch] that allows the value to be saved, /// validated, etc. -class TypeAheadFieldQuickSearch extends StatefulWidget { +class TypeAheadFieldQuickSearch extends StatefulWidget { /// Called with the search pattern to get the search suggestions. /// /// This callback must not be null. It is be called by the TypeAhead widget @@ -517,8 +523,14 @@ class TypeAheadFieldQuickSearch extends StatefulWidget { final BoxDecoration? decoration; /// Max height search input final double? maxHeight; - + /// Check direction text input final bool isDirectionRTL; + /// Widget contact item + final ItemBuilder

? contactItemBuilder; + /// Get all contact callback + final SuggestionsCallback

? contactSuggestionsCallback; + /// On listen select contact + final SuggestionSelectionCallback

? onContactSuggestionSelected; /// Creates a [TypeAheadFieldQuickSearch] const TypeAheadFieldQuickSearch( @@ -562,6 +574,9 @@ class TypeAheadFieldQuickSearch extends StatefulWidget { this.decoration, this.maxHeight, this.isDirectionRTL = false, + this.contactItemBuilder, + this.contactSuggestionsCallback, + this.onContactSuggestionSelected, }) : assert(animationStart >= 0.0 && animationStart <= 1.0), assert( direction == AxisDirection.down || direction == AxisDirection.up), @@ -569,10 +584,10 @@ class TypeAheadFieldQuickSearch extends StatefulWidget { super(key: key); @override - State> createState() => _TypeAheadFieldQuickSearchState(); + State> createState() => _TypeAheadFieldQuickSearchState(); } -class _TypeAheadFieldQuickSearchState extends State> +class _TypeAheadFieldQuickSearchState extends State> with WidgetsBindingObserver { FocusNode? _focusNode; TextEditingController? _textEditingController; @@ -707,7 +722,7 @@ class _TypeAheadFieldQuickSearchState extends State( + final suggestionsList = _SuggestionsList( suggestionsBox: _suggestionsBox, decoration: widget.suggestionsBoxDecoration, debounceDuration: widget.debounceDuration, @@ -754,6 +769,15 @@ class _TypeAheadFieldQuickSearchState extends State extends State extends StatefulWidget { +class _SuggestionsList extends StatefulWidget { final _SuggestionsBox? suggestionsBox; final TextEditingController? controller; final bool getImmediateSuggestions; @@ -915,6 +939,9 @@ class _SuggestionsList extends StatefulWidget { final EdgeInsets? listActionPadding; final bool hideSuggestionsBox; final bool isDirectionRTL; + final ItemBuilder

? contactSuggestionBuilder; + final SuggestionsCallback

? contactSuggestionCallback; + final SuggestionSelectionCallback

? onContactSuggestionSelected; const _SuggestionsList({ required this.suggestionsBox, @@ -949,16 +976,20 @@ class _SuggestionsList extends StatefulWidget { this.listActionPadding, this.hideSuggestionsBox = false, this.isDirectionRTL = false, + this.contactSuggestionBuilder, + this.contactSuggestionCallback, + this.onContactSuggestionSelected, }); @override - _SuggestionsListState createState() => _SuggestionsListState(); + _SuggestionsListState createState() => _SuggestionsListState(); } -class _SuggestionsListState extends State<_SuggestionsList> +class _SuggestionsListState extends State<_SuggestionsList> with SingleTickerProviderStateMixin { Iterable? _suggestions; Iterable? _recentItems; + Iterable

? _contacts; late bool _suggestionsValid; late VoidCallback _controllerListener; Timer? _debounceTimer; @@ -991,6 +1022,7 @@ class _SuggestionsListState extends State<_SuggestionsList> setState(() { _isLoading = false; _suggestions = null; + _contacts = null; _recentItems = recentItems; _suggestionsValid = true; }); @@ -1015,7 +1047,7 @@ class _SuggestionsListState extends State<_SuggestionsList> } @override - void didUpdateWidget(_SuggestionsList oldWidget) { + void didUpdateWidget(_SuggestionsList oldWidget) { super.didUpdateWidget(oldWidget); widget.controller!.addListener(_controllerListener); _getSuggestions(); @@ -1067,6 +1099,7 @@ class _SuggestionsListState extends State<_SuggestionsList> Iterable? suggestions; Iterable? recentItems; + Iterable

? contacts; Object? error; try { @@ -1074,6 +1107,9 @@ class _SuggestionsListState extends State<_SuggestionsList> if (widget.fetchRecentActionCallback != null) { recentItems = await widget.fetchRecentActionCallback!(widget.controller!.text); } + if (widget.contactSuggestionCallback != null) { + contacts = await widget.contactSuggestionCallback!(widget.controller!.text); + } } catch (e) { error = e; } @@ -1091,6 +1127,7 @@ class _SuggestionsListState extends State<_SuggestionsList> _isLoading = false; _suggestions = suggestions; _recentItems = recentItems; + _contacts = contacts; }); } } @@ -1111,7 +1148,7 @@ class _SuggestionsListState extends State<_SuggestionsList> Widget child; - if (_suggestions?.isNotEmpty == true && widget.controller?.text.isNotEmpty == true) { + if ((_suggestions?.isNotEmpty == true || _contacts?.isNotEmpty == true) && widget.controller?.text.isNotEmpty == true) { child = createSuggestionsWidget(); } else { child = createRecentWidget(); @@ -1165,12 +1202,10 @@ class _SuggestionsListState extends State<_SuggestionsList> Widget createSuggestionsWidget() { final listItemSuggestionWidget = _suggestions?.map((T suggestion) { - if ( widget.itemBuilder != null) { + if (widget.itemBuilder != null) { return InkWell( child: widget.itemBuilder!(context, suggestion), - onTap: () { - widget.onSuggestionSelected!(suggestion); - }, + onTap: () => widget.onSuggestionSelected?.call(suggestion), ); } else { return const SizedBox.shrink(); @@ -1212,6 +1247,20 @@ class _SuggestionsListState extends State<_SuggestionsList> } }).toList()); + final listItemContactWidget = _contacts?.map((P contact) { + if (widget.contactSuggestionBuilder != null) { + return Material( + color: Colors.transparent, + child: InkWell( + child: widget.contactSuggestionBuilder!(context, contact), + onTap: () => widget.onContactSuggestionSelected?.call(contact), + ), + ); + } else { + return const SizedBox.shrink(); + } + }).toList() ?? []; + Widget child = ListView( padding: EdgeInsets.zero, primary: false, @@ -1226,6 +1275,8 @@ class _SuggestionsListState extends State<_SuggestionsList> ), if (_isLoading == true && widget.hideOnLoading == false && widget.keepSuggestionsOnLoading == false) loadingWidget, + if (listItemContactWidget.isNotEmpty) + ... listItemContactWidget, if (widget.buttonShowAllResult != null && widget.controller?.text.isNotEmpty == true) widget.buttonShowAllResult!(context, widget.controller?.text), if (listItemSuggestionWidget.isNotEmpty) diff --git a/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart b/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart index 1a0aa73dd5..c04c85e1b7 100644 --- a/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart +++ b/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart @@ -1,5 +1,6 @@ import 'package:flutter/cupertino.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; 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'; @@ -119,4 +120,14 @@ class ClearDateRangeToAdvancedSearch extends DashBoardAction { @override List get props => [receiveTime]; -} \ No newline at end of file +} + +class SearchEmailByFromFieldsAction extends DashBoardAction { + + final EmailAddress emailAddress; + + SearchEmailByFromFieldsAction(this.emailAddress); + + @override + List get props => [emailAddress]; +} 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 844c90cf04..41ecf941d6 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart @@ -447,10 +447,21 @@ class AdvancedFilterController extends BaseController { ); } else if (action is ClearDateRangeToAdvancedSearch) { _updateDateRangeTime(action.receiveTime); - } else if (action is StartSearchEmailAction) { - if (action.filter == QuickSearchFilter.fromMe) { - _updateFromField(); - } + } else if (action is StartSearchEmailAction && action.filter == QuickSearchFilter.fromMe) { + _updateFromField(); + } else if (action is SearchEmailByFromFieldsAction) { + searchController.clearSearchFilter(); + _resetAllToOriginalValue(); + _clearAllTextFieldInput(); + searchController.searchInputController.clear(); + searchController.deactivateAdvancedSearch(); + searchController.isAdvancedSearchViewOpen.value = false; + + listFromEmailAddress = List.from({action.emailAddress}); + final listAddress = listFromEmailAddress.map((emailAddress) => emailAddress.emailAddress).toSet(); + searchController.updateFilterEmail(fromOption: Some(listAddress)); + + _mailboxDashBoardController.dispatchAction(StartSearchEmailAction()); } } ); 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 cd8406d122..9df0d0a238 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -15,6 +15,7 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:jmap_dart_client/jmap/mail/vacation/vacation_response.dart'; import 'package:model/model.dart'; @@ -26,9 +27,11 @@ import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dar import 'package:tmail_ui_user/features/composer/domain/exceptions/set_email_method_exception.dart'; import 'package:tmail_ui_user/features/composer/domain/extensions/email_request_extension.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_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'; import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_as_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/update_email_drafts_interactor.dart'; @@ -184,6 +187,7 @@ class MailboxDashBoardController extends ReloadableController { DeleteEmailStateToRefreshInteractor? _deleteEmailStateToRefreshInteractor; GetMailboxStateToRefreshInteractor? _getMailboxStateToRefreshInteractor; DeleteMailboxStateToRefreshInteractor? _deleteMailboxStateToRefreshInteractor; + GetAutoCompleteInteractor? _getAutoCompleteInteractor; final scaffoldKey = GlobalKey(); final selectedMailbox = Rxn(); @@ -2174,6 +2178,34 @@ class MailboxDashBoardController extends ReloadableController { } } + Future> getContactSuggestion(String query) async { + _getAutoCompleteInteractor = getBinding(); + + if (_getAutoCompleteInteractor == null || accountId.value == null) { + return []; + } + + final listEmailAddress = await _getAutoCompleteInteractor! + .execute(AutoCompletePattern(word: query, accountId: accountId.value!, limit: 2)) + .then((value) => value.fold( + (failure) => [], + (success) => success is GetAutoCompleteSuccess ? success.listEmailAddress : [] + )); + log('MailboxDashBoardController::getAutoCompleteSuggestion:listEmailAddress: $listEmailAddress'); + return listEmailAddress; + } + + void searchEmailByFromFields(BuildContext context, EmailAddress emailAddress) { + KeyboardUtils.hideKeyboard(context); + clearFilterMessageOption(); + searchController.clearFilterSuggestion(); + if (_searchInsideEmailDetailedViewIsActive(context)) { + _closeEmailDetailedView(); + } + _unSelectedMailbox(); + dispatchAction(SearchEmailByFromFieldsAction(emailAddress)); + } + @override void onClose() { _emailReceiveManager.closeEmailReceiveManagerStream(); diff --git a/lib/features/mailbox_dashboard/presentation/widgets/quick_search/contact_quick_search_item.dart b/lib/features/mailbox_dashboard/presentation/widgets/quick_search/contact_quick_search_item.dart new file mode 100644 index 0000000000..1418414211 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/widgets/quick_search/contact_quick_search_item.dart @@ -0,0 +1,59 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +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/widgets/advanced_search/avatar_suggestion_item_widget.dart'; + +class ContactQuickSearchItem extends StatelessWidget { + final EmailAddress emailAddress; + + const ContactQuickSearchItem({ + super.key, + required this.emailAddress, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + AvatarSuggestionItemWidget(emailAddress: emailAddress), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + emailAddress.asString(), + maxLines: 1, + softWrap: CommonTextStyle.defaultSoftWrap, + overflow: CommonTextStyle.defaultTextOverFlow, + style: const TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.normal + ) + ), + if (emailAddress.displayName.isNotEmpty) + Text( + emailAddress.emailAddress, + maxLines: 1, + softWrap: CommonTextStyle.defaultSoftWrap, + overflow: CommonTextStyle.defaultTextOverFlow, + style: const TextStyle( + color: AppColor.colorHintSearchBar, + fontSize: 13, + fontWeight: FontWeight.normal + ) + ) + ], + ), + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/widgets/email_quick_search_item_tile_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/quick_search/email_quick_search_item_tile_widget.dart similarity index 100% rename from lib/features/mailbox_dashboard/presentation/widgets/email_quick_search_item_tile_widget.dart rename to lib/features/mailbox_dashboard/presentation/widgets/quick_search/email_quick_search_item_tile_widget.dart diff --git a/lib/features/mailbox_dashboard/presentation/widgets/recent_search_item_tile_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/quick_search/recent_search_item_tile_widget.dart similarity index 100% rename from lib/features/mailbox_dashboard/presentation/widgets/recent_search_item_tile_widget.dart rename to lib/features/mailbox_dashboard/presentation/widgets/quick_search/recent_search_item_tile_widget.dart 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 7a712bd4e3..0553872d7a 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 @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.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/email/presentation_email.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; @@ -18,8 +19,9 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_overlay.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/icon_open_advanced_search_widget.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/email_quick_search_item_tile_widget.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/recent_search_item_tile_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/quick_search/contact_quick_search_item.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/quick_search/email_quick_search_item_tile_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/quick_search/recent_search_item_tile_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search; @@ -55,7 +57,7 @@ class SearchInputFormWidget extends StatelessWidget with AppLoaderMixin { ), ), portalFollower: const AdvancedSearchFilterOverlay(), - child: QuickSearchInputForm( + child: QuickSearchInputForm( maxHeight: 52, suggestionsBoxVerticalOffset: 0.0, textFieldConfiguration: _createConfiguration(context), @@ -105,7 +107,11 @@ class SearchInputFormWidget extends StatelessWidget with AppLoaderMixin { onRecentSelected: (recent) => _invokeSelectRecentItem(context, recent), suggestionsCallback: _dashBoardController.quickSearchEmails, itemBuilder: (context, email) => EmailQuickSearchItemTileWidget(email, _dashBoardController.selectedMailbox.value), - onSuggestionSelected: (presentationEmail) => _invokeSelectSuggestionItem(context, presentationEmail)) + onSuggestionSelected: (presentationEmail) => _invokeSelectSuggestionItem(context, presentationEmail), + contactItemBuilder: (context, emailAddress) => ContactQuickSearchItem(emailAddress: emailAddress), + contactSuggestionsCallback: _dashBoardController.getContactSuggestion, + onContactSuggestionSelected: (emailAddress) => _invokeSelectContactSuggestion(context, emailAddress), + ) ), ); }); @@ -246,4 +252,11 @@ class SearchInputFormWidget extends StatelessWidget with AppLoaderMixin { ); }); } + + void _invokeSelectContactSuggestion(BuildContext context, EmailAddress emailAddress) { + _searchController.searchInputController.clear(); + _searchController.searchFocus.unfocus(); + _searchController.enableSearch(); + _dashBoardController.searchEmailByFromFields(context, emailAddress); + } } \ No newline at end of file diff --git a/lib/features/search/email/presentation/search_email_view.dart b/lib/features/search/email/presentation/search_email_view.dart index 9576703299..3d3a4759fa 100644 --- a/lib/features/search/email/presentation/search_email_view.dart +++ b/lib/features/search/email/presentation/search_email_view.dart @@ -16,8 +16,8 @@ import 'package:tmail_ui_user/features/email/presentation/widgets/email_action_c import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.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/widgets/email_quick_search_item_tile_widget.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/recent_search_item_tile_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/quick_search/email_quick_search_item_tile_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/quick_search/recent_search_item_tile_widget.dart'; import 'package:tmail_ui_user/features/search/email/presentation/model/search_more_state.dart'; import 'package:tmail_ui_user/features/search/email/presentation/model/simple_search_filter.dart'; import 'package:tmail_ui_user/features/search/email/presentation/search_email_controller.dart';