TF-3947 Thread Detail Thread level move & delete

This commit is contained in:
DatDang
2025-08-06 11:23:35 +07:00
committed by Dat H. Pham
parent 4e4053a361
commit d34ddf5d39
9 changed files with 150 additions and 24 deletions
@@ -209,7 +209,7 @@ extension EmailActionTypeExtension on EmailActionType {
case EmailActionType.replyToList: case EmailActionType.replyToList:
return appLocalizations.replyToList; return appLocalizations.replyToList;
case EmailActionType.moveToMailbox: case EmailActionType.moveToMailbox:
return appLocalizations.move_message; return appLocalizations.moveMessage;
case EmailActionType.markAsStarred: case EmailActionType.markAsStarred:
return appLocalizations.starred; return appLocalizations.starred;
case EmailActionType.unMarkAsStarred: case EmailActionType.unMarkAsStarred:
@@ -165,7 +165,7 @@ class EmailViewAppBarWidget extends StatelessWidget {
icon: _imagePaths.icMoveEmail, icon: _imagePaths.icMoveEmail,
iconSize: EmailViewAppBarWidgetStyles.buttonIconSize, iconSize: EmailViewAppBarWidgetStyles.buttonIconSize,
iconColor: EmailViewAppBarWidgetStyles.iconColor, iconColor: EmailViewAppBarWidgetStyles.iconColor,
tooltipMessage: appLocalizations.move_message, tooltipMessage: appLocalizations.moveMessage,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
onTapActionCallback: () => onEmailActionClick?.call( onTapActionCallback: () => onEmailActionClick?.call(
presentationEmail, presentationEmail,
@@ -1613,6 +1613,33 @@ class MailboxDashBoardController extends ReloadableController
); );
} }
void permanentDeleteMultipleEmailInThreadDetail(
List<EmailInThreadDetailInfo> 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( void deleteSelectionEmailsPermanently(
BuildContext context, BuildContext context,
DeleteActionType actionType, DeleteActionType actionType,
@@ -1620,6 +1647,7 @@ class MailboxDashBoardController extends ReloadableController
List<PresentationEmail>? listEmails, List<PresentationEmail>? listEmails,
PresentationMailbox? mailboxCurrent, PresentationMailbox? mailboxCurrent,
Function? onCancelSelectionEmail, Function? onCancelSelectionEmail,
VoidCallback? onConfirm,
} }
) { ) {
if (responsiveUtils.isScreenWithShortestSide(context)) { if (responsiveUtils.isScreenWithShortestSide(context)) {
@@ -1631,10 +1659,14 @@ class MailboxDashBoardController extends ReloadableController
..onCancelAction(AppLocalizations.of(context).cancel, () => popBack()) ..onCancelAction(AppLocalizations.of(context).cancel, () => popBack())
..onConfirmAction( ..onConfirmAction(
actionType.getConfirmActionName(context), actionType.getConfirmActionName(context),
() => _deleteSelectionEmailsPermanentlyAction( () {
onConfirm?.call();
_deleteSelectionEmailsPermanentlyAction(
actionType, actionType,
listEmails: listEmails, listEmails: listEmails,
onCancelSelectionEmail: onCancelSelectionEmail))) onCancelSelectionEmail: onCancelSelectionEmail,
);
}))
.show(); .show();
} else { } else {
Get.dialog( Get.dialog(
@@ -1649,11 +1681,14 @@ class MailboxDashBoardController extends ReloadableController
), ),
confirmText: actionType.getConfirmActionName(context), confirmText: actionType.getConfirmActionName(context),
cancelText: AppLocalizations.of(context).cancel, cancelText: AppLocalizations.of(context).cancel,
onConfirmButtonAction: () => _deleteSelectionEmailsPermanentlyAction( onConfirmButtonAction: () {
actionType, onConfirm?.call();
listEmails: listEmails, _deleteSelectionEmailsPermanentlyAction(
onCancelSelectionEmail: onCancelSelectionEmail, actionType,
), listEmails: listEmails,
onCancelSelectionEmail: onCancelSelectionEmail,
);
},
onCancelButtonAction: popBack, onCancelButtonAction: popBack,
onCloseButtonAction: popBack, onCloseButtonAction: popBack,
)), )),
@@ -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;
}
} }
@@ -1,3 +1,4 @@
import 'package:core/utils/platform_info.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.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/email/read_actions.dart';
import 'package:model/mailbox/presentation_mailbox.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/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/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/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/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/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'; 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/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/features/thread_detail/presentation/thread_detail_controller.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/app_routes.dart';
import 'package:tmail_ui_user/main/routes/dialog_router.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart';
extension OnThreadDetailActionClick on ThreadDetailController { extension OnThreadDetailActionClick on ThreadDetailController {
void onThreadDetailActionClick(EmailActionType threadDetailActionType) { Future<void> onThreadDetailActionClick(EmailActionType threadDetailActionType) async {
switch (threadDetailActionType) { switch (threadDetailActionType) {
case EmailActionType.markAsRead: case EmailActionType.markAsRead:
case EmailActionType.markAsUnread: case EmailActionType.markAsUnread:
@@ -62,6 +67,26 @@ extension OnThreadDetailActionClick on ThreadDetailController {
: MarkStarAction.unMarkStar, : MarkStarAction.unMarkStar,
)); ));
break; 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: case EmailActionType.archiveMessage:
final mailboxId = mailboxDashBoardController.getMailboxIdByRole( final mailboxId = mailboxDashBoardController.getMailboxIdByRole(
PresentationMailbox.roleArchive, PresentationMailbox.roleArchive,
@@ -103,8 +128,12 @@ extension OnThreadDetailActionClick on ThreadDetailController {
threadDetailIsStarred threadDetailIsStarred
? EmailActionType.unMarkAsStarred ? EmailActionType.unMarkAsStarred
: EmailActionType.markAsStarred, : EmailActionType.markAsStarred,
EmailActionType.moveToMailbox,
if (!threadDetailIsArchived) EmailActionType.archiveMessage, if (!threadDetailIsArchived) EmailActionType.archiveMessage,
threadDetailIsSpam ? EmailActionType.unSpam : EmailActionType.moveToSpam, threadDetailIsSpam ? EmailActionType.unSpam : EmailActionType.moveToSpam,
threadDetailIsTrashed
? EmailActionType.deletePermanently
: EmailActionType.moveToTrash,
]; ];
if (position == null) { if (position == null) {
@@ -157,4 +186,33 @@ extension OnThreadDetailActionClick on ThreadDetailController {
emailActionType: emailActionType, emailActionType: emailActionType,
); );
} }
Future<MailboxId?> _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;
}
} }
@@ -42,6 +42,7 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
threadActionReady: controller.emailsInThreadDetailInfo.isNotEmpty && threadActionReady: controller.emailsInThreadDetailInfo.isNotEmpty &&
controller.emailIdsPresentation.length > 1, controller.emailIdsPresentation.length > 1,
threadDetailIsStarred: controller.threadDetailIsStarred, threadDetailIsStarred: controller.threadDetailIsStarred,
threadDetailCanPermanentlyDelete: controller.threadDetailCanPermanentlyDelete,
onThreadActionClick: controller.onThreadDetailActionClick, onThreadActionClick: controller.onThreadDetailActionClick,
onThreadMoreActionClick: controller.onThreadDetailMoreActionClick, onThreadMoreActionClick: controller.onThreadDetailMoreActionClick,
optionWidgets: [ optionWidgets: [
@@ -1,3 +1,4 @@
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/utils/responsive_utils.dart'; import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart';
@@ -23,6 +24,7 @@ class ThreadDetailAppBar extends StatelessWidget {
required this.threadActionReady, required this.threadActionReady,
required this.threadDetailIsStarred, required this.threadDetailIsStarred,
required this.isThreadDetailEnabled, required this.isThreadDetailEnabled,
required this.threadDetailCanPermanentlyDelete,
this.mailboxContain, this.mailboxContain,
this.optionWidgets = const [], this.optionWidgets = const [],
this.onThreadActionClick, this.onThreadActionClick,
@@ -36,6 +38,7 @@ class ThreadDetailAppBar extends StatelessWidget {
final bool threadActionReady; final bool threadActionReady;
final bool threadDetailIsStarred; final bool threadDetailIsStarred;
final bool isThreadDetailEnabled; final bool isThreadDetailEnabled;
final bool threadDetailCanPermanentlyDelete;
final PresentationMailbox? mailboxContain; final PresentationMailbox? mailboxContain;
final List<Widget> optionWidgets; final List<Widget> optionWidgets;
final OnThreadActionClick? onThreadActionClick; final OnThreadActionClick? onThreadActionClick;
@@ -81,7 +84,7 @@ class ThreadDetailAppBar extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
if (_supportDisplayMailboxNameTitle(context)) backButton, if (_supportDisplayMailboxNameTitle(context)) backButton,
if (isThreadDetailEnabled && threadActionReady) if (isThreadDetailEnabled && threadActionReady) ...[
_ThreadDetailAppBarButton( _ThreadDetailAppBarButton(
icon: threadDetailIsStarred icon: threadDetailIsStarred
? imagePaths.icStar ? imagePaths.icStar
@@ -95,6 +98,26 @@ class ThreadDetailAppBar extends StatelessWidget {
? (_) => onThreadActionClick?.call(EmailActionType.unMarkAsStarred) ? (_) => onThreadActionClick?.call(EmailActionType.unMarkAsStarred)
: (_) => onThreadActionClick?.call(EmailActionType.markAsStarred), : (_) => 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 (!responsiveUtils.isMobile(context)) const Spacer(),
if (AppUtils.getCurrentDirection(context) == TextDirection.rtl) if (AppUtils.getCurrentDirection(context) == TextDirection.rtl)
...optionWidgets.reversed ...optionWidgets.reversed
-6
View File
@@ -1154,12 +1154,6 @@
"placeholders_order": [], "placeholders_order": [],
"placeholders": {} "placeholders": {}
}, },
"move_message": "Move message",
"@move_message": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"forwarded_message": "Forwarded message", "forwarded_message": "Forwarded message",
"@forwarded_message": { "@forwarded_message": {
"type": "text", "type": "text",
@@ -1167,13 +1167,6 @@ class AppLocalizations {
); );
} }
String get move_message {
return Intl.message(
'Move message',
name: 'move_message',
);
}
String get forwarded_message { String get forwarded_message {
return Intl.message( return Intl.message(
'Forwarded message', 'Forwarded message',