TF-3759 Update style popup context menu for email more action on thread view

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2025-06-13 13:44:12 +07:00
committed by Dat H. Pham
parent 0a7ec53885
commit 5fe264c670
8 changed files with 173 additions and 139 deletions
@@ -64,24 +64,23 @@ mixin PopupContextMenuActionMixin {
});
}
void openPopupMenuAction(
Future<void> openPopupMenuAction(
BuildContext context,
RelativeRect? position,
List<PopupMenuEntry> popupMenuItems,
{
double? radius,
}
) async {
await showMenu(
context: context,
position: position ?? const RelativeRect.fromLTRB(16, 40, 16, 16),
color: Colors.white,
surfaceTintColor: Colors.white,
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(radius ?? 16))
menuPadding: const EdgeInsets.only(top: 8, bottom: 8),
elevation: 8,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(6)),
),
items: popupMenuItems
constraints: const BoxConstraints(maxWidth: 220),
items: popupMenuItems,
);
}
@@ -0,0 +1,38 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
typedef OnPopupMenuActionClick = void Function(PopupMenuItemAction action);
abstract class PopupMenuItemAction<T> with EquatableMixin {
final T action;
PopupMenuItemAction(this.action);
@override
List<Object?> get props => [action];
String get actionName;
String getActionNameWithLimitation({int limitCharacters = 22}) =>
actionName.length > limitCharacters
? '${actionName.substring(0, limitCharacters)}...'
: actionName;
Color get actionNameColor => Colors.black;
void onClick(OnPopupMenuActionClick callback) => callback(this);
}
mixin OptionalPopupIcon {
String get actionIcon;
Color get actionIconColor => AppColor.steelGrayA540;
double get actionIconSize => 20.0;
}
abstract class PopupMenuItemActionRequiredIcon<T>
extends PopupMenuItemAction<T> with OptionalPopupIcon {
PopupMenuItemActionRequiredIcon(super.action);
}
@@ -0,0 +1,59 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/utils/theme_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tmail_ui_user/features/base/model/popup_menu_item_action.dart';
class PopupMenuItemActionWidget extends StatelessWidget {
final PopupMenuItemAction menuAction;
final OnPopupMenuActionClick menuActionClick;
const PopupMenuItemActionWidget({
super.key,
required this.menuAction,
required this.menuActionClick,
});
@override
Widget build(BuildContext context) {
Widget? iconWidget;
if (menuAction is PopupMenuItemActionRequiredIcon) {
final specificMenuAction = menuAction as PopupMenuItemActionRequiredIcon;
iconWidget = Padding(
padding: const EdgeInsetsDirectional.only(end: 16),
child: SvgPicture.asset(
specificMenuAction.actionIcon,
width: specificMenuAction.actionIconSize,
height: specificMenuAction.actionIconSize,
colorFilter: specificMenuAction.actionIconColor.asFilter(),
fit: BoxFit.fill,
),
);
}
return Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () => menuAction.onClick(menuActionClick),
child: Container(
height: 48,
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
if (iconWidget != null) iconWidget,
Expanded(
child: Text(
menuAction.getActionNameWithLimitation(),
style: ThemeUtils.textStyleBodyBody3(color: Colors.black),
maxLines: 1,
),
),
],
),
),
),
);
}
}
@@ -61,7 +61,6 @@ class ComposerView extends GetWidget<ComposerController> {
context,
position,
_createMoreOptionPopupItems(context),
radius: ComposerStyle.popupMenuRadius
);
},
isNetworkConnectionAvailable: controller.isNetworkConnectionAvailable,
@@ -87,7 +86,6 @@ class ComposerView extends GetWidget<ComposerController> {
context,
position,
_createMoreOptionPopupItems(context),
radius: ComposerStyle.popupMenuRadius
);
},
isNetworkConnectionAvailable: controller.isNetworkConnectionAvailable,
@@ -150,6 +150,8 @@ extension EmailActionTypeExtension on EmailActionType {
return imagePaths.icDownloadAttachment;
case EmailActionType.editAsNewEmail:
return imagePaths.icEdit;
case EmailActionType.openInNewTab:
return imagePaths.icOpenInNewTab;
default:
return '';
}
@@ -173,6 +175,8 @@ extension EmailActionTypeExtension on EmailActionType {
return appLocalizations.downloadMessageAsEML;
case EmailActionType.editAsNewEmail:
return appLocalizations.editAsNewEmail;
case EmailActionType.openInNewTab:
return appLocalizations.openInNewTab;
default:
return '';
}
@@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
class ComposerStyle {
static const double radius = 28;
static const double keyboardToolBarHeight = 200;
static const double popupMenuRadius = 8;
static const double suggestionItemHeight = 60;
static const double normalWidth = 600;
static const double normalHeight = 525;
@@ -0,0 +1,23 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:model/email/email_action_type.dart';
import 'package:tmail_ui_user/features/base/model/popup_menu_item_action.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class PopupMenuItemEmailAction
extends PopupMenuItemActionRequiredIcon<EmailActionType> {
final AppLocalizations appLocalizations;
final ImagePaths imagePaths;
PopupMenuItemEmailAction(
super.action,
this.appLocalizations,
this.imagePaths,
);
@override
String get actionIcon => action.getIcon(imagePaths);
@override
String get actionName => action.getTitle(appLocalizations);
}
+42 -128
View File
@@ -10,8 +10,10 @@ import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart';
import 'package:tmail_ui_user/features/base/mixin/popup_menu_widget_mixin.dart';
import 'package:tmail_ui_user/features/base/widget/clean_messages_banner.dart';
import 'package:tmail_ui_user/features/base/widget/compose_floating_button.dart';
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart';
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
import 'package:tmail_ui_user/features/email/presentation/model/context_item_email_action.dart';
import 'package:tmail_ui_user/features/email/presentation/model/popup_menu_item_email_action.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_dashboard/presentation/extensions/open_and_close_composer_extension.dart';
@@ -622,23 +624,25 @@ class ThreadView extends GetWidget<ThreadController>
PresentationEmail presentationEmail,
RelativeRect? position
) {
final mailboxContain = presentationEmail.mailboxContain;
final isDrafts = mailboxContain?.isDrafts ?? false;
final isChildOfTeamMailboxes =
mailboxContain?.isChildOfTeamMailboxes ?? false;
final isSpam = mailboxContain?.isSpam ?? false;
final isArchive = mailboxContain?.isArchive ?? false;
final isTemplates = mailboxContain?.isTemplates ?? false;
final listEmailActions = [
EmailActionType.openInNewTab,
if (!isDrafts && !isChildOfTeamMailboxes)
isSpam ? EmailActionType.unSpam : EmailActionType.moveToSpam,
if (!isArchive) EmailActionType.archiveMessage,
if (!isDrafts && !isTemplates) EmailActionType.editAsNewEmail,
];
if (listEmailActions.isEmpty) return;
if (controller.responsiveUtils.isScreenWithShortestSide(context)) {
final mailboxContain = presentationEmail.mailboxContain;
final listEmailActions = [
EmailActionType.openInNewTab,
if (mailboxContain?.isDrafts == false && mailboxContain?.isChildOfTeamMailboxes == false)
mailboxContain?.isSpam == true
? EmailActionType.unSpam
: EmailActionType.moveToSpam,
if (mailboxContain?.isArchive == false)
EmailActionType.archiveMessage,
if (mailboxContain?.isDrafts == false && mailboxContain?.isTemplates == false)
EmailActionType.editAsNewEmail,
];
if (listEmailActions.isEmpty) return;
final contextMenuActions = listEmailActions
.map((action) => ContextItemEmailAction(
action,
@@ -659,10 +663,31 @@ class ThreadView extends GetWidget<ThreadController>
},
);
} else {
final popupMenuActions = listEmailActions.map((actionType) {
return PopupMenuItem(
padding: EdgeInsets.zero,
child: PopupMenuItemActionWidget(
menuAction: PopupMenuItemEmailAction(
actionType,
AppLocalizations.of(context),
controller.imagePaths,
),
menuActionClick: (menuAction) {
popBack();
controller.handleEmailActionType(
menuAction.action,
presentationEmail,
mailboxContain: mailboxContain,
);
},
),
);
}).toList();
controller.openPopupMenuAction(
context,
position,
_popupMenuActionTile(context, presentationEmail)
popupMenuActions,
);
}
}
@@ -737,117 +762,6 @@ class ThreadView extends GetWidget<ThreadController>
));
}
List<PopupMenuEntry> _popupMenuActionTile(BuildContext context, PresentationEmail email) {
final mailboxContain = email.mailboxContain;
return [
_buildOpenInNewTabPopupMenuItem(context, email, mailboxContain),
if (mailboxContain?.isDrafts == false && mailboxContain?.isChildOfTeamMailboxes == false)
_buildMarkAsSpamPopupMenuItem(context, email, mailboxContain),
if (mailboxContain?.isArchive == false)
_buildArchiveMessagePopupMenuItem(context, email),
if (mailboxContain?.isDrafts == false && mailboxContain?.isTemplates == false)
_buildEditAsNewEmailPopupMenuItem(AppLocalizations.of(context), email),
];
}
PopupMenuEntry _buildMarkAsSpamPopupMenuItem(
BuildContext context,
PresentationEmail email,
PresentationMailbox? mailboxContain
) {
return PopupMenuItem(
padding: EdgeInsets.zero,
child: popupItem(
mailboxContain?.isSpam == true ? controller.imagePaths.icNotSpam : controller.imagePaths.icSpam,
mailboxContain?.isSpam == true
? AppLocalizations.of(context).remove_from_spam
: AppLocalizations.of(context).mark_as_spam,
colorIcon: AppColor.colorTextButton,
styleName: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
color: Colors.black
),
onCallbackAction: () => controller.handleEmailActionType(
mailboxContain?.isSpam == true ? EmailActionType.unSpam : EmailActionType.moveToSpam,
email,
mailboxContain: mailboxContain,
)
)
);
}
PopupMenuEntry _buildOpenInNewTabPopupMenuItem(
BuildContext context,
PresentationEmail email,
PresentationMailbox? mailboxContain
) {
return PopupMenuItem(
padding: EdgeInsets.zero,
child: popupItem(
controller.imagePaths.icOpenInNewTab,
AppLocalizations.of(context).openInNewTab,
colorIcon: AppColor.colorTextButton,
styleName: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
color: Colors.black
),
onCallbackAction: () {
popBack();
controller.openEmailInNewTabAction(email);
}
)
);
}
PopupMenuEntry _buildArchiveMessagePopupMenuItem(
BuildContext context,
PresentationEmail email
) {
return PopupMenuItem(
padding: EdgeInsets.zero,
child: popupItem(
controller.imagePaths.icMailboxArchived,
AppLocalizations.of(context).archiveMessage,
colorIcon: AppColor.colorTextButton,
styleName: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
color: Colors.black
),
onCallbackAction: () {
popBack();
controller.archiveMessage(context, email);
}
)
);
}
PopupMenuEntry _buildEditAsNewEmailPopupMenuItem(
AppLocalizations appLocalizations,
PresentationEmail email,
) {
return PopupMenuItem(
padding: EdgeInsets.zero,
child: popupItem(
controller.imagePaths.icEdit,
appLocalizations.editAsNewEmail,
colorIcon: AppColor.colorTextButton,
styleName: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
color: Colors.black
),
onCallbackAction: () {
popBack();
controller.editAsNewEmail(email);
}
)
);
}
Widget _buildMailboxActionProgressBanner(BuildContext context) {
return Obx(() {
return _MailboxActionProgressBanner(