From d34ddf5d399b7c6bcb8e5daf8af796565d1771d4 Mon Sep 17 00:00:00 2001 From: DatDang Date: Wed, 6 Aug 2025 11:23:35 +0700 Subject: [PATCH] TF-3947 Thread Detail Thread level move & delete --- .../email_action_type_extension.dart | 2 +- .../widgets/email_view_app_bar_widget.dart | 2 +- .../mailbox_dashboard_controller.dart | 49 ++++++++++++--- .../get_thread_detail_action_status.dart | 22 +++++++ .../on_thread_detail_action_click.dart | 60 ++++++++++++++++++- .../presentation/thread_detail_view.dart | 1 + .../widgets/thread_detail_app_bar.dart | 25 +++++++- lib/l10n/intl_messages.arb | 6 -- lib/main/localizations/app_localizations.dart | 7 --- 9 files changed, 150 insertions(+), 24 deletions(-) diff --git a/lib/features/composer/presentation/extensions/email_action_type_extension.dart b/lib/features/composer/presentation/extensions/email_action_type_extension.dart index fb84d708c..ad3b2d32a 100644 --- a/lib/features/composer/presentation/extensions/email_action_type_extension.dart +++ b/lib/features/composer/presentation/extensions/email_action_type_extension.dart @@ -209,7 +209,7 @@ extension EmailActionTypeExtension on EmailActionType { case EmailActionType.replyToList: return appLocalizations.replyToList; case EmailActionType.moveToMailbox: - return appLocalizations.move_message; + return appLocalizations.moveMessage; case EmailActionType.markAsStarred: return appLocalizations.starred; case EmailActionType.unMarkAsStarred: diff --git a/lib/features/email/presentation/widgets/email_view_app_bar_widget.dart b/lib/features/email/presentation/widgets/email_view_app_bar_widget.dart index 35e622c0f..391010ff5 100644 --- a/lib/features/email/presentation/widgets/email_view_app_bar_widget.dart +++ b/lib/features/email/presentation/widgets/email_view_app_bar_widget.dart @@ -165,7 +165,7 @@ class EmailViewAppBarWidget extends StatelessWidget { icon: _imagePaths.icMoveEmail, iconSize: EmailViewAppBarWidgetStyles.buttonIconSize, iconColor: EmailViewAppBarWidgetStyles.iconColor, - tooltipMessage: appLocalizations.move_message, + tooltipMessage: appLocalizations.moveMessage, backgroundColor: Colors.transparent, onTapActionCallback: () => onEmailActionClick?.call( presentationEmail, diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index c85123c80..40d73cd68 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -1613,6 +1613,33 @@ class MailboxDashBoardController extends ReloadableController ); } + void permanentDeleteMultipleEmailInThreadDetail( + List emailsInThreadDetailInfo, { + VoidCallback? onConfirm, + }) { + if (currentContext == null) return; + + final mailboxContainId = emailsInThreadDetailInfo.firstOrNull?.mailboxIdContain; + final mailboxContain = mailboxContainId == null + ? null + : mapMailboxById[mailboxContainId]; + + deleteSelectionEmailsPermanently( + currentContext!, + DeleteActionType.multiple, + listEmails: emailsInThreadDetailInfo + .map( + (email) => PresentationEmail( + id: email.emailId, + mailboxContain: mailboxContain, + ), + ) + .toList(), + mailboxCurrent: mailboxContain, + onConfirm: onConfirm, + ); + } + void deleteSelectionEmailsPermanently( BuildContext context, DeleteActionType actionType, @@ -1620,6 +1647,7 @@ class MailboxDashBoardController extends ReloadableController List? listEmails, PresentationMailbox? mailboxCurrent, Function? onCancelSelectionEmail, + VoidCallback? onConfirm, } ) { if (responsiveUtils.isScreenWithShortestSide(context)) { @@ -1631,10 +1659,14 @@ class MailboxDashBoardController extends ReloadableController ..onCancelAction(AppLocalizations.of(context).cancel, () => popBack()) ..onConfirmAction( actionType.getConfirmActionName(context), - () => _deleteSelectionEmailsPermanentlyAction( + () { + onConfirm?.call(); + _deleteSelectionEmailsPermanentlyAction( actionType, listEmails: listEmails, - onCancelSelectionEmail: onCancelSelectionEmail))) + onCancelSelectionEmail: onCancelSelectionEmail, + ); + })) .show(); } else { Get.dialog( @@ -1649,11 +1681,14 @@ class MailboxDashBoardController extends ReloadableController ), confirmText: actionType.getConfirmActionName(context), cancelText: AppLocalizations.of(context).cancel, - onConfirmButtonAction: () => _deleteSelectionEmailsPermanentlyAction( - actionType, - listEmails: listEmails, - onCancelSelectionEmail: onCancelSelectionEmail, - ), + onConfirmButtonAction: () { + onConfirm?.call(); + _deleteSelectionEmailsPermanentlyAction( + actionType, + listEmails: listEmails, + onCancelSelectionEmail: onCancelSelectionEmail, + ); + }, onCancelButtonAction: popBack, onCloseButtonAction: popBack, )), diff --git a/lib/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart b/lib/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart index 70ac3643b..59d23ec40 100644 --- a/lib/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart +++ b/lib/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart @@ -46,4 +46,26 @@ extension GetThreadDetailActionStatus on ThreadDetailController { }, ); } + + bool get threadDetailIsTrashed { + final trashMailboxId = getMailboxIdByRole(PresentationMailbox.roleTrash); + return emailsInThreadDetailInfo.every( + (email) { + return email.mailboxIds?[trashMailboxId] == true; + }, + ); + } + + bool get threadDetailIsDraft { + final draftMailboxId = getMailboxIdByRole(PresentationMailbox.roleDrafts); + return emailsInThreadDetailInfo.every( + (email) { + return email.mailboxIds?[draftMailboxId] == true; + }, + ); + } + + bool get threadDetailCanPermanentlyDelete { + return threadDetailIsTrashed || threadDetailIsSpam || threadDetailIsDraft; + } } diff --git a/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart b/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart index 2e9bb85c9..9bd012293 100644 --- a/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart +++ b/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart @@ -1,3 +1,4 @@ +import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; @@ -6,8 +7,10 @@ import 'package:model/email/mark_star_action.dart'; import 'package:model/email/read_actions.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart'; +import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_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/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_open_context_menu_extension.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; @@ -16,10 +19,12 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/extension/clos import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; extension OnThreadDetailActionClick on ThreadDetailController { - void onThreadDetailActionClick(EmailActionType threadDetailActionType) { + Future onThreadDetailActionClick(EmailActionType threadDetailActionType) async { switch (threadDetailActionType) { case EmailActionType.markAsRead: case EmailActionType.markAsUnread: @@ -62,6 +67,26 @@ extension OnThreadDetailActionClick on ThreadDetailController { : MarkStarAction.unMarkStar, )); break; + case EmailActionType.deletePermanently: + mailboxDashBoardController.permanentDeleteMultipleEmailInThreadDetail( + emailsInThreadDetailInfo, + onConfirm: () => closeThreadDetailAction(currentContext), + ); + break; + case EmailActionType.moveToMailbox: + final mailboxId = await _pickDestinationMailboxId(); + if (mailboxId == null) return; + + _moveToMailbox(mailboxId, threadDetailActionType); + break; + case EmailActionType.moveToTrash: + final mailboxId = mailboxDashBoardController.getMailboxIdByRole( + PresentationMailbox.roleTrash, + ); + if (mailboxId == null) return; + + _moveToMailbox(mailboxId, threadDetailActionType); + break; case EmailActionType.archiveMessage: final mailboxId = mailboxDashBoardController.getMailboxIdByRole( PresentationMailbox.roleArchive, @@ -103,8 +128,12 @@ extension OnThreadDetailActionClick on ThreadDetailController { threadDetailIsStarred ? EmailActionType.unMarkAsStarred : EmailActionType.markAsStarred, + EmailActionType.moveToMailbox, if (!threadDetailIsArchived) EmailActionType.archiveMessage, threadDetailIsSpam ? EmailActionType.unSpam : EmailActionType.moveToSpam, + threadDetailIsTrashed + ? EmailActionType.deletePermanently + : EmailActionType.moveToTrash, ]; if (position == null) { @@ -157,4 +186,33 @@ extension OnThreadDetailActionClick on ThreadDetailController { emailActionType: emailActionType, ); } + + Future _pickDestinationMailboxId() async { + if (session == null || accountId == null) return null; + + final threadMailboxIds = emailsInThreadDetailInfo + .map((e) => e.mailboxIdContain) + .nonNulls + .toSet(); + MailboxId? mailboxIdSelected; + if (threadMailboxIds.length == 1) { // All emails in the same mailbox + mailboxIdSelected = threadMailboxIds.first; + } + + final arguments = DestinationPickerArguments( + accountId!, + MailboxActions.moveEmail, + session, + mailboxIdSelected: mailboxIdSelected, + ); + final destinationMailbox = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog( + routeName: AppRoutes.destinationPicker, + arguments: arguments, + ) + : await push(AppRoutes.destinationPicker, arguments: arguments); + if (destinationMailbox is! PresentationMailbox) return null; + + return destinationMailbox.id; + } } \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/thread_detail_view.dart b/lib/features/thread_detail/presentation/thread_detail_view.dart index 531242c60..19ee70a52 100644 --- a/lib/features/thread_detail/presentation/thread_detail_view.dart +++ b/lib/features/thread_detail/presentation/thread_detail_view.dart @@ -42,6 +42,7 @@ class ThreadDetailView extends GetWidget { threadActionReady: controller.emailsInThreadDetailInfo.isNotEmpty && controller.emailIdsPresentation.length > 1, threadDetailIsStarred: controller.threadDetailIsStarred, + threadDetailCanPermanentlyDelete: controller.threadDetailCanPermanentlyDelete, onThreadActionClick: controller.onThreadDetailActionClick, onThreadMoreActionClick: controller.onThreadDetailMoreActionClick, optionWidgets: [ diff --git a/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart b/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart index 48f51b266..626c90cb3 100644 --- a/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart +++ b/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart @@ -1,3 +1,4 @@ +import 'package:core/presentation/extensions/color_extension.dart'; 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'; @@ -23,6 +24,7 @@ class ThreadDetailAppBar extends StatelessWidget { required this.threadActionReady, required this.threadDetailIsStarred, required this.isThreadDetailEnabled, + required this.threadDetailCanPermanentlyDelete, this.mailboxContain, this.optionWidgets = const [], this.onThreadActionClick, @@ -36,6 +38,7 @@ class ThreadDetailAppBar extends StatelessWidget { final bool threadActionReady; final bool threadDetailIsStarred; final bool isThreadDetailEnabled; + final bool threadDetailCanPermanentlyDelete; final PresentationMailbox? mailboxContain; final List optionWidgets; final OnThreadActionClick? onThreadActionClick; @@ -81,7 +84,7 @@ class ThreadDetailAppBar extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ if (_supportDisplayMailboxNameTitle(context)) backButton, - if (isThreadDetailEnabled && threadActionReady) + if (isThreadDetailEnabled && threadActionReady) ...[ _ThreadDetailAppBarButton( icon: threadDetailIsStarred ? imagePaths.icStar @@ -95,6 +98,26 @@ class ThreadDetailAppBar extends StatelessWidget { ? (_) => onThreadActionClick?.call(EmailActionType.unMarkAsStarred) : (_) => onThreadActionClick?.call(EmailActionType.markAsStarred), ), + _ThreadDetailAppBarButton( + icon: imagePaths.icMoveEmail, + tooltipMessage: AppLocalizations.of(context).moveMessage, + responsiveUtils: responsiveUtils, + onTapActionCallback: (_) => onThreadActionClick?.call(EmailActionType.moveToMailbox), + ), + _ThreadDetailAppBarButton( + icon: imagePaths.icDeleteComposer, + iconColor: threadDetailCanPermanentlyDelete + ? AppColor.redFF3347 + : EmailViewAppBarWidgetStyles.iconColor, + tooltipMessage: threadDetailCanPermanentlyDelete + ? AppLocalizations.of(context).delete_permanently + : AppLocalizations.of(context).move_to_trash, + responsiveUtils: responsiveUtils, + onTapActionCallback: threadDetailCanPermanentlyDelete + ? (_) => onThreadActionClick?.call(EmailActionType.deletePermanently) + : (_) => onThreadActionClick?.call(EmailActionType.moveToTrash), + ), + ], if (!responsiveUtils.isMobile(context)) const Spacer(), if (AppUtils.getCurrentDirection(context) == TextDirection.rtl) ...optionWidgets.reversed diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 2244a6467..96a2da001 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1154,12 +1154,6 @@ "placeholders_order": [], "placeholders": {} }, - "move_message": "Move message", - "@move_message": { - "type": "text", - "placeholders_order": [], - "placeholders": {} - }, "forwarded_message": "Forwarded message", "@forwarded_message": { "type": "text", diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 98654555b..a837bd31a 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -1167,13 +1167,6 @@ class AppLocalizations { ); } - String get move_message { - return Intl.message( - 'Move message', - name: 'move_message', - ); - } - String get forwarded_message { return Intl.message( 'Forwarded message',