From a35b263dcab1ce4ba66c9604af5163fa44461a6a Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 5 Jan 2026 11:25:29 +0700 Subject: [PATCH] TF-4229 Implement search by label filter in mobile --- .../base/model/popup_menu_item_action.dart | 2 + .../context_menu_dialog_item.dart | 12 +- .../context_menu_item_action.dart | 2 + .../model/search/quick_search_filter.dart | 17 ++- .../search_filters/search_filter_button.dart | 4 + .../update_search_filter_extension.dart | 54 +++++++ .../search_label_filter_modal_mixin.dart | 92 ++++++++++++ .../model/context_item_label_type_action.dart | 34 +++++ .../popup_menu_item_label_type_action.dart | 35 +++++ .../presentation/search_email_controller.dart | 140 +++++++++++------- .../email/presentation/search_email_view.dart | 41 +++-- 11 files changed, 362 insertions(+), 71 deletions(-) create mode 100644 lib/features/search/email/presentation/extension/update_search_filter_extension.dart create mode 100644 lib/features/search/email/presentation/mixin/search_label_filter_modal_mixin.dart create mode 100644 lib/features/search/email/presentation/model/context_item_label_type_action.dart create mode 100644 lib/features/search/email/presentation/model/popup_menu_item_label_type_action.dart diff --git a/lib/features/base/model/popup_menu_item_action.dart b/lib/features/base/model/popup_menu_item_action.dart index 9e5351668..bcd7f421d 100644 --- a/lib/features/base/model/popup_menu_item_action.dart +++ b/lib/features/base/model/popup_menu_item_action.dart @@ -45,6 +45,8 @@ mixin OptionalPopupSelectedIcon { Color get selectedIconColor => AppColor.primaryMain; double get selectedIconSize => 16.0; + + bool get isArrangeRTL => true; } mixin OptionalPopupHoverIcon { diff --git a/lib/features/base/widget/context_menu/context_menu_dialog_item.dart b/lib/features/base/widget/context_menu/context_menu_dialog_item.dart index 45f371a72..c3f94f730 100644 --- a/lib/features/base/widget/context_menu/context_menu_dialog_item.dart +++ b/lib/features/base/widget/context_menu/context_menu_dialog_item.dart @@ -19,6 +19,7 @@ class ContextMenuDialogItem extends StatelessWidget { Widget? iconWidget; Widget? selectedIconWidget; bool isSelected = false; + bool isArrangeRTL = true; if (menuAction is ContextMenuItemActionRequiredIcon) { final specificMenuAction = @@ -47,6 +48,7 @@ class ContextMenuDialogItem extends StatelessWidget { ), ); isSelected = specificMenuAction.selectedAction == menuAction.action; + isArrangeRTL = specificMenuAction.isArrangeRTL; } else if (menuAction is ContextMenuItemActionRequiredFull) { final specificMenuAction = menuAction as ContextMenuItemActionRequiredFull; @@ -71,8 +73,12 @@ class ContextMenuDialogItem extends StatelessWidget { ), ); isSelected = specificMenuAction.selectedAction == menuAction.action; + isArrangeRTL = specificMenuAction.isArrangeRTL; } + final leadingWidget = + iconWidget ?? (isArrangeRTL ? const SizedBox(width: 24) : null); + return Material( type: MaterialType.transparency, child: InkWell( @@ -81,7 +87,9 @@ class ContextMenuDialogItem extends StatelessWidget { height: 48, child: Row( children: [ - if (iconWidget != null) iconWidget else const SizedBox(width: 24), + if (leadingWidget != null) leadingWidget, + if (!isArrangeRTL && selectedIconWidget != null) + selectedIconWidget, Expanded( child: Text( menuAction.actionName, @@ -95,7 +103,7 @@ class ContextMenuDialogItem extends StatelessWidget { overflow: TextOverflow.ellipsis, ), ), - if (isSelected && selectedIconWidget != null) + if (isArrangeRTL && isSelected && selectedIconWidget != null) selectedIconWidget else const SizedBox(width: 24), diff --git a/lib/features/base/widget/context_menu/context_menu_item_action.dart b/lib/features/base/widget/context_menu/context_menu_item_action.dart index 4e3c06972..107ac9de7 100644 --- a/lib/features/base/widget/context_menu/context_menu_item_action.dart +++ b/lib/features/base/widget/context_menu/context_menu_item_action.dart @@ -31,6 +31,8 @@ mixin OptionalSelectedIcon { String get selectedIcon; Color get selectedIconColor => AppColor.primaryMain; + + bool get isArrangeRTL => true; } abstract class ContextMenuItemActionRequiredIcon diff --git a/lib/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart b/lib/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart index a26a99034..a80eb33b0 100644 --- a/lib/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart +++ b/lib/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart @@ -5,6 +5,8 @@ import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:flutter/cupertino.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; +import 'package:labels/extensions/label_extension.dart'; +import 'package:labels/model/label.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; @@ -22,7 +24,8 @@ enum QuickSearchFilter { dateTime, from, to, - folder; + folder, + labels; String getTitle( BuildContext context, { @@ -33,7 +36,8 @@ enum QuickSearchFilter { Set? listAddressOfFrom, UserName? userName, Set? listAddressOfTo, - PresentationMailbox? mailbox + PresentationMailbox? mailbox, + Label? label, }) { switch(this) { case QuickSearchFilter.hasAttachment: @@ -61,6 +65,8 @@ enum QuickSearchFilter { return AppLocalizations.of(context).starred; case QuickSearchFilter.unread: return AppLocalizations.of(context).unread; + case QuickSearchFilter.labels: + return label?.safeDisplayName ?? AppLocalizations.of(context).allLabels; } } @@ -86,6 +92,8 @@ enum QuickSearchFilter { return isSelected ? imagePaths.icSelectedSB : imagePaths.icUnStar; case QuickSearchFilter.unread: return isSelected ? imagePaths.icSelectedSB : imagePaths.icUnread; + case QuickSearchFilter.labels: + return imagePaths.icTag; } } @@ -152,6 +160,8 @@ enum QuickSearchFilter { return searchFilter.hasKeyword.contains(KeyWordIdentifier.emailFlagged.value) == true; case QuickSearchFilter.unread: return searchFilter.unread; + case QuickSearchFilter.labels: + return searchFilter.label != null; } } @@ -171,5 +181,6 @@ enum QuickSearchFilter { this == QuickSearchFilter.from || this == QuickSearchFilter.to || this == QuickSearchFilter.folder || - this == QuickSearchFilter.sortBy; + this == QuickSearchFilter.sortBy || + this == QuickSearchFilter.labels; } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/widgets/search_filters/search_filter_button.dart b/lib/features/mailbox_dashboard/presentation/widgets/search_filters/search_filter_button.dart index e2a810a43..94055c772 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/search_filters/search_filter_button.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/search_filters/search_filter_button.dart @@ -4,6 +4,7 @@ import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:labels/model/label.dart'; import 'package:model/mailbox/presentation_mailbox.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/email_sort_order_type.dart'; @@ -29,6 +30,7 @@ class SearchFilterButton extends StatefulWidget { final Set? listAddressOfFrom; final Set? listAddressOfTo; final PresentationMailbox? mailbox; + final Label? label; final Color? backgroundColor; final EdgeInsetsGeometry? buttonPadding; final bool isContextMenuAlignEndButton; @@ -49,6 +51,7 @@ class SearchFilterButton extends StatefulWidget { this.listAddressOfFrom, this.listAddressOfTo, this.mailbox, + this.label, this.backgroundColor, this.buttonPadding, this.onSelectSearchFilterAction, @@ -73,6 +76,7 @@ class _SearchFilterButtonState extends State { sortOrderType: widget.sortOrderType, listAddressOfFrom: widget.listAddressOfFrom, mailbox: widget.mailbox, + label: widget.label, listAddressOfTo: widget.listAddressOfTo, ); diff --git a/lib/features/search/email/presentation/extension/update_search_filter_extension.dart b/lib/features/search/email/presentation/extension/update_search_filter_extension.dart new file mode 100644 index 000000000..c30dc72d0 --- /dev/null +++ b/lib/features/search/email/presentation/extension/update_search_filter_extension.dart @@ -0,0 +1,54 @@ +import 'package:dartz/dartz.dart'; +import 'package:labels/model/label.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/search/email/presentation/search_email_controller.dart'; + +extension UpdateSearchFilterExtension on SearchEmailController { + void deleteQuickSearchFilter({required QuickSearchFilter filter}) { + switch (filter) { + case QuickSearchFilter.hasAttachment: + updateSimpleSearchFilter(hasAttachmentOption: const None()); + break; + case QuickSearchFilter.starred: + updateSimpleSearchFilter(hasKeywordOption: const None()); + break; + case QuickSearchFilter.unread: + updateSimpleSearchFilter(unreadOption: const None()); + break; + case QuickSearchFilter.sortBy: + updateSimpleSearchFilter( + sortOrderTypeOption: const Some(SearchEmailFilter.defaultSortOrder), + ); + break; + case QuickSearchFilter.dateTime: + updateSimpleSearchFilter( + emailReceiveTimeTypeOption: const Some(EmailReceiveTimeType.allTime), + startDateOption: const None(), + endDateOption: const None(), + ); + break; + case QuickSearchFilter.from: + updateSimpleSearchFilter(fromOption: const None()); + break; + case QuickSearchFilter.to: + updateSimpleSearchFilter(toOption: const None()); + break; + case QuickSearchFilter.folder: + updateSimpleSearchFilter(mailboxOption: const None()); + break; + case QuickSearchFilter.labels: + updateSimpleSearchFilter(labelOption: const None()); + break; + default: + break; + } + searchEmailAction(); + } + + void onSelectLabelFilter(Label? newLabel) { + updateSimpleSearchFilter(labelOption: optionOf(newLabel)); + searchEmailAction(); + } +} diff --git a/lib/features/search/email/presentation/mixin/search_label_filter_modal_mixin.dart b/lib/features/search/email/presentation/mixin/search_label_filter_modal_mixin.dart new file mode 100644 index 000000000..c2b1ecabd --- /dev/null +++ b/lib/features/search/email/presentation/mixin/search_label_filter_modal_mixin.dart @@ -0,0 +1,92 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter/material.dart'; +import 'package:labels/model/label.dart'; +import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixin.dart'; +import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/label_drop_down_button.dart'; +import 'package:tmail_ui_user/features/search/email/presentation/model/context_item_label_type_action.dart'; +import 'package:tmail_ui_user/features/search/email/presentation/model/popup_menu_item_label_type_action.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +mixin SearchLabelFilterModalMixin on PopupContextMenuActionMixin { + void openLabelsFilterModal({ + required BuildContext context, + required List