TF-3759 Update style popup context menu for filter message option on mailbox dashboard view

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2025-06-14 01:17:15 +07:00
committed by Dat H. Pham
parent b7e570293c
commit 506603b44f
7 changed files with 131 additions and 171 deletions
@@ -261,6 +261,7 @@ extension AppColor on Color {
static const gray686E76 = Color(0xFF686E76); static const gray686E76 = Color(0xFF686E76);
static const textSecondary = Color(0xFF1C1B1F); static const textSecondary = Color(0xFF1C1B1F);
static const profileMenuDivider = Color(0xFF1D192B); static const profileMenuDivider = Color(0xFF1D192B);
static const popupMenuItemHovered = Color(0xFFF8F8F8);
static const mapGradientColor = [ static const mapGradientColor = [
[Color(0xFF21D4FD), Color(0xFFB721FF)], [Color(0xFF21D4FD), Color(0xFFB721FF)],
@@ -76,6 +76,7 @@ class PopupMenuItemActionWidget extends StatelessWidget {
type: MaterialType.transparency, type: MaterialType.transparency,
child: InkWell( child: InkWell(
onTap: () => menuAction.onClick(menuActionClick), onTap: () => menuAction.onClick(menuActionClick),
hoverColor: AppColor.popupMenuItemHovered,
child: Container( child: Container(
height: 48, height: 48,
width: double.infinity, width: double.infinity,
@@ -2,10 +2,8 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.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/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mixin/filter_email_popup_menu_mixin.dart';
abstract class BaseMailboxDashBoardView extends GetWidget<MailboxDashBoardController> abstract class BaseMailboxDashBoardView extends GetWidget<MailboxDashBoardController>
with FilterEmailPopupMenuMixin, with AppLoaderMixin {
AppLoaderMixin {
BaseMailboxDashBoardView({Key? key}) : super(key: key); BaseMailboxDashBoardView({Key? key}) : super(key: key);
} }
@@ -8,6 +8,7 @@ import 'package:model/extensions/presentation_mailbox_extension.dart';
import 'package:model/extensions/session_extension.dart'; import 'package:model/extensions/session_extension.dart';
import 'package:tmail_ui_user/features/base/widget/clean_messages_banner.dart'; import 'package:tmail_ui_user/features/base/widget/clean_messages_banner.dart';
import 'package:tmail_ui_user/features/base/widget/popup_item_no_icon_widget.dart'; import 'package:tmail_ui_user/features/base/widget/popup_item_no_icon_widget.dart';
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart';
import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart';
import 'package:tmail_ui_user/features/composer/presentation/view/web/composer_overlay_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/view/web/composer_overlay_view.dart';
import 'package:tmail_ui_user/features/email/presentation/email_view.dart'; import 'package:tmail_ui_user/features/email/presentation/email_view.dart';
@@ -42,9 +43,11 @@ import 'package:tmail_ui_user/features/quotas/presentation/widget/quotas_banner_
import 'package:tmail_ui_user/features/search/email/presentation/search_email_view.dart'; import 'package:tmail_ui_user/features/search/email/presentation/search_email_view.dart';
import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_view.dart'; import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_view.dart';
import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/popup_menu_item_filter_message_action.dart';
import 'package:tmail_ui_user/features/thread/presentation/thread_view.dart'; import 'package:tmail_ui_user/features/thread/presentation/thread_view.dart';
import 'package:tmail_ui_user/features/thread/presentation/widgets/spam_banner/spam_report_banner_web_widget.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/spam_banner/spam_report_banner_web_widget.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
class MailboxDashBoardView extends BaseMailboxDashBoardView { class MailboxDashBoardView extends BaseMailboxDashBoardView {
@@ -437,18 +440,30 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView {
FilterMessageOption filterMessageCurrent, FilterMessageOption filterMessageCurrent,
RelativeRect buttonPosition RelativeRect buttonPosition
) { ) {
controller.openPopupMenuAction( final popupMenuItems = [
context, if (!controller.searchController.isSearchEmailRunning)
buttonPosition, FilterMessageOption.attachments,
popupMenuFilterEmailActionTile( FilterMessageOption.unread,
context, FilterMessageOption.starred,
filterMessageCurrent, ].map((filterOption) {
(filterMessageSelected) { return PopupMenuItem(
controller.dispatchAction(FilterMessageAction(filterMessageSelected)); padding: EdgeInsets.zero,
}, child: PopupMenuItemActionWidget(
isSearchEmailRunning: controller.searchController.isSearchEmailRunning menuAction: PopupMenuItemFilterMessageAction(
) filterOption,
); filterMessageCurrent,
AppLocalizations.of(context),
controller.imagePaths,
),
menuActionClick: (menuAction) {
popBack();
controller.dispatchAction(FilterMessageAction(menuAction.action));
},
),
);
}).toList();
controller.openPopupMenuAction(context, buttonPosition, popupMenuItems);
} }
void _onDeleteFilterMessageOptionAction() { void _onDeleteFilterMessageOptionAction() {
@@ -1,90 +0,0 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
mixin FilterEmailPopupMenuMixin {
final _imagePaths = Get.find<ImagePaths>();
List<PopupMenuEntry> popupMenuFilterEmailActionTile(
BuildContext context,
FilterMessageOption optionSelected,
Function(FilterMessageOption)? onCallBack,
{
bool isSearchEmailRunning = false
}
) {
return [
if (!isSearchEmailRunning)
...[
PopupMenuItem(
padding: EdgeInsets.zero,
child: _filterEmailAction(
context,
optionSelected,
FilterMessageOption.attachments,
onCallBack)),
const PopupMenuDivider(height: 0.5)
],
PopupMenuItem(
padding: EdgeInsets.zero,
child: _filterEmailAction(
context,
optionSelected,
FilterMessageOption.unread,
onCallBack)),
const PopupMenuDivider(height: 0.5),
PopupMenuItem(
padding: EdgeInsets.zero,
child: _filterEmailAction(
context,
optionSelected,
FilterMessageOption.starred,
onCallBack)),
];
}
Widget _filterEmailAction(
BuildContext context,
FilterMessageOption optionSelected,
FilterMessageOption option,
Function(FilterMessageOption)? onCallBack
) {
return InkWell(
onTap: () => onCallBack?.call(option),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: SizedBox(
child: Row(children: [
SvgPicture.asset(option.getContextMenuIcon(_imagePaths),
width: 20,
height: 20,
fit: BoxFit.fill,
colorFilter: option != FilterMessageOption.starred
? AppColor.colorTextButton.asFilter()
: null),
const SizedBox(width: 12),
Expanded(child: Text(
option.getName(AppLocalizations.of(context)),
style: const TextStyle(
fontSize: 15,
color: Colors.black,
fontWeight: FontWeight.w500))),
if (optionSelected == option)
... [
const SizedBox(width: 12),
SvgPicture.asset(_imagePaths.icFilterSelected,
width: 16,
height: 16,
fit: BoxFit.fill),
]
])
),
)
);
}
}
@@ -1,4 +1,3 @@
import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart';
@@ -7,57 +6,66 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/filter_message_button_style.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/filter_message_button_style.dart';
import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart';
typedef OnSelectFilterMessageOptionAction = Function( typedef OnSelectFilterMessageOptionAction = Function(BuildContext context,
BuildContext context, FilterMessageOption filterMessageOption, RelativeRect buttonPosition);
FilterMessageOption filterMessageOption, typedef OnDeleteFilterMessageOptionAction = Function(
RelativeRect buttonPosition); FilterMessageOption filterMessageOption);
typedef OnDeleteFilterMessageOptionAction = Function(FilterMessageOption filterMessageOption);
class FilterMessageButton extends StatelessWidget {
class FilterMessageButton extends StatefulWidget {
static const int _titleCharactersMaximum = 20; static const int _titleCharactersMaximum = 20;
final FilterMessageOption filterMessageOption; final FilterMessageOption filterMessageOption;
final bool isSelected; final bool isSelected;
final ImagePaths imagePaths; final ImagePaths imagePaths;
final OnSelectFilterMessageOptionAction? onSelectFilterMessageOptionAction; final OnSelectFilterMessageOptionAction onSelectFilterMessageOptionAction;
final OnDeleteFilterMessageOptionAction? onDeleteFilterMessageOptionAction; final OnDeleteFilterMessageOptionAction onDeleteFilterMessageOptionAction;
const FilterMessageButton({ const FilterMessageButton({
super.key, super.key,
required this.filterMessageOption, required this.filterMessageOption,
required this.imagePaths, required this.imagePaths,
this.isSelected = false, this.isSelected = false,
this.onSelectFilterMessageOptionAction, required this.onSelectFilterMessageOptionAction,
this.onDeleteFilterMessageOptionAction, required this.onDeleteFilterMessageOptionAction,
}); });
@override
State<FilterMessageButton> createState() => _FilterMessageButtonState();
}
class _FilterMessageButtonState extends State<FilterMessageButton> {
final GlobalKey _buttonKey = GlobalKey();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final filterMessageTitle = filterMessageOption.getTitle(context); final filterMessageTitle = widget.filterMessageOption.getTitle(context);
final deleteButtonWidget = TMailButtonWidget.fromIcon( final deleteButtonWidget = TMailButtonWidget.fromIcon(
icon: imagePaths.icDeleteSelection, icon: widget.imagePaths.icDeleteSelection,
iconSize: FilterMessageButtonStyle.deleteIconSize, iconSize: FilterMessageButtonStyle.deleteIconSize,
iconColor: AppColor.colorFilterMessageIcon, iconColor: AppColor.colorFilterMessageIcon,
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
onTapActionCallback: () => onDeleteFilterMessageOptionAction?.call(filterMessageOption), onTapActionCallback: () => widget.onDeleteFilterMessageOptionAction
.call(widget.filterMessageOption),
); );
final listComponentsWidget = [ final listComponentsWidget = [
SvgPicture.asset( SvgPicture.asset(
filterMessageOption.getIcon(imagePaths), widget.filterMessageOption.getIcon(widget.imagePaths),
width: FilterMessageButtonStyle.iconSize, width: FilterMessageButtonStyle.iconSize,
height: FilterMessageButtonStyle.iconSize, height: FilterMessageButtonStyle.iconSize,
colorFilter: filterMessageOption.getIconColor().asFilter(), colorFilter: widget.filterMessageOption.getIconColor().asFilter(),
fit: BoxFit.fill fit: BoxFit.fill,
), ),
const SizedBox(width: FilterMessageButtonStyle.spaceSize), const SizedBox(width: FilterMessageButtonStyle.spaceSize),
Text( Text(
filterMessageTitle.length > _titleCharactersMaximum filterMessageTitle.length > FilterMessageButton._titleCharactersMaximum
? '${filterMessageTitle.substring(0, _titleCharactersMaximum)}...' ? '${filterMessageTitle.substring(
: filterMessageTitle, 0,
FilterMessageButton._titleCharactersMaximum,
)}...'
: filterMessageTitle,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: FilterMessageButtonStyle.titleStyle, style: FilterMessageButtonStyle.titleStyle,
@@ -65,78 +73,108 @@ class FilterMessageButton extends StatelessWidget {
Padding( Padding(
padding: FilterMessageButtonStyle.elementPadding, padding: FilterMessageButtonStyle.elementPadding,
child: SvgPicture.asset( child: SvgPicture.asset(
imagePaths.icDropDown, widget.imagePaths.icDropDown,
width: FilterMessageButtonStyle.arrowDownIconSize, width: FilterMessageButtonStyle.arrowDownIconSize,
height: FilterMessageButtonStyle.arrowDownIconSize, height: FilterMessageButtonStyle.arrowDownIconSize,
colorFilter: AppColor.colorFilterMessageIcon.asFilter(), colorFilter: AppColor.colorFilterMessageIcon.asFilter(),
fit: BoxFit.fill fit: BoxFit.fill,
), ),
), ),
]; ];
final childItem = _buildContainerForComponents( final childItem = _buildContainerForComponents(
children: [ children: [
... listComponentsWidget, ...listComponentsWidget,
if (isSelected) deleteButtonWidget, if (widget.isSelected) deleteButtonWidget,
] ],
); );
if (onSelectFilterMessageOptionAction == null) { if (widget.isSelected) {
return childItem; return _buildContainerForComponents(
} key: _buttonKey,
children: [
if (!isSelected) { InkWell(
onTapDown: (details) => _onTapDownAction(context, details),
borderRadius: FilterMessageButtonStyle.borderRadius,
child: Row(
mainAxisSize: MainAxisSize.min,
children: listComponentsWidget,
),
),
if (widget.isSelected) deleteButtonWidget,
],
);
} else {
return Material( return Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: InkWell( child: InkWell(
key: _buttonKey,
onTapDown: (details) => _onTapDownAction(context, details), onTapDown: (details) => _onTapDownAction(context, details),
borderRadius: FilterMessageButtonStyle.borderRadius, borderRadius: FilterMessageButtonStyle.borderRadius,
child: childItem child: childItem,
), ),
); );
} }
return _buildContainerForComponents(
children: [
InkWell(
onTapDown: (details) => _onTapDownAction(context, details),
borderRadius: FilterMessageButtonStyle.borderRadius,
child: Row(
mainAxisSize: MainAxisSize.min,
children: listComponentsWidget,
),
),
if (isSelected) deleteButtonWidget,
]
);
} }
Widget _buildContainerForComponents({required List<Widget> children}) { Widget _buildContainerForComponents({
required List<Widget> children,
Key? key,
}) {
return Container( return Container(
key: key,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: FilterMessageButtonStyle.borderRadius, borderRadius: FilterMessageButtonStyle.borderRadius,
color: filterMessageOption.getBackgroundColor(isSelected: isSelected) color: widget.filterMessageOption.getBackgroundColor(
isSelected: widget.isSelected,
),
), ),
padding: FilterMessageButtonStyle.getButtonPadding(isSelected), padding: FilterMessageButtonStyle.getButtonPadding(widget.isSelected),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: children children: children,
) ),
); );
} }
void _onTapDownAction(BuildContext context, TapDownDetails details) { void _onTapDownAction(BuildContext context, TapDownDetails details) {
final screenSize = MediaQuery.sizeOf(context); RelativeRect? position;
final offset = details.globalPosition;
final position = RelativeRect.fromLTRB(
offset.dx,
offset.dy,
screenSize.width - offset.dx,
screenSize.height - offset.dy);
onSelectFilterMessageOptionAction?.call( final buttonBox =
_buttonKey.currentContext?.findRenderObject() as RenderBox?;
final overlayBox =
Overlay.maybeOf(context)?.context.findRenderObject() as RenderBox?;
if (buttonBox == null || overlayBox == null) {
final screenSize = MediaQuery.sizeOf(context);
final offset = details.globalPosition;
position = RelativeRect.fromLTRB(
offset.dx,
offset.dy,
screenSize.width - offset.dx,
screenSize.height - offset.dy,
);
} else {
final buttonPosition = buttonBox.localToGlobal(
Offset.zero,
ancestor: overlayBox,
);
final buttonSize = buttonBox.size;
position = RelativeRect.fromRect(
Rect.fromLTWH(
buttonPosition.dx,
buttonPosition.dy + buttonSize.height + 8,
0,
0,
),
Offset.zero & overlayBox.size,
);
}
widget.onSelectFilterMessageOptionAction.call(
context, context,
filterMessageOption, widget.filterMessageOption,
position); position,
);
} }
} }
@@ -17,7 +17,6 @@ import 'package:tmail_ui_user/features/email/presentation/model/popup_menu_item_
import 'package:tmail_ui_user/features/mailbox/domain/state/clear_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/clear_mailbox_state.dart';
import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/open_and_close_composer_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/open_and_close_composer_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mixin/filter_email_popup_menu_mixin.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/recover_deleted_message_loading_banner_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/recover_deleted_message_loading_banner_widget.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart';
@@ -50,9 +49,7 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart';
class ThreadView extends GetWidget<ThreadController> class ThreadView extends GetWidget<ThreadController>
with AppLoaderMixin, with AppLoaderMixin, PopupMenuWidgetMixin {
FilterEmailPopupMenuMixin,
PopupMenuWidgetMixin {
ThreadView({Key? key}) : super(key: key); ThreadView({Key? key}) : super(key: key);