TF-3939 Replace email selection bottom actions to top actions

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2025-08-05 11:50:14 +07:00
committed by Dat H. Pham
parent 6e86946a1a
commit b7af668d4a
10 changed files with 258 additions and 127 deletions
@@ -0,0 +1,32 @@
import 'package:model/email/email_action_type.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/email_selection_action_type.dart';
extension EmailSelectionActionTypeExtension on EmailSelectionActionType {
EmailActionType? toEmailActionType() {
switch (this) {
case EmailSelectionActionType.markAsRead:
return EmailActionType.markAsRead;
case EmailSelectionActionType.markAsUnread:
return EmailActionType.markAsUnread;
case EmailSelectionActionType.markAsStarred:
return EmailActionType.markAsStarred;
case EmailSelectionActionType.unMarkAsStarred:
return EmailActionType.unMarkAsStarred;
case EmailSelectionActionType.moveToFolder:
return EmailActionType.moveToMailbox;
case EmailSelectionActionType.moveToTrash:
return EmailActionType.moveToTrash;
case EmailSelectionActionType.markAsSpam:
return EmailActionType.moveToSpam;
case EmailSelectionActionType.markAsNotSpam:
return EmailActionType.unSpam;
case EmailSelectionActionType.archiveMessage:
return EmailActionType.archiveMessage;
case EmailSelectionActionType.deletePermanently:
return EmailActionType.deletePermanently;
case EmailSelectionActionType.selectAll:
case EmailSelectionActionType.moreAction:
return null;
}
}
}
@@ -0,0 +1,16 @@
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/thread/presentation/extensions/email_selection_action_type_extension.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/email_selection_action_type.dart';
import 'package:tmail_ui_user/features/thread/presentation/thread_controller.dart';
extension HandlePressEmailSelectionActionExtension on ThreadController {
void handlePressEmailSelectionAction(
EmailSelectionActionType type,
List<PresentationEmail> emails,
) {
final emailActionType = type.toEmailActionType();
if (emailActionType != null) {
pressEmailSelectionAction(emailActionType, emails);
}
}
}
@@ -0,0 +1,86 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:flutter/material.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
enum EmailSelectionActionType {
selectAll,
markAsRead,
markAsUnread,
markAsStarred,
unMarkAsStarred,
moveToFolder,
moveToTrash,
markAsSpam,
markAsNotSpam,
archiveMessage,
deletePermanently,
moreAction;
String getTitle(AppLocalizations appLocalizations) {
switch (this) {
case EmailSelectionActionType.selectAll:
return appLocalizations.select_all;
case EmailSelectionActionType.markAsRead:
return appLocalizations.mark_as_read;
case EmailSelectionActionType.markAsUnread:
return appLocalizations.mark_as_unread;
case EmailSelectionActionType.markAsStarred:
return appLocalizations.star;
case EmailSelectionActionType.unMarkAsStarred:
return appLocalizations.un_star;
case EmailSelectionActionType.moveToFolder:
return appLocalizations.move;
case EmailSelectionActionType.moveToTrash:
return appLocalizations.move_to_trash;
case EmailSelectionActionType.markAsSpam:
return appLocalizations.markAsSpam;
case EmailSelectionActionType.markAsNotSpam:
return appLocalizations.marked_as_not_spam;
case EmailSelectionActionType.archiveMessage:
return appLocalizations.archiveMessage;
case EmailSelectionActionType.deletePermanently:
return appLocalizations.delete_permanently;
case EmailSelectionActionType.moreAction:
return appLocalizations.more;
}
}
String getIcon(ImagePaths imagePaths) {
switch (this) {
case EmailSelectionActionType.selectAll:
return imagePaths.icCheckboxSelected;
case EmailSelectionActionType.markAsRead:
return imagePaths.icRead;
case EmailSelectionActionType.markAsUnread:
return imagePaths.icUnread;
case EmailSelectionActionType.markAsStarred:
return imagePaths.icStar;
case EmailSelectionActionType.unMarkAsStarred:
return imagePaths.icUnStar;
case EmailSelectionActionType.moveToFolder:
return imagePaths.icMoveMailbox;
case EmailSelectionActionType.moveToTrash:
case EmailSelectionActionType.deletePermanently:
return imagePaths.icMailboxTrash;
case EmailSelectionActionType.markAsSpam:
return imagePaths.icSpam;
case EmailSelectionActionType.markAsNotSpam:
return imagePaths.icNotSpam;
case EmailSelectionActionType.archiveMessage:
return imagePaths.icMailboxArchived;
case EmailSelectionActionType.moreAction:
return imagePaths.icMoreVertical;
}
}
Color getIconColor() {
if (this == EmailSelectionActionType.deletePermanently) {
return AppColor.redFF3347;
} else {
return AppColor.steelGrayA540;
}
}
double getIconSize() => 20.0;
}
@@ -8,25 +8,29 @@ import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option
class MobileAppBarThreadWidgetStyle {
static const double buttonMaxWidth = 80;
static const double titleOffset = 180;
static const double minHeight = 56;
static const double height = 52;
static const double actionIconSize = 20;
static const Color backgroundColor = Colors.white;
static const Color backButtonColor = AppColor.steelGrayA540;
static const Color actionIconColor = AppColor.steelGrayA540;
static const Color actionIconActiveColor = AppColor.primaryMain;
static EdgeInsetsGeometry getPadding(BuildContext context, ResponsiveUtils responsiveUtils) {
if (responsiveUtils.isPortraitMobile(context) || responsiveUtils.isLandscapeTablet(context)) {
return const EdgeInsets.symmetric(horizontal: 16, vertical: 8);
static EdgeInsetsGeometry getPadding(
BuildContext context,
ResponsiveUtils responsiveUtils,
) {
if (responsiveUtils.isPortraitMobile(context) ||
responsiveUtils.isLandscapeTablet(context)) {
return const EdgeInsets.symmetric(horizontal: 16);
} else {
return const EdgeInsets.symmetric(horizontal: 32, vertical: 8);
return const EdgeInsets.symmetric(horizontal: 32);
}
}
static const EdgeInsetsGeometry mailboxMenuPadding = EdgeInsets.all(5);
static const EdgeInsetsGeometry titlePadding = EdgeInsets.symmetric(horizontal: 16);
static TextStyle emailCounterTitleStyle =
ThemeUtils.defaultTextStyleInterFont.copyWith(
fontSize: 15,
fontWeight: FontWeight.w400,
static TextStyle emailCounterTitleStyle = ThemeUtils.textStyleBodyBody2(
color: AppColor.steelGrayA540,
);
static TextStyle titleTextStyle =
@@ -32,6 +32,7 @@ import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_st
import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart';
import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart';
import 'package:tmail_ui_user/features/thread/presentation/extensions/handle_open_context_menu_filter_email_action_extension.dart';
import 'package:tmail_ui_user/features/thread/presentation/extensions/handle_press_email_selection_action.dart';
import 'package:tmail_ui_user/features/thread/presentation/extensions/handle_pull_to_refresh_list_email_extension.dart';
import 'package:tmail_ui_user/features/thread/presentation/extensions/handle_select_message_filter_extension.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart';
@@ -41,7 +42,6 @@ import 'package:tmail_ui_user/features/thread/presentation/styles/scroll_to_top_
import 'package:tmail_ui_user/features/thread/presentation/styles/thread_view_style.dart';
import 'package:tmail_ui_user/features/thread/presentation/thread_controller.dart';
import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/app_bar_thread_widget.dart';
import 'package:tmail_ui_user/features/thread/presentation/widgets/bottom_bar_thread_selection_widget.dart';
import 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_builder.dart'
if (dart.library.html) 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_web_builder.dart';
import 'package:tmail_ui_user/features/thread/presentation/widgets/empty_emails_widget.dart';
@@ -90,8 +90,9 @@ class ThreadView extends GetWidget<ThreadController>
: null,
onPopupMenuFilterEmailAction: !controller.responsiveUtils.isScreenWithShortestSide(context)
? (filterOption, position) => controller.handleOpenContextMenuFilterEmailAction(context, position, filterOption)
: null
);
: null,
onPressEmailSelectionActionClick: controller.handlePressEmailSelectionAction,
);
}),
if (PlatformInfo.isMobile)
Obx(() {
@@ -169,7 +170,7 @@ class ThreadView extends GetWidget<ThreadController>
return const SizedBox.shrink();
}
}),
Obx(() => RecoverDeletedMessageLoadingBannerWidget(
Obx(() => RecoverDeletedMessageLoadingBannerWidget(
isLoading: controller.mailboxDashBoardController.isRecoveringDeletedMessage.value,
horizontalLoadingWidget: horizontalLoadingWidget,
responsiveUtils: controller.responsiveUtils,
@@ -252,7 +253,6 @@ class ThreadView extends GetWidget<ThreadController>
})
)
),
_buildListButtonSelectionForMobile(context),
]
)
))
@@ -294,54 +294,6 @@ class ThreadView extends GetWidget<ThreadController>
}
}
Widget _buildListButtonSelectionForMobile(BuildContext context) {
return Obx(() {
final listEmailSelected = controller.mailboxDashBoardController.emailsInCurrentMailbox.listEmailSelected;
final currentSelectMode = controller.mailboxDashBoardController.currentSelectMode.value;
final isSearchEmailRunning = controller.searchController.simpleSearchIsActivated.isTrue
|| controller.searchController.advancedSearchIsActivated.isTrue;
if (_validateDisplayBottomBarSelection(
context: context,
listEmailSelected: listEmailSelected,
isSearchEmailRunning: isSearchEmailRunning,
currentSelectMode: currentSelectMode
)) {
return BottomBarThreadSelectionWidget(
controller.imagePaths,
controller.responsiveUtils,
listEmailSelected,
controller.mailboxDashBoardController.selectedMailbox.value,
onPressEmailSelectionActionClick: controller.pressEmailSelectionAction
);
} else {
return const SizedBox.shrink();
}
});
}
bool _validateDisplayBottomBarSelection({
required BuildContext context,
required List<PresentationEmail> listEmailSelected,
required bool isSearchEmailRunning,
required SelectMode currentSelectMode,
}) {
if (PlatformInfo.isMobile && listEmailSelected.isNotEmpty) {
return true;
}
if (PlatformInfo.isWeb
&& currentSelectMode == SelectMode.ACTIVE
&& isSearchEmailRunning
&& !controller.responsiveUtils.isDesktop(context)
&& listEmailSelected.isNotEmpty
) {
return true;
}
return false;
}
Widget _buildFloatingButtonCompose(BuildContext context) {
if (controller.responsiveUtils.isWebDesktop(context)) {
return const SizedBox.shrink();
@@ -1,4 +1,3 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/utils/platform_info.dart';
@@ -8,17 +7,29 @@ import 'package:model/email/presentation_email.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:model/mailbox/select_mode.dart';
import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/email_selection_action_type.dart';
import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/mobile_app_bar_thread_widget.dart';
import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/web_app_bar_thread_widget.dart';
typedef OnPopupMenuFilterEmailAction = void Function(FilterMessageOption, RelativeRect);
typedef OnContextMenuFilterEmailAction = void Function(FilterMessageOption);
typedef OnPopupMenuFilterEmailAction = void Function(
FilterMessageOption option,
RelativeRect position,
);
typedef OnContextMenuFilterEmailAction = void Function(
FilterMessageOption option,
);
typedef OnOpenMailboxMenuActionClick = void Function();
typedef OnCancelEditThreadAction = void Function();
typedef OnEmailSelectionAction = void Function(EmailActionType, List<PresentationEmail>);
typedef OnEmailSelectionAction = void Function(
EmailActionType type,
List<PresentationEmail> emails,
);
typedef OnPressEmailSelectionActionClick = void Function(
EmailSelectionActionType type,
List<PresentationEmail> emails,
);
class AppBarThreadWidget extends StatelessWidget {
final ResponsiveUtils responsiveUtils;
final ImagePaths imagePaths;
final OnPopupMenuFilterEmailAction? onPopupMenuFilterEmailAction;
@@ -30,6 +41,7 @@ class AppBarThreadWidget extends StatelessWidget {
final List<PresentationEmail> listEmailSelected;
final SelectMode selectMode;
final FilterMessageOption filterOption;
final OnPressEmailSelectionActionClick? onPressEmailSelectionActionClick;
const AppBarThreadWidget({
Key? key,
@@ -44,6 +56,7 @@ class AppBarThreadWidget extends StatelessWidget {
required this.emailSelectionAction,
this.onPopupMenuFilterEmailAction,
this.onContextMenuFilterEmailAction,
this.onPressEmailSelectionActionClick,
}) : super(key: key);
@override
@@ -76,7 +89,8 @@ class AppBarThreadWidget extends StatelessWidget {
cancelEditThreadAction: cancelEditThreadAction,
onPopupMenuFilterEmailAction: onPopupMenuFilterEmailAction,
onContextMenuFilterEmailAction: onContextMenuFilterEmailAction,
onPressEmailSelectionActionClick: onPressEmailSelectionActionClick,
);
}
}
}
}
@@ -44,8 +44,11 @@ class DefaultMobileAppBarThreadWidget extends StatelessWidget {
return LayoutBuilder(builder: (context, constraints) {
return Container(
color: MobileAppBarThreadWidgetStyle.backgroundColor,
padding: MobileAppBarThreadWidgetStyle.getPadding(context, responsiveUtils),
constraints: const BoxConstraints(minHeight: MobileAppBarThreadWidgetStyle.minHeight),
padding: MobileAppBarThreadWidgetStyle.getPadding(
context,
responsiveUtils,
),
height: MobileAppBarThreadWidgetStyle.height,
child: Row(
children: [
TMailButtonWidget.fromIcon(
@@ -2,15 +2,17 @@ import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:flutter/material.dart';
import 'package:model/email/presentation_email.dart';
import 'package:model/extensions/list_presentation_email_extension.dart';
import 'package:model/extensions/presentation_mailbox_extension.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:model/mailbox/select_mode.dart';
import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/email_selection_action_type.dart';
import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/app_bar_thread_widget.dart';
import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/default_mobile_app_bar_thread_widget.dart';
import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/selection_mobile_app_bar_thread_widget.dart';
class MobileAppBarThreadWidget extends StatelessWidget {
final ResponsiveUtils responsiveUtils;
final ImagePaths imagePaths;
final PresentationMailbox? mailboxSelected;
@@ -21,6 +23,7 @@ class MobileAppBarThreadWidget extends StatelessWidget {
final OnPopupMenuFilterEmailAction? onPopupMenuFilterEmailAction;
final OnContextMenuFilterEmailAction? onContextMenuFilterEmailAction;
final OnCancelEditThreadAction cancelEditThreadAction;
final OnPressEmailSelectionActionClick? onPressEmailSelectionActionClick;
const MobileAppBarThreadWidget({
super.key,
@@ -34,26 +37,38 @@ class MobileAppBarThreadWidget extends StatelessWidget {
required this.cancelEditThreadAction,
this.onPopupMenuFilterEmailAction,
this.onContextMenuFilterEmailAction,
this.onPressEmailSelectionActionClick,
});
@override
Widget build(BuildContext context) {
if (selectMode == SelectMode.ACTIVE) {
final actionTypes = <EmailSelectionActionType>[
EmailSelectionActionType.selectAll,
if (mailboxSelected?.isArchive != true)
EmailSelectionActionType.archiveMessage,
if (mailboxSelected?.isDeletePermanentlyEnabled == true)
EmailSelectionActionType.deletePermanently
else
EmailSelectionActionType.moveToTrash,
if (listEmailSelected.isAllEmailRead)
EmailSelectionActionType.markAsUnread
else
EmailSelectionActionType.markAsRead,
EmailSelectionActionType.moreAction,
];
return SelectionMobileAppBarThreadWidget(
key: const Key('selection_mobile_app_bar_thread_widget'),
imagePaths: imagePaths,
responsiveUtils: responsiveUtils,
listEmailSelected: listEmailSelected,
mailboxSelected: mailboxSelected,
selectMode: selectMode,
filterOption: filterOption,
selectedEmails: listEmailSelected,
cancelEditThreadAction: cancelEditThreadAction,
onPopupMenuFilterEmailAction: onPopupMenuFilterEmailAction,
onContextMenuFilterEmailAction: onContextMenuFilterEmailAction,
emailSelectionActionTypes: actionTypes,
onPressEmailSelectionActionClick: (type, emails) =>
onPressEmailSelectionActionClick?.call(type, emails),
);
} else {
return DefaultMobileAppBarThreadWidget(
key: const Key('default_mobile_app_bar_thread_widget'),
imagePaths: imagePaths,
responsiveUtils: responsiveUtils,
listEmailSelected: listEmailSelected,
@@ -1,73 +1,79 @@
import 'package:core/presentation/resources/image_paths.dart';
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:model/email/presentation_email.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:model/mailbox/select_mode.dart';
import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/email_selection_action_type.dart';
import 'package:tmail_ui_user/features/thread/presentation/styles/app_bar/mobile_app_bar_thread_widget_style.dart';
import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/app_bar_thread_widget.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class SelectionMobileAppBarThreadWidget extends StatelessWidget {
final ResponsiveUtils responsiveUtils;
final ImagePaths imagePaths;
final PresentationMailbox? mailboxSelected;
final List<PresentationEmail> listEmailSelected;
final SelectMode selectMode;
final FilterMessageOption filterOption;
final OnPopupMenuFilterEmailAction? onPopupMenuFilterEmailAction;
final OnContextMenuFilterEmailAction? onContextMenuFilterEmailAction;
final List<PresentationEmail> selectedEmails;
final List<EmailSelectionActionType> emailSelectionActionTypes;
final OnPressEmailSelectionActionClick onPressEmailSelectionActionClick;
final OnCancelEditThreadAction cancelEditThreadAction;
const SelectionMobileAppBarThreadWidget({
super.key,
required this.responsiveUtils,
required this.imagePaths,
required this.listEmailSelected,
required this.mailboxSelected,
required this.selectMode,
required this.filterOption,
required this.selectedEmails,
required this.emailSelectionActionTypes,
required this.onPressEmailSelectionActionClick,
required this.cancelEditThreadAction,
this.onPopupMenuFilterEmailAction,
this.onContextMenuFilterEmailAction,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return Container(
color: MobileAppBarThreadWidgetStyle.backgroundColor,
padding: MobileAppBarThreadWidgetStyle.getPadding(context, responsiveUtils),
constraints: const BoxConstraints(minHeight: MobileAppBarThreadWidgetStyle.minHeight),
child: Row(
children: [
TMailButtonWidget(
key: const Key('mobile_cancel_selection_thread_button'),
text: AppLocalizations.of(context).count_email_selected(listEmailSelected.length),
icon: imagePaths.icCancel,
iconColor: MobileAppBarThreadWidgetStyle.backButtonColor,
textStyle: MobileAppBarThreadWidgetStyle.emailCounterTitleStyle,
backgroundColor: Colors.transparent,
onTapActionCallback: cancelEditThreadAction,
),
const Spacer(),
TMailButtonWidget.fromIcon(
key: const Key('mobile_filter_message_button'),
icon: imagePaths.icFilter,
iconColor: MobileAppBarThreadWidgetStyle.getFilterButtonColor(filterOption),
backgroundColor: Colors.transparent,
maxWidth: MobileAppBarThreadWidgetStyle.buttonMaxWidth,
tooltipMessage: AppLocalizations.of(context).filter_messages,
onTapActionCallback: () => onContextMenuFilterEmailAction?.call(filterOption),
onTapActionAtPositionCallback: (position) => onPopupMenuFilterEmailAction?.call(filterOption, position),
),
]
return Container(
color: MobileAppBarThreadWidgetStyle.backgroundColor,
padding: MobileAppBarThreadWidgetStyle.getPadding(
context,
responsiveUtils,
),
height: MobileAppBarThreadWidgetStyle.height,
child: Row(children: [
Expanded(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: TMailButtonWidget(
text: AppLocalizations.of(context).count_email_selected(
selectedEmails.length,
),
icon: imagePaths.icCancel,
iconColor: MobileAppBarThreadWidgetStyle.actionIconColor,
iconSize: MobileAppBarThreadWidgetStyle.actionIconSize,
textStyle:
MobileAppBarThreadWidgetStyle.emailCounterTitleStyle,
backgroundColor: Colors.transparent,
flexibleText: true,
mainAxisSize: MainAxisSize.min,
maxLines: 1,
onTapActionCallback: cancelEditThreadAction,
),
)
],
),
),
);
});
...emailSelectionActionTypes.map(
(type) => TMailButtonWidget.fromIcon(
icon: type.getIcon(imagePaths),
iconColor: type.getIconColor(),
iconSize: type.getIconSize(),
tooltipMessage: type.getTitle(AppLocalizations.of(context)),
backgroundColor: Colors.transparent,
onTapActionCallback: () => onPressEmailSelectionActionClick(
type,
selectedEmails,
),
),
),
]),
);
}
}
@@ -58,6 +58,9 @@ extension PresentationMailboxExtension on PresentationMailbox {
bool get allowedHasEmptyAction => (isTrash || isSpam) && countTotalEmails > 0;
bool get isDeletePermanentlyEnabled =>
isTrash == true || isDrafts == true || isSpam == true;
String get countTotalEmailsAsString {
if (countTotalEmails <= 0) return '';
return countTotalEmails <= 999 ? '$countTotalEmails' : '999+';