diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index d9498eeb3..b6cddb738 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -3,8 +3,9 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.dart'; +import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixin.dart'; -abstract class BaseController extends GetxController with MessageDialogActionMixin { +abstract class BaseController extends GetxController with MessageDialogActionMixin, PopupContextMenuActionMixin { final viewState = Rx>(Right(UIState.idle)); final connectivityResult = Rxn(); diff --git a/lib/features/base/mixin/popup_context_menu_action_mixin.dart b/lib/features/base/mixin/popup_context_menu_action_mixin.dart new file mode 100644 index 000000000..0eb935bd7 --- /dev/null +++ b/lib/features/base/mixin/popup_context_menu_action_mixin.dart @@ -0,0 +1,35 @@ + +import 'package:core/core.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +mixin PopupContextMenuActionMixin { + + void openContextMenuAction(BuildContext context, List actionTiles, {Widget? cancelButton}) { + (CupertinoActionSheetBuilder(context) + ..addTiles(actionTiles) + ..addCancelButton(cancelButton ?? buildCancelButton(context))) + .show(); + } + + void openPopupMenuAction(BuildContext context, RelativeRect? position, List popupMenuItems) async { + await showMenu( + context: context, + position: position ?? RelativeRect.fromLTRB(16, 40, 16, 16), + color: Colors.white, + elevation: 5, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + items: popupMenuItems); + } + + Widget buildCancelButton(BuildContext context) { + return CupertinoActionSheetAction( + child: Text( + AppLocalizations.of(context).cancel, + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20, color: AppColor.colorTextButton)), + onPressed: () => popBack(), + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/email_controller.dart b/lib/features/email/presentation/email_controller.dart index 0cfb2d880..6aa97b564 100644 --- a/lib/features/email/presentation/email_controller.dart +++ b/lib/features/email/presentation/email_controller.dart @@ -505,23 +505,6 @@ class EmailController extends BaseController { } } - void openMoreMenuEmailAction(BuildContext context, List actionTiles, {Widget? cancelButton}) { - (CupertinoActionSheetBuilder(context) - ..addTiles(actionTiles) - ..addCancelButton(cancelButton)) - .show(); - } - - void openMoreMenuEmailActionForTablet(BuildContext context, RelativeRect? position, List popupMenuItems) async { - await showMenu( - context: context, - position: position ?? RelativeRect.fromLTRB(16, 40, 16, 16), - color: Colors.white, - elevation: 5, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), - items: popupMenuItems); - } - void showFullEmailAddress() { isDisplayFullEmailAddress.value = true; } @@ -625,10 +608,6 @@ class EmailController extends BaseController { mailboxDashBoardController.deleteEmailPermanently(email); } - void closeMoreMenu() { - popBack(); - } - void backToThreadView(BuildContext context) { mailboxDashBoardController.clearSelectedEmail(); if (responsiveUtils.isDesktop(context) || responsiveUtils.isTabletLarge(context)) { diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index caa8d4691..cdbad3e0e 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -126,8 +126,8 @@ class EmailView extends GetView with UserSettingPopupMenuMixin, NetworkConnectio ..onBackActionClick(() => emailController.backToThreadView(context)) ..addOnEmailActionClick((email, action) => emailController.handleEmailAction(context, email, action)) ..addOnMoreActionClick((email, position) => responsiveUtils.isMobile(context) - ? emailController.openMoreMenuEmailAction(context, _emailActionMoreActionTile(context, email), cancelButton: _buildCancelButton(context)) - : emailController.openMoreMenuEmailActionForTablet(context, position, _popupMenuEmailActionTile(context, email)))) + ? emailController.openContextMenuAction(context, _emailActionMoreActionTile(context, email)) + : emailController.openPopupMenuAction(context, position, _popupMenuEmailActionTile(context, email)))) .build())); } @@ -548,15 +548,6 @@ class EmailView extends GetView with UserSettingPopupMenuMixin, NetworkConnectio ]; } - Widget _buildCancelButton(BuildContext context) { - return CupertinoActionSheetAction( - child: Text( - AppLocalizations.of(context).cancel, - style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20, color: AppColor.colorTextButton)), - onPressed: () => emailController.closeMoreMenu(), - ); - } - Widget _markAsEmailUnreadAction(BuildContext context, PresentationEmail email) { return (EmailActionCupertinoActionSheetActionBuilder( Key('mark_as_unread_action'), diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 553da86c1..6156b2f87 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -630,7 +630,7 @@ class ThreadController extends BaseController { final newCurrentMailboxId = destinationMailboxId; final newDestinationMailboxId = currentMailboxId; if (newCurrentMailboxId != null && newDestinationMailboxId != null) { - _revertedToOriginalMailbox(MoveToMailboxRequest( + _revertedSelectionEmailToOriginalMailbox(MoveToMailboxRequest( movedEmailIds, newCurrentMailboxId, newDestinationMailboxId, @@ -703,7 +703,7 @@ class ThreadController extends BaseController { } } - void _revertedToOriginalMailbox(MoveToMailboxRequest newMoveRequest) { + void _revertedSelectionEmailToOriginalMailbox(MoveToMailboxRequest newMoveRequest) { if (_accountId != null) { consumeState(_moveMultipleEmailToMailboxInteractor.execute(_accountId!, newMoveRequest)); } @@ -890,20 +890,28 @@ class ThreadController extends BaseController { markAsStarEmail(selectedEmail, MarkStarAction.unMarkStar); break; case EmailActionType.moveToMailbox: - moveToMailboxAction(context, selectedEmail); + moveToMailbox(context, selectedEmail); break; case EmailActionType.moveToTrash: - moveToTrashAction(selectedEmail); + moveToTrash(selectedEmail); break; case EmailActionType.deletePermanently: deleteEmailPermanently(context, selectedEmail); break; + case EmailActionType.moveToSpam: + popBack(); + moveToSpam(selectedEmail); + break; + case EmailActionType.unSpam: + popBack(); + unSpam(selectedEmail); + break; default: break; } } - void moveToMailboxAction(BuildContext context, PresentationEmail email) async { + void moveToMailbox(BuildContext context, PresentationEmail email) async { final currentMailbox = mailboxDashBoardController.selectedMailbox.value; final accountId = mailboxDashBoardController.accountId.value; @@ -914,15 +922,22 @@ class ThreadController extends BaseController { ); if (destinationMailbox != null && destinationMailbox is PresentationMailbox) { - if (destinationMailbox.role == PresentationMailbox.roleTrash) { - _moveToTrash(accountId, MoveToMailboxRequest( + if (destinationMailbox.isTrash) { + _moveToTrashAction(accountId, MoveToMailboxRequest( [email.id], currentMailbox.id, destinationMailbox.id, MoveAction.moving, EmailActionType.moveToTrash)); + } else if (destinationMailbox.isSpam) { + _moveToSpamAction(accountId, MoveToMailboxRequest( + [email.id], + currentMailbox.id, + destinationMailbox.id, + MoveAction.moving, + EmailActionType.moveToSpam)); } else { - _moveToMailbox(accountId, MoveToMailboxRequest( + _moveToMailboxAction(accountId, MoveToMailboxRequest( [email.id], currentMailbox.id, destinationMailbox.id, @@ -934,7 +949,7 @@ class ThreadController extends BaseController { } } - void _moveToMailbox(AccountId accountId, MoveToMailboxRequest moveRequest) { + void _moveToMailboxAction(AccountId accountId, MoveToMailboxRequest moveRequest) { consumeState(_moveToMailboxInteractor.execute(accountId, moveRequest)); } @@ -945,37 +960,36 @@ class ThreadController extends BaseController { _appToast.showToastWithAction( currentOverlayContext!, AppLocalizations.of(currentContext!).moved_to_mailbox(success.destinationPath ?? ''), - AppLocalizations.of(currentContext!).undo_action, - () { - final newMoveRequest = MoveToMailboxRequest( + AppLocalizations.of(currentContext!).undo_action, () { + _revertedToOriginalMailbox(MoveToMailboxRequest( [success.emailId], success.destinationMailboxId, success.currentMailboxId, MoveAction.undo, - success.emailActionType); - _undoMoveToMailbox(newMoveRequest); + success.emailActionType)); } ); } } - void _undoMoveToMailbox(MoveToMailboxRequest newMoveRequest) { - if (_accountId != null) { - _moveToMailbox(_accountId!, newMoveRequest); + void _revertedToOriginalMailbox(MoveToMailboxRequest newMoveRequest) { + final accountId = mailboxDashBoardController.accountId.value; + if (accountId != null) { + _moveToMailboxAction(accountId, newMoveRequest); } } - void _moveToTrash(AccountId accountId, MoveToMailboxRequest moveRequest) { + void _moveToTrashAction(AccountId accountId, MoveToMailboxRequest moveRequest) { mailboxDashBoardController.moveToMailbox(accountId, moveRequest); } - void moveToTrashAction(PresentationEmail email) async { + void moveToTrash(PresentationEmail email) async { final currentMailbox = mailboxDashBoardController.selectedMailbox.value; final accountId = mailboxDashBoardController.accountId.value; final trashMailboxId = mailboxDashBoardController.mapDefaultMailboxId[PresentationMailbox.roleTrash]; if (currentMailbox != null && accountId != null && trashMailboxId != null) { - _moveToTrash(accountId, MoveToMailboxRequest( + _moveToTrashAction(accountId, MoveToMailboxRequest( [email.id], currentMailbox.id, trashMailboxId, @@ -985,6 +999,42 @@ class ThreadController extends BaseController { } } + void moveToSpam(PresentationEmail email) async { + final currentMailbox = mailboxDashBoardController.selectedMailbox.value; + final accountId = mailboxDashBoardController.accountId.value; + final spamMailboxId = mailboxDashBoardController.getMailboxIdByRole(PresentationMailbox.roleSpam); + + if (currentMailbox != null && accountId != null && spamMailboxId != null) { + _moveToSpamAction(accountId, MoveToMailboxRequest( + [email.id], + currentMailbox.id, + spamMailboxId, + MoveAction.moving, + EmailActionType.moveToSpam) + ); + } + } + + void unSpam(PresentationEmail email) async { + final accountId = mailboxDashBoardController.accountId.value; + final spamMailboxId = mailboxDashBoardController.getMailboxIdByRole(PresentationMailbox.roleSpam); + final inboxMailboxId = mailboxDashBoardController.getMailboxIdByRole(PresentationMailbox.roleInbox); + + if (inboxMailboxId != null && accountId != null && spamMailboxId != null) { + _moveToSpamAction(accountId, MoveToMailboxRequest( + [email.id], + spamMailboxId, + inboxMailboxId, + MoveAction.moving, + EmailActionType.unSpam) + ); + } + } + + void _moveToSpamAction(AccountId accountId, MoveToMailboxRequest moveRequest) { + mailboxDashBoardController.moveToMailbox(accountId, moveRequest); + } + void deleteEmailPermanently(BuildContext context, PresentationEmail email) { if (_responsiveUtils.isMobile(context)) { (ConfirmationDialogActionSheetBuilder(context) diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index e60ca2418..a7d8232ec 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/user_setting_popup_menu_mixin.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_more_email_state.dart'; @@ -600,7 +601,7 @@ class ThreadView extends GetWidget with UserSettingPopupMenuMi physics: AlwaysScrollableScrollPhysics(), key: PageStorageKey('list_presentation_email_in_threads'), itemCount: listPresentationEmail.length, - padding: EdgeInsets.zero, + padding: EdgeInsets.only(top: kIsWeb && !_responsiveUtils.isDesktop(context) ? 10 : 0), itemBuilder: (context, index) => Obx(() => (EmailTileBuilder( context, listPresentationEmail[index], @@ -608,7 +609,10 @@ class ThreadView extends GetWidget with UserSettingPopupMenuMi controller.currentSelectMode.value, controller.mailboxDashBoardController.searchState.value.searchStatus, controller.searchQuery) - ..addOnPressEmailActionClick((action, email) => controller.pressEmailAction(context, action, email))) + ..addOnPressEmailActionClick((action, email) => controller.pressEmailAction(context, action, email)) + ..addOnMoreActionClick((email, position) => _responsiveUtils.isMobile(context) + ? controller.openContextMenuAction(context, _contextMenuActionTile(context, email)) + : controller.openPopupMenuAction(context, position, _popupMenuActionTile(context, email)))) .build()), ) ); @@ -703,4 +707,40 @@ class ThreadView extends GetWidget with UserSettingPopupMenuMi ]), ); } + + List _contextMenuActionTile(BuildContext context, PresentationEmail email) { + return [ + _markAsEmailSpamOrUnSpamAction(context, email), + ]; + } + + Widget _markAsEmailSpamOrUnSpamAction(BuildContext context, PresentationEmail email) { + return (EmailActionCupertinoActionSheetActionBuilder( + Key('mark_as_spam_or_un_spam_action'), + SvgPicture.asset( + controller.currentMailbox?.isSpam == true ? _imagePaths.icNotSpam : _imagePaths.icSpam, + width: 28, height: 28, fit: BoxFit.fill, color: AppColor.colorTextButton), + controller.currentMailbox?.isSpam == true + ? AppLocalizations.of(context).remove_from_spam + : AppLocalizations.of(context).mark_as_spam, + email, + iconLeftPadding: _responsiveUtils.isMobile(context) + ? EdgeInsets.only(left: 12, right: 16) + : EdgeInsets.only(right: 12), + iconRightPadding: _responsiveUtils.isMobile(context) + ? EdgeInsets.only(right: 12) + : EdgeInsets.zero) + ..onActionClick((email) => controller.pressEmailAction(context, + controller.currentMailbox?.isSpam == true + ? EmailActionType.unSpam + : EmailActionType.moveToSpam, + email))) + .build(); + } + + List _popupMenuActionTile(BuildContext context, PresentationEmail email) { + return [ + PopupMenuItem(padding: EdgeInsets.symmetric(horizontal: 8), child: _markAsEmailSpamOrUnSpamAction(context, email)), + ]; + } } \ No newline at end of file diff --git a/lib/features/thread/presentation/widgets/email_tile_builder.dart b/lib/features/thread/presentation/widgets/email_tile_builder.dart index 2942ba542..094f1cb61 100644 --- a/lib/features/thread/presentation/widgets/email_tile_builder.dart +++ b/lib/features/thread/presentation/widgets/email_tile_builder.dart @@ -9,6 +9,7 @@ import 'package:tmail_ui_user/features/thread/presentation/model/search_status.d import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; typedef OnPressEmailActionClick = void Function(EmailActionType, PresentationEmail); +typedef OnMoreActionClick = void Function(PresentationEmail, RelativeRect?); class EmailTileBuilder { @@ -23,6 +24,7 @@ class EmailTileBuilder { final SearchQuery? _searchQuery; OnPressEmailActionClick? _emailActionClick; + OnMoreActionClick? _onMoreActionClick; bool isHoverItem = false; bool isHoverItemSelected = false; @@ -40,6 +42,10 @@ class EmailTileBuilder { _emailActionClick = actionClick; } + void addOnMoreActionClick(OnMoreActionClick onMoreActionClick) { + _onMoreActionClick = onMoreActionClick; + } + Widget build() { return Theme( key: Key('thread_tile'), diff --git a/lib/features/thread/presentation/widgets/email_tile_web_builder.dart b/lib/features/thread/presentation/widgets/email_tile_web_builder.dart index 5e9b88fe0..dae887260 100644 --- a/lib/features/thread/presentation/widgets/email_tile_web_builder.dart +++ b/lib/features/thread/presentation/widgets/email_tile_web_builder.dart @@ -9,6 +9,7 @@ import 'package:tmail_ui_user/features/thread/presentation/model/search_status.d import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; typedef OnPressEmailActionClick = void Function(EmailActionType, PresentationEmail); +typedef OnMoreActionClick = void Function(PresentationEmail, RelativeRect?); class EmailTileBuilder { @@ -23,6 +24,7 @@ class EmailTileBuilder { final SearchQuery? _searchQuery; OnPressEmailActionClick? _emailActionClick; + OnMoreActionClick? _onMoreActionClick; bool isHoverItem = false; @@ -39,6 +41,10 @@ class EmailTileBuilder { _emailActionClick = actionClick; } + void addOnMoreActionClick(OnMoreActionClick onMoreActionClick) { + _onMoreActionClick = onMoreActionClick; + } + Widget build() { return Theme( key: Key('thread_tile'), @@ -398,38 +404,56 @@ class EmailTileBuilder { icon: SvgPicture.asset( _presentationEmail.hasRead ? _imagePaths.icRead : _imagePaths.icUnread, color: AppColor.colorActionButtonHover, - width: _responsiveUtils.isDesktop(_context) ? 16 : 14, - height: _responsiveUtils.isDesktop(_context) ? 16 : 14, + width: 16, + height: 16, fit: BoxFit.fill), - splashRadius: _responsiveUtils.isDesktop(_context) ? 15 : 10, tooltip: _presentationEmail.hasRead ? AppLocalizations.of(_context).mark_as_unread : AppLocalizations.of(_context).mark_as_read, onTap: () => _emailActionClick?.call( _presentationEmail.hasRead ? EmailActionType.markAsUnread : EmailActionType.markAsRead, _presentationEmail)), - buildIconWeb( - icon: SvgPicture.asset( - _imagePaths.icMove, - width: _responsiveUtils.isDesktop(_context) ? 16 : 14, - height: _responsiveUtils.isDesktop(_context) ? 16 : 14, - color: AppColor.colorActionButtonHover, - fit: BoxFit.fill), - splashRadius: _responsiveUtils.isDesktop(_context) ? 15 : 10, - tooltip: AppLocalizations.of(_context).move, - onTap: () => _emailActionClick?.call(EmailActionType.moveToMailbox, _presentationEmail)), + if (_mailboxRole != PresentationMailbox.roleDrafts) + buildIconWeb( + icon: SvgPicture.asset( + _imagePaths.icMove, + width: 16, + height: 16, + color: AppColor.colorActionButtonHover, + fit: BoxFit.fill), + tooltip: AppLocalizations.of(_context).move, + onTap: () => _emailActionClick?.call(EmailActionType.moveToMailbox, _presentationEmail)), buildIconWeb( icon: SvgPicture.asset( _imagePaths.icDelete, - width: _responsiveUtils.isDesktop(_context) ? 16 : 14, - height: _responsiveUtils.isDesktop(_context) ? 16 : 14, + width: 16, + height: 16, color: AppColor.colorActionButtonHover, fit: BoxFit.fill), - splashRadius: _responsiveUtils.isDesktop(_context) ? 15 : 10, tooltip: _mailboxRole != PresentationMailbox.roleTrash ? AppLocalizations.of(_context).move_to_trash : AppLocalizations.of(_context).delete_permanently, onTap: () => _emailActionClick?.call( _mailboxRole != PresentationMailbox.roleTrash ? EmailActionType.moveToTrash : EmailActionType.deletePermanently, _presentationEmail)), + if (_mailboxRole != PresentationMailbox.roleDrafts) + buildIconWebHasPosition( + _context, + icon: SvgPicture.asset( + _imagePaths.icMore, + width: 16, + height: 16, + color: AppColor.colorActionButtonHover, + fit: BoxFit.fill), + tooltip: AppLocalizations.of(_context).more, + onTap: () { + if (_responsiveUtils.isMobile(_context)) { + _onMoreActionClick?.call(_presentationEmail, null); + } + }, + onTapDown: (position) { + if (!_responsiveUtils.isMobile(_context)) { + _onMoreActionClick?.call(_presentationEmail, position); + } + }), if (_responsiveUtils.isDesktop(_context)) SizedBox(width: 16), ]); }