From 2bbe6e4ae6b2b6a26a6afc6ba758039c099519be Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 25 Apr 2024 20:52:33 +0700 Subject: [PATCH 01/15] TF-2646 Show banner select all emails when selection enabled --- .../thread/presentation/thread_view.dart | 14 ++++ .../select_all_emails_banner_widget.dart | 67 +++++++++++++++++++ lib/main/localizations/app_localizations.dart | 15 +++++ 3 files changed, 96 insertions(+) create mode 100644 lib/features/thread/presentation/widgets/select_all_banner/select_all_emails_banner_widget.dart diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index 3d03d3453e..2041adf8c5 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -11,12 +11,14 @@ import 'package:tmail_ui_user/features/base/widget/compose_floating_button.dart' import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.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/domain/state/mark_as_mailbox_read_state.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mixin/filter_email_popup_menu_mixin.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/recover_deleted_message_loading_banner_widget.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_banner_widget.dart'; import 'package:tmail_ui_user/features/quotas/presentation/widget/quotas_banner_widget.dart'; +import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; @@ -35,6 +37,7 @@ import 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_bu import 'package:tmail_ui_user/features/thread/presentation/widgets/empty_emails_widget.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/filter_message_cupertino_action_sheet_action_builder.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/scroll_to_top_button_widget.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/select_all_banner/select_all_emails_banner_widget.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/spam_banner/spam_report_banner_widget.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/thread_view_loading_bar_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -172,6 +175,17 @@ class ThreadView extends GetWidget }), if (!controller.responsiveUtils.isDesktop(context)) _buildMarkAsMailboxReadLoading(context), + if (PlatformInfo.isWeb) + Obx(() { + if (controller.isSelectionEnabled()) { + return SelectAllEmailBannerWidget( + limitEmailsInPage: ThreadConstants.defaultLimit.value.toInt(), + folderName: controller.mailboxDashBoardController.selectedMailbox.value?.getDisplayName(context), + ); + } else { + return const SizedBox.shrink(); + } + }), Obx(() => ThreadViewLoadingBarWidget(viewState: controller.viewState.value)), Expanded( child: Container( diff --git a/lib/features/thread/presentation/widgets/select_all_banner/select_all_emails_banner_widget.dart b/lib/features/thread/presentation/widgets/select_all_banner/select_all_emails_banner_widget.dart new file mode 100644 index 0000000000..6caf3bb2d2 --- /dev/null +++ b/lib/features/thread/presentation/widgets/select_all_banner/select_all_emails_banner_widget.dart @@ -0,0 +1,67 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class SelectAllEmailBannerWidget extends StatelessWidget { + final int limitEmailsInPage; + final String? folderName; + + const SelectAllEmailBannerWidget({ + Key? key, + required this.limitEmailsInPage, + this.folderName + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + color: AppColor.colorBgDesktop, + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 16, + vertical: 12, + ), + alignment: Alignment.center, + child: Text.rich( + TextSpan( + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.black, + fontSize: 14 + ), + children: [ + TextSpan( + text: AppLocalizations.of(context).all, + ), + TextSpan( + text: ' $limitEmailsInPage ', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 14 + ), + ), + TextSpan( + text: AppLocalizations.of(context).mailsOnThisPageAreSelected, + ), + TextSpan( + text: AppLocalizations.of(context).selectAllMailInMailbox( + limitEmailsInPage, + folderName ?? AppLocalizations.of(context).folder.toLowerCase() + ), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: AppColor.primaryColor, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + recognizer: TapGestureRecognizer()..onTap = () { + log('SelectAllEmailBannerWidget::build: Select all mail in mailbox'); + } + ) + ] + ) + ), + ); + } +} diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index a9880e1a61..b8c358e75a 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3971,4 +3971,19 @@ class AppLocalizations { 'Show less', name: 'showLess'); } + + String get mailsOnThisPageAreSelected { + return Intl.message( + 'mails on this page are selected. ', + name: 'mailsOnThisPageAreSelected' + ); + } + + String selectAllMailInMailbox(int total, String folderName) { + return Intl.message( + 'Select all $total mails in $folderName', + name: 'selectAllMailInMailbox', + args: [total, folderName] + ); + } } \ No newline at end of file From bc7c422593531f815e3dc6658aacfcc84a54ac19 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 26 Apr 2024 00:29:12 +0700 Subject: [PATCH 02/15] TF-2646 Perform action enable selection all email & clear selection --- .../mailbox_dashboard_controller.dart | 1 + .../mailbox_dashboard_view_web.dart | 5 +- .../widgets/top_bar_thread_selection.dart | 39 ++++++----- .../presentation/thread_controller.dart | 5 ++ .../thread/presentation/thread_view.dart | 10 ++- ...ge_select_all_email_in_mailbox_widget.dart | 56 ++++++++++++++++ .../message_select_email_in_page_widget.dart | 61 +++++++++++++++++ .../select_all_emails_banner_widget.dart | 67 ------------------- .../select_all_emails_in_mailbox_banner.dart | 55 +++++++++++++++ lib/l10n/intl_messages.arb | 36 +++++++++- lib/main/localizations/app_localizations.dart | 15 +++++ 11 files changed, 259 insertions(+), 91 deletions(-) create mode 100644 lib/features/thread/presentation/widgets/select_all_banner/message_select_all_email_in_mailbox_widget.dart create mode 100644 lib/features/thread/presentation/widgets/select_all_banner/message_select_email_in_page_widget.dart delete mode 100644 lib/features/thread/presentation/widgets/select_all_banner/select_all_emails_banner_widget.dart create mode 100644 lib/features/thread/presentation/widgets/select_all_banner/select_all_emails_in_mailbox_banner.dart 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 1245377259..ed2883b5e4 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -219,6 +219,7 @@ class MailboxDashBoardController extends ReloadableController { final attachmentDraggableAppState = Rxn(); final isRecoveringDeletedMessage = RxBool(false); final localFileDraggableAppState = Rxn(); + final isSelectAllEmailsEnabled = RxBool(false); Session? sessionCurrent; Map mapDefaultMailboxIdByRole = {}; diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index 622fa1d07c..291f22055e 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -256,8 +256,9 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { return Padding( padding: const EdgeInsets.symmetric(vertical: 8.5, horizontal: 16), child: TopBarThreadSelection( - listEmailSelected, - controller.mapMailboxById, + listEmail: listEmailSelected, + mapMailbox: controller.mapMailboxById, + isSelectAllEmailsEnabled: controller.isSelectAllEmailsEnabled.value, onCancelSelection: () => controller.dispatchAction(CancelSelectionAllEmailAction()), onEmailActionTypeAction: (listEmails, actionType) => diff --git a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart index 82270ce269..3c6d6f4004 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart @@ -21,16 +21,16 @@ class TopBarThreadSelection extends StatelessWidget{ final Map mapMailbox; final OnEmailActionTypeAction? onEmailActionTypeAction; final VoidCallback? onCancelSelection; + final bool isSelectAllEmailsEnabled; - TopBarThreadSelection( - this.listEmail, - this.mapMailbox, - { - super.key, - this.onEmailActionTypeAction, - this.onCancelSelection, - } - ); + TopBarThreadSelection({ + super.key, + required this.listEmail, + required this.mapMailbox, + required this.isSelectAllEmailsEnabled, + this.onEmailActionTypeAction, + this.onCancelSelection, + }); @override Widget build(BuildContext context) { @@ -49,15 +49,18 @@ class TopBarThreadSelection extends StatelessWidget{ padding: const EdgeInsets.all(3), onTapActionCallback: onCancelSelection ), - Text( - AppLocalizations.of(context).count_email_selected(listEmail.length), - style: const TextStyle( - fontSize: 17, - fontWeight: FontWeight.w500, - color: AppColor.colorTextButton - ) - ), - const SizedBox(width: 30), + if (!isSelectAllEmailsEnabled) + Padding( + padding: const EdgeInsetsDirectional.only(end: 30), + child: Text( + AppLocalizations.of(context).count_email_selected(listEmail.length), + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: AppColor.colorTextButton + ) + ), + ), TMailButtonWidget.fromIcon( icon: listEmail.isAllEmailRead ? imagePaths.icUnread diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 671a961ac8..1cdac12e2a 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -733,11 +733,16 @@ class ThreadController extends BaseController with EmailActionController { final newEmailList = mailboxDashBoardController.emailsInCurrentMailbox .map((email) => email.toSelectedEmail(selectMode: SelectMode.INACTIVE)) .toList(); + mailboxDashBoardController.isSelectAllEmailsEnabled.value = false; mailboxDashBoardController.updateEmailList(newEmailList); mailboxDashBoardController.currentSelectMode.value = SelectMode.INACTIVE; mailboxDashBoardController.listEmailSelected.clear(); } + void enableSelectAllEmails() { + mailboxDashBoardController.isSelectAllEmailsEnabled.value = true; + } + void closeFilterMessageActionSheet() { popBack(); } diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index 2041adf8c5..83a994adf4 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -37,7 +37,7 @@ import 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_bu import 'package:tmail_ui_user/features/thread/presentation/widgets/empty_emails_widget.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/filter_message_cupertino_action_sheet_action_builder.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/scroll_to_top_button_widget.dart'; -import 'package:tmail_ui_user/features/thread/presentation/widgets/select_all_banner/select_all_emails_banner_widget.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/select_all_banner/select_all_emails_in_mailbox_banner.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/spam_banner/spam_report_banner_widget.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/thread_view_loading_bar_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -178,9 +178,13 @@ class ThreadView extends GetWidget if (PlatformInfo.isWeb) Obx(() { if (controller.isSelectionEnabled()) { - return SelectAllEmailBannerWidget( + return SelectAllEmailInMailboxBanner( limitEmailsInPage: ThreadConstants.defaultLimit.value.toInt(), - folderName: controller.mailboxDashBoardController.selectedMailbox.value?.getDisplayName(context), + totalEmails: controller.mailboxDashBoardController.selectedMailbox.value?.totalEmails?.value.value.toInt() ?? 0, + folderName: controller.mailboxDashBoardController.selectedMailbox.value?.getDisplayName(context) + ?? AppLocalizations.of(context).folder.toLowerCase(), + onSelectAllEmailAction: controller.enableSelectAllEmails, + onClearSelection: controller.cancelSelectEmail ); } else { return const SizedBox.shrink(); diff --git a/lib/features/thread/presentation/widgets/select_all_banner/message_select_all_email_in_mailbox_widget.dart b/lib/features/thread/presentation/widgets/select_all_banner/message_select_all_email_in_mailbox_widget.dart new file mode 100644 index 0000000000..79dbaa0bd2 --- /dev/null +++ b/lib/features/thread/presentation/widgets/select_all_banner/message_select_all_email_in_mailbox_widget.dart @@ -0,0 +1,56 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class MessageSelectAllEmailInMailboxWidget extends StatelessWidget { + + final int totalEmails; + final String folderName; + final VoidCallback onClearSelection; + + const MessageSelectAllEmailInMailboxWidget({ + super.key, + required this.totalEmails, + required this.folderName, + required this.onClearSelection + }); + + @override + Widget build(BuildContext context) { + return Text.rich( + TextSpan( + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.black, + fontSize: 14 + ), + children: [ + TextSpan( + text: AppLocalizations.of(context).all, + ), + TextSpan( + text: ' $totalEmails ', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 14 + ), + ), + TextSpan( + text: AppLocalizations.of(context).mailsInMailboxAreSelected(folderName), + ), + TextSpan( + text: AppLocalizations.of(context).clearSelection, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: AppColor.primaryColor, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + recognizer: TapGestureRecognizer()..onTap = onClearSelection + ) + ] + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/widgets/select_all_banner/message_select_email_in_page_widget.dart b/lib/features/thread/presentation/widgets/select_all_banner/message_select_email_in_page_widget.dart new file mode 100644 index 0000000000..5e85eeef44 --- /dev/null +++ b/lib/features/thread/presentation/widgets/select_all_banner/message_select_email_in_page_widget.dart @@ -0,0 +1,61 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class MessageSelectEmailInPageWidget extends StatelessWidget { + + final int limitEmailsInPage; + final int totalEmails; + final String folderName; + final VoidCallback onSelectAllEmailAction; + + const MessageSelectEmailInPageWidget({ + super.key, + required this.limitEmailsInPage, + required this.totalEmails, + required this.folderName, + required this.onSelectAllEmailAction + }); + + @override + Widget build(BuildContext context) { + return Text.rich( + TextSpan( + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.black, + fontSize: 14 + ), + children: [ + TextSpan( + text: AppLocalizations.of(context).all, + ), + TextSpan( + text: ' $limitEmailsInPage ', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 14 + ), + ), + TextSpan( + text: AppLocalizations.of(context).mailsOnThisPageAreSelected, + ), + TextSpan( + text: AppLocalizations.of(context).selectAllMailInMailbox( + totalEmails, + folderName + ), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: AppColor.primaryColor, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + recognizer: TapGestureRecognizer()..onTap = onSelectAllEmailAction + ) + ] + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/widgets/select_all_banner/select_all_emails_banner_widget.dart b/lib/features/thread/presentation/widgets/select_all_banner/select_all_emails_banner_widget.dart deleted file mode 100644 index 6caf3bb2d2..0000000000 --- a/lib/features/thread/presentation/widgets/select_all_banner/select_all_emails_banner_widget.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/utils/app_logger.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -class SelectAllEmailBannerWidget extends StatelessWidget { - final int limitEmailsInPage; - final String? folderName; - - const SelectAllEmailBannerWidget({ - Key? key, - required this.limitEmailsInPage, - this.folderName - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - color: AppColor.colorBgDesktop, - padding: const EdgeInsetsDirectional.symmetric( - horizontal: 16, - vertical: 12, - ), - alignment: Alignment.center, - child: Text.rich( - TextSpan( - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Colors.black, - fontSize: 14 - ), - children: [ - TextSpan( - text: AppLocalizations.of(context).all, - ), - TextSpan( - text: ' $limitEmailsInPage ', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 14 - ), - ), - TextSpan( - text: AppLocalizations.of(context).mailsOnThisPageAreSelected, - ), - TextSpan( - text: AppLocalizations.of(context).selectAllMailInMailbox( - limitEmailsInPage, - folderName ?? AppLocalizations.of(context).folder.toLowerCase() - ), - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: AppColor.primaryColor, - fontWeight: FontWeight.w500, - fontSize: 14 - ), - recognizer: TapGestureRecognizer()..onTap = () { - log('SelectAllEmailBannerWidget::build: Select all mail in mailbox'); - } - ) - ] - ) - ), - ); - } -} diff --git a/lib/features/thread/presentation/widgets/select_all_banner/select_all_emails_in_mailbox_banner.dart b/lib/features/thread/presentation/widgets/select_all_banner/select_all_emails_in_mailbox_banner.dart new file mode 100644 index 0000000000..1c29260828 --- /dev/null +++ b/lib/features/thread/presentation/widgets/select_all_banner/select_all_emails_in_mailbox_banner.dart @@ -0,0 +1,55 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/select_all_banner/message_select_all_email_in_mailbox_widget.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/select_all_banner/message_select_email_in_page_widget.dart'; + +class SelectAllEmailInMailboxBanner extends StatefulWidget { + final int limitEmailsInPage; + final int totalEmails; + final String folderName; + final VoidCallback onSelectAllEmailAction; + final VoidCallback onClearSelection; + + const SelectAllEmailInMailboxBanner({ + Key? key, + required this.limitEmailsInPage, + required this.totalEmails, + required this.folderName, + required this.onSelectAllEmailAction, + required this.onClearSelection, + }) : super(key: key); + + @override + State createState() => _SelectAllEmailInMailboxBannerState(); +} + +class _SelectAllEmailInMailboxBannerState extends State { + + bool _isSelectAllEmailsEnabled = false; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + color: AppColor.colorBgDesktop, + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 16, + vertical: 12, + ), + alignment: Alignment.center, + child: _isSelectAllEmailsEnabled + ? MessageSelectAllEmailInMailboxWidget( + totalEmails: widget.totalEmails, + folderName: widget.folderName, + onClearSelection: widget.onClearSelection) + : MessageSelectEmailInPageWidget( + limitEmailsInPage: widget.limitEmailsInPage, + totalEmails: widget.totalEmails, + folderName: widget.folderName, + onSelectAllEmailAction: () { + widget.onSelectAllEmailAction(); + setState(() => _isSelectAllEmailsEnabled = true); + }) + ); + } +} diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index e13bbe0ea5..066a675d77 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-03-19T12:10:23.549474", + "@@last_modified": "2024-04-26T00:28:37.274277", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3813,5 +3813,39 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "mailsOnThisPageAreSelected": "mails on this page are selected. ", + "@mailsOnThisPageAreSelected": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "selectAllMailInMailbox": "Select all {total} mails in {folderName}", + "@selectAllMailInMailbox": { + "type": "text", + "placeholders_order": [ + "total", + "folderName" + ], + "placeholders": { + "total": {}, + "folderName": {} + } + }, + "mailsInMailboxAreSelected": "mails in {folderName} are selected. ", + "@mailsInMailboxAreSelected": { + "type": "text", + "placeholders_order": [ + "folderName" + ], + "placeholders": { + "folderName": {} + } + }, + "clearSelection": "Clear selection", + "@clearSelection": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index b8c358e75a..3277383df4 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3986,4 +3986,19 @@ class AppLocalizations { args: [total, folderName] ); } + + String mailsInMailboxAreSelected(String folderName) { + return Intl.message( + 'mails in $folderName are selected. ', + name: 'mailsInMailboxAreSelected', + args: [folderName] + ); + } + + String get clearSelection { + return Intl.message( + 'Clear selection', + name: 'clearSelection', + ); + } } \ No newline at end of file From 44f11b8b7bf6b7c35ad2e9f7744c5836bbe14494 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 26 Apr 2024 00:40:05 +0700 Subject: [PATCH 03/15] TF-2646 Validate to show selection emails banner --- .../thread/presentation/thread_controller.dart | 7 +++++++ lib/features/thread/presentation/thread_view.dart | 12 +++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 1cdac12e2a..ca24e91030 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -1324,4 +1324,11 @@ class ThreadController extends BaseController with EmailActionController { _loadMoreEmails(); } } + + bool validateToShowSelectionEmailsBanner() { + return isSelectionEnabled() + && mailboxDashBoardController.selectedMailbox.value != null + && mailboxDashBoardController.selectedMailbox.value!.totalEmails != null + && mailboxDashBoardController.listEmailSelected.length < mailboxDashBoardController.selectedMailbox.value!.totalEmails!.value.value.toInt(); + } } \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index 83a994adf4..b6e82688c7 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -18,7 +18,6 @@ import 'package:tmail_ui_user/features/manage_account/presentation/extensions/va import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_banner_widget.dart'; import 'package:tmail_ui_user/features/quotas/presentation/widget/quotas_banner_widget.dart'; -import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; @@ -175,14 +174,13 @@ class ThreadView extends GetWidget }), if (!controller.responsiveUtils.isDesktop(context)) _buildMarkAsMailboxReadLoading(context), - if (PlatformInfo.isWeb) + if (controller.responsiveUtils.isWebDesktop(context)) Obx(() { - if (controller.isSelectionEnabled()) { + if (controller.validateToShowSelectionEmailsBanner()) { return SelectAllEmailInMailboxBanner( - limitEmailsInPage: ThreadConstants.defaultLimit.value.toInt(), - totalEmails: controller.mailboxDashBoardController.selectedMailbox.value?.totalEmails?.value.value.toInt() ?? 0, - folderName: controller.mailboxDashBoardController.selectedMailbox.value?.getDisplayName(context) - ?? AppLocalizations.of(context).folder.toLowerCase(), + limitEmailsInPage: controller.mailboxDashBoardController.listEmailSelected.length, + totalEmails: controller.mailboxDashBoardController.selectedMailbox.value!.totalEmails!.value.value.toInt(), + folderName: controller.mailboxDashBoardController.selectedMailbox.value!.getDisplayName(context), onSelectAllEmailAction: controller.enableSelectAllEmails, onClearSelection: controller.cancelSelectEmail ); From 8542bd3f4ab810765c1022038f5b20d86c0d7e32 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 26 Apr 2024 01:04:52 +0700 Subject: [PATCH 04/15] TF-2646 Show more button & popup menu more action --- .../email_action_type_extension.dart | 12 +++++++ .../presentation/action/dashboard_action.dart | 13 ++++++- .../mailbox_dashboard_view_web.dart | 1 + .../widgets/top_bar_thread_selection.dart | 13 +++++++ .../presentation/thread_controller.dart | 36 ++++++++++++++++++- 5 files changed, 73 insertions(+), 2 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 17a0e36752..ffd92616c8 100644 --- a/lib/features/composer/presentation/extensions/email_action_type_extension.dart +++ b/lib/features/composer/presentation/extensions/email_action_type_extension.dart @@ -133,6 +133,12 @@ extension EmailActionTypeExtension on EmailActionType { return imagePaths.icUnsubscribe; case EmailActionType.archiveMessage: return imagePaths.icMailboxArchived; + case EmailActionType.markAsRead: + return imagePaths.icRead; + case EmailActionType.moveToMailbox: + return imagePaths.icMove; + case EmailActionType.moveToTrash: + return imagePaths.icDeleteComposer; default: return ''; } @@ -152,6 +158,12 @@ extension EmailActionTypeExtension on EmailActionType { return AppLocalizations.of(context).unsubscribe; case EmailActionType.archiveMessage: return AppLocalizations.of(context).archiveMessage; + case EmailActionType.markAsRead: + return AppLocalizations.of(context).mark_as_read; + case EmailActionType.moveToMailbox: + return AppLocalizations.of(context).move; + case EmailActionType.moveToTrash: + return AppLocalizations.of(context).move_to_trash; default: return ''; } diff --git a/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart b/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart index 0503b05103..19f13933e1 100644 --- a/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart +++ b/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart @@ -163,4 +163,15 @@ class SearchEmailByFromFieldsAction extends DashBoardAction { class CloseSearchEmailViewAction extends DashBoardAction {} -class CancelSelectionSearchEmailAction extends DashBoardAction {} \ No newline at end of file +class CancelSelectionSearchEmailAction extends DashBoardAction {} + +class MoreSelectedEmailAction extends DashBoardAction { + + final BuildContext context; + final RelativeRect position; + + MoreSelectedEmailAction(this.context, this.position); + + @override + List get props => [context, position]; +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index 291f22055e..0f01e52641 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -267,6 +267,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { listEmails, actionType )), + onMoreSelectedEmailAction: (position) => controller.dispatchAction(MoreSelectedEmailAction(context, position)), ), ); } else { diff --git a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart index 3c6d6f4004..ebe5a7d76a 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart @@ -12,6 +12,7 @@ import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; typedef OnEmailActionTypeAction = Function(List listEmail, EmailActionType actionType); +typedef OnMoreSelectedEmailAction = Function(RelativeRect position); class TopBarThreadSelection extends StatelessWidget{ @@ -22,6 +23,7 @@ class TopBarThreadSelection extends StatelessWidget{ final OnEmailActionTypeAction? onEmailActionTypeAction; final VoidCallback? onCancelSelection; final bool isSelectAllEmailsEnabled; + final OnMoreSelectedEmailAction? onMoreSelectedEmailAction; TopBarThreadSelection({ super.key, @@ -30,6 +32,7 @@ class TopBarThreadSelection extends StatelessWidget{ required this.isSelectAllEmailsEnabled, this.onEmailActionTypeAction, this.onCancelSelection, + this.onMoreSelectedEmailAction, }); @override @@ -148,6 +151,16 @@ class TopBarThreadSelection extends StatelessWidget{ } } ), + const Spacer(), + if (isSelectAllEmailsEnabled) + TMailButtonWidget.fromIcon( + icon: imagePaths.icMoreVertical, + iconSize: 22, + iconColor: AppColor.primaryColor, + tooltipMessage: AppLocalizations.of(context).more, + backgroundColor: Colors.transparent, + onTapActionAtPositionCallback: onMoreSelectedEmailAction + ), ]); } } \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index ca24e91030..8d63f52547 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -1,3 +1,4 @@ +import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; @@ -17,9 +18,11 @@ import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; +import 'package:tmail_ui_user/features/base/mixin/popup_menu_widget_mixin.dart'; import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; @@ -83,7 +86,7 @@ import 'package:universal_html/html.dart' as html; typedef StartRangeSelection = int; typedef EndRangeSelection = int; -class ThreadController extends BaseController with EmailActionController { +class ThreadController extends BaseController with EmailActionController, PopupMenuWidgetMixin { final networkConnectionController = Get.find(); @@ -314,6 +317,9 @@ class ThreadController extends BaseController with EmailActionController { } canSearchMore = true; mailboxDashBoardController.emailsInCurrentMailbox.clear(); + } else if (action is MoreSelectedEmailAction) { + showPopupMenuSelectionEmailAction(action.context, action.position); + mailboxDashBoardController.clearDashBoardAction(); } }); @@ -1331,4 +1337,32 @@ class ThreadController extends BaseController with EmailActionController { && mailboxDashBoardController.selectedMailbox.value!.totalEmails != null && mailboxDashBoardController.listEmailSelected.length < mailboxDashBoardController.selectedMailbox.value!.totalEmails!.value.value.toInt(); } + + void showPopupMenuSelectionEmailAction(BuildContext context, RelativeRect position) { + final listSelectionEmailActions = [ + EmailActionType.markAsRead, + EmailActionType.markAsUnread, + EmailActionType.moveToMailbox, + EmailActionType.moveToTrash, + ]; + + openPopupMenuAction( + context, + position, + listSelectionEmailActions.map((action) => PopupMenuItem( + padding: EdgeInsets.zero, + child: popupItem( + action.getIcon(imagePaths), + action.getTitle(context), + colorIcon: AppColor.colorTextButton, + styleName: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + color: Colors.black + ), + onCallbackAction: () {} + ) + )).toList() + ); + } } \ No newline at end of file From 4024a95426c422cd3ebb115973abdf556182add5 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 26 Apr 2024 11:15:04 +0700 Subject: [PATCH 05/15] TF-2646 Show confirm dialog when make to bulk action with selection emails --- .../presentation/thread_controller.dart | 51 ++++++++++++++++++- lib/l10n/intl_messages.arb | 26 +++++++++- lib/main/localizations/app_localizations.dart | 20 ++++++++ 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 8d63f52547..35e09bba45 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -7,6 +7,7 @@ import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; @@ -34,6 +35,7 @@ import 'package:tmail_ui_user/features/email/domain/state/unsubscribe_email_stat import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search; @@ -275,7 +277,19 @@ class ThreadController extends BaseController with EmailActionController, PopupM filterMessagesAction(action.context, action.option); mailboxDashBoardController.clearDashBoardAction(); } else if (action is HandleEmailActionTypeAction) { - pressEmailSelectionAction(action.context, action.emailAction, action.listEmailSelected); + if (_validateToShowConfirmBulkActionEmailsDialog()) { + _showConfirmDialogWhenMakeToActionForSelectionAllEmails( + context: action.context, + totalEmails: mailboxDashBoardController.selectedMailbox.value!.totalEmails!.value.value.toInt(), + folderName: mailboxDashBoardController.selectedMailbox.value!.getDisplayName(action.context), + actionType: action.emailAction + ); + } else { + pressEmailSelectionAction( + action.context, + action.emailAction, + action.listEmailSelected); + } mailboxDashBoardController.clearDashBoardAction(); } else if (action is OpenEmailDetailedFromSuggestionQuickSearchAction) { final mailboxContain = action.presentationEmail.findMailboxContain(mailboxDashBoardController.mapMailboxById); @@ -1360,9 +1374,42 @@ class ThreadController extends BaseController with EmailActionController, PopupM fontSize: 14, color: Colors.black ), - onCallbackAction: () {} + onCallbackAction: () { + popBack(); + + if (!isSearchActive) { + _showConfirmDialogWhenMakeToActionForSelectionAllEmails( + context: context, + totalEmails: mailboxDashBoardController.selectedMailbox.value!.totalEmails!.value.value.toInt(), + folderName: mailboxDashBoardController.selectedMailbox.value!.getDisplayName(context), + actionType: action + ); + } + } ) )).toList() ); } + + bool _validateToShowConfirmBulkActionEmailsDialog() { + return mailboxDashBoardController.isSelectAllEmailsEnabled.isTrue; + } + + Future _showConfirmDialogWhenMakeToActionForSelectionAllEmails({ + required BuildContext context, + required int totalEmails, + required String folderName, + required EmailActionType actionType + }) async { + await showConfirmDialogAction( + context, + AppLocalizations.of(context).messageConfirmationDialogWhenMakeToActionForSelectionAllEmailsInMailbox(totalEmails, folderName), + AppLocalizations.of(context).ok, + title: AppLocalizations.of(context).confirmBulkAction, + icon: SvgPicture.asset( + imagePaths.icQuotasWarning, + colorFilter: AppColor.colorBackgroundQuotasWarning.asFilter() + ), + ); + } } \ No newline at end of file diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 066a675d77..3eb1b0bfca 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-04-26T00:28:37.274277", + "@@last_modified": "2024-04-26T11:14:14.060748", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3847,5 +3847,29 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "confirmBulkAction": "Confirm bulk action", + "@confirmBulkAction": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "ok": "OK", + "@ok": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageConfirmationDialogWhenMakeToActionForSelectionAllEmailsInMailbox": "This action will affect all {total} mails in {folderName}. Are you sure you want to continue?", + "@messageConfirmationDialogWhenMakeToActionForSelectionAllEmailsInMailbox": { + "type": "text", + "placeholders_order": [ + "total", + "folderName" + ], + "placeholders": { + "total": {}, + "folderName": {} + } } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 3277383df4..23f3294fde 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4001,4 +4001,24 @@ class AppLocalizations { name: 'clearSelection', ); } + + String get confirmBulkAction { + return Intl.message( + 'Confirm bulk action', + name: 'confirmBulkAction'); + } + + String get ok { + return Intl.message( + 'OK', + name: 'ok'); + } + + String messageConfirmationDialogWhenMakeToActionForSelectionAllEmailsInMailbox(int total, String folderName) { + return Intl.message( + 'This action will affect all $total mails in $folderName. Are you sure you want to continue?', + name: 'messageConfirmationDialogWhenMakeToActionForSelectionAllEmailsInMailbox', + args: [total, folderName] + ); + } } \ No newline at end of file From 846f6643b4cfb4a0ec23784e042753bc32b06069 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 29 Apr 2024 09:46:53 +0700 Subject: [PATCH 06/15] TF-2646 Handle mark as all read for selection emails --- .../email_action_type_extension.dart | 3 ++ .../email_datasource_impl.dart | 2 +- .../email/data/network/email_api.dart | 37 ++++++++++++++- .../data/datasource/mailbox_datasource.dart | 2 +- .../mailbox_cache_datasource_impl.dart | 2 +- .../mailbox_datasource_impl.dart | 2 +- .../data/network/mailbox_isolate_worker.dart | 30 ++++++------ .../repository/mailbox_repository_impl.dart | 2 +- .../domain/repository/mailbox_repository.dart | 2 +- .../state/mark_as_mailbox_read_state.dart | 2 +- .../mark_as_mailbox_read_interactor.dart | 8 ++-- .../mailbox_dashboard_view_web.dart | 12 +---- .../widgets/top_bar_thread_selection.dart | 40 +++++++++++++--- .../presentation/thread_controller.dart | 47 ++++++++++++++++--- model/lib/email/email_action_type.dart | 3 +- 15 files changed, 141 insertions(+), 53 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 ffd92616c8..5f05d56053 100644 --- a/lib/features/composer/presentation/extensions/email_action_type_extension.dart +++ b/lib/features/composer/presentation/extensions/email_action_type_extension.dart @@ -134,6 +134,7 @@ extension EmailActionTypeExtension on EmailActionType { case EmailActionType.archiveMessage: return imagePaths.icMailboxArchived; case EmailActionType.markAsRead: + case EmailActionType.markAllAsRead: return imagePaths.icRead; case EmailActionType.moveToMailbox: return imagePaths.icMove; @@ -164,6 +165,8 @@ extension EmailActionTypeExtension on EmailActionType { return AppLocalizations.of(context).move; case EmailActionType.moveToTrash: return AppLocalizations.of(context).move_to_trash; + case EmailActionType.markAllAsRead: + return AppLocalizations.of(context).mark_all_as_read; default: return ''; } diff --git a/lib/features/email/data/datasource_impl/email_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_datasource_impl.dart index df0de5bb5b..1310baed96 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -71,7 +71,7 @@ class EmailDataSourceImpl extends EmailDataSource { ReadActions readActions ) { return Future.sync(() async { - return await emailAPI.markAsRead(session, accountId, emails, readActions); + return await emailAPI.markAsReadAndGetResult(session, accountId, emails, readActions); }).catchError(_exceptionThrower.throwException); } diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index f88c8109a4..6bc2cb5176 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -229,7 +229,7 @@ class EmailAPI with HandleSetErrorMixin { } } - Future> markAsRead( + Future> markAsReadAndGetResult( Session session, AccountId accountId, List emails, @@ -267,6 +267,41 @@ class EmailAPI with HandleSetErrorMixin { }); } + Future> markAsRead( + Session session, + AccountId accountId, + List emails, + ReadActions readActions + ) async { + final setEmailMethod = SetEmailMethod(accountId) + ..addUpdates(emails.listEmailIds.generateMapUpdateObjectMarkAsRead(readActions)); + + final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); + final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + + final response = await (requestBuilder..usings(capabilities)) + .build() + .execute(); + + final setEmailResponse = response.parse( + setEmailInvocation.methodCallId, + SetEmailResponse.deserialize + ); + + final listIdUpdated = setEmailResponse?.updated?.keys.toList(); + final mapErrors = handleSetResponse([setEmailResponse]); + + if (listIdUpdated != null && mapErrors.isEmpty) { + final listEmailIdUpdated = listIdUpdated.map((id) => EmailId(id)).toList(); + return listEmailIdUpdated; + } else { + throw SetMethodException(mapErrors); + } + } + Future> downloadAttachments( List attachments, AccountId accountId, diff --git a/lib/features/mailbox/data/datasource/mailbox_datasource.dart b/lib/features/mailbox/data/datasource/mailbox_datasource.dart index 4b9f30c7ef..dc5238178e 100644 --- a/lib/features/mailbox/data/datasource/mailbox_datasource.dart +++ b/lib/features/mailbox/data/datasource/mailbox_datasource.dart @@ -38,7 +38,7 @@ abstract class MailboxDataSource { Future moveMailbox(Session session, AccountId accountId, MoveMailboxRequest request); - Future> markAsMailboxRead( + Future> markAsMailboxRead( Session session, AccountId accountId, MailboxId mailboxId, diff --git a/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart b/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart index b52e767b24..e0c604a39a 100644 --- a/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart +++ b/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart @@ -78,7 +78,7 @@ class MailboxCacheDataSourceImpl extends MailboxDataSource { } @override - Future> markAsMailboxRead( + Future> markAsMailboxRead( Session session, AccountId accountId, MailboxId mailboxId, diff --git a/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart b/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart index 062e7effa3..fdcd4e7f5d 100644 --- a/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart +++ b/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart @@ -87,7 +87,7 @@ class MailboxDataSourceImpl extends MailboxDataSource { } @override - Future> markAsMailboxRead( + Future> markAsMailboxRead( Session session, AccountId accountId, MailboxId mailboxId, diff --git a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart index 58a3306f1f..04991d12a9 100644 --- a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart +++ b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart @@ -37,7 +37,7 @@ class MailboxIsolateWorker { MailboxIsolateWorker(this._threadApi, this._emailApi, this._isolateExecutor); - Future> markAsMailboxRead( + Future> markAsMailboxRead( Session session, AccountId accountId, MailboxId mailboxId, @@ -68,7 +68,7 @@ class MailboxIsolateWorker { ), fun1: _handleMarkAsMailboxReadAction, notification: (value) { - if (value is List) { + if (value is List) { log('MailboxIsolateWorker::markAsMailboxRead(): onUpdateProgress: PERCENT ${value.length / totalEmailUnread}'); onProgressController.add(Right(UpdatingMarkAsMailboxReadState( mailboxId: mailboxId, @@ -80,7 +80,7 @@ class MailboxIsolateWorker { } } - static Future> _handleMarkAsMailboxReadAction( + static Future> _handleMarkAsMailboxReadAction( MailboxMarkAsReadArguments args, TypeSendPort sendPort ) async { @@ -88,7 +88,7 @@ class MailboxIsolateWorker { BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken); await HiveCacheConfig.instance.setUp(); - List emailListCompleted = List.empty(growable: true); + List emailIdListCompleted = List.empty(growable: true); try { bool mailboxHasEmails = true; UTCDate? lastReceivedDate; @@ -109,7 +109,6 @@ class MailboxIsolateWorker { ..setIsAscending(false)), properties: Properties({ EmailProperty.id, - EmailProperty.keywords, EmailProperty.receivedAt, })) .then((response) { @@ -138,25 +137,25 @@ class MailboxIsolateWorker { ReadActions.markAsRead); log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): MARK_READ: ${result.length}'); - emailListCompleted.addAll(result); - sendPort.send(emailListCompleted); + emailIdListCompleted.addAll(result); + sendPort.send(emailIdListCompleted); } } } catch (e) { log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): ERROR: $e'); } - log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): TOTAL_READ: ${emailListCompleted.length}'); - return emailListCompleted; + log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): TOTAL_READ: ${emailIdListCompleted.length}'); + return emailIdListCompleted; } - Future> _handleMarkAsMailboxReadActionOnWeb( + Future> _handleMarkAsMailboxReadActionOnWeb( Session session, AccountId accountId, MailboxId mailboxId, int totalEmailUnread, StreamController> onProgressController ) async { - List emailListCompleted = List.empty(growable: true); + List emailIdListCompleted = List.empty(growable: true); try { bool mailboxHasEmails = true; UTCDate? lastReceivedDate; @@ -177,7 +176,6 @@ class MailboxIsolateWorker { ..setIsAscending(false)), properties: Properties({ EmailProperty.id, - EmailProperty.keywords, EmailProperty.receivedAt, }) ).then((response) { @@ -201,18 +199,18 @@ class MailboxIsolateWorker { final result = await _emailApi.markAsRead(session, accountId, listEmailUnread, ReadActions.markAsRead); log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): MARK_READ: ${result.length}'); - emailListCompleted.addAll(result); + emailIdListCompleted.addAll(result); onProgressController.add(Right(UpdatingMarkAsMailboxReadState( mailboxId: mailboxId, totalUnread: totalEmailUnread, - countRead: emailListCompleted.length))); + countRead: emailIdListCompleted.length))); } } } catch (e) { log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): ERROR: $e'); } - log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): TOTAL_READ: ${emailListCompleted.length}'); - return emailListCompleted; + log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): TOTAL_READ: ${emailIdListCompleted.length}'); + return emailIdListCompleted; } } diff --git a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart index a0fde06d72..bfc6fcc068 100644 --- a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart +++ b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart @@ -216,7 +216,7 @@ class MailboxRepositoryImpl extends MailboxRepository { } @override - Future> markAsMailboxRead( + Future> markAsMailboxRead( Session session, AccountId accountId, MailboxId mailboxId, diff --git a/lib/features/mailbox/domain/repository/mailbox_repository.dart b/lib/features/mailbox/domain/repository/mailbox_repository.dart index a3f40851e4..57b8b17de7 100644 --- a/lib/features/mailbox/domain/repository/mailbox_repository.dart +++ b/lib/features/mailbox/domain/repository/mailbox_repository.dart @@ -31,7 +31,7 @@ abstract class MailboxRepository { Future renameMailbox(Session session, AccountId accountId, RenameMailboxRequest request); - Future> markAsMailboxRead( + Future> markAsMailboxRead( Session session, AccountId accountId, MailboxId mailboxId, diff --git a/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart b/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart index 6f16d3e4e9..b79d7aed7a 100644 --- a/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart +++ b/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart @@ -4,7 +4,7 @@ import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; -class MarkAsMailboxReadLoading extends UIState {} +class MarkAsMailboxReadLoading extends LoadingState {} class UpdatingMarkAsMailboxReadState extends UIState { diff --git a/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart b/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart index bd892595a3..17b99fc4bc 100644 --- a/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart +++ b/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart @@ -35,22 +35,22 @@ class MarkAsMailboxReadInteractor { final currentMailboxState = listState.first; final currentEmailState = listState.last; - final listEmails = await _mailboxRepository.markAsMailboxRead( + final listEmailId = await _mailboxRepository.markAsMailboxRead( session, accountId, mailboxId, totalEmailUnread, onProgressController); - if (totalEmailUnread == listEmails.length) { + if (totalEmailUnread == listEmailId.length) { yield Right(MarkAsMailboxReadAllSuccess( mailboxDisplayName, currentEmailState: currentEmailState, currentMailboxState: currentMailboxState)); - } else if (listEmails.isNotEmpty) { + } else if (listEmailId.isNotEmpty) { yield Right(MarkAsMailboxReadHasSomeEmailFailure( mailboxDisplayName, - listEmails.length, + listEmailId.length, currentEmailState: currentEmailState, currentMailboxState: currentMailboxState)); } else { diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index 0f01e52641..663a880712 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -487,20 +487,12 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { (success) { if (success is MarkAsMailboxReadLoading) { return Padding( - padding: EdgeInsets.only( - top: controller.responsiveUtils.isDesktop(context) ? 16 : 0, - left: 16, - right: 16, - bottom: controller.responsiveUtils.isDesktop(context) ? 0 : 16), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: horizontalLoadingWidget); } else if (success is UpdatingMarkAsMailboxReadState) { final percent = success.countRead / success.totalUnread; return Padding( - padding: EdgeInsets.only( - top: controller.responsiveUtils.isDesktop(context) ? 16 : 0, - left: 16, - right: 16, - bottom: controller.responsiveUtils.isDesktop(context) ? 0 : 16), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: horizontalPercentLoadingWidget(percent)); } return const SizedBox.shrink(); diff --git a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart index ebe5a7d76a..504c367f39 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart @@ -65,17 +65,13 @@ class TopBarThreadSelection extends StatelessWidget{ ), ), TMailButtonWidget.fromIcon( - icon: listEmail.isAllEmailRead - ? imagePaths.icUnread - : imagePaths.icRead, - tooltipMessage: listEmail.isAllEmailRead - ? AppLocalizations.of(context).mark_as_unread - : AppLocalizations.of(context).mark_as_read, + icon: _getIconForMarkAsRead(), + tooltipMessage: _getTooltipMessageForMarkAsRead(context), backgroundColor: Colors.transparent, iconSize: 24, onTapActionCallback: () => onEmailActionTypeAction?.call( List.from(listEmail), - listEmail.isAllEmailRead ? EmailActionType.markAsUnread : EmailActionType.markAsRead + _getActionTypeForMarkAsRead() ) ), TMailButtonWidget.fromIcon( @@ -163,4 +159,34 @@ class TopBarThreadSelection extends StatelessWidget{ ), ]); } + + EmailActionType _getActionTypeForMarkAsRead() { + if (isSelectAllEmailsEnabled) { + return EmailActionType.markAllAsRead; + } else { + return listEmail.isAllEmailRead + ? EmailActionType.markAsUnread + : EmailActionType.markAsRead; + } + } + + String _getIconForMarkAsRead() { + if (isSelectAllEmailsEnabled) { + return imagePaths.icRead; + } else { + return listEmail.isAllEmailRead + ? imagePaths.icUnread + : imagePaths.icRead; + } + } + + String _getTooltipMessageForMarkAsRead(BuildContext context) { + if (isSelectAllEmailsEnabled) { + return AppLocalizations.of(context).mark_all_as_read; + } else { + return listEmail.isAllEmailRead + ? AppLocalizations.of(context).mark_as_unread + : AppLocalizations.of(context).mark_as_read; + } + } } \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 35e09bba45..c052f1915a 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -280,8 +280,7 @@ class ThreadController extends BaseController with EmailActionController, PopupM if (_validateToShowConfirmBulkActionEmailsDialog()) { _showConfirmDialogWhenMakeToActionForSelectionAllEmails( context: action.context, - totalEmails: mailboxDashBoardController.selectedMailbox.value!.totalEmails!.value.value.toInt(), - folderName: mailboxDashBoardController.selectedMailbox.value!.getDisplayName(action.context), + selectedMailbox: mailboxDashBoardController.selectedMailbox.value!, actionType: action.emailAction ); } else { @@ -1354,7 +1353,7 @@ class ThreadController extends BaseController with EmailActionController, PopupM void showPopupMenuSelectionEmailAction(BuildContext context, RelativeRect position) { final listSelectionEmailActions = [ - EmailActionType.markAsRead, + EmailActionType.markAllAsRead, EmailActionType.markAsUnread, EmailActionType.moveToMailbox, EmailActionType.moveToTrash, @@ -1380,8 +1379,7 @@ class ThreadController extends BaseController with EmailActionController, PopupM if (!isSearchActive) { _showConfirmDialogWhenMakeToActionForSelectionAllEmails( context: context, - totalEmails: mailboxDashBoardController.selectedMailbox.value!.totalEmails!.value.value.toInt(), - folderName: mailboxDashBoardController.selectedMailbox.value!.getDisplayName(context), + selectedMailbox: mailboxDashBoardController.selectedMailbox.value!, actionType: action ); } @@ -1397,10 +1395,12 @@ class ThreadController extends BaseController with EmailActionController, PopupM Future _showConfirmDialogWhenMakeToActionForSelectionAllEmails({ required BuildContext context, - required int totalEmails, - required String folderName, + required PresentationMailbox selectedMailbox, required EmailActionType actionType }) async { + final totalEmails = selectedMailbox.totalEmails?.value.value.toInt() ?? 0; + final folderName = selectedMailbox.getDisplayName(context); + await showConfirmDialogAction( context, AppLocalizations.of(context).messageConfirmationDialogWhenMakeToActionForSelectionAllEmailsInMailbox(totalEmails, folderName), @@ -1410,6 +1410,39 @@ class ThreadController extends BaseController with EmailActionController, PopupM imagePaths.icQuotasWarning, colorFilter: AppColor.colorBackgroundQuotasWarning.asFilter() ), + onConfirmAction: () { + _handleActionsForSelectionAllEmails( + context: context, + selectedMailbox: selectedMailbox, + actionType: actionType + ); + } ); } + + void _handleActionsForSelectionAllEmails({ + required BuildContext context, + required PresentationMailbox selectedMailbox, + required EmailActionType actionType + }) { + if (_session == null || _accountId == null) { + logError('ThreadController::_handleActionsForSelectionAllEmails: SESSION & ACCOUNT_ID is null'); + return; + } + + switch(actionType) { + case EmailActionType.markAllAsRead: + cancelSelectEmail(); + mailboxDashBoardController.markAsReadMailbox( + _session!, + _accountId!, + selectedMailbox.mailboxId!, + selectedMailbox.getDisplayName(context), + selectedMailbox.unreadEmails?.value.value.toInt() ?? 0 + ); + break; + default: + break; + } + } } \ No newline at end of file diff --git a/model/lib/email/email_action_type.dart b/model/lib/email/email_action_type.dart index e9a7b4460f..fa77b2c148 100644 --- a/model/lib/email/email_action_type.dart +++ b/model/lib/email/email_action_type.dart @@ -27,5 +27,6 @@ enum EmailActionType { unsubscribe, composeFromUnsubscribeMailtoLink, archiveMessage, - printAll + printAll, + markAllAsRead } \ No newline at end of file From 82f1f7db988b4c777baee4803204173915301fe8 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 29 Apr 2024 11:57:20 +0700 Subject: [PATCH 07/15] TF-2646 Handle mark all as unread for selection emails --- lib/features/base/base_controller.dart | 2 + .../mixin/mailbox_action_handler_mixin.dart | 3 +- .../base/toast/app_toast_manager.dart | 73 +++++++++ .../email_action_type_extension.dart | 3 + .../state/mark_as_mailbox_read_state.dart | 3 + .../presentation/mailbox_controller.dart | 5 + .../bindings/mailbox_dashboard_bindings.dart | 7 + .../mailbox_dashboard_controller.dart | 143 +++++++++++------- .../controller/spam_report_controller.dart | 2 +- .../mailbox_dashboard_view_web.dart | 27 +--- ...d_selection_all_emails_loading_widget.dart | 36 +++++ .../mark_mailbox_as_read_loading_widget.dart | 36 +++++ .../search_mailbox_controller.dart | 5 + .../data/datasource/thread_datasource.dart | 11 ++ .../local_thread_datasource_impl.dart | 14 ++ .../thread_datasource_impl.dart | 22 +++ .../data/network/thread_isolate_worker.dart | 80 ++++++++++ .../repository/thread_repository_impl.dart | 21 +++ .../domain/repository/thread_repository.dart | 11 ++ ..._as_unread_selection_all_emails_state.dart | 85 +++++++++++ ...nread_selection_all_emails_interactor.dart | 75 +++++++++ .../presentation/thread_controller.dart | 19 ++- lib/l10n/intl_messages.arb | 40 ++++- lib/main/bindings/core/core_bindings.dart | 5 + lib/main/localizations/app_localizations.dart | 34 +++++ model/lib/email/email_action_type.dart | 3 +- .../presentation_mailbox_extension.dart | 2 + .../single_email_controller_test.dart | 4 + .../mailbox_dashboard_controller_test.dart | 8 + 29 files changed, 696 insertions(+), 83 deletions(-) create mode 100644 lib/features/base/toast/app_toast_manager.dart create mode 100644 lib/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_unread_selection_all_emails_loading_widget.dart create mode 100644 lib/features/mailbox_dashboard/presentation/widgets/loading/mark_mailbox_as_read_loading_widget.dart create mode 100644 lib/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart create mode 100644 lib/features/thread/domain/usecases/mark_all_as_unread_selection_all_emails_interactor.dart diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index eacf5b8cb3..b015e288b5 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -25,6 +25,7 @@ import 'package:model/account/authentication_type.dart'; import 'package:rule_filter/rule_filter/capability_rule_filter.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'; +import 'package:tmail_ui_user/features/base/toast/app_toast_manager.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; import 'package:tmail_ui_user/features/email/presentation/bindings/mdn_interactor_bindings.dart'; import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart'; @@ -78,6 +79,7 @@ abstract class BaseController extends GetxController final ResponsiveUtils responsiveUtils = Get.find(); final Uuid uuid = Get.find(); final ApplicationManager applicationManager = Get.find(); + final AppToastManager appToastManager = Get.find(); bool _isFcmEnabled = false; diff --git a/lib/features/base/mixin/mailbox_action_handler_mixin.dart b/lib/features/base/mixin/mailbox_action_handler_mixin.dart index f124709534..37cc523220 100644 --- a/lib/features/base/mixin/mailbox_action_handler_mixin.dart +++ b/lib/features/base/mixin/mailbox_action_handler_mixin.dart @@ -34,14 +34,13 @@ mixin MailboxActionHandlerMixin { final session = dashboardController.sessionCurrent; final accountId = dashboardController.accountId.value; final mailboxId = presentationMailbox.id; - final countEmailsUnread = presentationMailbox.unreadEmails?.value.value ?? 0; if (session != null && accountId != null) { dashboardController.markAsReadMailbox( session, accountId, mailboxId, presentationMailbox.getDisplayName(context), - countEmailsUnread.toInt() + presentationMailbox.countUnreadEmails ); onCallbackAction?.call(context); diff --git a/lib/features/base/toast/app_toast_manager.dart b/lib/features/base/toast/app_toast_manager.dart new file mode 100644 index 0000000000..68ee712e7b --- /dev/null +++ b/lib/features/base/toast/app_toast_manager.dart @@ -0,0 +1,73 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/app_toast.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class AppToastManager { + final AppToast _appToast; + final ImagePaths _imagePaths; + + AppToastManager(this._appToast, this._imagePaths); + + void showSuccessMessage({ + required BuildContext context, + required BuildContext overlayContext, + required Success success + }) { + if (success is MarkAsMailboxReadAllSuccess) { + _appToast.showToastSuccessMessage( + overlayContext, + AppLocalizations.of(context).toastMessageMarkAsMailboxReadSuccess(success.mailboxDisplayName), + leadingSVGIcon: _imagePaths.icReadToast); + } else if (success is MarkAsMailboxReadHasSomeEmailFailure) { + _appToast.showToastSuccessMessage( + overlayContext, + AppLocalizations.of(context).toastMessageMarkAsMailboxReadHasSomeEmailFailure(success.mailboxDisplayName, success.countEmailsRead), + leadingSVGIcon: _imagePaths.icReadToast); + } else if (success is MarkAllAsUnreadSelectionAllEmailsAllSuccess) { + _appToast.showToastSuccessMessage( + overlayContext, + AppLocalizations.of(context).toastMessageMarkAllAsUnreadSelectionAllEmailsSuccess, + leadingSVGIcon: _imagePaths.icUnreadToast); + } else if (success is MarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure) { + _appToast.showToastSuccessMessage( + overlayContext, + AppLocalizations.of(context).toastMessageMarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure(success.countEmailsUnread), + leadingSVGIcon: _imagePaths.icUnreadToast); + } + } + + void showFailureMessage({ + required BuildContext context, + required BuildContext overlayContext, + required Failure failure + }) { + if (failure is MarkAsMailboxReadFailure) { + _appToast.showToastErrorMessage( + overlayContext, + AppLocalizations.of(context).toastMessageMarkAsReadFolderFailureWithReason( + failure.mailboxDisplayName, + failure.exception.toString() + )); + } else if (failure is MarkAsMailboxReadAllFailure) { + _appToast.showToastErrorMessage( + overlayContext, + AppLocalizations.of(context).toastMessageMarkAsReadFolderAllFailure(failure.mailboxDisplayName)); + } else if (failure is MarkAllAsUnreadSelectionAllEmailsFailure) { + _appToast.showToastErrorMessage( + overlayContext, + AppLocalizations.of(context).toastMessageMarkAllAsUnreadSelectionAllEmailsFailureWithReason( + failure.exception.toString() + )); + } else if (failure is MarkAllAsUnreadSelectionAllEmailsAllFailure) { + _appToast.showToastErrorMessage( + overlayContext, + AppLocalizations.of(context).toastMessageMarkAllAsUnreadSelectionAllEmailsAllFailure); + } + } +} \ No newline at end of file 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 5f05d56053..bab74d22b9 100644 --- a/lib/features/composer/presentation/extensions/email_action_type_extension.dart +++ b/lib/features/composer/presentation/extensions/email_action_type_extension.dart @@ -122,6 +122,7 @@ extension EmailActionTypeExtension on EmailActionType { String getIcon(ImagePaths imagePaths) { switch(this) { case EmailActionType.markAsUnread: + case EmailActionType.markAllAsUnread: return imagePaths.icUnreadEmail; case EmailActionType.unSpam: return imagePaths.icNotSpam; @@ -167,6 +168,8 @@ extension EmailActionTypeExtension on EmailActionType { return AppLocalizations.of(context).move_to_trash; case EmailActionType.markAllAsRead: return AppLocalizations.of(context).mark_all_as_read; + case EmailActionType.markAllAsUnread: + return AppLocalizations.of(context).markAllAsUnread; default: return ''; } diff --git a/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart b/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart index b79d7aed7a..d77581b4cc 100644 --- a/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart +++ b/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart @@ -78,4 +78,7 @@ class MarkAsMailboxReadFailure extends FeatureFailure { required this.mailboxDisplayName, dynamic exception }) : super(exception: exception); + + @override + List get props => [mailboxDisplayName, ...super.props]; } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 25247f7891..50d2117383 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -76,6 +76,7 @@ import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbo import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.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/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -267,6 +268,10 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); } else if (success is GetRestoredDeletedMessageSuccess) { _refreshMailboxChanges(properties: MailboxConstants.propertiesDefault); + } else if (success is MarkAllAsUnreadSelectionAllEmailsAllSuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is MarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); } }); }); diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index 375143f3ec..519a2e0204 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -112,6 +112,7 @@ import 'package:tmail_ui_user/features/thread/domain/repository/thread_repositor import 'package:tmail_ui_user/features/thread/domain/usecases/empty_spam_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_trash_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/mark_all_as_unread_selection_all_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart'; @@ -177,6 +178,7 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); Get.put(AdvancedFilterController()); } @@ -343,6 +345,11 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find() )); + Get.lazyPut(() => MarkAllAsUnreadSelectionAllEmailsInteractor( + Get.find(), + Get.find(), + Get.find(), + )); } @override 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 ed2883b5e4..7ef21112ba 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -134,12 +134,14 @@ import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_sta import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_email_by_id_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.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_star_multiple_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_spam_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_trash_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/mark_all_as_unread_selection_all_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart'; @@ -188,6 +190,7 @@ class MailboxDashBoardController extends ReloadableController { final GetRestoredDeletedMessageInterator _getRestoredDeletedMessageInteractor; final RemoveComposerCacheOnWebInteractor _removeComposerCacheOnWebInteractor; final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; + final MarkAllAsUnreadSelectionAllEmailsInteractor _markAllAsUnreadSelectionAllEmailsInteractor; GetAllVacationInteractor? _getAllVacationInteractor; UpdateVacationInteractor? _updateVacationInteractor; @@ -220,6 +223,7 @@ class MailboxDashBoardController extends ReloadableController { final isRecoveringDeletedMessage = RxBool(false); final localFileDraggableAppState = Rxn(); final isSelectAllEmailsEnabled = RxBool(false); + final markAllAsUnreadSelectionAllEmailsViewState = Rx>(Right(UIState.idle)); Session? sessionCurrent; Map mapDefaultMailboxIdByRole = {}; @@ -233,13 +237,16 @@ class MailboxDashBoardController extends ReloadableController { late StreamSubscription _emailAddressStreamSubscription; late StreamSubscription _emailContentStreamSubscription; late StreamSubscription _fileReceiveManagerStreamSubscription; + late StreamSubscription _markAsReadMailboxStreamSubscription; + late StreamSubscription _refreshActionStreamSubscription; + late StreamSubscription _markAllAsUnreadSelectionAllEmailsStreamSubscription; - final StreamController> _progressStateController = + final StreamController> _markAsReadMailboxStreamController = StreamController>.broadcast(); - Stream> get progressState => _progressStateController.stream; - final StreamController _refreshActionEventController = StreamController.broadcast(); + final StreamController> _markAllAsUnreadSelectionAllEmailsStreamController = + StreamController>.broadcast(); final _notificationManager = LocalNotificationManager.instance; final _fcmService = FcmService.instance; @@ -269,6 +276,7 @@ class MailboxDashBoardController extends ReloadableController { this._getRestoredDeletedMessageInteractor, this._removeComposerCacheOnWebInteractor, this._getAllIdentitiesInteractor, + this._markAllAsUnreadSelectionAllEmailsInteractor, ); @override @@ -327,7 +335,7 @@ class MailboxDashBoardController extends ReloadableController { _deleteEmailPermanentlySuccess(success); } else if (success is MarkAsMailboxReadAllSuccess || success is MarkAsMailboxReadHasSomeEmailFailure) { - _markAsReadMailboxSuccess(success); + _handleMarkMailboxAsReadSuccess(success); } else if (success is GetAllVacationSuccess) { if (success.listVacationResponse.isNotEmpty) { vacationResponse.value = success.listVacationResponse.first; @@ -373,6 +381,9 @@ class MailboxDashBoardController extends ReloadableController { _handleGetRestoredDeletedMessageSuccess(success); } else if (success is GetAllIdentitiesSuccess) { _handleGetAllIdentitiesSuccess(success); + } else if (success is MarkAllAsUnreadSelectionAllEmailsAllSuccess + || success is MarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure) { + _handleMarkAllAsUnreadSelectionAllEmailsSuccess(success); } } @@ -387,16 +398,18 @@ class MailboxDashBoardController extends ReloadableController { _handleUpdateEmailAsDraftsFailure(failure); } else if (failure is RemoveEmailDraftsFailure) { clearState(); - } else if (failure is MarkAsMailboxReadAllFailure) { - _markAsReadMailboxAllFailure(failure); - } else if (failure is MarkAsMailboxReadFailure) { - _markAsReadMailboxFailure(failure); + } else if (failure is MarkAsMailboxReadAllFailure + || failure is MarkAsMailboxReadFailure) { + _handleMarkMailboxAsReadFailure(failure); } else if (failure is GetEmailByIdFailure) { _handleGetEmailDetailedFailed(failure); } else if (failure is RestoreDeletedMessageFailure) { _handleRestoreDeletedMessageFailed(); } else if (failure is GetRestoredDeletedMessageFailure) { _handleRestoreDeletedMessageFailed(); + } else if (failure is MarkAllAsUnreadSelectionAllEmailsFailure + || failure is MarkAllAsUnreadSelectionAllEmailsAllFailure) { + _handleMarkAllAsUnreadSelectionAllEmailsFailure(failure); } } @@ -443,14 +456,18 @@ class MailboxDashBoardController extends ReloadableController { } void _registerStreamListener() { - progressState.listen((state) { + _markAsReadMailboxStreamSubscription = _markAsReadMailboxStreamController.stream.listen((state) { viewStateMarkAsReadMailbox.value = state; }); - _refreshActionEventController.stream + _refreshActionStreamSubscription = _refreshActionEventController.stream .debounceTime(const Duration(milliseconds: FcmUtils.durationMessageComing)) .listen(_handleRefreshActionWhenBackToApp); + _markAllAsUnreadSelectionAllEmailsStreamSubscription = _markAllAsUnreadSelectionAllEmailsStreamController.stream.listen((state) { + markAllAsUnreadSelectionAllEmailsViewState.value = state; + }); + _registerLocalNotificationStreamListener(); } @@ -1330,15 +1347,13 @@ class MailboxDashBoardController extends ReloadableController { void markAsReadMailboxAction(BuildContext context) { final session = sessionCurrent; final currentAccountId = accountId.value; - final mailboxId = selectedMailbox.value?.id; - final countEmailsUnread = selectedMailbox.value?.unreadEmails?.value.value ?? 0; - if (session != null && currentAccountId != null && mailboxId != null) { + if (session != null && currentAccountId != null && selectedMailbox.value != null) { markAsReadMailbox( session, currentAccountId, - mailboxId, - selectedMailbox.value?.getDisplayName(context) ?? '', - countEmailsUnread.toInt() + selectedMailbox.value!.id, + selectedMailbox.value!.getDisplayName(context), + selectedMailbox.value!.countUnreadEmails ); } } @@ -1356,50 +1371,29 @@ class MailboxDashBoardController extends ReloadableController { mailboxId, mailboxDisplayName, totalEmailsUnread, - _progressStateController)); + _markAsReadMailboxStreamController)); } - void _markAsReadMailboxSuccess(Success success) { + void _handleMarkMailboxAsReadSuccess(Success success) { viewStateMarkAsReadMailbox.value = Right(UIState.idle); - if (success is MarkAsMailboxReadAllSuccess) { - if (currentContext != null && currentOverlayContext != null) { - appToast.showToastSuccessMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).toastMessageMarkAsMailboxReadSuccess(success.mailboxDisplayName), - leadingSVGIcon: imagePaths.icReadToast); - } - } else if (success is MarkAsMailboxReadHasSomeEmailFailure) { - if (currentContext != null && currentOverlayContext != null) { - appToast.showToastSuccessMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).toastMessageMarkAsMailboxReadHasSomeEmailFailure(success.mailboxDisplayName, success.countEmailsRead), - leadingSVGIcon: imagePaths.icReadToast); - } - } - } + if (currentContext == null || currentOverlayContext == null) return; - void _markAsReadMailboxFailure(MarkAsMailboxReadFailure failure) { - viewStateMarkAsReadMailbox.value = Right(UIState.idle); - if (currentOverlayContext != null && currentContext != null) { - appToast.showToastErrorMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).toastMessageMarkAsReadFolderFailureWithReason( - failure.mailboxDisplayName, - failure.exception.toString() - ) - ); - } + appToastManager.showSuccessMessage( + context: currentContext!, + overlayContext: currentOverlayContext!, + success: success); } - void _markAsReadMailboxAllFailure(MarkAsMailboxReadAllFailure failure) { + void _handleMarkMailboxAsReadFailure(Failure failure) { viewStateMarkAsReadMailbox.value = Right(UIState.idle); - if (currentOverlayContext != null && currentContext != null) { - appToast.showToastErrorMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).toastMessageMarkAsReadFolderAllFailure(failure.mailboxDisplayName) - ); - } + + if (currentContext == null || currentOverlayContext == null) return; + + appToastManager.showFailureMessage( + context: currentContext!, + overlayContext: currentOverlayContext!, + failure: failure); } void goToComposer(ComposerArguments arguments) async { @@ -2524,14 +2518,57 @@ class MailboxDashBoardController extends ReloadableController { log('MailboxDashBoardController::_handleGetAllIdentitiesSuccess: IDENTITIES_SIZE = ${_identities?.length}'); } + void markAllAsUnreadSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + String mailboxDisplayName, + int totalEmailsRead + ) { + consumeState(_markAllAsUnreadSelectionAllEmailsInteractor.execute( + session, + accountId, + mailboxId, + mailboxDisplayName, + totalEmailsRead, + _markAllAsUnreadSelectionAllEmailsStreamController + )); + } + + void _handleMarkAllAsUnreadSelectionAllEmailsSuccess(Success success) { + markAllAsUnreadSelectionAllEmailsViewState.value = Right(UIState.idle); + + if (currentContext == null || currentOverlayContext == null) return; + + appToastManager.showSuccessMessage( + context: currentContext!, + overlayContext: currentOverlayContext!, + success: success); + } + + void _handleMarkAllAsUnreadSelectionAllEmailsFailure(Failure failure) { + markAllAsUnreadSelectionAllEmailsViewState.value = Right(UIState.idle); + + if (currentContext == null || currentOverlayContext == null) return; + + appToastManager.showFailureMessage( + context: currentContext!, + overlayContext: currentOverlayContext!, + failure: failure); + } + @override void onClose() { _emailReceiveManager.closeEmailReceiveManagerStream(); _emailAddressStreamSubscription.cancel(); _emailContentStreamSubscription.cancel(); _fileReceiveManagerStreamSubscription.cancel(); - _progressStateController.close(); + _markAsReadMailboxStreamSubscription.cancel(); + _markAsReadMailboxStreamController.close(); + _refreshActionStreamSubscription.cancel(); _refreshActionEventController.close(); + _markAllAsUnreadSelectionAllEmailsStreamSubscription.cancel(); + _markAllAsUnreadSelectionAllEmailsStreamController.close(); _notificationManager.closeStream(); _fcmService.closeStream(); applicationManager.releaseUserAgent(); diff --git a/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart index 2ad681b616..124404b316 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart @@ -84,7 +84,7 @@ class SpamReportController extends BaseController { accountId, spamMailbox.id, spamMailbox.getDisplayName(context), - spamMailbox.unreadEmails?.value.value.toInt() ?? 0 + spamMailbox.countUnreadEmails ); _presentationSpamMailbox.value = null; } diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index 663a880712..80e514c841 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -10,7 +10,6 @@ import 'package:tmail_ui_user/features/base/widget/popup_item_no_icon_widget.dar import 'package:tmail_ui_user/features/composer/presentation/composer_view_web.dart'; import 'package:tmail_ui_user/features/email/presentation/email_view.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/mailbox_view_web.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/base_mailbox_dashboard_view.dart'; @@ -20,6 +19,8 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/sear import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/download/download_task_item_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_unread_selection_all_emails_loading_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/loading/mark_mailbox_as_read_loading_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/recover_deleted_message_loading_banner_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart'; @@ -150,7 +151,8 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { responsiveUtils: controller.responsiveUtils, )), _buildListButtonQuickSearchFilter(context), - _buildMarkAsMailboxReadLoading(context), + Obx(() => MarkMailboxAsReadLoadingWidget(viewState: controller.viewStateMarkAsReadMailbox.value)), + Obx(() => MarkAllAsUnreadSelectionAllEmailsLoadingWidget(viewState: controller.markAllAsUnreadSelectionAllEmailsViewState.value)), Expanded(child: Obx(() { switch(controller.dashboardRoute.value) { case DashboardRoutes.thread: @@ -479,27 +481,6 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { ]); } - Widget _buildMarkAsMailboxReadLoading(BuildContext context) { - return Obx(() { - final viewState = controller.viewStateMarkAsReadMailbox.value; - return viewState.fold( - (failure) => const SizedBox.shrink(), - (success) { - if (success is MarkAsMailboxReadLoading) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: horizontalLoadingWidget); - } else if (success is UpdatingMarkAsMailboxReadState) { - final percent = success.countRead / success.totalUnread; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: horizontalPercentLoadingWidget(percent)); - } - return const SizedBox.shrink(); - }); - }); - } - Widget _buildDownloadTaskStateWidget() { return Obx(() { if (controller.downloadController.notEmptyListDownloadTask) { diff --git a/lib/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_unread_selection_all_emails_loading_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_unread_selection_all_emails_loading_widget.dart new file mode 100644 index 0000000000..910d9c1dd7 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_unread_selection_all_emails_loading_widget.dart @@ -0,0 +1,36 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart'; + +class MarkAllAsUnreadSelectionAllEmailsLoadingWidget extends StatelessWidget with AppLoaderMixin { + + final Either viewState; + + const MarkAllAsUnreadSelectionAllEmailsLoadingWidget({super.key, required this.viewState}); + + @override + Widget build(BuildContext context) { + return viewState.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is MarkAllAsUnreadSelectionAllEmailsLoading) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: horizontalLoadingWidget + ); + } else if (success is MarkAllAsUnreadSelectionAllEmailsUpdating) { + final percent = success.countUnread / success.totalRead; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: horizontalPercentLoadingWidget(percent) + ); + } + return const SizedBox.shrink(); + } + ); + } +} diff --git a/lib/features/mailbox_dashboard/presentation/widgets/loading/mark_mailbox_as_read_loading_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/loading/mark_mailbox_as_read_loading_widget.dart new file mode 100644 index 0000000000..1e34ee9212 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/widgets/loading/mark_mailbox_as_read_loading_widget.dart @@ -0,0 +1,36 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; + +class MarkMailboxAsReadLoadingWidget extends StatelessWidget with AppLoaderMixin { + + final Either viewState; + + const MarkMailboxAsReadLoadingWidget({super.key, required this.viewState}); + + @override + Widget build(BuildContext context) { + return viewState.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is MarkAsMailboxReadLoading) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: horizontalLoadingWidget + ); + } else if (success is UpdatingMarkAsMailboxReadState) { + final percent = success.countRead / success.totalUnread; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: horizontalPercentLoadingWidget(percent) + ); + } + return const SizedBox.shrink(); + } + ); + } +} diff --git a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart index 83a0fe9d88..c62b75cba7 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart @@ -61,6 +61,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.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'; @@ -164,6 +165,10 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa _handleSubscribeMultipleMailboxHasSomeSuccess(success); } else if (success is CreateNewMailboxSuccess) { _createNewMailboxSuccess(success); + } else if (success is MarkAllAsUnreadSelectionAllEmailsAllSuccess) { + _refreshMailboxChanges(mailboxState: success.currentMailboxState); + } else if (success is MarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure) { + _refreshMailboxChanges(mailboxState: success.currentMailboxState); } } diff --git a/lib/features/thread/data/datasource/thread_datasource.dart b/lib/features/thread/data/datasource/thread_datasource.dart index 6c324c855e..217d99e4e2 100644 --- a/lib/features/thread/data/datasource/thread_datasource.dart +++ b/lib/features/thread/data/datasource/thread_datasource.dart @@ -1,5 +1,8 @@ import 'dart:async'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart' as dartz; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; @@ -58,4 +61,12 @@ abstract class ThreadDataSource { ); Future getEmailById(Session session, AccountId accountId, EmailId emailId, {Properties? properties}); + + Future> markAllAsUnreadForSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmailRead, + StreamController> onProgressController + ); } \ No newline at end of file diff --git a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart index b18202dabc..796288f18b 100644 --- a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart @@ -1,5 +1,8 @@ import 'dart:async'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart' as dartz; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; @@ -104,4 +107,15 @@ class LocalThreadDataSourceImpl extends ThreadDataSource { Future getEmailById(Session session, AccountId accountId, EmailId emailId, {Properties? properties}) { throw UnimplementedError(); } + + @override + Future> markAllAsUnreadForSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmailRead, + StreamController> onProgressController + ) { + throw UnimplementedError(); + } } \ No newline at end of file diff --git a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart index 1f1974c2a3..d039127afb 100644 --- a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart @@ -1,5 +1,8 @@ import 'dart:async'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart' as dartz; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; @@ -108,4 +111,23 @@ class ThreadDataSourceImpl extends ThreadDataSource { return email.toPresentationEmail(); }).catchError(_exceptionThrower.throwException); } + + @override + Future> markAllAsUnreadForSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmailRead, + StreamController> onProgressController + ) { + return Future.sync(() async { + return await _threadIsolateWorker.markAllAsUnreadForSelectionAllEmails( + session, + accountId, + mailboxId, + totalEmailRead, + onProgressController + ); + }).catchError(_exceptionThrower.throwException); + } } \ No newline at end of file diff --git a/lib/features/thread/data/network/thread_isolate_worker.dart b/lib/features/thread/data/network/thread_isolate_worker.dart index 83062371c8..beb1dce292 100644 --- a/lib/features/thread/data/network/thread_isolate_worker.dart +++ b/lib/features/thread/data/network/thread_isolate_worker.dart @@ -1,17 +1,24 @@ import 'dart:async'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart' as dartz; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/utc_date.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_comparator.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_comparator_property.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_filter_condition.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/email_property.dart'; +import 'package:model/email/read_actions.dart'; import 'package:model/extensions/list_email_extension.dart'; import 'package:tmail_ui_user/features/base/isolate/background_isolate_binary_messenger/background_isolate_binary_messenger.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; @@ -19,6 +26,8 @@ import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/thread/data/model/empty_mailbox_folder_arguments.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; import 'package:tmail_ui_user/features/thread/domain/exceptions/thread_exceptions.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart'; import 'package:tmail_ui_user/main/exceptions/isolate_exception.dart'; import 'package:worker_manager/worker_manager.dart'; @@ -156,4 +165,75 @@ class ThreadIsolateWorker { log('ThreadIsolateWorker::_emptyMailboxFolderOnWeb(): TOTAL_REMOVE: ${emailListCompleted.length}'); return emailListCompleted; } + + Future> markAllAsUnreadForSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmailRead, + StreamController> onProgressController + ) async { + List emailIdListCompleted = List.empty(growable: true); + try { + bool mailboxHasEmails = true; + UTCDate? lastReceivedDate; + EmailId? lastEmailId; + + while (mailboxHasEmails) { + final emailResponse = await _threadAPI.getAllEmail( + session, + accountId, + limit: UnsignedInt(30), + filter: EmailFilterCondition( + inMailbox: mailboxId, + hasKeyword: KeyWordIdentifier.emailSeen.value, + before: lastReceivedDate + ), + sort: {}..add( + EmailComparator(EmailComparatorProperty.receivedAt)..setIsAscending(false) + ), + properties: Properties({ + EmailProperty.id, + EmailProperty.receivedAt, + }) + ).then((response) { + var listEmails = response.emailList; + if (listEmails != null && listEmails.isNotEmpty && lastEmailId != null) { + listEmails = listEmails + .where((email) => email.id != lastEmailId) + .toList(); + } + return EmailsResponse(emailList: listEmails, state: response.state); + }); + final listEmailRead = emailResponse.emailList; + log('ThreadIsolateWorker::markAllAsUnreadForSelectionAllEmails: LIST_EMAIL_READ = ${listEmailRead?.length}'); + if (listEmailRead == null || listEmailRead.isEmpty) { + mailboxHasEmails = false; + } else { + lastEmailId = listEmailRead.last.id; + lastReceivedDate = listEmailRead.last.receivedAt; + + final listEmailId = await _emailAPI.markAsRead( + session, + accountId, + listEmailRead, + ReadActions.markAsUnread + ); + log('ThreadIsolateWorker::markAllAsUnreadForSelectionAllEmails(): MARK_UNREAD: ${listEmailId.length}'); + emailIdListCompleted.addAll(listEmailId); + onProgressController.add( + dartz.Right(MarkAllAsUnreadSelectionAllEmailsUpdating( + mailboxId: mailboxId, + totalRead: totalEmailRead, + countUnread: emailIdListCompleted.length + )) + ); + } + } + } catch (e) { + log('ThreadIsolateWorker::markAllAsUnreadForSelectionAllEmails(): ERROR: $e'); + } + log('ThreadIsolateWorker::markAllAsUnreadForSelectionAllEmails(): TOTAL_UNREAD: ${emailIdListCompleted.length}'); + return emailIdListCompleted; + } } diff --git a/lib/features/thread/data/repository/thread_repository_impl.dart b/lib/features/thread/data/repository/thread_repository_impl.dart index 6a29faf5b2..60ab5e1aa0 100644 --- a/lib/features/thread/data/repository/thread_repository_impl.dart +++ b/lib/features/thread/data/repository/thread_repository_impl.dart @@ -1,5 +1,9 @@ +import 'dart:async'; + import 'package:core/data/model/source_type/data_source_type.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart' as dartz; import 'package:jmap_dart_client/jmap/account_id.dart'; @@ -404,4 +408,21 @@ class ThreadRepositoryImpl extends ThreadRepository { return listEmailIdDeleted; } + + @override + Future> markAllAsUnreadForSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmailRead, + StreamController> onProgressController + ) { + return mapDataSource[DataSourceType.network]!.markAllAsUnreadForSelectionAllEmails( + session, + accountId, + mailboxId, + totalEmailRead, + onProgressController + ); + } } \ No newline at end of file diff --git a/lib/features/thread/domain/repository/thread_repository.dart b/lib/features/thread/domain/repository/thread_repository.dart index c1ba1e1808..69eb85c354 100644 --- a/lib/features/thread/domain/repository/thread_repository.dart +++ b/lib/features/thread/domain/repository/thread_repository.dart @@ -1,5 +1,8 @@ import 'dart:async'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart' as dartz; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; @@ -71,4 +74,12 @@ abstract class ThreadRepository { AccountId accountId, MailboxId spamMailboxId, ); + + Future> markAllAsUnreadForSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmailRead, + StreamController> onProgressController + ); } \ No newline at end of file diff --git a/lib/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart b/lib/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart new file mode 100644 index 0000000000..7a41d31a70 --- /dev/null +++ b/lib/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart @@ -0,0 +1,85 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; + +class MarkAllAsUnreadSelectionAllEmailsLoading extends LoadingState {} + +class MarkAllAsUnreadSelectionAllEmailsUpdating extends UIState { + + final MailboxId mailboxId; + final int totalRead; + final int countUnread; + + MarkAllAsUnreadSelectionAllEmailsUpdating({ + required this.mailboxId, + required this.totalRead, + required this.countUnread + }); + + @override + List get props => [mailboxId, totalRead, countUnread]; +} + +class MarkAllAsUnreadSelectionAllEmailsAllSuccess extends UIActionState { + + final String mailboxDisplayName; + + MarkAllAsUnreadSelectionAllEmailsAllSuccess(this.mailboxDisplayName, + { + jmap.State? currentEmailState, + jmap.State? currentMailboxState, + } + ) : super(currentMailboxState, currentEmailState); + + @override + List get props => [ + mailboxDisplayName, + ...super.props + ]; +} + +class MarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure extends UIActionState { + + final String mailboxDisplayName; + final int countEmailsUnread; + + MarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure( + this.mailboxDisplayName, + this.countEmailsUnread, + { + jmap.State? currentEmailState, + jmap.State? currentMailboxState, + } + ) : super(currentMailboxState, currentEmailState); + + @override + List get props => [ + mailboxDisplayName, + countEmailsUnread, + ...super.props + ]; +} + +class MarkAllAsUnreadSelectionAllEmailsAllFailure extends FeatureFailure { + final String mailboxDisplayName; + + MarkAllAsUnreadSelectionAllEmailsAllFailure({required this.mailboxDisplayName}); + + @override + List get props => [mailboxDisplayName]; +} + +class MarkAllAsUnreadSelectionAllEmailsFailure extends FeatureFailure { + + final String mailboxDisplayName; + + MarkAllAsUnreadSelectionAllEmailsFailure({ + required this.mailboxDisplayName, + dynamic exception + }) : super(exception: exception); + + @override + List get props => [mailboxDisplayName, ...super.props]; +} \ No newline at end of file diff --git a/lib/features/thread/domain/usecases/mark_all_as_unread_selection_all_emails_interactor.dart b/lib/features/thread/domain/usecases/mark_all_as_unread_selection_all_emails_interactor.dart new file mode 100644 index 0000000000..a166b4916f --- /dev/null +++ b/lib/features/thread/domain/usecases/mark_all_as_unread_selection_all_emails_interactor.dart @@ -0,0 +1,75 @@ +import 'dart:async'; + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; +import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart'; + +class MarkAllAsUnreadSelectionAllEmailsInteractor { + final MailboxRepository _mailboxRepository; + final EmailRepository _emailRepository; + final ThreadRepository _threadRepository; + + MarkAllAsUnreadSelectionAllEmailsInteractor( + this._mailboxRepository, + this._emailRepository, + this._threadRepository, + ); + + Stream> execute( + Session session, + AccountId accountId, + MailboxId mailboxId, + String mailboxDisplayName, + int totalEmailRead, + StreamController> onProgressController + ) async* { + try { + yield Right(MarkAllAsUnreadSelectionAllEmailsLoading()); + onProgressController.add(Right(MarkAllAsUnreadSelectionAllEmailsLoading())); + + final listState = await Future.wait([ + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), + ], eagerError: true); + + final currentMailboxState = listState.first; + final currentEmailState = listState.last; + + final listEmailId = await _threadRepository.markAllAsUnreadForSelectionAllEmails( + session, + accountId, + mailboxId, + totalEmailRead, + onProgressController); + + if (totalEmailRead == listEmailId.length) { + yield Right(MarkAllAsUnreadSelectionAllEmailsAllSuccess( + mailboxDisplayName, + currentEmailState: currentEmailState, + currentMailboxState: currentMailboxState)); + } else if (listEmailId.isNotEmpty) { + yield Right(MarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure( + mailboxDisplayName, + listEmailId.length, + currentEmailState: currentEmailState, + currentMailboxState: currentMailboxState)); + } else { + yield Left(MarkAllAsUnreadSelectionAllEmailsAllFailure( + mailboxDisplayName: mailboxDisplayName + )); + } + } catch (e) { + yield Left(MarkAllAsUnreadSelectionAllEmailsFailure( + mailboxDisplayName: mailboxDisplayName, + exception: e + )); + } + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index c052f1915a..57c768be24 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -59,6 +59,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/get_email_by_id_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/load_more_emails_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.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_star_multiple_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; @@ -387,6 +388,10 @@ class ThreadController extends BaseController with EmailActionController, PopupM refreshAllEmail(); } else if (success is UnsubscribeEmailSuccess) { _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is MarkAllAsUnreadSelectionAllEmailsAllSuccess) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is MarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); } }); }); @@ -1354,7 +1359,7 @@ class ThreadController extends BaseController with EmailActionController, PopupM void showPopupMenuSelectionEmailAction(BuildContext context, RelativeRect position) { final listSelectionEmailActions = [ EmailActionType.markAllAsRead, - EmailActionType.markAsUnread, + EmailActionType.markAllAsUnread, EmailActionType.moveToMailbox, EmailActionType.moveToTrash, ]; @@ -1438,7 +1443,17 @@ class ThreadController extends BaseController with EmailActionController, PopupM _accountId!, selectedMailbox.mailboxId!, selectedMailbox.getDisplayName(context), - selectedMailbox.unreadEmails?.value.value.toInt() ?? 0 + selectedMailbox.countUnreadEmails + ); + break; + case EmailActionType.markAllAsUnread: + cancelSelectEmail(); + mailboxDashBoardController.markAllAsUnreadSelectionAllEmails( + _session!, + _accountId!, + selectedMailbox.mailboxId!, + selectedMailbox.getDisplayName(context), + selectedMailbox.countReadEmails ); break; default: diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 3eb1b0bfca..5d6fd95b6b 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-04-26T11:14:14.060748", + "@@last_modified": "2024-04-29T11:56:08.573350", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3871,5 +3871,43 @@ "total": {}, "folderName": {} } + }, + "markAllAsUnread": "Mark all as unread", + "@markAllAsUnread": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageMarkAllAsUnreadSelectionAllEmailsSuccess": "You’ve marked all mails as unread", + "@toastMessageMarkAllAsUnreadSelectionAllEmailsSuccess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageMarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure": "You’ve marked {count} mails as unread", + "@toastMessageMarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "toastMessageMarkAllAsUnreadSelectionAllEmailsAllFailure": "All mails could not be marked as unread", + "@toastMessageMarkAllAsUnreadSelectionAllEmailsAllFailure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageMarkAllAsUnreadSelectionAllEmailsFailureWithReason": "All mails could not be marked as unread. Due \"{reason}\"", + "@toastMessageMarkAllAsUnreadSelectionAllEmailsFailureWithReason": { + "type": "text", + "placeholders_order": [ + "reason" + ], + "placeholders": { + "reason": {} + } } } \ No newline at end of file diff --git a/lib/main/bindings/core/core_bindings.dart b/lib/main/bindings/core/core_bindings.dart index fea26aee3c..fbdd0890be 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -12,6 +12,7 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tmail_ui_user/features/base/toast/app_toast_manager.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/utils/sending_queue_isolate_manager.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; @@ -46,6 +47,10 @@ class CoreBindings extends Bindings { void _bindingToast() { Get.put(AppToast()); + Get.put(AppToastManager( + Get.find(), + Get.find(), + )); } void _bindingDeviceManager() { diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 23f3294fde..933399ed37 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4021,4 +4021,38 @@ class AppLocalizations { args: [total, folderName] ); } + + String get markAllAsUnread { + return Intl.message( + 'Mark all as unread', + name: 'markAllAsUnread', + ); + } + + String get toastMessageMarkAllAsUnreadSelectionAllEmailsSuccess { + return Intl.message( + 'You’ve marked all mails as unread', + name: 'toastMessageMarkAllAsUnreadSelectionAllEmailsSuccess'); + } + + String toastMessageMarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure(int count) { + return Intl.message( + 'You’ve marked $count mails as unread', + name: 'toastMessageMarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure', + args: [count]); + } + + String get toastMessageMarkAllAsUnreadSelectionAllEmailsAllFailure { + return Intl.message( + 'All mails could not be marked as unread', + name: 'toastMessageMarkAllAsUnreadSelectionAllEmailsAllFailure'); + } + + String toastMessageMarkAllAsUnreadSelectionAllEmailsFailureWithReason(String reason) { + return Intl.message( + 'All mails could not be marked as unread. Due "$reason"', + name: 'toastMessageMarkAllAsUnreadSelectionAllEmailsFailureWithReason', + args: [reason] + ); + } } \ No newline at end of file diff --git a/model/lib/email/email_action_type.dart b/model/lib/email/email_action_type.dart index fa77b2c148..9e3b79d466 100644 --- a/model/lib/email/email_action_type.dart +++ b/model/lib/email/email_action_type.dart @@ -28,5 +28,6 @@ enum EmailActionType { composeFromUnsubscribeMailtoLink, archiveMessage, printAll, - markAllAsRead + markAllAsRead, + markAllAsUnread, } \ No newline at end of file diff --git a/model/lib/extensions/presentation_mailbox_extension.dart b/model/lib/extensions/presentation_mailbox_extension.dart index 6419a5aea4..b9cacdce74 100644 --- a/model/lib/extensions/presentation_mailbox_extension.dart +++ b/model/lib/extensions/presentation_mailbox_extension.dart @@ -29,6 +29,8 @@ extension PresentationMailboxExtension on PresentationMailbox { int get countTotalEmails => totalEmails?.value.value.toInt() ?? 0; + int get countReadEmails => countTotalEmails > countUnreadEmails ? countTotalEmails - countUnreadEmails : 0; + bool get isInbox => role == PresentationMailbox.roleInbox; bool get isSpam => role == PresentationMailbox.roleSpam; diff --git a/test/features/email/presentation/controller/single_email_controller_test.dart b/test/features/email/presentation/controller/single_email_controller_test.dart index 798484e07b..837928e45c 100644 --- a/test/features/email/presentation/controller/single_email_controller_test.dart +++ b/test/features/email/presentation/controller/single_email_controller_test.dart @@ -13,6 +13,7 @@ import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/base/toast/app_toast_manager.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; import 'package:tmail_ui_user/features/email/domain/state/view_attachment_for_web_state.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart'; @@ -73,6 +74,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -105,6 +107,7 @@ void main() { final uuid = MockUuid(); final printEmailInteractor = MockPrintEmailInteractor(); final applicationManager = MockApplicationManager(); + final appToastManager = MockAppToastManager(); late SingleEmailController singleEmailController = SingleEmailController( getEmailContentInteractor, @@ -149,6 +152,7 @@ void main() { Get.put(responsiveUtils); Get.put(uuid); Get.put(applicationManager); + Get.put(appToastManager); when(mailboxDashboardController.accountId).thenReturn(Rxn(testAccountId)); when(uuid.v4()).thenReturn(testTaskId); diff --git a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart index 2431c8045e..b02b243dfc 100644 --- a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart +++ b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart @@ -19,6 +19,7 @@ import 'package:mockito/mockito.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:rxdart/subjects.dart'; +import 'package:tmail_ui_user/features/base/toast/app_toast_manager.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/delete_email_permanently_interactor.dart'; @@ -79,6 +80,7 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/empty_trash_folder import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/load_more_emails_in_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/mark_all_as_unread_selection_all_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart'; @@ -152,6 +154,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() { // mock mailbox dashboard controller direct dependencies @@ -213,6 +216,7 @@ void main() { final responsiveUtils = MockResponsiveUtils(); final uuid = MockUuid(); final applicationManager = MockApplicationManager(); + final appToastManager = MockAppToastManager(); // mock reloadable controller Get dependencies final getSessionInteractor = MockGetSessionInteractor(); @@ -233,6 +237,7 @@ void main() { final refreshAllMailboxInteractor = MockRefreshAllMailboxInteractor(); final removeComposerCacheOnWebInteractor = MockRemoveComposerCacheOnWebInteractor(); final getAllIdentitiesInteractor = MockGetAllIdentitiesInteractor(); + final markAllAsUnreadSelectionAllEmailsInteractor = MockMarkAllAsUnreadSelectionAllEmailsInteractor(); late MailboxController mailboxController; // mock thread controller direct dependencies @@ -280,11 +285,13 @@ void main() { Get.put(responsiveUtils); Get.put(uuid); Get.put(applicationManager); + Get.put(appToastManager); Get.put(getSessionInteractor); Get.put(getAuthenticatedAccountInteractor); Get.put(updateAuthenticationAccountInteractor); Get.put(getAllIdentitiesInteractor); Get.put(removeComposerCacheOnWebInteractor); + Get.put(markAllAsUnreadSelectionAllEmailsInteractor); Get.testMode = true; PackageInfo.setMockInitialValues( @@ -329,6 +336,7 @@ void main() { getRestoredDeletedMessageInteractor, removeComposerCacheOnWebInteractor, getAllIdentitiesInteractor, + markAllAsUnreadSelectionAllEmailsInteractor, ); Get.put(mailboxDashboardController); mailboxDashboardController.onReady(); From b302907da03e5a9fe87d3eb3fb5d54cd819e75cf Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 29 Apr 2024 14:29:37 +0700 Subject: [PATCH 08/15] TF-2646 Handle move all to folder for selection emails --- .../base/toast/app_toast_manager.dart | 24 ++++++ .../email_action_type_extension.dart | 3 + .../email/data/network/email_api.dart | 41 ++++++++++ .../state/mark_as_mailbox_read_state.dart | 9 ++- .../mark_as_mailbox_read_interactor.dart | 2 + .../presentation/mailbox_controller.dart | 5 ++ .../bindings/mailbox_dashboard_bindings.dart | 7 ++ .../mailbox_dashboard_controller.dart | 73 +++++++++++++++++ .../mailbox_dashboard_view_web.dart | 2 + ...l_selection_all_emails_loading_widget.dart | 36 +++++++++ .../widgets/top_bar_thread_selection.dart | 20 ++++- .../search_mailbox_controller.dart | 5 ++ .../data/datasource/thread_datasource.dart | 9 +++ .../local_thread_datasource_impl.dart | 12 +++ .../thread_datasource_impl.dart | 21 +++++ .../data/network/thread_isolate_worker.dart | 73 ++++++++++++++++- .../repository/thread_repository_impl.dart | 19 +++++ .../domain/repository/thread_repository.dart | 9 +++ ..._as_unread_selection_all_emails_state.dart | 41 ++-------- .../move_all_selection_all_emails_state.dart | 81 +++++++++++++++++++ ...nread_selection_all_emails_interactor.dart | 11 +-- ...e_all_selection_all_emails_interactor.dart | 72 +++++++++++++++++ .../presentation/thread_controller.dart | 45 +++++++---- lib/l10n/intl_messages.arb | 52 +++++++++++- lib/main/localizations/app_localizations.dart | 35 ++++++++ model/lib/email/email_action_type.dart | 1 + model/lib/extensions/mailbox_extension.dart | 2 +- .../mailbox_dashboard_controller_test.dart | 5 ++ 28 files changed, 650 insertions(+), 65 deletions(-) create mode 100644 lib/features/mailbox_dashboard/presentation/widgets/loading/move_all_selection_all_emails_loading_widget.dart create mode 100644 lib/features/thread/domain/state/move_all_selection_all_emails_state.dart create mode 100644 lib/features/thread/domain/usecases/move_all_selection_all_emails_interactor.dart diff --git a/lib/features/base/toast/app_toast_manager.dart b/lib/features/base/toast/app_toast_manager.dart index 68ee712e7b..969985aa12 100644 --- a/lib/features/base/toast/app_toast_manager.dart +++ b/lib/features/base/toast/app_toast_manager.dart @@ -6,6 +6,7 @@ import 'package:core/presentation/utils/app_toast.dart'; import 'package:flutter/material.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/move_all_selection_all_emails_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class AppToastManager { @@ -39,6 +40,18 @@ class AppToastManager { overlayContext, AppLocalizations.of(context).toastMessageMarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure(success.countEmailsUnread), leadingSVGIcon: _imagePaths.icUnreadToast); + } else if (success is MoveAllSelectionAllEmailsAllSuccess) { + _appToast.showToastSuccessMessage( + overlayContext, + AppLocalizations.of(context).toastMessageMoveAllSelectionAllEmailsSuccess(success.destinationPath), + leadingSVGIconColor: Colors.white, + leadingSVGIcon: _imagePaths.icFolderMailbox); + } else if (success is MoveAllSelectionAllEmailsHasSomeEmailFailure) { + _appToast.showToastSuccessMessage( + overlayContext, + AppLocalizations.of(context).toastMessageMoveAllSelectionAllEmailsHasSomeEmailFailure(success.countEmailsMoved, success.destinationPath), + leadingSVGIconColor: Colors.white, + leadingSVGIcon: _imagePaths.icFolderMailbox); } } @@ -68,6 +81,17 @@ class AppToastManager { _appToast.showToastErrorMessage( overlayContext, AppLocalizations.of(context).toastMessageMarkAllAsUnreadSelectionAllEmailsAllFailure); + } else if (failure is MoveAllSelectionAllEmailsFailure) { + _appToast.showToastErrorMessage( + overlayContext, + AppLocalizations.of(context).toastMessageMoveAllSelectionAllEmailsFailureWithReason( + failure.destinationPath, + failure.exception.toString() + )); + } else if (failure is MoveAllSelectionAllEmailsAllFailure) { + _appToast.showToastErrorMessage( + overlayContext, + AppLocalizations.of(context).toastMessageMoveAllSelectionAllEmailsAllFailure(failure.destinationPath)); } } } \ No newline at end of file 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 bab74d22b9..041d728337 100644 --- a/lib/features/composer/presentation/extensions/email_action_type_extension.dart +++ b/lib/features/composer/presentation/extensions/email_action_type_extension.dart @@ -138,6 +138,7 @@ extension EmailActionTypeExtension on EmailActionType { case EmailActionType.markAllAsRead: return imagePaths.icRead; case EmailActionType.moveToMailbox: + case EmailActionType.moveAll: return imagePaths.icMove; case EmailActionType.moveToTrash: return imagePaths.icDeleteComposer; @@ -170,6 +171,8 @@ extension EmailActionTypeExtension on EmailActionType { return AppLocalizations.of(context).mark_all_as_read; case EmailActionType.markAllAsUnread: return AppLocalizations.of(context).markAllAsUnread; + case EmailActionType.moveAll: + return AppLocalizations.of(context).moveAll; default: return ''; } diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 6bc2cb5176..734c87dc80 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -52,6 +52,7 @@ import 'package:model/extensions/email_id_extensions.dart'; import 'package:model/extensions/keyword_identifier_extension.dart'; import 'package:model/extensions/list_email_extension.dart'; import 'package:model/extensions/list_email_id_extension.dart'; +import 'package:model/extensions/mailbox_extension.dart'; import 'package:model/extensions/mailbox_id_extension.dart'; import 'package:model/extensions/session_extension.dart'; import 'package:model/oidc/token_oidc.dart'; @@ -778,4 +779,44 @@ class EmailAPI with HandleSetErrorMixin { throw NotFoundEmailRecoveryActionException(); } } + + Future> moveSelectionAllEmailsToFolder( + Session session, + AccountId accountId, + MailboxId currentMailboxId, + Mailbox destinationMailbox, + List listEmailId, + ) async { + final moveProperties = destinationMailbox.isSpam + ? listEmailId.generateMapUpdateObjectMoveToSpam(currentMailboxId, destinationMailbox.id!) + : listEmailId.generateMapUpdateObjectMoveToMailbox(currentMailboxId, destinationMailbox.id!); + + final setEmailMethod = SetEmailMethod(accountId) + ..addUpdates(moveProperties); + + final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); + final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + + final response = await (requestBuilder..usings(capabilities)) + .build() + .execute(); + + final setEmailResponse = response.parse( + setEmailInvocation.methodCallId, + SetEmailResponse.deserialize + ); + + final listIdUpdated = setEmailResponse?.updated?.keys.toList(); + final mapErrors = handleSetResponse([setEmailResponse]); + + if (listIdUpdated != null && mapErrors.isEmpty) { + final listEmailIdUpdated = listIdUpdated.map((id) => EmailId(id)).toList(); + return listEmailIdUpdated; + } else { + throw SetMethodException(mapErrors); + } + } } \ No newline at end of file diff --git a/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart b/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart index d77581b4cc..c5b259aec0 100644 --- a/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart +++ b/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart @@ -23,9 +23,12 @@ class UpdatingMarkAsMailboxReadState extends UIState { class MarkAsMailboxReadAllSuccess extends UIActionState { + final MailboxId mailboxId; final String mailboxDisplayName; - MarkAsMailboxReadAllSuccess(this.mailboxDisplayName, + MarkAsMailboxReadAllSuccess( + this.mailboxId, + this.mailboxDisplayName, { jmap.State? currentEmailState, jmap.State? currentMailboxState, @@ -34,6 +37,7 @@ class MarkAsMailboxReadAllSuccess extends UIActionState { @override List get props => [ + mailboxId, mailboxDisplayName, ...super.props ]; @@ -41,10 +45,12 @@ class MarkAsMailboxReadAllSuccess extends UIActionState { class MarkAsMailboxReadHasSomeEmailFailure extends UIActionState { + final MailboxId mailboxId; final String mailboxDisplayName; final int countEmailsRead; MarkAsMailboxReadHasSomeEmailFailure( + this.mailboxId, this.mailboxDisplayName, this.countEmailsRead, { @@ -55,6 +61,7 @@ class MarkAsMailboxReadHasSomeEmailFailure extends UIActionState { @override List get props => [ + mailboxId, mailboxDisplayName, countEmailsRead, ...super.props diff --git a/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart b/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart index 17b99fc4bc..b2a15a11be 100644 --- a/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart +++ b/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart @@ -44,11 +44,13 @@ class MarkAsMailboxReadInteractor { if (totalEmailUnread == listEmailId.length) { yield Right(MarkAsMailboxReadAllSuccess( + mailboxId, mailboxDisplayName, currentEmailState: currentEmailState, currentMailboxState: currentMailboxState)); } else if (listEmailId.isNotEmpty) { yield Right(MarkAsMailboxReadHasSomeEmailFailure( + mailboxId, mailboxDisplayName, listEmailId.length, currentEmailState: currentEmailState, diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 50d2117383..a54fdba4e3 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -78,6 +78,7 @@ import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_sta import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.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/move_all_selection_all_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; @@ -272,6 +273,10 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); } else if (success is MarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure) { _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is MoveAllSelectionAllEmailsAllSuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is MoveAllSelectionAllEmailsHasSomeEmailFailure) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); } }); }); diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index 519a2e0204..756349a763 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -115,6 +115,7 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_in import 'package:tmail_ui_user/features/thread/domain/usecases/mark_all_as_unread_selection_all_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/move_all_selection_all_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/search_email_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/search_more_email_interactor.dart'; @@ -179,6 +180,7 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); Get.put(AdvancedFilterController()); } @@ -350,6 +352,11 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find(), )); + Get.lazyPut(() => MoveAllSelectionAllEmailsInteractor( + Get.find(), + Get.find(), + Get.find(), + )); } @override 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 7ef21112ba..ab54a471ca 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -137,6 +137,7 @@ import 'package:tmail_ui_user/features/thread/domain/state/get_email_by_id_state import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.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_star_multiple_email_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/move_all_selection_all_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_spam_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_trash_folder_interactor.dart'; @@ -144,6 +145,7 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_in import 'package:tmail_ui_user/features/thread/domain/usecases/mark_all_as_unread_selection_all_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/move_all_selection_all_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; @@ -191,6 +193,7 @@ class MailboxDashBoardController extends ReloadableController { final RemoveComposerCacheOnWebInteractor _removeComposerCacheOnWebInteractor; final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; final MarkAllAsUnreadSelectionAllEmailsInteractor _markAllAsUnreadSelectionAllEmailsInteractor; + final MoveAllSelectionAllEmailsInteractor _moveAllSelectionAllEmailsInteractor; GetAllVacationInteractor? _getAllVacationInteractor; UpdateVacationInteractor? _updateVacationInteractor; @@ -224,6 +227,7 @@ class MailboxDashBoardController extends ReloadableController { final localFileDraggableAppState = Rxn(); final isSelectAllEmailsEnabled = RxBool(false); final markAllAsUnreadSelectionAllEmailsViewState = Rx>(Right(UIState.idle)); + final moveAllSelectionAllEmailsViewState = Rx>(Right(UIState.idle)); Session? sessionCurrent; Map mapDefaultMailboxIdByRole = {}; @@ -240,6 +244,7 @@ class MailboxDashBoardController extends ReloadableController { late StreamSubscription _markAsReadMailboxStreamSubscription; late StreamSubscription _refreshActionStreamSubscription; late StreamSubscription _markAllAsUnreadSelectionAllEmailsStreamSubscription; + late StreamSubscription _moveAllSelectionAllEmailsStreamSubscription; final StreamController> _markAsReadMailboxStreamController = StreamController>.broadcast(); @@ -247,6 +252,8 @@ class MailboxDashBoardController extends ReloadableController { StreamController.broadcast(); final StreamController> _markAllAsUnreadSelectionAllEmailsStreamController = StreamController>.broadcast(); + final StreamController> _moveAllSelectionAllEmailsStreamController = + StreamController>.broadcast(); final _notificationManager = LocalNotificationManager.instance; final _fcmService = FcmService.instance; @@ -277,6 +284,7 @@ class MailboxDashBoardController extends ReloadableController { this._removeComposerCacheOnWebInteractor, this._getAllIdentitiesInteractor, this._markAllAsUnreadSelectionAllEmailsInteractor, + this._moveAllSelectionAllEmailsInteractor, ); @override @@ -384,6 +392,9 @@ class MailboxDashBoardController extends ReloadableController { } else if (success is MarkAllAsUnreadSelectionAllEmailsAllSuccess || success is MarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure) { _handleMarkAllAsUnreadSelectionAllEmailsSuccess(success); + } else if (success is MoveAllSelectionAllEmailsAllSuccess + || success is MoveAllSelectionAllEmailsHasSomeEmailFailure) { + _handleMoveAllSelectionAllEmailsSuccess(success); } } @@ -410,6 +421,9 @@ class MailboxDashBoardController extends ReloadableController { } else if (failure is MarkAllAsUnreadSelectionAllEmailsFailure || failure is MarkAllAsUnreadSelectionAllEmailsAllFailure) { _handleMarkAllAsUnreadSelectionAllEmailsFailure(failure); + } else if (failure is MoveAllSelectionAllEmailsFailure + || failure is MoveAllSelectionAllEmailsAllFailure) { + _handleMoveAllSelectionAllEmailsFailure(failure); } } @@ -468,6 +482,10 @@ class MailboxDashBoardController extends ReloadableController { markAllAsUnreadSelectionAllEmailsViewState.value = state; }); + _moveAllSelectionAllEmailsStreamSubscription = _moveAllSelectionAllEmailsStreamController.stream.listen((state) { + moveAllSelectionAllEmailsViewState.value = state; + }); + _registerLocalNotificationStreamListener(); } @@ -2557,6 +2575,59 @@ class MailboxDashBoardController extends ReloadableController { failure: failure); } + Future moveAllSelectionAllEmails( + BuildContext context, + Session session, + AccountId accountId, + PresentationMailbox currentMailbox + ) async { + final arguments = DestinationPickerArguments( + accountId, + MailboxActions.moveEmail, + session, + mailboxIdSelected: currentMailbox.id); + + final destinationMailbox = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog( + routeName: AppRoutes.destinationPicker, + arguments: arguments) + : await push(AppRoutes.destinationPicker, arguments: arguments); + + if (destinationMailbox is PresentationMailbox) { + consumeState(_moveAllSelectionAllEmailsInteractor.execute( + session, + accountId, + currentMailbox.id, + destinationMailbox.toMailbox(), + destinationMailbox.mailboxPath ?? (context.mounted ? destinationMailbox.getDisplayName(context) : ''), + currentMailbox.countTotalEmails, + _moveAllSelectionAllEmailsStreamController + )); + } + } + + void _handleMoveAllSelectionAllEmailsSuccess(Success success) { + moveAllSelectionAllEmailsViewState.value = Right(UIState.idle); + + if (currentContext == null || currentOverlayContext == null) return; + + appToastManager.showSuccessMessage( + context: currentContext!, + overlayContext: currentOverlayContext!, + success: success); + } + + void _handleMoveAllSelectionAllEmailsFailure(Failure failure) { + moveAllSelectionAllEmailsViewState.value = Right(UIState.idle); + + if (currentContext == null || currentOverlayContext == null) return; + + appToastManager.showFailureMessage( + context: currentContext!, + overlayContext: currentOverlayContext!, + failure: failure); + } + @override void onClose() { _emailReceiveManager.closeEmailReceiveManagerStream(); @@ -2569,6 +2640,8 @@ class MailboxDashBoardController extends ReloadableController { _refreshActionEventController.close(); _markAllAsUnreadSelectionAllEmailsStreamSubscription.cancel(); _markAllAsUnreadSelectionAllEmailsStreamController.close(); + _moveAllSelectionAllEmailsStreamSubscription.cancel(); + _moveAllSelectionAllEmailsStreamController.close(); _notificationManager.closeStream(); _fcmService.closeStream(); applicationManager.releaseUserAgent(); diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index 80e514c841..c48df308c2 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -21,6 +21,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/sear import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/download/download_task_item_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_unread_selection_all_emails_loading_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/loading/mark_mailbox_as_read_loading_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/loading/move_all_selection_all_emails_loading_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/recover_deleted_message_loading_banner_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart'; @@ -153,6 +154,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { _buildListButtonQuickSearchFilter(context), Obx(() => MarkMailboxAsReadLoadingWidget(viewState: controller.viewStateMarkAsReadMailbox.value)), Obx(() => MarkAllAsUnreadSelectionAllEmailsLoadingWidget(viewState: controller.markAllAsUnreadSelectionAllEmailsViewState.value)), + Obx(() => MoveAllSelectionAllEmailsLoadingWidget(viewState: controller.moveAllSelectionAllEmailsViewState.value)), Expanded(child: Obx(() { switch(controller.dashboardRoute.value) { case DashboardRoutes.thread: diff --git a/lib/features/mailbox_dashboard/presentation/widgets/loading/move_all_selection_all_emails_loading_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/loading/move_all_selection_all_emails_loading_widget.dart new file mode 100644 index 0000000000..35c0fb1be8 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/widgets/loading/move_all_selection_all_emails_loading_widget.dart @@ -0,0 +1,36 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/move_all_selection_all_emails_state.dart'; + +class MoveAllSelectionAllEmailsLoadingWidget extends StatelessWidget with AppLoaderMixin { + + final Either viewState; + + const MoveAllSelectionAllEmailsLoadingWidget({super.key, required this.viewState}); + + @override + Widget build(BuildContext context) { + return viewState.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is MoveAllSelectionAllEmailsLoading) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: horizontalLoadingWidget + ); + } else if (success is MoveAllSelectionAllEmailsUpdating) { + final percent = success.countMoved / success.total; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: horizontalPercentLoadingWidget(percent) + ); + } + return const SizedBox.shrink(); + } + ); + } +} diff --git a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart index 504c367f39..ef58c504fa 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart @@ -93,11 +93,11 @@ class TopBarThreadSelection extends StatelessWidget{ TMailButtonWidget.fromIcon( icon: imagePaths.icMove, iconSize: 22, - tooltipMessage: AppLocalizations.of(context).move, + tooltipMessage: _getTooltipMessageForMove(context), backgroundColor: Colors.transparent, onTapActionCallback: () => onEmailActionTypeAction?.call( List.from(listEmail), - EmailActionType.moveToMailbox + _getActionTypeForMove() ) ), TMailButtonWidget.fromIcon( @@ -189,4 +189,20 @@ class TopBarThreadSelection extends StatelessWidget{ : AppLocalizations.of(context).mark_as_read; } } + + String _getTooltipMessageForMove(BuildContext context) { + if (isSelectAllEmailsEnabled) { + return AppLocalizations.of(context).moveAll; + } else { + return AppLocalizations.of(context).move; + } + } + + EmailActionType _getActionTypeForMove() { + if (isSelectAllEmailsEnabled) { + return EmailActionType.moveAll; + } else { + return EmailActionType.moveToMailbox; + } + } } \ No newline at end of file diff --git a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart index c62b75cba7..dfcb49b10c 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart @@ -62,6 +62,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dash import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/move_all_selection_all_emails_state.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'; @@ -169,6 +170,10 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa _refreshMailboxChanges(mailboxState: success.currentMailboxState); } else if (success is MarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure) { _refreshMailboxChanges(mailboxState: success.currentMailboxState); + } else if (success is MoveAllSelectionAllEmailsAllSuccess) { + _refreshMailboxChanges(mailboxState: success.currentMailboxState); + } else if (success is MoveAllSelectionAllEmailsHasSomeEmailFailure) { + _refreshMailboxChanges(mailboxState: success.currentMailboxState); } } diff --git a/lib/features/thread/data/datasource/thread_datasource.dart b/lib/features/thread/data/datasource/thread_datasource.dart index 217d99e4e2..58ba266a20 100644 --- a/lib/features/thread/data/datasource/thread_datasource.dart +++ b/lib/features/thread/data/datasource/thread_datasource.dart @@ -69,4 +69,13 @@ abstract class ThreadDataSource { int totalEmailRead, StreamController> onProgressController ); + + Future> moveAllSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId currentMailboxId, + Mailbox destinationMailbox, + int totalEmails, + StreamController> onProgressController + ); } \ No newline at end of file diff --git a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart index 796288f18b..ba44cf96b7 100644 --- a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart @@ -118,4 +118,16 @@ class LocalThreadDataSourceImpl extends ThreadDataSource { ) { throw UnimplementedError(); } + + @override + Future> moveAllSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId currentMailboxId, + Mailbox destinationMailbox, + int totalEmails, + StreamController> onProgressController + ) { + throw UnimplementedError(); + } } \ No newline at end of file diff --git a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart index d039127afb..4b1e05e8b9 100644 --- a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart @@ -130,4 +130,25 @@ class ThreadDataSourceImpl extends ThreadDataSource { ); }).catchError(_exceptionThrower.throwException); } + + @override + Future> moveAllSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId currentMailboxId, + Mailbox destinationMailbox, + int totalEmails, + StreamController> onProgressController + ) { + return Future.sync(() async { + return await _threadIsolateWorker.moveAllSelectionAllEmails( + session, + accountId, + currentMailboxId, + destinationMailbox, + totalEmails, + onProgressController + ); + }).catchError(_exceptionThrower.throwException); + } } \ No newline at end of file diff --git a/lib/features/thread/data/network/thread_isolate_worker.dart b/lib/features/thread/data/network/thread_isolate_worker.dart index beb1dce292..21f1a3791f 100644 --- a/lib/features/thread/data/network/thread_isolate_worker.dart +++ b/lib/features/thread/data/network/thread_isolate_worker.dart @@ -28,6 +28,7 @@ import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; import 'package:tmail_ui_user/features/thread/domain/exceptions/thread_exceptions.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/move_all_selection_all_emails_state.dart'; import 'package:tmail_ui_user/main/exceptions/isolate_exception.dart'; import 'package:worker_manager/worker_manager.dart'; @@ -223,7 +224,6 @@ class ThreadIsolateWorker { emailIdListCompleted.addAll(listEmailId); onProgressController.add( dartz.Right(MarkAllAsUnreadSelectionAllEmailsUpdating( - mailboxId: mailboxId, totalRead: totalEmailRead, countUnread: emailIdListCompleted.length )) @@ -236,4 +236,75 @@ class ThreadIsolateWorker { log('ThreadIsolateWorker::markAllAsUnreadForSelectionAllEmails(): TOTAL_UNREAD: ${emailIdListCompleted.length}'); return emailIdListCompleted; } + + Future> moveAllSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId currentMailboxId, + Mailbox destinationMailbox, + int totalEmails, + StreamController> onProgressController + ) async { + List emailIdListCompleted = List.empty(growable: true); + try { + bool mailboxHasEmails = true; + UTCDate? lastReceivedDate; + EmailId? lastEmailId; + + while (mailboxHasEmails) { + final emailResponse = await _threadAPI.getAllEmail( + session, + accountId, + limit: UnsignedInt(30), + filter: EmailFilterCondition( + inMailbox: currentMailboxId, + before: lastReceivedDate + ), + sort: {}..add( + EmailComparator(EmailComparatorProperty.receivedAt)..setIsAscending(false) + ), + properties: Properties({ + EmailProperty.id, + EmailProperty.receivedAt, + }) + ).then((response) { + var listEmails = response.emailList; + if (listEmails != null && listEmails.isNotEmpty && lastEmailId != null) { + listEmails = listEmails + .where((email) => email.id != lastEmailId) + .toList(); + } + return EmailsResponse(emailList: listEmails, state: response.state); + }); + final listEmail = emailResponse.emailList; + log('ThreadIsolateWorker::moveAllSelectionAllEmails: LIST_EMAIL = ${listEmail?.length}'); + if (listEmail == null || listEmail.isEmpty) { + mailboxHasEmails = false; + } else { + lastEmailId = listEmail.last.id; + lastReceivedDate = listEmail.last.receivedAt; + + final listEmailId = await _emailAPI.moveSelectionAllEmailsToFolder( + session, + accountId, + currentMailboxId, + destinationMailbox, + listEmail.listEmailIds, + ); + log('ThreadIsolateWorker::moveAllSelectionAllEmails(): MOVED: ${listEmailId.length}'); + emailIdListCompleted.addAll(listEmailId); + onProgressController.add( + dartz.Right(MoveAllSelectionAllEmailsUpdating( + total: totalEmails, + countMoved: emailIdListCompleted.length + )) + ); + } + } + } catch (e) { + log('ThreadIsolateWorker::moveAllSelectionAllEmails(): ERROR: $e'); + } + log('ThreadIsolateWorker::moveAllSelectionAllEmails(): TOTAL_MOVED: ${emailIdListCompleted.length}'); + return emailIdListCompleted; + } } diff --git a/lib/features/thread/data/repository/thread_repository_impl.dart b/lib/features/thread/data/repository/thread_repository_impl.dart index 60ab5e1aa0..6c0fda574b 100644 --- a/lib/features/thread/data/repository/thread_repository_impl.dart +++ b/lib/features/thread/data/repository/thread_repository_impl.dart @@ -425,4 +425,23 @@ class ThreadRepositoryImpl extends ThreadRepository { onProgressController ); } + + @override + Future> moveAllSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId currentMailboxId, + Mailbox destinationMailbox, + int totalEmails, + StreamController> onProgressController + ) { + return mapDataSource[DataSourceType.network]!.moveAllSelectionAllEmails( + session, + accountId, + currentMailboxId, + destinationMailbox, + totalEmails, + onProgressController + ); + } } \ No newline at end of file diff --git a/lib/features/thread/domain/repository/thread_repository.dart b/lib/features/thread/domain/repository/thread_repository.dart index 69eb85c354..85ce2f929f 100644 --- a/lib/features/thread/domain/repository/thread_repository.dart +++ b/lib/features/thread/domain/repository/thread_repository.dart @@ -82,4 +82,13 @@ abstract class ThreadRepository { int totalEmailRead, StreamController> onProgressController ); + + Future> moveAllSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId currentMailboxId, + Mailbox destinationMailbox, + int totalEmails, + StreamController> onProgressController + ); } \ No newline at end of file diff --git a/lib/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart b/lib/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart index 7a41d31a70..d910e092bc 100644 --- a/lib/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart +++ b/lib/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart @@ -1,52 +1,37 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; class MarkAllAsUnreadSelectionAllEmailsLoading extends LoadingState {} class MarkAllAsUnreadSelectionAllEmailsUpdating extends UIState { - final MailboxId mailboxId; final int totalRead; final int countUnread; MarkAllAsUnreadSelectionAllEmailsUpdating({ - required this.mailboxId, required this.totalRead, required this.countUnread }); @override - List get props => [mailboxId, totalRead, countUnread]; + List get props => [totalRead, countUnread]; } class MarkAllAsUnreadSelectionAllEmailsAllSuccess extends UIActionState { - final String mailboxDisplayName; - - MarkAllAsUnreadSelectionAllEmailsAllSuccess(this.mailboxDisplayName, - { - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - } - ) : super(currentMailboxState, currentEmailState); - - @override - List get props => [ - mailboxDisplayName, - ...super.props - ]; + MarkAllAsUnreadSelectionAllEmailsAllSuccess({ + jmap.State? currentEmailState, + jmap.State? currentMailboxState, + }) : super(currentMailboxState, currentEmailState); } class MarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure extends UIActionState { - final String mailboxDisplayName; final int countEmailsUnread; MarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure( - this.mailboxDisplayName, this.countEmailsUnread, { jmap.State? currentEmailState, @@ -56,30 +41,16 @@ class MarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure extends UIActionState @override List get props => [ - mailboxDisplayName, countEmailsUnread, ...super.props ]; } -class MarkAllAsUnreadSelectionAllEmailsAllFailure extends FeatureFailure { - final String mailboxDisplayName; - - MarkAllAsUnreadSelectionAllEmailsAllFailure({required this.mailboxDisplayName}); - - @override - List get props => [mailboxDisplayName]; -} +class MarkAllAsUnreadSelectionAllEmailsAllFailure extends FeatureFailure {} class MarkAllAsUnreadSelectionAllEmailsFailure extends FeatureFailure { - final String mailboxDisplayName; - MarkAllAsUnreadSelectionAllEmailsFailure({ - required this.mailboxDisplayName, dynamic exception }) : super(exception: exception); - - @override - List get props => [mailboxDisplayName, ...super.props]; } \ No newline at end of file diff --git a/lib/features/thread/domain/state/move_all_selection_all_emails_state.dart b/lib/features/thread/domain/state/move_all_selection_all_emails_state.dart new file mode 100644 index 0000000000..3911508642 --- /dev/null +++ b/lib/features/thread/domain/state/move_all_selection_all_emails_state.dart @@ -0,0 +1,81 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; + +class MoveAllSelectionAllEmailsLoading extends LoadingState {} + +class MoveAllSelectionAllEmailsUpdating extends UIState { + + final int total; + final int countMoved; + + MoveAllSelectionAllEmailsUpdating({ + required this.total, + required this.countMoved + }); + + @override + List get props => [total, countMoved]; +} + +class MoveAllSelectionAllEmailsAllSuccess extends UIActionState { + final String destinationPath; + + MoveAllSelectionAllEmailsAllSuccess( + this.destinationPath, + { + jmap.State? currentEmailState, + jmap.State? currentMailboxState, + } + ) : super(currentEmailState, currentMailboxState); + + @override + List get props => [ + destinationPath, + ...super.props + ]; +} + +class MoveAllSelectionAllEmailsHasSomeEmailFailure extends UIActionState { + final String destinationPath; + final int countEmailsMoved; + + MoveAllSelectionAllEmailsHasSomeEmailFailure( + this.destinationPath, + this.countEmailsMoved, + { + jmap.State? currentEmailState, + jmap.State? currentMailboxState, + } + ) : super(currentEmailState, currentMailboxState); + + @override + List get props => [ + countEmailsMoved, + destinationPath, + ...super.props + ]; +} + +class MoveAllSelectionAllEmailsAllFailure extends FeatureFailure { + final String destinationPath; + + MoveAllSelectionAllEmailsAllFailure(this.destinationPath); + + @override + List get props => [destinationPath]; +} + +class MoveAllSelectionAllEmailsFailure extends FeatureFailure { + + final String destinationPath; + + MoveAllSelectionAllEmailsFailure({ + required this.destinationPath, + dynamic exception + }) : super(exception: exception); + + @override + List get props => [destinationPath, ...super.props]; +} \ No newline at end of file diff --git a/lib/features/thread/domain/usecases/mark_all_as_unread_selection_all_emails_interactor.dart b/lib/features/thread/domain/usecases/mark_all_as_unread_selection_all_emails_interactor.dart index a166b4916f..94068cf929 100644 --- a/lib/features/thread/domain/usecases/mark_all_as_unread_selection_all_emails_interactor.dart +++ b/lib/features/thread/domain/usecases/mark_all_as_unread_selection_all_emails_interactor.dart @@ -51,25 +51,18 @@ class MarkAllAsUnreadSelectionAllEmailsInteractor { if (totalEmailRead == listEmailId.length) { yield Right(MarkAllAsUnreadSelectionAllEmailsAllSuccess( - mailboxDisplayName, currentEmailState: currentEmailState, currentMailboxState: currentMailboxState)); } else if (listEmailId.isNotEmpty) { yield Right(MarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure( - mailboxDisplayName, listEmailId.length, currentEmailState: currentEmailState, currentMailboxState: currentMailboxState)); } else { - yield Left(MarkAllAsUnreadSelectionAllEmailsAllFailure( - mailboxDisplayName: mailboxDisplayName - )); + yield Left(MarkAllAsUnreadSelectionAllEmailsAllFailure()); } } catch (e) { - yield Left(MarkAllAsUnreadSelectionAllEmailsFailure( - mailboxDisplayName: mailboxDisplayName, - exception: e - )); + yield Left(MarkAllAsUnreadSelectionAllEmailsFailure(exception: e)); } } } \ No newline at end of file diff --git a/lib/features/thread/domain/usecases/move_all_selection_all_emails_interactor.dart b/lib/features/thread/domain/usecases/move_all_selection_all_emails_interactor.dart new file mode 100644 index 0000000000..864ca594d9 --- /dev/null +++ b/lib/features/thread/domain/usecases/move_all_selection_all_emails_interactor.dart @@ -0,0 +1,72 @@ +import 'dart:async'; + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; +import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/move_all_selection_all_emails_state.dart'; + +class MoveAllSelectionAllEmailsInteractor { + final EmailRepository _emailRepository; + final MailboxRepository _mailboxRepository; + final ThreadRepository _threadRepository; + + MoveAllSelectionAllEmailsInteractor( + this._emailRepository, + this._mailboxRepository, + this._threadRepository, + ); + + Stream> execute( + Session session, + AccountId accountId, + MailboxId currentMailboxId, + Mailbox destinationMailbox, + String destinationPath, + int totalEmails, + StreamController> onProgressController + ) async* { + try { + yield Right(MoveAllSelectionAllEmailsLoading()); + onProgressController.add(Right(MoveAllSelectionAllEmailsLoading())); + + final listState = await Future.wait([ + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), + ], eagerError: true); + + final currentMailboxState = listState.first; + final currentEmailState = listState.last; + + final listEmailId = await _threadRepository.moveAllSelectionAllEmails( + session, + accountId, + currentMailboxId, + destinationMailbox, + totalEmails, + onProgressController); + + if (totalEmails == listEmailId.length) { + yield Right(MoveAllSelectionAllEmailsAllSuccess( + destinationPath, + currentEmailState: currentEmailState, + currentMailboxState: currentMailboxState)); + } else if (listEmailId.isNotEmpty) { + yield Right(MoveAllSelectionAllEmailsHasSomeEmailFailure( + destinationPath, + listEmailId.length, + currentEmailState: currentEmailState, + currentMailboxState: currentMailboxState)); + } else { + yield Left(MoveAllSelectionAllEmailsAllFailure(destinationPath)); + } + } catch (e) { + yield Left(MoveAllSelectionAllEmailsFailure(destinationPath: destinationPath, exception: e)); + } + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 57c768be24..19625ef232 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -62,6 +62,7 @@ import 'package:tmail_ui_user/features/thread/domain/state/load_more_emails_stat import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.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_star_multiple_email_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/move_all_selection_all_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/refresh_changes_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; @@ -281,7 +282,7 @@ class ThreadController extends BaseController with EmailActionController, PopupM if (_validateToShowConfirmBulkActionEmailsDialog()) { _showConfirmDialogWhenMakeToActionForSelectionAllEmails( context: action.context, - selectedMailbox: mailboxDashBoardController.selectedMailbox.value!, + selectedMailbox: currentMailbox!, actionType: action.emailAction ); } else { @@ -365,8 +366,14 @@ class ThreadController extends BaseController with EmailActionController, PopupM } else if (success is UpdateEmailDraftsSuccess) { _refreshEmailChanges(currentEmailState: success.currentEmailState); } else if (success is MarkAsMailboxReadAllSuccess) { + if (success.mailboxId == currentMailbox?.id) { + cancelSelectEmail(); + } _refreshEmailChanges(currentEmailState: success.currentEmailState); } else if (success is MarkAsMailboxReadHasSomeEmailFailure) { + if (success.mailboxId == currentMailbox?.id) { + cancelSelectEmail(); + } _refreshEmailChanges(currentEmailState: success.currentEmailState); } else if (success is MoveMultipleEmailToMailboxAllSuccess) { _refreshEmailChanges(currentEmailState: success.currentEmailState); @@ -389,8 +396,16 @@ class ThreadController extends BaseController with EmailActionController, PopupM } else if (success is UnsubscribeEmailSuccess) { _refreshEmailChanges(currentEmailState: success.currentEmailState); } else if (success is MarkAllAsUnreadSelectionAllEmailsAllSuccess) { + cancelSelectEmail(); _refreshEmailChanges(currentEmailState: success.currentEmailState); } else if (success is MarkAllAsUnreadSelectionAllEmailsHasSomeEmailFailure) { + cancelSelectEmail(); + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is MoveAllSelectionAllEmailsAllSuccess) { + cancelSelectEmail(); + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is MoveAllSelectionAllEmailsHasSomeEmailFailure) { + cancelSelectEmail(); _refreshEmailChanges(currentEmailState: success.currentEmailState); } }); @@ -1066,8 +1081,6 @@ class ThreadController extends BaseController with EmailActionController, PopupM } } - bool get isMailboxTrash => mailboxDashBoardController.selectedMailbox.value?.isTrash == true; - void openMailboxLeftMenu() { mailboxDashBoardController.openMailboxMenuDrawer(); } @@ -1227,16 +1240,12 @@ class ThreadController extends BaseController with EmailActionController, PopupM } bool get isNewFolderCreated { - final currentMailbox = mailboxDashBoardController.selectedMailbox.value; - return currentMailbox != null && - currentMailbox.isPersonal && - !currentMailbox.isDefault; + return currentMailbox != null && currentMailbox!.isPersonal && !currentMailbox!.isDefault; } void goToCreateEmailRuleView() async { final accountId = mailboxDashBoardController.accountId.value; final session = mailboxDashBoardController.sessionCurrent; - final currentMailbox = mailboxDashBoardController.selectedMailbox.value; if (accountId != null && session != null) { final arguments = RulesFilterCreatorArguments( accountId, @@ -1351,16 +1360,16 @@ class ThreadController extends BaseController with EmailActionController, PopupM bool validateToShowSelectionEmailsBanner() { return isSelectionEnabled() - && mailboxDashBoardController.selectedMailbox.value != null - && mailboxDashBoardController.selectedMailbox.value!.totalEmails != null - && mailboxDashBoardController.listEmailSelected.length < mailboxDashBoardController.selectedMailbox.value!.totalEmails!.value.value.toInt(); + && currentMailbox != null + && currentMailbox!.totalEmails != null + && mailboxDashBoardController.listEmailSelected.length < currentMailbox!.countTotalEmails; } void showPopupMenuSelectionEmailAction(BuildContext context, RelativeRect position) { final listSelectionEmailActions = [ EmailActionType.markAllAsRead, EmailActionType.markAllAsUnread, - EmailActionType.moveToMailbox, + EmailActionType.moveAll, EmailActionType.moveToTrash, ]; @@ -1384,7 +1393,7 @@ class ThreadController extends BaseController with EmailActionController, PopupM if (!isSearchActive) { _showConfirmDialogWhenMakeToActionForSelectionAllEmails( context: context, - selectedMailbox: mailboxDashBoardController.selectedMailbox.value!, + selectedMailbox: currentMailbox!, actionType: action ); } @@ -1437,7 +1446,6 @@ class ThreadController extends BaseController with EmailActionController, PopupM switch(actionType) { case EmailActionType.markAllAsRead: - cancelSelectEmail(); mailboxDashBoardController.markAsReadMailbox( _session!, _accountId!, @@ -1447,7 +1455,6 @@ class ThreadController extends BaseController with EmailActionController, PopupM ); break; case EmailActionType.markAllAsUnread: - cancelSelectEmail(); mailboxDashBoardController.markAllAsUnreadSelectionAllEmails( _session!, _accountId!, @@ -1456,6 +1463,14 @@ class ThreadController extends BaseController with EmailActionController, PopupM selectedMailbox.countReadEmails ); break; + case EmailActionType.moveAll: + mailboxDashBoardController.moveAllSelectionAllEmails( + context, + _session!, + _accountId!, + selectedMailbox, + ); + break; default: break; } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 5d6fd95b6b..0933fe1ac3 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-04-29T11:56:08.573350", + "@@last_modified": "2024-04-29T14:28:31.400882", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3909,5 +3909,55 @@ "placeholders": { "reason": {} } + }, + "toastMessageMoveAllSelectionAllEmailsSuccess": "You’ve moved all mails to {destinationPath}", + "@toastMessageMoveAllSelectionAllEmailsSuccess": { + "type": "text", + "placeholders_order": [ + "destinationPath" + ], + "placeholders": { + "destinationPath": {} + } + }, + "toastMessageMoveAllSelectionAllEmailsHasSomeEmailFailure": "You’ve moved {count} mails to {destinationPath}", + "@toastMessageMoveAllSelectionAllEmailsHasSomeEmailFailure": { + "type": "text", + "placeholders_order": [ + "count", + "destinationPath" + ], + "placeholders": { + "count": {}, + "destinationPath": {} + } + }, + "toastMessageMoveAllSelectionAllEmailsAllFailure": "All mails could not be moved to {destinationPath}", + "@toastMessageMoveAllSelectionAllEmailsAllFailure": { + "type": "text", + "placeholders_order": [ + "destinationPath" + ], + "placeholders": { + "destinationPath": {} + } + }, + "toastMessageMoveAllSelectionAllEmailsFailureWithReason": "All mails could not be moved to {destinationPath}. Due \"{reason}\"", + "@toastMessageMoveAllSelectionAllEmailsFailureWithReason": { + "type": "text", + "placeholders_order": [ + "destinationPath", + "reason" + ], + "placeholders": { + "destinationPath": {}, + "reason": {} + } + }, + "moveAll": "Move all", + "@moveAll": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 933399ed37..1d546a7123 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4055,4 +4055,39 @@ class AppLocalizations { args: [reason] ); } + + String toastMessageMoveAllSelectionAllEmailsSuccess(String destinationPath) { + return Intl.message( + 'You’ve moved all mails to $destinationPath', + name: 'toastMessageMoveAllSelectionAllEmailsSuccess', + args: [destinationPath]); + } + + String toastMessageMoveAllSelectionAllEmailsHasSomeEmailFailure(int count, String destinationPath) { + return Intl.message( + 'You’ve moved $count mails to $destinationPath', + name: 'toastMessageMoveAllSelectionAllEmailsHasSomeEmailFailure', + args: [count, destinationPath]); + } + + String toastMessageMoveAllSelectionAllEmailsAllFailure(String destinationPath) { + return Intl.message( + 'All mails could not be moved to $destinationPath', + name: 'toastMessageMoveAllSelectionAllEmailsAllFailure', + args: [destinationPath]); + } + + String toastMessageMoveAllSelectionAllEmailsFailureWithReason(String destinationPath, String reason) { + return Intl.message( + 'All mails could not be moved to $destinationPath. Due "$reason"', + name: 'toastMessageMoveAllSelectionAllEmailsFailureWithReason', + args: [destinationPath, reason] + ); + } + + String get moveAll { + return Intl.message( + 'Move all', + name: 'moveAll'); + } } \ No newline at end of file diff --git a/model/lib/email/email_action_type.dart b/model/lib/email/email_action_type.dart index 9e3b79d466..4d736f12a5 100644 --- a/model/lib/email/email_action_type.dart +++ b/model/lib/email/email_action_type.dart @@ -30,4 +30,5 @@ enum EmailActionType { printAll, markAllAsRead, markAllAsUnread, + moveAll, } \ No newline at end of file diff --git a/model/lib/extensions/mailbox_extension.dart b/model/lib/extensions/mailbox_extension.dart index a39976b584..4ca8f89754 100644 --- a/model/lib/extensions/mailbox_extension.dart +++ b/model/lib/extensions/mailbox_extension.dart @@ -4,7 +4,7 @@ import 'package:model/model.dart'; extension MailboxExtension on Mailbox { - bool hasRole() => role != null && role!.value.isNotEmpty; + bool get isSpam => role == PresentationMailbox.roleSpam; PresentationMailbox toPresentationMailbox() { return PresentationMailbox( diff --git a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart index b02b243dfc..60d65de528 100644 --- a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart +++ b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart @@ -83,6 +83,7 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/load_more_emails_i import 'package:tmail_ui_user/features/thread/domain/usecases/mark_all_as_unread_selection_all_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/move_all_selection_all_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/search_email_interactor.dart'; @@ -155,6 +156,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() { // mock mailbox dashboard controller direct dependencies @@ -238,6 +240,7 @@ void main() { final removeComposerCacheOnWebInteractor = MockRemoveComposerCacheOnWebInteractor(); final getAllIdentitiesInteractor = MockGetAllIdentitiesInteractor(); final markAllAsUnreadSelectionAllEmailsInteractor = MockMarkAllAsUnreadSelectionAllEmailsInteractor(); + final moveAllSelectionAllEmailsInteractor = MockMoveAllSelectionAllEmailsInteractor(); late MailboxController mailboxController; // mock thread controller direct dependencies @@ -292,6 +295,7 @@ void main() { Get.put(getAllIdentitiesInteractor); Get.put(removeComposerCacheOnWebInteractor); Get.put(markAllAsUnreadSelectionAllEmailsInteractor); + Get.put(moveAllSelectionAllEmailsInteractor); Get.testMode = true; PackageInfo.setMockInitialValues( @@ -337,6 +341,7 @@ void main() { removeComposerCacheOnWebInteractor, getAllIdentitiesInteractor, markAllAsUnreadSelectionAllEmailsInteractor, + moveAllSelectionAllEmailsInteractor, ); Get.put(mailboxDashboardController); mailboxDashboardController.onReady(); From 3b5adb85486d43cfe86141a39e2ddd1977263f70 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 29 Apr 2024 16:06:26 +0700 Subject: [PATCH 09/15] TF-2646 Handle move all to trash for selection emails --- .../email_action_type_extension.dart | 16 ++++ .../email/data/network/email_api.dart | 12 +-- .../mailbox_dashboard_controller.dart | 28 ++++++- .../mailbox_dashboard_view_web.dart | 1 + .../widgets/top_bar_thread_selection.dart | 80 ++++++++++++++----- .../data/datasource/thread_datasource.dart | 7 +- .../local_thread_datasource_impl.dart | 7 +- .../thread_datasource_impl.dart | 12 ++- .../data/network/thread_isolate_worker.dart | 10 ++- .../repository/thread_repository_impl.dart | 12 ++- .../domain/repository/thread_repository.dart | 7 +- ...e_all_selection_all_emails_interactor.dart | 12 ++- .../presentation/thread_controller.dart | 17 +++- lib/l10n/intl_messages.arb | 14 +++- lib/main/localizations/app_localizations.dart | 13 +++ model/lib/email/email_action_type.dart | 2 + 16 files changed, 196 insertions(+), 54 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 041d728337..440efce998 100644 --- a/lib/features/composer/presentation/extensions/email_action_type_extension.dart +++ b/lib/features/composer/presentation/extensions/email_action_type_extension.dart @@ -1,4 +1,5 @@ +import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/extensions/html_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:flutter/cupertino.dart'; @@ -141,6 +142,8 @@ extension EmailActionTypeExtension on EmailActionType { case EmailActionType.moveAll: return imagePaths.icMove; case EmailActionType.moveToTrash: + case EmailActionType.moveAllToTrash: + case EmailActionType.deleteAllPermanently: return imagePaths.icDeleteComposer; default: return ''; @@ -173,8 +176,21 @@ extension EmailActionTypeExtension on EmailActionType { return AppLocalizations.of(context).markAllAsUnread; case EmailActionType.moveAll: return AppLocalizations.of(context).moveAll; + case EmailActionType.moveAllToTrash: + return AppLocalizations.of(context).moveAllToTrash; + case EmailActionType.deleteAllPermanently: + return AppLocalizations.of(context).deleteAllPermanently; default: return ''; } } + + Color getIconColor() { + switch(this) { + case EmailActionType.deleteAllPermanently: + return AppColor.colorDeletePermanentlyButton; + default: + return AppColor.primaryColor; + } + } } \ No newline at end of file diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 734c87dc80..bef713d2ab 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -52,7 +52,6 @@ import 'package:model/extensions/email_id_extensions.dart'; import 'package:model/extensions/keyword_identifier_extension.dart'; import 'package:model/extensions/list_email_extension.dart'; import 'package:model/extensions/list_email_id_extension.dart'; -import 'package:model/extensions/mailbox_extension.dart'; import 'package:model/extensions/mailbox_id_extension.dart'; import 'package:model/extensions/session_extension.dart'; import 'package:model/oidc/token_oidc.dart'; @@ -784,12 +783,15 @@ class EmailAPI with HandleSetErrorMixin { Session session, AccountId accountId, MailboxId currentMailboxId, - Mailbox destinationMailbox, + MailboxId destinationMailboxId, List listEmailId, + { + bool isDestinationSpamMailbox = false + } ) async { - final moveProperties = destinationMailbox.isSpam - ? listEmailId.generateMapUpdateObjectMoveToSpam(currentMailboxId, destinationMailbox.id!) - : listEmailId.generateMapUpdateObjectMoveToMailbox(currentMailboxId, destinationMailbox.id!); + final moveProperties = isDestinationSpamMailbox + ? listEmailId.generateMapUpdateObjectMoveToSpam(currentMailboxId, destinationMailboxId) + : listEmailId.generateMapUpdateObjectMoveToMailbox(currentMailboxId, destinationMailboxId); final setEmailMethod = SetEmailMethod(accountId) ..addUpdates(moveProperties); 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 ab54a471ca..0c78bcf1dc 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -2598,10 +2598,11 @@ class MailboxDashBoardController extends ReloadableController { session, accountId, currentMailbox.id, - destinationMailbox.toMailbox(), + destinationMailbox.id, destinationMailbox.mailboxPath ?? (context.mounted ? destinationMailbox.getDisplayName(context) : ''), currentMailbox.countTotalEmails, - _moveAllSelectionAllEmailsStreamController + _moveAllSelectionAllEmailsStreamController, + isDestinationSpamMailbox: destinationMailbox.isSpam )); } } @@ -2628,6 +2629,29 @@ class MailboxDashBoardController extends ReloadableController { failure: failure); } + Future moveAllToTrashSelectionAllEmails( + BuildContext context, + Session session, + AccountId accountId, + PresentationMailbox currentMailbox + ) async { + final trashMailboxId = getMailboxIdByRole(PresentationMailbox.roleTrash); + + if (trashMailboxId == null) return; + + final trashMailboxPath = mapMailboxById[trashMailboxId]?.getDisplayName(context) ?? ''; + + consumeState(_moveAllSelectionAllEmailsInteractor.execute( + session, + accountId, + currentMailbox.id, + trashMailboxId, + trashMailboxPath, + currentMailbox.countTotalEmails, + _moveAllSelectionAllEmailsStreamController + )); + } + @override void onClose() { _emailReceiveManager.closeEmailReceiveManagerStream(); diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index c48df308c2..821cd55ccb 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -263,6 +263,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { listEmail: listEmailSelected, mapMailbox: controller.mapMailboxById, isSelectAllEmailsEnabled: controller.isSelectAllEmailsEnabled.value, + selectedMailbox: controller.selectedMailbox.value, onCancelSelection: () => controller.dispatchAction(CancelSelectionAllEmailAction()), onEmailActionTypeAction: (listEmails, actionType) => diff --git a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart index ef58c504fa..122c3104b4 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart @@ -8,6 +8,7 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/email_action_type.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:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -23,6 +24,7 @@ class TopBarThreadSelection extends StatelessWidget{ final OnEmailActionTypeAction? onEmailActionTypeAction; final VoidCallback? onCancelSelection; final bool isSelectAllEmailsEnabled; + final PresentationMailbox? selectedMailbox; final OnMoreSelectedEmailAction? onMoreSelectedEmailAction; TopBarThreadSelection({ @@ -30,6 +32,7 @@ class TopBarThreadSelection extends StatelessWidget{ required this.listEmail, required this.mapMailbox, required this.isSelectAllEmailsEnabled, + this.selectedMailbox, this.onEmailActionTypeAction, this.onCancelSelection, this.onMoreSelectedEmailAction, @@ -37,11 +40,6 @@ class TopBarThreadSelection extends StatelessWidget{ @override Widget build(BuildContext context) { - final canDeletePermanently = listEmail.isAllCanDeletePermanently(mapMailbox); - final canSpamAndMove = listEmail.isAllCanSpamAndMove(mapMailbox); - final isAllSpam = listEmail.isAllSpam(mapMailbox); - final isAllBelongToTheSameMailbox = listEmail.isAllBelongToTheSameMailbox(mapMailbox); - return Row(children: [ TMailButtonWidget.fromIcon( icon: imagePaths.icClose, @@ -127,24 +125,13 @@ class TopBarThreadSelection extends StatelessWidget{ icon: imagePaths.icDeleteComposer, backgroundColor: Colors.transparent, iconSize: 20, - iconColor: canDeletePermanently - ? AppColor.colorDeletePermanentlyButton - : AppColor.primaryColor, - tooltipMessage: canDeletePermanently - ? AppLocalizations.of(context).delete_permanently - : AppLocalizations.of(context).move_to_trash, + iconColor: _getIconColorForMoveToTrash(), + tooltipMessage: _getTooltipMessageForMoveToTrash(context), onTapActionCallback: () { - if (canDeletePermanently) { - onEmailActionTypeAction?.call( - List.from(listEmail), - EmailActionType.deletePermanently - ); - } else { - onEmailActionTypeAction?.call( - List.from(listEmail), - EmailActionType.moveToTrash - ); - } + onEmailActionTypeAction?.call( + List.from(listEmail), + _getActionTypeForMoveToTrash() + ); } ), const Spacer(), @@ -160,6 +147,19 @@ class TopBarThreadSelection extends StatelessWidget{ ]); } + bool get canDeletePermanently => listEmail.isAllCanDeletePermanently(mapMailbox); + + bool get canDeleteAllPermanently => isSelectAllEmailsEnabled + && (selectedMailbox?.isTrash == true + || selectedMailbox?.isSpam == true + || selectedMailbox?.isDrafts == true); + + bool get canSpamAndMove => listEmail.isAllCanSpamAndMove(mapMailbox); + + bool get isAllSpam => listEmail.isAllSpam(mapMailbox); + + bool get isAllBelongToTheSameMailbox => listEmail.isAllBelongToTheSameMailbox(mapMailbox); + EmailActionType _getActionTypeForMarkAsRead() { if (isSelectAllEmailsEnabled) { return EmailActionType.markAllAsRead; @@ -205,4 +205,40 @@ class TopBarThreadSelection extends StatelessWidget{ return EmailActionType.moveToMailbox; } } + + Color _getIconColorForMoveToTrash() { + if (canDeleteAllPermanently) { + return AppColor.colorDeletePermanentlyButton; + } else if (isSelectAllEmailsEnabled) { + return AppColor.primaryColor; + } else if (canDeletePermanently) { + return AppColor.colorDeletePermanentlyButton; + } else { + return AppColor.primaryColor; + } + } + + String _getTooltipMessageForMoveToTrash(BuildContext context) { + if (canDeleteAllPermanently) { + return AppLocalizations.of(context).deleteAllPermanently; + } else if (isSelectAllEmailsEnabled) { + return AppLocalizations.of(context).moveAllToTrash; + } else if (canDeletePermanently) { + return AppLocalizations.of(context).delete_permanently; + } else { + return AppLocalizations.of(context).move_to_trash; + } + } + + EmailActionType _getActionTypeForMoveToTrash() { + if (canDeleteAllPermanently) { + return EmailActionType.deleteAllPermanently; + } else if (isSelectAllEmailsEnabled) { + return EmailActionType.moveAllToTrash; + } else if (canDeletePermanently) { + return EmailActionType.deletePermanently; + } else { + return EmailActionType.moveToTrash; + } + } } \ No newline at end of file diff --git a/lib/features/thread/data/datasource/thread_datasource.dart b/lib/features/thread/data/datasource/thread_datasource.dart index 58ba266a20..b0e7e46095 100644 --- a/lib/features/thread/data/datasource/thread_datasource.dart +++ b/lib/features/thread/data/datasource/thread_datasource.dart @@ -74,8 +74,11 @@ abstract class ThreadDataSource { Session session, AccountId accountId, MailboxId currentMailboxId, - Mailbox destinationMailbox, + MailboxId destinationMailboxId, int totalEmails, - StreamController> onProgressController + StreamController> onProgressController, + { + bool isDestinationSpamMailbox = false + } ); } \ No newline at end of file diff --git a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart index ba44cf96b7..1527b40016 100644 --- a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart @@ -124,9 +124,12 @@ class LocalThreadDataSourceImpl extends ThreadDataSource { Session session, AccountId accountId, MailboxId currentMailboxId, - Mailbox destinationMailbox, + MailboxId destinationMailboxId, int totalEmails, - StreamController> onProgressController + StreamController> onProgressController, + { + bool isDestinationSpamMailbox = false + } ) { throw UnimplementedError(); } diff --git a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart index 4b1e05e8b9..8188809e0c 100644 --- a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart @@ -136,18 +136,22 @@ class ThreadDataSourceImpl extends ThreadDataSource { Session session, AccountId accountId, MailboxId currentMailboxId, - Mailbox destinationMailbox, + MailboxId destinationMailboxId, int totalEmails, - StreamController> onProgressController + StreamController> onProgressController, + { + bool isDestinationSpamMailbox = false + } ) { return Future.sync(() async { return await _threadIsolateWorker.moveAllSelectionAllEmails( session, accountId, currentMailboxId, - destinationMailbox, + destinationMailboxId, totalEmails, - onProgressController + onProgressController, + isDestinationSpamMailbox: isDestinationSpamMailbox ); }).catchError(_exceptionThrower.throwException); } diff --git a/lib/features/thread/data/network/thread_isolate_worker.dart b/lib/features/thread/data/network/thread_isolate_worker.dart index 21f1a3791f..988c883eac 100644 --- a/lib/features/thread/data/network/thread_isolate_worker.dart +++ b/lib/features/thread/data/network/thread_isolate_worker.dart @@ -241,9 +241,12 @@ class ThreadIsolateWorker { Session session, AccountId accountId, MailboxId currentMailboxId, - Mailbox destinationMailbox, + MailboxId destinationMailboxId, int totalEmails, - StreamController> onProgressController + StreamController> onProgressController, + { + bool isDestinationSpamMailbox = false + } ) async { List emailIdListCompleted = List.empty(growable: true); try { @@ -288,8 +291,9 @@ class ThreadIsolateWorker { session, accountId, currentMailboxId, - destinationMailbox, + destinationMailboxId, listEmail.listEmailIds, + isDestinationSpamMailbox: isDestinationSpamMailbox ); log('ThreadIsolateWorker::moveAllSelectionAllEmails(): MOVED: ${listEmailId.length}'); emailIdListCompleted.addAll(listEmailId); diff --git a/lib/features/thread/data/repository/thread_repository_impl.dart b/lib/features/thread/data/repository/thread_repository_impl.dart index 6c0fda574b..c38955a813 100644 --- a/lib/features/thread/data/repository/thread_repository_impl.dart +++ b/lib/features/thread/data/repository/thread_repository_impl.dart @@ -431,17 +431,21 @@ class ThreadRepositoryImpl extends ThreadRepository { Session session, AccountId accountId, MailboxId currentMailboxId, - Mailbox destinationMailbox, + MailboxId destinationMailboxId, int totalEmails, - StreamController> onProgressController + StreamController> onProgressController, + { + bool isDestinationSpamMailbox = false + } ) { return mapDataSource[DataSourceType.network]!.moveAllSelectionAllEmails( session, accountId, currentMailboxId, - destinationMailbox, + destinationMailboxId, totalEmails, - onProgressController + onProgressController, + isDestinationSpamMailbox: isDestinationSpamMailbox ); } } \ No newline at end of file diff --git a/lib/features/thread/domain/repository/thread_repository.dart b/lib/features/thread/domain/repository/thread_repository.dart index 85ce2f929f..efcaee302e 100644 --- a/lib/features/thread/domain/repository/thread_repository.dart +++ b/lib/features/thread/domain/repository/thread_repository.dart @@ -87,8 +87,11 @@ abstract class ThreadRepository { Session session, AccountId accountId, MailboxId currentMailboxId, - Mailbox destinationMailbox, + MailboxId destinationMailboxId, int totalEmails, - StreamController> onProgressController + StreamController> onProgressController, + { + bool isDestinationSpamMailbox = false + } ); } \ No newline at end of file diff --git a/lib/features/thread/domain/usecases/move_all_selection_all_emails_interactor.dart b/lib/features/thread/domain/usecases/move_all_selection_all_emails_interactor.dart index 864ca594d9..e7c81711fb 100644 --- a/lib/features/thread/domain/usecases/move_all_selection_all_emails_interactor.dart +++ b/lib/features/thread/domain/usecases/move_all_selection_all_emails_interactor.dart @@ -26,10 +26,13 @@ class MoveAllSelectionAllEmailsInteractor { Session session, AccountId accountId, MailboxId currentMailboxId, - Mailbox destinationMailbox, + MailboxId destinationMailboxId, String destinationPath, int totalEmails, - StreamController> onProgressController + StreamController> onProgressController, + { + bool isDestinationSpamMailbox = false + } ) async* { try { yield Right(MoveAllSelectionAllEmailsLoading()); @@ -47,9 +50,10 @@ class MoveAllSelectionAllEmailsInteractor { session, accountId, currentMailboxId, - destinationMailbox, + destinationMailboxId, totalEmails, - onProgressController); + onProgressController, + isDestinationSpamMailbox: isDestinationSpamMailbox); if (totalEmails == listEmailId.length) { yield Right(MoveAllSelectionAllEmailsAllSuccess( diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 19625ef232..999220831e 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -1361,7 +1361,7 @@ class ThreadController extends BaseController with EmailActionController, PopupM bool validateToShowSelectionEmailsBanner() { return isSelectionEnabled() && currentMailbox != null - && currentMailbox!.totalEmails != null + && currentMailbox!.countTotalEmails > ThreadConstants.maxCountEmails && mailboxDashBoardController.listEmailSelected.length < currentMailbox!.countTotalEmails; } @@ -1370,7 +1370,10 @@ class ThreadController extends BaseController with EmailActionController, PopupM EmailActionType.markAllAsRead, EmailActionType.markAllAsUnread, EmailActionType.moveAll, - EmailActionType.moveToTrash, + if (currentMailbox?.isTrash == true || currentMailbox?.isSpam == true || currentMailbox?.isDrafts == true) + EmailActionType.deleteAllPermanently + else + EmailActionType.moveAllToTrash ]; openPopupMenuAction( @@ -1381,7 +1384,7 @@ class ThreadController extends BaseController with EmailActionController, PopupM child: popupItem( action.getIcon(imagePaths), action.getTitle(context), - colorIcon: AppColor.colorTextButton, + colorIcon: action.getIconColor(), styleName: const TextStyle( fontWeight: FontWeight.w500, fontSize: 14, @@ -1471,6 +1474,14 @@ class ThreadController extends BaseController with EmailActionController, PopupM selectedMailbox, ); break; + case EmailActionType.moveAllToTrash: + mailboxDashBoardController.moveAllToTrashSelectionAllEmails( + context, + _session!, + _accountId!, + selectedMailbox, + ); + break; default: break; } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 0933fe1ac3..896da3df4b 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-04-29T14:28:31.400882", + "@@last_modified": "2024-04-29T15:22:50.815697", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3959,5 +3959,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "moveAllToTrash": "Move all to trash", + "@moveAllToTrash": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deleteAllPermanently": "Delete all permanently", + "@deleteAllPermanently": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 1d546a7123..284d057af9 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4090,4 +4090,17 @@ class AppLocalizations { 'Move all', name: 'moveAll'); } + + String get moveAllToTrash { + return Intl.message( + 'Move all to trash', + name: 'moveAllToTrash', + ); + } + + String get deleteAllPermanently { + return Intl.message( + 'Delete all permanently', + name: 'deleteAllPermanently'); + } } \ No newline at end of file diff --git a/model/lib/email/email_action_type.dart b/model/lib/email/email_action_type.dart index 4d736f12a5..1b8d3a888e 100644 --- a/model/lib/email/email_action_type.dart +++ b/model/lib/email/email_action_type.dart @@ -31,4 +31,6 @@ enum EmailActionType { markAllAsRead, markAllAsUnread, moveAll, + moveAllToTrash, + deleteAllPermanently, } \ No newline at end of file From b6d7f9ce559cac9d6fd6e1dd302ad99d5dbd019e Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 29 Apr 2024 18:35:59 +0700 Subject: [PATCH 10/15] TF-2646 Handle delete all permanently for selection emails --- contact/pubspec.lock | 4 +- contact/pubspec.yaml | 2 +- email_recovery/pubspec.lock | 4 +- email_recovery/pubspec.yaml | 2 +- fcm/pubspec.lock | 4 +- fcm/pubspec.yaml | 2 +- forward/pubspec.lock | 4 +- forward/pubspec.yaml | 2 +- .../base/toast/app_toast_manager.dart | 11 ++++ .../presentation/mailbox_controller.dart | 3 + .../bindings/mailbox_dashboard_bindings.dart | 7 +++ .../mailbox_dashboard_controller.dart | 55 +++++++++++++++++ .../mailbox_dashboard_view_web.dart | 2 + ...all_permanently_emails_loading_widget.dart | 38 ++++++++++++ ...d_selection_all_emails_loading_widget.dart | 4 +- .../mark_mailbox_as_read_loading_widget.dart | 4 +- ...l_selection_all_emails_loading_widget.dart | 4 +- .../search_mailbox_controller.dart | 3 + .../data/datasource/thread_datasource.dart | 8 +++ .../local_thread_datasource_impl.dart | 11 ++++ .../thread_datasource_impl.dart | 19 ++++++ .../thread/data/network/thread_api.dart | 59 ++++++++++++++++++ .../data/network/thread_isolate_worker.dart | 50 +++++++++++++-- .../repository/thread_repository_impl.dart | 24 ++++++++ .../domain/repository/thread_repository.dart | 8 +++ .../delete_all_permanently_emails_state.dart | 40 ++++++++++++ ...ete_all_permanently_emails_interactor.dart | 61 +++++++++++++++++++ .../presentation/thread_controller.dart | 12 ++++ lib/l10n/intl_messages.arb | 12 +++- lib/main/localizations/app_localizations.dart | 8 +++ model/pubspec.lock | 4 +- model/pubspec.yaml | 2 +- pubspec.lock | 4 +- pubspec.yaml | 2 +- rule_filter/pubspec.lock | 4 +- rule_filter/pubspec.yaml | 2 +- server_settings/pubspec.yaml | 2 +- .../mailbox_dashboard_controller_test.dart | 5 ++ 38 files changed, 462 insertions(+), 30 deletions(-) create mode 100644 lib/features/mailbox_dashboard/presentation/widgets/loading/delete_all_permanently_emails_loading_widget.dart create mode 100644 lib/features/thread/domain/state/delete_all_permanently_emails_state.dart create mode 100644 lib/features/thread/domain/usecases/delete_all_permanently_emails_interactor.dart diff --git a/contact/pubspec.lock b/contact/pubspec.lock index 1d54914a08..32d35aea3e 100644 --- a/contact/pubspec.lock +++ b/contact/pubspec.lock @@ -539,8 +539,8 @@ packages: dependency: "direct main" description: path: "." - ref: cnb_support - resolved-ref: "10f5838aa1c6c4bffc5690f46d05d0cc4e489e1c" + ref: add-reference-destroy-property + resolved-ref: "9bb94370504cc6eb5a1b0b7180c14c31477be2be" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/contact/pubspec.yaml b/contact/pubspec.yaml index 62d0f3c70f..8fdc1e3eaf 100644 --- a/contact/pubspec.yaml +++ b/contact/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: cnb_support + ref: add-reference-destroy-property ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/email_recovery/pubspec.lock b/email_recovery/pubspec.lock index 8ad7771be5..33e961c1e5 100644 --- a/email_recovery/pubspec.lock +++ b/email_recovery/pubspec.lock @@ -295,8 +295,8 @@ packages: dependency: "direct main" description: path: "." - ref: cnb_support - resolved-ref: "10f5838aa1c6c4bffc5690f46d05d0cc4e489e1c" + ref: add-reference-destroy-property + resolved-ref: "9bb94370504cc6eb5a1b0b7180c14c31477be2be" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/email_recovery/pubspec.yaml b/email_recovery/pubspec.yaml index 08d4375edf..1342e490fc 100644 --- a/email_recovery/pubspec.yaml +++ b/email_recovery/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: cnb_support + ref: add-reference-destroy-property ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/fcm/pubspec.lock b/fcm/pubspec.lock index f7f8195e79..a7874ac8db 100644 --- a/fcm/pubspec.lock +++ b/fcm/pubspec.lock @@ -295,8 +295,8 @@ packages: dependency: "direct main" description: path: "." - ref: cnb_support - resolved-ref: "10f5838aa1c6c4bffc5690f46d05d0cc4e489e1c" + ref: add-reference-destroy-property + resolved-ref: "9bb94370504cc6eb5a1b0b7180c14c31477be2be" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/fcm/pubspec.yaml b/fcm/pubspec.yaml index 0a6244b89b..d8e424d5e5 100644 --- a/fcm/pubspec.yaml +++ b/fcm/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: cnb_support + ref: add-reference-destroy-property ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/forward/pubspec.lock b/forward/pubspec.lock index f7f8195e79..a7874ac8db 100644 --- a/forward/pubspec.lock +++ b/forward/pubspec.lock @@ -295,8 +295,8 @@ packages: dependency: "direct main" description: path: "." - ref: cnb_support - resolved-ref: "10f5838aa1c6c4bffc5690f46d05d0cc4e489e1c" + ref: add-reference-destroy-property + resolved-ref: "9bb94370504cc6eb5a1b0b7180c14c31477be2be" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/forward/pubspec.yaml b/forward/pubspec.yaml index decaa8b942..fbe54f5d0a 100644 --- a/forward/pubspec.yaml +++ b/forward/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: cnb_support + ref: add-reference-destroy-property ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/lib/features/base/toast/app_toast_manager.dart b/lib/features/base/toast/app_toast_manager.dart index 969985aa12..ca4e9df826 100644 --- a/lib/features/base/toast/app_toast_manager.dart +++ b/lib/features/base/toast/app_toast_manager.dart @@ -5,6 +5,7 @@ import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:flutter/material.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/delete_all_permanently_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_all_selection_all_emails_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -52,6 +53,10 @@ class AppToastManager { AppLocalizations.of(context).toastMessageMoveAllSelectionAllEmailsHasSomeEmailFailure(success.countEmailsMoved, success.destinationPath), leadingSVGIconColor: Colors.white, leadingSVGIcon: _imagePaths.icFolderMailbox); + } else if (success is DeleteAllPermanentlyEmailsSuccess) { + _appToast.showToastSuccessMessage( + overlayContext, + AppLocalizations.of(context).toast_message_empty_trash_folder_success); } } @@ -92,6 +97,12 @@ class AppToastManager { _appToast.showToastErrorMessage( overlayContext, AppLocalizations.of(context).toastMessageMoveAllSelectionAllEmailsAllFailure(failure.destinationPath)); + } else if (failure is DeleteAllPermanentlyEmailsFailure) { + _appToast.showToastSuccessMessage( + overlayContext, + AppLocalizations.of(context).toastMessageDeleteAllPermanentlyEmailsFailureWithReason( + failure.exception.toString() + )); } } } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index a54fdba4e3..c3c8a0bcd9 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -74,6 +74,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/delete_all_permanently_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart'; @@ -277,6 +278,8 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); } else if (success is MoveAllSelectionAllEmailsHasSomeEmailFailure) { _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is DeleteAllPermanentlyEmailsSuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); } }); }); diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index 756349a763..faed62d17a 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -109,6 +109,7 @@ import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_isolate_worker.dart'; import 'package:tmail_ui_user/features/thread/data/repository/thread_repository_impl.dart'; import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/delete_all_permanently_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_spam_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_trash_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; @@ -181,6 +182,7 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); Get.put(AdvancedFilterController()); } @@ -357,6 +359,11 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find(), )); + Get.lazyPut(() => DeleteAllPermanentlyEmailsInteractor( + Get.find(), + Get.find(), + Get.find(), + )); } @override 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 0c78bcf1dc..790e30bd13 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -130,6 +130,7 @@ import 'package:tmail_ui_user/features/sending_queue/domain/usecases/update_send import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/delete_all_permanently_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; @@ -139,6 +140,7 @@ import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_emai import 'package:tmail_ui_user/features/thread/domain/state/mark_as_star_multiple_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_all_selection_all_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/delete_all_permanently_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_spam_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_trash_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; @@ -194,6 +196,7 @@ class MailboxDashBoardController extends ReloadableController { final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; final MarkAllAsUnreadSelectionAllEmailsInteractor _markAllAsUnreadSelectionAllEmailsInteractor; final MoveAllSelectionAllEmailsInteractor _moveAllSelectionAllEmailsInteractor; + final DeleteAllPermanentlyEmailsInteractor _deleteAllPermanentlyEmailsInteractor; GetAllVacationInteractor? _getAllVacationInteractor; UpdateVacationInteractor? _updateVacationInteractor; @@ -228,6 +231,7 @@ class MailboxDashBoardController extends ReloadableController { final isSelectAllEmailsEnabled = RxBool(false); final markAllAsUnreadSelectionAllEmailsViewState = Rx>(Right(UIState.idle)); final moveAllSelectionAllEmailsViewState = Rx>(Right(UIState.idle)); + final deleteAllPermanentlyEmailsViewState = Rx>(Right(UIState.idle)); Session? sessionCurrent; Map mapDefaultMailboxIdByRole = {}; @@ -245,6 +249,7 @@ class MailboxDashBoardController extends ReloadableController { late StreamSubscription _refreshActionStreamSubscription; late StreamSubscription _markAllAsUnreadSelectionAllEmailsStreamSubscription; late StreamSubscription _moveAllSelectionAllEmailsStreamSubscription; + late StreamSubscription _deleteAllPermanentlyEmailsStreamSubscription; final StreamController> _markAsReadMailboxStreamController = StreamController>.broadcast(); @@ -254,6 +259,8 @@ class MailboxDashBoardController extends ReloadableController { StreamController>.broadcast(); final StreamController> _moveAllSelectionAllEmailsStreamController = StreamController>.broadcast(); + final StreamController> _deleteAllPermanentlyEmailsStreamController = + StreamController>.broadcast(); final _notificationManager = LocalNotificationManager.instance; final _fcmService = FcmService.instance; @@ -285,6 +292,7 @@ class MailboxDashBoardController extends ReloadableController { this._getAllIdentitiesInteractor, this._markAllAsUnreadSelectionAllEmailsInteractor, this._moveAllSelectionAllEmailsInteractor, + this._deleteAllPermanentlyEmailsInteractor, ); @override @@ -395,6 +403,8 @@ class MailboxDashBoardController extends ReloadableController { } else if (success is MoveAllSelectionAllEmailsAllSuccess || success is MoveAllSelectionAllEmailsHasSomeEmailFailure) { _handleMoveAllSelectionAllEmailsSuccess(success); + } else if (success is DeleteAllPermanentlyEmailsSuccess) { + _handleDeleteAllPermanentlyEmailsSuccess(success); } } @@ -424,6 +434,8 @@ class MailboxDashBoardController extends ReloadableController { } else if (failure is MoveAllSelectionAllEmailsFailure || failure is MoveAllSelectionAllEmailsAllFailure) { _handleMoveAllSelectionAllEmailsFailure(failure); + } else if (failure is DeleteAllPermanentlyEmailsFailure) { + _handleDeleteAllPermanentlyEmailsFailure(failure); } } @@ -486,6 +498,10 @@ class MailboxDashBoardController extends ReloadableController { moveAllSelectionAllEmailsViewState.value = state; }); + _deleteAllPermanentlyEmailsStreamSubscription = _deleteAllPermanentlyEmailsStreamController.stream.listen((state) { + deleteAllPermanentlyEmailsViewState.value = state; + }); + _registerLocalNotificationStreamListener(); } @@ -2652,6 +2668,43 @@ class MailboxDashBoardController extends ReloadableController { )); } + Future deleteAllPermanentlyEmails( + BuildContext context, + Session session, + AccountId accountId, + PresentationMailbox currentMailbox + ) async { + consumeState(_deleteAllPermanentlyEmailsInteractor.execute( + session, + accountId, + currentMailbox.id, + currentMailbox.countTotalEmails, + _deleteAllPermanentlyEmailsStreamController + )); + } + + void _handleDeleteAllPermanentlyEmailsSuccess(Success success) { + deleteAllPermanentlyEmailsViewState.value = Right(UIState.idle); + + if (currentContext == null || currentOverlayContext == null) return; + + appToastManager.showSuccessMessage( + context: currentContext!, + overlayContext: currentOverlayContext!, + success: success); + } + + void _handleDeleteAllPermanentlyEmailsFailure(Failure failure) { + deleteAllPermanentlyEmailsViewState.value = Right(UIState.idle); + + if (currentContext == null || currentOverlayContext == null) return; + + appToastManager.showFailureMessage( + context: currentContext!, + overlayContext: currentOverlayContext!, + failure: failure); + } + @override void onClose() { _emailReceiveManager.closeEmailReceiveManagerStream(); @@ -2666,6 +2719,8 @@ class MailboxDashBoardController extends ReloadableController { _markAllAsUnreadSelectionAllEmailsStreamController.close(); _moveAllSelectionAllEmailsStreamSubscription.cancel(); _moveAllSelectionAllEmailsStreamController.close(); + _deleteAllPermanentlyEmailsStreamSubscription.cancel(); + _deleteAllPermanentlyEmailsStreamController.close(); _notificationManager.closeStream(); _fcmService.closeStream(); applicationManager.releaseUserAgent(); diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index 821cd55ccb..7fa196febc 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -19,6 +19,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/sear import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/download/download_task_item_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/loading/delete_all_permanently_emails_loading_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_unread_selection_all_emails_loading_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/loading/mark_mailbox_as_read_loading_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/loading/move_all_selection_all_emails_loading_widget.dart'; @@ -155,6 +156,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { Obx(() => MarkMailboxAsReadLoadingWidget(viewState: controller.viewStateMarkAsReadMailbox.value)), Obx(() => MarkAllAsUnreadSelectionAllEmailsLoadingWidget(viewState: controller.markAllAsUnreadSelectionAllEmailsViewState.value)), Obx(() => MoveAllSelectionAllEmailsLoadingWidget(viewState: controller.moveAllSelectionAllEmailsViewState.value)), + Obx(() => DeleteAllPermanentlyEmailsLoadingWidget(viewState: controller.deleteAllPermanentlyEmailsViewState.value)), Expanded(child: Obx(() { switch(controller.dashboardRoute.value) { case DashboardRoutes.thread: diff --git a/lib/features/mailbox_dashboard/presentation/widgets/loading/delete_all_permanently_emails_loading_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/loading/delete_all_permanently_emails_loading_widget.dart new file mode 100644 index 0000000000..d9c0a5ec27 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/widgets/loading/delete_all_permanently_emails_loading_widget.dart @@ -0,0 +1,38 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/delete_all_permanently_emails_state.dart'; + +class DeleteAllPermanentlyEmailsLoadingWidget extends StatelessWidget with AppLoaderMixin { + + final Either viewState; + + const DeleteAllPermanentlyEmailsLoadingWidget({super.key, required this.viewState}); + + @override + Widget build(BuildContext context) { + return viewState.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is DeleteAllPermanentlyEmailsLoading) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: horizontalLoadingWidget + ); + } else if (success is DeleteAllPermanentlyEmailsUpdating) { + final percent = success.total > 0 + ? success.countDeleted / success.total + : 0.0; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: horizontalPercentLoadingWidget(percent) + ); + } + return const SizedBox.shrink(); + } + ); + } +} diff --git a/lib/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_unread_selection_all_emails_loading_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_unread_selection_all_emails_loading_widget.dart index 910d9c1dd7..2752e9fb82 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_unread_selection_all_emails_loading_widget.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_unread_selection_all_emails_loading_widget.dart @@ -23,7 +23,9 @@ class MarkAllAsUnreadSelectionAllEmailsLoadingWidget extends StatelessWidget wit child: horizontalLoadingWidget ); } else if (success is MarkAllAsUnreadSelectionAllEmailsUpdating) { - final percent = success.countUnread / success.totalRead; + final percent = success.totalRead > 0 + ? success.countUnread / success.totalRead + : 0.0; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: horizontalPercentLoadingWidget(percent) diff --git a/lib/features/mailbox_dashboard/presentation/widgets/loading/mark_mailbox_as_read_loading_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/loading/mark_mailbox_as_read_loading_widget.dart index 1e34ee9212..5926b23db6 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/loading/mark_mailbox_as_read_loading_widget.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/loading/mark_mailbox_as_read_loading_widget.dart @@ -23,7 +23,9 @@ class MarkMailboxAsReadLoadingWidget extends StatelessWidget with AppLoaderMixin child: horizontalLoadingWidget ); } else if (success is UpdatingMarkAsMailboxReadState) { - final percent = success.countRead / success.totalUnread; + final percent = success.totalUnread > 0 + ? success.countRead / success.totalUnread + : 0.0; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: horizontalPercentLoadingWidget(percent) diff --git a/lib/features/mailbox_dashboard/presentation/widgets/loading/move_all_selection_all_emails_loading_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/loading/move_all_selection_all_emails_loading_widget.dart index 35c0fb1be8..951b8d7ad4 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/loading/move_all_selection_all_emails_loading_widget.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/loading/move_all_selection_all_emails_loading_widget.dart @@ -23,7 +23,9 @@ class MoveAllSelectionAllEmailsLoadingWidget extends StatelessWidget with AppLoa child: horizontalLoadingWidget ); } else if (success is MoveAllSelectionAllEmailsUpdating) { - final percent = success.countMoved / success.total; + final percent = success.total > 0 + ? success.countMoved / success.total + : 0.0; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: horizontalPercentLoadingWidget(percent) diff --git a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart index dfcb49b10c..94e42a07fe 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart @@ -61,6 +61,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/delete_all_permanently_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_all_selection_all_emails_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -174,6 +175,8 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa _refreshMailboxChanges(mailboxState: success.currentMailboxState); } else if (success is MoveAllSelectionAllEmailsHasSomeEmailFailure) { _refreshMailboxChanges(mailboxState: success.currentMailboxState); + } else if (success is DeleteAllPermanentlyEmailsSuccess) { + _refreshMailboxChanges(mailboxState: success.currentMailboxState); } } diff --git a/lib/features/thread/data/datasource/thread_datasource.dart b/lib/features/thread/data/datasource/thread_datasource.dart index b0e7e46095..cceb3677d6 100644 --- a/lib/features/thread/data/datasource/thread_datasource.dart +++ b/lib/features/thread/data/datasource/thread_datasource.dart @@ -81,4 +81,12 @@ abstract class ThreadDataSource { bool isDestinationSpamMailbox = false } ); + + Future> deleteAllPermanentlyEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController, + ); } \ No newline at end of file diff --git a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart index 1527b40016..3c9376d3d9 100644 --- a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart @@ -133,4 +133,15 @@ class LocalThreadDataSourceImpl extends ThreadDataSource { ) { throw UnimplementedError(); } + + @override + Future> deleteAllPermanentlyEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController, + ) { + throw UnimplementedError(); + } } \ No newline at end of file diff --git a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart index 8188809e0c..bc017eddee 100644 --- a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart @@ -155,4 +155,23 @@ class ThreadDataSourceImpl extends ThreadDataSource { ); }).catchError(_exceptionThrower.throwException); } + + @override + Future> deleteAllPermanentlyEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController, + ) { + return Future.sync(() async { + return await _threadIsolateWorker.deleteAllPermanentlyEmails( + session, + accountId, + mailboxId, + totalEmails, + onProgressController, + ); + }).catchError(_exceptionThrower.throwException); + } } \ No newline at end of file diff --git a/lib/features/thread/data/network/thread_api.dart b/lib/features/thread/data/network/thread_api.dart index 93cc0ff5c3..c46a96ac3c 100644 --- a/lib/features/thread/data/network/thread_api.dart +++ b/lib/features/thread/data/network/thread_api.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart' as dartz; import 'package:jmap_dart_client/http/http_client.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; @@ -13,9 +15,16 @@ import 'package:jmap_dart_client/jmap/jmap_request.dart'; import 'package:jmap_dart_client/jmap/mail/email/changes/changes_email_method.dart'; import 'package:jmap_dart_client/jmap/mail/email/changes/changes_email_response.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_comparator.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_comparator_property.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_filter_condition.dart'; import 'package:jmap_dart_client/jmap/mail/email/get/get_email_method.dart'; import 'package:jmap_dart_client/jmap/mail/email/get/get_email_response.dart'; import 'package:jmap_dart_client/jmap/mail/email/query/query_email_method.dart'; +import 'package:jmap_dart_client/jmap/mail/email/query/query_email_response.dart'; +import 'package:jmap_dart_client/jmap/mail/email/set/set_email_method.dart'; +import 'package:jmap_dart_client/jmap/mail/email/set/set_email_response.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; @@ -180,4 +189,54 @@ class ThreadAPI { return resultList!.list.first; } + + Future, List>> deleteAllPermanentlyEmails( + Session session, + AccountId accountId, + MailboxId mailboxId + ) async { + final processingInvocation = ProcessingInvocation(); + + final jmapRequestBuilder = JmapRequestBuilder(httpClient, processingInvocation); + + final queryEmailMethod = QueryEmailMethod(accountId) + ..addLimit(UnsignedInt(30)) + ..addFilters(EmailFilterCondition(inMailbox: mailboxId)) + ..addSorts({}..add( + EmailComparator(EmailComparatorProperty.receivedAt)..setIsAscending(false) + )); + + final queryEmailInvocation = jmapRequestBuilder.invocation(queryEmailMethod); + + final setEmailMethod = SetEmailMethod(accountId) + ..addReferenceDestroy( + processingInvocation.createResultReference( + queryEmailInvocation.methodCallId, + ReferencePath.idsPath + ) + ); + + final setEmailInvocation = jmapRequestBuilder.invocation(setEmailMethod); + + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + + final result = await (jmapRequestBuilder..usings(capabilities)) + .build() + .execute(); + + final queryEmailResponse = result.parse( + queryEmailInvocation.methodCallId, + QueryEmailResponse.deserialize); + + final setEmailResponse = result.parse( + setEmailInvocation.methodCallId, + SetEmailResponse.deserialize); + + final listMatchedEmailId = queryEmailResponse?.ids.map((id) => EmailId(id)).toList() ?? []; + final listDeletedEmailId = setEmailResponse?.destroyed?.map((id) => EmailId(id)).toList() ?? []; + log('ThreadAPI::deleteAllPermanentlyEmails:listMatchedEmailId = $listMatchedEmailId'); + log('ThreadAPI::deleteAllPermanentlyEmails:listDeletedEmailId = $listDeletedEmailId'); + return dartz.Tuple2(listMatchedEmailId, listDeletedEmailId); + } } \ No newline at end of file diff --git a/lib/features/thread/data/network/thread_isolate_worker.dart b/lib/features/thread/data/network/thread_isolate_worker.dart index 988c883eac..5bac5998df 100644 --- a/lib/features/thread/data/network/thread_isolate_worker.dart +++ b/lib/features/thread/data/network/thread_isolate_worker.dart @@ -115,7 +115,7 @@ class ThreadIsolateWorker { } } } catch (e) { - log('ThreadIsolateWorker::_emptyMailboxFolderAction(): ERROR: $e'); + logError('ThreadIsolateWorker::_emptyMailboxFolderAction(): ERROR: $e'); } log('ThreadIsolateWorker::_emptyMailboxFolderAction(): TOTAL_REMOVE: ${emailListCompleted.length}'); return emailListCompleted; @@ -161,7 +161,7 @@ class ThreadIsolateWorker { } } } catch (e) { - log('ThreadIsolateWorker::_emptyMailboxFolderOnWeb(): ERROR: $e'); + logError('ThreadIsolateWorker::_emptyMailboxFolderOnWeb(): ERROR: $e'); } log('ThreadIsolateWorker::_emptyMailboxFolderOnWeb(): TOTAL_REMOVE: ${emailListCompleted.length}'); return emailListCompleted; @@ -231,7 +231,7 @@ class ThreadIsolateWorker { } } } catch (e) { - log('ThreadIsolateWorker::markAllAsUnreadForSelectionAllEmails(): ERROR: $e'); + logError('ThreadIsolateWorker::markAllAsUnreadForSelectionAllEmails(): ERROR: $e'); } log('ThreadIsolateWorker::markAllAsUnreadForSelectionAllEmails(): TOTAL_UNREAD: ${emailIdListCompleted.length}'); return emailIdListCompleted; @@ -306,9 +306,51 @@ class ThreadIsolateWorker { } } } catch (e) { - log('ThreadIsolateWorker::moveAllSelectionAllEmails(): ERROR: $e'); + logError('ThreadIsolateWorker::moveAllSelectionAllEmails(): ERROR: $e'); } log('ThreadIsolateWorker::moveAllSelectionAllEmails(): TOTAL_MOVED: ${emailIdListCompleted.length}'); return emailIdListCompleted; } + + Future> deleteAllPermanentlyEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController, + ) async { + List emailIdListCompleted = List.empty(growable: true); + try { + bool mailboxHasEmails = true; + + while (mailboxHasEmails) { + final listResult = await _threadAPI.deleteAllPermanentlyEmails( + session, + accountId, + mailboxId + ); + + if (listResult.value1.isNotEmpty) { + mailboxHasEmails = true; + } else { + mailboxHasEmails = false; + } + + if (listResult.value2.isNotEmpty) { + emailIdListCompleted.addAll(listResult.value2); + + onProgressController.add( + dartz.Right(MoveAllSelectionAllEmailsUpdating( + total: totalEmails, + countMoved: emailIdListCompleted.length + )) + ); + } + } + } catch (e) { + logError('ThreadIsolateWorker::deleteAllPermanentlyEmails(): ERROR: $e'); + } + log('ThreadIsolateWorker::deleteAllPermanentlyEmails(): TOTAL_DELETED_PERMANENTLY: ${emailIdListCompleted.length}'); + return emailIdListCompleted; + } } diff --git a/lib/features/thread/data/repository/thread_repository_impl.dart b/lib/features/thread/data/repository/thread_repository_impl.dart index c38955a813..8fffa51782 100644 --- a/lib/features/thread/data/repository/thread_repository_impl.dart +++ b/lib/features/thread/data/repository/thread_repository_impl.dart @@ -448,4 +448,28 @@ class ThreadRepositoryImpl extends ThreadRepository { isDestinationSpamMailbox: isDestinationSpamMailbox ); } + + @override + Future> deleteAllPermanentlyEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController, + ) async { + final listEmailIdDeleted = await mapDataSource[DataSourceType.network]!.deleteAllPermanentlyEmails( + session, + accountId, + mailboxId, + totalEmails, + onProgressController, + ); + + await _updateEmailCache( + accountId, + session.username, + newDestroyed: listEmailIdDeleted); + + return listEmailIdDeleted; + } } \ No newline at end of file diff --git a/lib/features/thread/domain/repository/thread_repository.dart b/lib/features/thread/domain/repository/thread_repository.dart index efcaee302e..a6a26de0ee 100644 --- a/lib/features/thread/domain/repository/thread_repository.dart +++ b/lib/features/thread/domain/repository/thread_repository.dart @@ -94,4 +94,12 @@ abstract class ThreadRepository { bool isDestinationSpamMailbox = false } ); + + Future> deleteAllPermanentlyEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController, + ); } \ No newline at end of file diff --git a/lib/features/thread/domain/state/delete_all_permanently_emails_state.dart b/lib/features/thread/domain/state/delete_all_permanently_emails_state.dart new file mode 100644 index 0000000000..205664f38a --- /dev/null +++ b/lib/features/thread/domain/state/delete_all_permanently_emails_state.dart @@ -0,0 +1,40 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; + +class DeleteAllPermanentlyEmailsLoading extends LoadingState {} + +class DeleteAllPermanentlyEmailsUpdating extends UIState { + + final int total; + final int countDeleted; + + DeleteAllPermanentlyEmailsUpdating({ + required this.total, + required this.countDeleted + }); + + @override + List get props => [total, countDeleted]; +} + +class DeleteAllPermanentlyEmailsSuccess extends UIActionState { + + final List emailIds; + + DeleteAllPermanentlyEmailsSuccess( + this.emailIds, { + jmap.State? currentEmailState, + jmap.State? currentMailboxState, + }) : super(currentEmailState, currentMailboxState); + + @override + List get props => [emailIds, ...super.props]; +} + +class DeleteAllPermanentlyEmailsFailure extends FeatureFailure { + + DeleteAllPermanentlyEmailsFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/thread/domain/usecases/delete_all_permanently_emails_interactor.dart b/lib/features/thread/domain/usecases/delete_all_permanently_emails_interactor.dart new file mode 100644 index 0000000000..d813143753 --- /dev/null +++ b/lib/features/thread/domain/usecases/delete_all_permanently_emails_interactor.dart @@ -0,0 +1,61 @@ +import 'dart:async'; + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; +import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/delete_all_permanently_emails_state.dart'; + +class DeleteAllPermanentlyEmailsInteractor { + final EmailRepository _emailRepository; + final MailboxRepository _mailboxRepository; + final ThreadRepository _threadRepository; + + DeleteAllPermanentlyEmailsInteractor( + this._emailRepository, + this._mailboxRepository, + this._threadRepository, + ); + + Stream> execute( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController + ) async* { + try { + yield Right(DeleteAllPermanentlyEmailsLoading()); + onProgressController.add(Right(DeleteAllPermanentlyEmailsLoading())); + + final listState = await Future.wait([ + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), + ], eagerError: true); + + final currentMailboxState = listState.first; + final currentEmailState = listState.last; + + final emailIdDeleted = await _threadRepository.deleteAllPermanentlyEmails( + session, + accountId, + mailboxId, + totalEmails, + onProgressController, + ); + + yield Right(DeleteAllPermanentlyEmailsSuccess( + emailIdDeleted, + currentMailboxState: currentMailboxState, + currentEmailState: currentEmailState, + )); + } catch (e) { + yield Left(DeleteAllPermanentlyEmailsFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 999220831e..a29b87d51b 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -54,6 +54,7 @@ import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/delete_all_permanently_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; @@ -407,6 +408,9 @@ class ThreadController extends BaseController with EmailActionController, PopupM } else if (success is MoveAllSelectionAllEmailsHasSomeEmailFailure) { cancelSelectEmail(); _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is DeleteAllPermanentlyEmailsSuccess) { + cancelSelectEmail(); + _refreshEmailChanges(currentEmailState: success.currentEmailState); } }); }); @@ -1482,6 +1486,14 @@ class ThreadController extends BaseController with EmailActionController, PopupM selectedMailbox, ); break; + case EmailActionType.deleteAllPermanently: + mailboxDashBoardController.deleteAllPermanentlyEmails( + context, + _session!, + _accountId!, + selectedMailbox, + ); + break; default: break; } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 896da3df4b..89cd7bfcfe 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-04-29T15:22:50.815697", + "@@last_modified": "2024-04-29T18:26:15.565574", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3971,5 +3971,15 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "toastMessageDeleteAllPermanentlyEmailsFailureWithReason": "All mails could not be delete forever. Due \"{reason}\"", + "@toastMessageDeleteAllPermanentlyEmailsFailureWithReason": { + "type": "text", + "placeholders_order": [ + "reason" + ], + "placeholders": { + "reason": {} + } } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 284d057af9..7636200b30 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4103,4 +4103,12 @@ class AppLocalizations { 'Delete all permanently', name: 'deleteAllPermanently'); } + + String toastMessageDeleteAllPermanentlyEmailsFailureWithReason(String reason) { + return Intl.message( + 'All mails could not be delete forever. Due "$reason"', + name: 'toastMessageDeleteAllPermanentlyEmailsFailureWithReason', + args: [reason] + ); + } } \ No newline at end of file diff --git a/model/pubspec.lock b/model/pubspec.lock index dd0ae42daa..e1471f8ab7 100644 --- a/model/pubspec.lock +++ b/model/pubspec.lock @@ -531,8 +531,8 @@ packages: dependency: "direct main" description: path: "." - ref: cnb_support - resolved-ref: "10f5838aa1c6c4bffc5690f46d05d0cc4e489e1c" + ref: add-reference-destroy-property + resolved-ref: "9bb94370504cc6eb5a1b0b7180c14c31477be2be" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/model/pubspec.yaml b/model/pubspec.yaml index 6255b55049..fe4ef5f980 100644 --- a/model/pubspec.yaml +++ b/model/pubspec.yaml @@ -33,7 +33,7 @@ dependencies: jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: cnb_support + ref: add-reference-destroy-property ### Dependencies from pub.dev ### cupertino_icons: 1.0.6 diff --git a/pubspec.lock b/pubspec.lock index 8715668e17..847aeb3a3d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1131,8 +1131,8 @@ packages: dependency: "direct main" description: path: "." - ref: cnb_support - resolved-ref: "43e9ae95a2b18f68da6fca5f3027655f070eef6f" + ref: add-reference-destroy-property + resolved-ref: "9bb94370504cc6eb5a1b0b7180c14c31477be2be" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index 5a78557456..ac47e09245 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,7 @@ dependencies: jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: cnb_support + ref: add-reference-destroy-property contacts_service: git: diff --git a/rule_filter/pubspec.lock b/rule_filter/pubspec.lock index f7f8195e79..a7874ac8db 100644 --- a/rule_filter/pubspec.lock +++ b/rule_filter/pubspec.lock @@ -295,8 +295,8 @@ packages: dependency: "direct main" description: path: "." - ref: cnb_support - resolved-ref: "10f5838aa1c6c4bffc5690f46d05d0cc4e489e1c" + ref: add-reference-destroy-property + resolved-ref: "9bb94370504cc6eb5a1b0b7180c14c31477be2be" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/rule_filter/pubspec.yaml b/rule_filter/pubspec.yaml index 8954d4a2b8..365f9613ec 100644 --- a/rule_filter/pubspec.yaml +++ b/rule_filter/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: cnb_support + ref: add-reference-destroy-property ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/server_settings/pubspec.yaml b/server_settings/pubspec.yaml index fb561c2775..23b9300f68 100644 --- a/server_settings/pubspec.yaml +++ b/server_settings/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: cnb_support + ref: add-reference-destroy-property ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart index 60d65de528..a44f46ecc3 100644 --- a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart +++ b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart @@ -75,6 +75,7 @@ import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants. import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/delete_all_permanently_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_spam_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_trash_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; @@ -157,6 +158,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() { // mock mailbox dashboard controller direct dependencies @@ -241,6 +243,7 @@ void main() { final getAllIdentitiesInteractor = MockGetAllIdentitiesInteractor(); final markAllAsUnreadSelectionAllEmailsInteractor = MockMarkAllAsUnreadSelectionAllEmailsInteractor(); final moveAllSelectionAllEmailsInteractor = MockMoveAllSelectionAllEmailsInteractor(); + final deleteAllPermanentlyEmailsInteractor = MockDeleteAllPermanentlyEmailsInteractor(); late MailboxController mailboxController; // mock thread controller direct dependencies @@ -296,6 +299,7 @@ void main() { Get.put(removeComposerCacheOnWebInteractor); Get.put(markAllAsUnreadSelectionAllEmailsInteractor); Get.put(moveAllSelectionAllEmailsInteractor); + Get.put(deleteAllPermanentlyEmailsInteractor); Get.testMode = true; PackageInfo.setMockInitialValues( @@ -342,6 +346,7 @@ void main() { getAllIdentitiesInteractor, markAllAsUnreadSelectionAllEmailsInteractor, moveAllSelectionAllEmailsInteractor, + deleteAllPermanentlyEmailsInteractor, ); Get.put(mailboxDashboardController); mailboxDashboardController.onReady(); From 18ff9f9bbfc88f3018e28caf1b3a595b29cfa3e7 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 29 Apr 2024 20:02:21 +0700 Subject: [PATCH 11/15] TF-2646 Handle mark all as starred for selection emails --- .../base/toast/app_toast_manager.dart | 21 +++ .../presentation/mailbox_controller.dart | 5 + .../bindings/mailbox_dashboard_bindings.dart | 7 + .../mailbox_dashboard_controller.dart | 59 +++++++++ .../mailbox_dashboard_view_web.dart | 2 + ...d_selection_all_emails_loading_widget.dart | 38 ++++++ .../widgets/top_bar_thread_selection.dart | 41 +++++- .../search_mailbox_controller.dart | 5 + .../data/datasource/thread_datasource.dart | 8 ++ .../local_thread_datasource_impl.dart | 11 ++ .../thread_datasource_impl.dart | 19 +++ .../data/network/thread_isolate_worker.dart | 122 ++++++++++++++---- .../repository/thread_repository_impl.dart | 17 +++ .../domain/repository/thread_repository.dart | 8 ++ ...as_starred_selection_all_emails_state.dart | 56 ++++++++ ...arred_selection_all_emails_interactor.dart | 68 ++++++++++ .../presentation/thread_controller.dart | 16 +++ lib/l10n/intl_messages.arb | 40 +++++- lib/main/localizations/app_localizations.dart | 34 +++++ model/lib/email/email_action_type.dart | 1 + .../mailbox_dashboard_controller_test.dart | 5 + 21 files changed, 549 insertions(+), 34 deletions(-) create mode 100644 lib/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_starred_selection_all_emails_loading_widget.dart create mode 100644 lib/features/thread/domain/state/mark_all_as_starred_selection_all_emails_state.dart create mode 100644 lib/features/thread/domain/usecases/mark_all_as_starred_selection_all_emails_interactor.dart diff --git a/lib/features/base/toast/app_toast_manager.dart b/lib/features/base/toast/app_toast_manager.dart index ca4e9df826..5970ec2986 100644 --- a/lib/features/base/toast/app_toast_manager.dart +++ b/lib/features/base/toast/app_toast_manager.dart @@ -6,6 +6,7 @@ import 'package:core/presentation/utils/app_toast.dart'; import 'package:flutter/material.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/delete_all_permanently_emails_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_starred_selection_all_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_all_selection_all_emails_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -57,6 +58,16 @@ class AppToastManager { _appToast.showToastSuccessMessage( overlayContext, AppLocalizations.of(context).toast_message_empty_trash_folder_success); + } else if (success is MarkAllAsStarredSelectionAllEmailsAllSuccess) { + _appToast.showToastSuccessMessage( + overlayContext, + AppLocalizations.of(context).toastMessageMarkAllAsStarredSelectionAllEmailsSuccess, + leadingSVGIcon: _imagePaths.icUnreadToast); + } else if (success is MarkAllAsStarredSelectionAllEmailsHasSomeEmailFailure) { + _appToast.showToastSuccessMessage( + overlayContext, + AppLocalizations.of(context).toastMessageMarkAllAsStarredSelectionAllEmailsHasSomeEmailFailure(success.countStarred), + leadingSVGIcon: _imagePaths.icUnreadToast); } } @@ -103,6 +114,16 @@ class AppToastManager { AppLocalizations.of(context).toastMessageDeleteAllPermanentlyEmailsFailureWithReason( failure.exception.toString() )); + } else if (failure is MarkAllAsStarredSelectionAllEmailsFailure) { + _appToast.showToastErrorMessage( + overlayContext, + AppLocalizations.of(context).toastMessageMarkAllAsStarredSelectionAllEmailsFailureWithReason( + failure.exception.toString() + )); + } else if (failure is MarkAllAsStarredSelectionAllEmailsAllFailure) { + _appToast.showToastErrorMessage( + overlayContext, + AppLocalizations.of(context).toastMessageMarkAllAsStarredSelectionAllEmailsAllFailure); } } } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index c3c8a0bcd9..322904c8ea 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -77,6 +77,7 @@ import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/domain/state/delete_all_permanently_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_starred_selection_all_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.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/move_all_selection_all_emails_state.dart'; @@ -280,6 +281,10 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); } else if (success is DeleteAllPermanentlyEmailsSuccess) { _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is MarkAllAsStarredSelectionAllEmailsAllSuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is MarkAllAsStarredSelectionAllEmailsHasSomeEmailFailure) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); } }); }); diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index faed62d17a..6d5306c8f6 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -113,6 +113,7 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/delete_all_permane import 'package:tmail_ui_user/features/thread/domain/usecases/empty_spam_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_trash_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/mark_all_as_starred_selection_all_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_all_as_unread_selection_all_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart'; @@ -183,6 +184,7 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); Get.put(AdvancedFilterController()); } @@ -364,6 +366,11 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find(), )); + Get.lazyPut(() => MarkAllAsStarredSelectionAllEmailsInteractor( + Get.find(), + Get.find(), + Get.find(), + )); } @override 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 790e30bd13..f71ad5f3b7 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -135,6 +135,7 @@ import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_sta import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_email_by_id_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_starred_selection_all_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.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_star_multiple_email_state.dart'; @@ -144,6 +145,7 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/delete_all_permane import 'package:tmail_ui_user/features/thread/domain/usecases/empty_spam_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_trash_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/mark_all_as_starred_selection_all_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_all_as_unread_selection_all_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart'; @@ -197,6 +199,7 @@ class MailboxDashBoardController extends ReloadableController { final MarkAllAsUnreadSelectionAllEmailsInteractor _markAllAsUnreadSelectionAllEmailsInteractor; final MoveAllSelectionAllEmailsInteractor _moveAllSelectionAllEmailsInteractor; final DeleteAllPermanentlyEmailsInteractor _deleteAllPermanentlyEmailsInteractor; + final MarkAllAsStarredSelectionAllEmailsInteractor _markAllAsStarredSelectionAllEmailsInteractor; GetAllVacationInteractor? _getAllVacationInteractor; UpdateVacationInteractor? _updateVacationInteractor; @@ -232,6 +235,7 @@ class MailboxDashBoardController extends ReloadableController { final markAllAsUnreadSelectionAllEmailsViewState = Rx>(Right(UIState.idle)); final moveAllSelectionAllEmailsViewState = Rx>(Right(UIState.idle)); final deleteAllPermanentlyEmailsViewState = Rx>(Right(UIState.idle)); + final markAllAsStarredSelectionAllEmailsViewState = Rx>(Right(UIState.idle)); Session? sessionCurrent; Map mapDefaultMailboxIdByRole = {}; @@ -250,6 +254,7 @@ class MailboxDashBoardController extends ReloadableController { late StreamSubscription _markAllAsUnreadSelectionAllEmailsStreamSubscription; late StreamSubscription _moveAllSelectionAllEmailsStreamSubscription; late StreamSubscription _deleteAllPermanentlyEmailsStreamSubscription; + late StreamSubscription _markAllAsStarredSelectionAllEmailsStreamSubscription; final StreamController> _markAsReadMailboxStreamController = StreamController>.broadcast(); @@ -261,6 +266,8 @@ class MailboxDashBoardController extends ReloadableController { StreamController>.broadcast(); final StreamController> _deleteAllPermanentlyEmailsStreamController = StreamController>.broadcast(); + final StreamController> _markAllAsStarredSelectionAllEmailsStreamController = + StreamController>.broadcast(); final _notificationManager = LocalNotificationManager.instance; final _fcmService = FcmService.instance; @@ -293,6 +300,7 @@ class MailboxDashBoardController extends ReloadableController { this._markAllAsUnreadSelectionAllEmailsInteractor, this._moveAllSelectionAllEmailsInteractor, this._deleteAllPermanentlyEmailsInteractor, + this._markAllAsStarredSelectionAllEmailsInteractor, ); @override @@ -405,6 +413,9 @@ class MailboxDashBoardController extends ReloadableController { _handleMoveAllSelectionAllEmailsSuccess(success); } else if (success is DeleteAllPermanentlyEmailsSuccess) { _handleDeleteAllPermanentlyEmailsSuccess(success); + } else if (success is MarkAllAsStarredSelectionAllEmailsAllSuccess + || success is MarkAllAsStarredSelectionAllEmailsHasSomeEmailFailure) { + _handleMarkAllAsStarredSelectionAllEmailsSuccess(success); } } @@ -436,6 +447,9 @@ class MailboxDashBoardController extends ReloadableController { _handleMoveAllSelectionAllEmailsFailure(failure); } else if (failure is DeleteAllPermanentlyEmailsFailure) { _handleDeleteAllPermanentlyEmailsFailure(failure); + } else if (failure is MarkAllAsStarredSelectionAllEmailsFailure + || failure is MarkAllAsStarredSelectionAllEmailsAllFailure) { + _handleMarkAllAsStarredSelectionAllEmailsFailure(failure); } } @@ -502,6 +516,10 @@ class MailboxDashBoardController extends ReloadableController { deleteAllPermanentlyEmailsViewState.value = state; }); + _markAllAsStarredSelectionAllEmailsStreamSubscription = _markAllAsStarredSelectionAllEmailsStreamController.stream.listen((state) { + markAllAsStarredSelectionAllEmailsViewState.value = state; + }); + _registerLocalNotificationStreamListener(); } @@ -2705,6 +2723,45 @@ class MailboxDashBoardController extends ReloadableController { failure: failure); } + void markAllAsStarredSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + String mailboxDisplayName, + int totalEmails + ) { + consumeState(_markAllAsStarredSelectionAllEmailsInteractor.execute( + session, + accountId, + mailboxId, + mailboxDisplayName, + totalEmails, + _markAllAsUnreadSelectionAllEmailsStreamController + )); + } + + void _handleMarkAllAsStarredSelectionAllEmailsSuccess(Success success) { + markAllAsStarredSelectionAllEmailsViewState.value = Right(UIState.idle); + + if (currentContext == null || currentOverlayContext == null) return; + + appToastManager.showSuccessMessage( + context: currentContext!, + overlayContext: currentOverlayContext!, + success: success); + } + + void _handleMarkAllAsStarredSelectionAllEmailsFailure(Failure failure) { + markAllAsStarredSelectionAllEmailsViewState.value = Right(UIState.idle); + + if (currentContext == null || currentOverlayContext == null) return; + + appToastManager.showFailureMessage( + context: currentContext!, + overlayContext: currentOverlayContext!, + failure: failure); + } + @override void onClose() { _emailReceiveManager.closeEmailReceiveManagerStream(); @@ -2721,6 +2778,8 @@ class MailboxDashBoardController extends ReloadableController { _moveAllSelectionAllEmailsStreamController.close(); _deleteAllPermanentlyEmailsStreamSubscription.cancel(); _deleteAllPermanentlyEmailsStreamController.close(); + _markAllAsStarredSelectionAllEmailsStreamSubscription.cancel(); + _markAllAsStarredSelectionAllEmailsStreamController.close(); _notificationManager.closeStream(); _fcmService.closeStream(); applicationManager.releaseUserAgent(); diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index 7fa196febc..bf59b8e6d1 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -20,6 +20,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/sear import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/download/download_task_item_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/loading/delete_all_permanently_emails_loading_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_starred_selection_all_emails_loading_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_unread_selection_all_emails_loading_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/loading/mark_mailbox_as_read_loading_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/loading/move_all_selection_all_emails_loading_widget.dart'; @@ -157,6 +158,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { Obx(() => MarkAllAsUnreadSelectionAllEmailsLoadingWidget(viewState: controller.markAllAsUnreadSelectionAllEmailsViewState.value)), Obx(() => MoveAllSelectionAllEmailsLoadingWidget(viewState: controller.moveAllSelectionAllEmailsViewState.value)), Obx(() => DeleteAllPermanentlyEmailsLoadingWidget(viewState: controller.deleteAllPermanentlyEmailsViewState.value)), + Obx(() => MarkAllAsStarredSelectionAllEmailsLoadingWidget(viewState: controller.markAllAsStarredSelectionAllEmailsViewState.value)), Expanded(child: Obx(() { switch(controller.dashboardRoute.value) { case DashboardRoutes.thread: diff --git a/lib/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_starred_selection_all_emails_loading_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_starred_selection_all_emails_loading_widget.dart new file mode 100644 index 0000000000..c859468221 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_starred_selection_all_emails_loading_widget.dart @@ -0,0 +1,38 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_starred_selection_all_emails_state.dart'; + +class MarkAllAsStarredSelectionAllEmailsLoadingWidget extends StatelessWidget with AppLoaderMixin { + + final Either viewState; + + const MarkAllAsStarredSelectionAllEmailsLoadingWidget({super.key, required this.viewState}); + + @override + Widget build(BuildContext context) { + return viewState.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is MarkAllAsStarredSelectionAllEmailsLoading) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: horizontalLoadingWidget + ); + } else if (success is MarkAllAsStarredSelectionAllEmailsUpdating) { + final percent = success.total > 0 + ? success.countStarred / success.total + : 0.0; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: horizontalPercentLoadingWidget(percent) + ); + } + return const SizedBox.shrink(); + } + ); + } +} diff --git a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart index 122c3104b4..0b26533bbb 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart @@ -73,17 +73,13 @@ class TopBarThreadSelection extends StatelessWidget{ ) ), TMailButtonWidget.fromIcon( - icon: listEmail.isAllEmailStarred - ? imagePaths.icUnStar - : imagePaths.icStar, - tooltipMessage: listEmail.isAllEmailStarred - ? AppLocalizations.of(context).un_star - : AppLocalizations.of(context).star, + icon: _getIconForMarkAsStar(), + tooltipMessage: _getTooltipMessageForMarkAsStar(context), backgroundColor: Colors.transparent, iconSize: 24, onTapActionCallback: () => onEmailActionTypeAction?.call( List.from(listEmail), - listEmail.isAllEmailStarred ? EmailActionType.unMarkAsStarred : EmailActionType.markAsStarred + _getActionTypeForMarkAsStar() ) ), if (canSpamAndMove) @@ -241,4 +237,35 @@ class TopBarThreadSelection extends StatelessWidget{ return EmailActionType.moveToTrash; } } + + String _getIconForMarkAsStar() { + if (isSelectAllEmailsEnabled) { + return imagePaths.icStar; + } else { + return listEmail.isAllEmailStarred + ? imagePaths.icUnStar + : imagePaths.icStar; + } + } + + String _getTooltipMessageForMarkAsStar(BuildContext context) { + if (isSelectAllEmailsEnabled) { + return AppLocalizations.of(context).allStarred; + } else { + return listEmail.isAllEmailStarred + ? AppLocalizations.of(context).un_star + : AppLocalizations.of(context).star; + } + } + + + EmailActionType _getActionTypeForMarkAsStar() { + if (isSelectAllEmailsEnabled) { + return EmailActionType.markAllAsStarred; + } else { + return listEmail.isAllEmailStarred + ? EmailActionType.unMarkAsStarred + : EmailActionType.markAsStarred; + } + } } \ No newline at end of file diff --git a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart index 94e42a07fe..70144b0c2d 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart @@ -62,6 +62,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dash import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/domain/state/delete_all_permanently_emails_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_starred_selection_all_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_all_selection_all_emails_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -177,6 +178,10 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa _refreshMailboxChanges(mailboxState: success.currentMailboxState); } else if (success is DeleteAllPermanentlyEmailsSuccess) { _refreshMailboxChanges(mailboxState: success.currentMailboxState); + } else if (success is MarkAllAsStarredSelectionAllEmailsAllSuccess) { + _refreshMailboxChanges(mailboxState: success.currentMailboxState); + } else if (success is MarkAllAsStarredSelectionAllEmailsHasSomeEmailFailure) { + _refreshMailboxChanges(mailboxState: success.currentMailboxState); } } diff --git a/lib/features/thread/data/datasource/thread_datasource.dart b/lib/features/thread/data/datasource/thread_datasource.dart index cceb3677d6..5d725613d1 100644 --- a/lib/features/thread/data/datasource/thread_datasource.dart +++ b/lib/features/thread/data/datasource/thread_datasource.dart @@ -89,4 +89,12 @@ abstract class ThreadDataSource { int totalEmails, StreamController> onProgressController, ); + + Future> markAllAsStarredForSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController + ); } \ No newline at end of file diff --git a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart index 3c9376d3d9..2edd8d224a 100644 --- a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart @@ -144,4 +144,15 @@ class LocalThreadDataSourceImpl extends ThreadDataSource { ) { throw UnimplementedError(); } + + @override + Future> markAllAsStarredForSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController + ) { + throw UnimplementedError(); + } } \ No newline at end of file diff --git a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart index bc017eddee..6b84da03a0 100644 --- a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart @@ -174,4 +174,23 @@ class ThreadDataSourceImpl extends ThreadDataSource { ); }).catchError(_exceptionThrower.throwException); } + + @override + Future> markAllAsStarredForSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController + ) { + return Future.sync(() async { + return await _threadIsolateWorker.markAllAsStarredForSelectionAllEmails( + session, + accountId, + mailboxId, + totalEmails, + onProgressController, + ); + }).catchError(_exceptionThrower.throwException); + } } \ No newline at end of file diff --git a/lib/features/thread/data/network/thread_isolate_worker.dart b/lib/features/thread/data/network/thread_isolate_worker.dart index 5bac5998df..9b7ca86638 100644 --- a/lib/features/thread/data/network/thread_isolate_worker.dart +++ b/lib/features/thread/data/network/thread_isolate_worker.dart @@ -18,6 +18,7 @@ import 'package:jmap_dart_client/jmap/mail/email/email_filter_condition.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/email_property.dart'; +import 'package:model/email/mark_star_action.dart'; import 'package:model/email/read_actions.dart'; import 'package:model/extensions/list_email_extension.dart'; import 'package:tmail_ui_user/features/base/isolate/background_isolate_binary_messenger/background_isolate_binary_messenger.dart'; @@ -27,6 +28,7 @@ import 'package:tmail_ui_user/features/thread/data/model/empty_mailbox_folder_ar import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; import 'package:tmail_ui_user/features/thread/domain/exceptions/thread_exceptions.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_starred_selection_all_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_all_selection_all_emails_state.dart'; import 'package:tmail_ui_user/main/exceptions/isolate_exception.dart'; @@ -139,15 +141,13 @@ class ThreadIsolateWorker { EmailComparator(EmailComparatorProperty.receivedAt) ..setIsAscending(false)), filter: EmailFilterCondition(inMailbox: mailboxId, before: lastEmail?.receivedAt), - properties: Properties({EmailProperty.id})); - - var newEmailList = emailsResponse.emailList ?? []; - if (lastEmail != null) { - newEmailList = newEmailList.where((email) => email.id != lastEmail!.id).toList(); - } - + properties: Properties({EmailProperty.id}) + ).then((response) => _removeDuplicatedLatestEmailFromEmailResponse( + emailsResponse: response, + latestEmailId: lastEmail?.id + )); + final newEmailList = emailsResponse.emailList ?? []; log('ThreadIsolateWorker::_emptyMailboxFolderOnWeb(): ${newEmailList.length}'); - if (newEmailList.isNotEmpty) { lastEmail = newEmailList.last; hasEmails = true; @@ -197,15 +197,10 @@ class ThreadIsolateWorker { EmailProperty.id, EmailProperty.receivedAt, }) - ).then((response) { - var listEmails = response.emailList; - if (listEmails != null && listEmails.isNotEmpty && lastEmailId != null) { - listEmails = listEmails - .where((email) => email.id != lastEmailId) - .toList(); - } - return EmailsResponse(emailList: listEmails, state: response.state); - }); + ).then((response) => _removeDuplicatedLatestEmailFromEmailResponse( + emailsResponse: response, + latestEmailId: lastEmailId + )); final listEmailRead = emailResponse.emailList; log('ThreadIsolateWorker::markAllAsUnreadForSelectionAllEmails: LIST_EMAIL_READ = ${listEmailRead?.length}'); if (listEmailRead == null || listEmailRead.isEmpty) { @@ -270,15 +265,10 @@ class ThreadIsolateWorker { EmailProperty.id, EmailProperty.receivedAt, }) - ).then((response) { - var listEmails = response.emailList; - if (listEmails != null && listEmails.isNotEmpty && lastEmailId != null) { - listEmails = listEmails - .where((email) => email.id != lastEmailId) - .toList(); - } - return EmailsResponse(emailList: listEmails, state: response.state); - }); + ).then((response) => _removeDuplicatedLatestEmailFromEmailResponse( + emailsResponse: response, + latestEmailId: lastEmailId + )); final listEmail = emailResponse.emailList; log('ThreadIsolateWorker::moveAllSelectionAllEmails: LIST_EMAIL = ${listEmail?.length}'); if (listEmail == null || listEmail.isEmpty) { @@ -353,4 +343,84 @@ class ThreadIsolateWorker { log('ThreadIsolateWorker::deleteAllPermanentlyEmails(): TOTAL_DELETED_PERMANENTLY: ${emailIdListCompleted.length}'); return emailIdListCompleted; } + + Future> markAllAsStarredForSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController + ) async { + List emailIdListCompleted = List.empty(growable: true); + try { + bool mailboxHasEmails = true; + UTCDate? lastReceivedDate; + EmailId? lastEmailId; + + while (mailboxHasEmails) { + final emailResponse = await _threadAPI.getAllEmail( + session, + accountId, + limit: UnsignedInt(30), + filter: EmailFilterCondition( + inMailbox: mailboxId, + notKeyword: KeyWordIdentifier.emailFlagged.value, + before: lastReceivedDate + ), + sort: {}..add( + EmailComparator(EmailComparatorProperty.receivedAt)..setIsAscending(false) + ), + properties: Properties({ + EmailProperty.id, + EmailProperty.receivedAt, + }) + ).then((response) => _removeDuplicatedLatestEmailFromEmailResponse( + emailsResponse: response, + latestEmailId: lastEmailId + )); + final listEmails = emailResponse.emailList; + + if (listEmails == null || listEmails.isEmpty) { + mailboxHasEmails = false; + } else { + lastEmailId = listEmails.last.id; + lastReceivedDate = listEmails.last.receivedAt; + + final listResult = await _emailAPI.markAsStar( + session, + accountId, + listEmails, + MarkStarAction.markStar + ); + + emailIdListCompleted.addAll(listResult.listEmailIds); + onProgressController.add( + dartz.Right(MarkAllAsStarredSelectionAllEmailsUpdating( + total: totalEmails, + countStarred: emailIdListCompleted.length + )) + ); + } + } + } catch (e) { + logError('ThreadIsolateWorker::markAllAsStarredForSelectionAllEmails(): ERROR: $e'); + } + return emailIdListCompleted; + } + + EmailsResponse _removeDuplicatedLatestEmailFromEmailResponse({ + required EmailsResponse emailsResponse, + EmailId? latestEmailId, + }) { + List listEmails = emailsResponse.emailList ?? []; + if (listEmails.isNotEmpty && latestEmailId != null) { + listEmails = listEmails + .where((email) => email.id != latestEmailId) + .toList(); + + return EmailsResponse(emailList: listEmails, state: emailsResponse.state); + } else { + return emailsResponse; + } + } } diff --git a/lib/features/thread/data/repository/thread_repository_impl.dart b/lib/features/thread/data/repository/thread_repository_impl.dart index 8fffa51782..6bbb20ff70 100644 --- a/lib/features/thread/data/repository/thread_repository_impl.dart +++ b/lib/features/thread/data/repository/thread_repository_impl.dart @@ -472,4 +472,21 @@ class ThreadRepositoryImpl extends ThreadRepository { return listEmailIdDeleted; } + + @override + Future> markAllAsStarredForSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController + ) { + return mapDataSource[DataSourceType.network]!.markAllAsStarredForSelectionAllEmails( + session, + accountId, + mailboxId, + totalEmails, + onProgressController + ); + } } \ No newline at end of file diff --git a/lib/features/thread/domain/repository/thread_repository.dart b/lib/features/thread/domain/repository/thread_repository.dart index a6a26de0ee..9ebd679da0 100644 --- a/lib/features/thread/domain/repository/thread_repository.dart +++ b/lib/features/thread/domain/repository/thread_repository.dart @@ -102,4 +102,12 @@ abstract class ThreadRepository { int totalEmails, StreamController> onProgressController, ); + + Future> markAllAsStarredForSelectionAllEmails( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController + ); } \ No newline at end of file diff --git a/lib/features/thread/domain/state/mark_all_as_starred_selection_all_emails_state.dart b/lib/features/thread/domain/state/mark_all_as_starred_selection_all_emails_state.dart new file mode 100644 index 0000000000..f489219b5d --- /dev/null +++ b/lib/features/thread/domain/state/mark_all_as_starred_selection_all_emails_state.dart @@ -0,0 +1,56 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; + +class MarkAllAsStarredSelectionAllEmailsLoading extends LoadingState {} + +class MarkAllAsStarredSelectionAllEmailsUpdating extends UIState { + + final int total; + final int countStarred; + + MarkAllAsStarredSelectionAllEmailsUpdating({ + required this.total, + required this.countStarred + }); + + @override + List get props => [total, countStarred]; +} + +class MarkAllAsStarredSelectionAllEmailsAllSuccess extends UIActionState { + + MarkAllAsStarredSelectionAllEmailsAllSuccess({ + jmap.State? currentEmailState, + jmap.State? currentMailboxState, + }) : super(currentMailboxState, currentEmailState); +} + +class MarkAllAsStarredSelectionAllEmailsHasSomeEmailFailure extends UIActionState { + + final int countStarred; + + MarkAllAsStarredSelectionAllEmailsHasSomeEmailFailure( + this.countStarred, + { + jmap.State? currentEmailState, + jmap.State? currentMailboxState, + } + ) : super(currentMailboxState, currentEmailState); + + @override + List get props => [ + countStarred, + ...super.props + ]; +} + +class MarkAllAsStarredSelectionAllEmailsAllFailure extends FeatureFailure {} + +class MarkAllAsStarredSelectionAllEmailsFailure extends FeatureFailure { + + MarkAllAsStarredSelectionAllEmailsFailure({ + dynamic exception + }) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/thread/domain/usecases/mark_all_as_starred_selection_all_emails_interactor.dart b/lib/features/thread/domain/usecases/mark_all_as_starred_selection_all_emails_interactor.dart new file mode 100644 index 0000000000..bbdb1707ad --- /dev/null +++ b/lib/features/thread/domain/usecases/mark_all_as_starred_selection_all_emails_interactor.dart @@ -0,0 +1,68 @@ +import 'dart:async'; + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; +import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_starred_selection_all_emails_state.dart'; + +class MarkAllAsStarredSelectionAllEmailsInteractor { + final EmailRepository _emailRepository; + final MailboxRepository _mailboxRepository; + final ThreadRepository _threadRepository; + + MarkAllAsStarredSelectionAllEmailsInteractor( + this._emailRepository, + this._mailboxRepository, + this._threadRepository, + ); + + Stream> execute( + Session session, + AccountId accountId, + MailboxId mailboxId, + String mailboxDisplayName, + int totalEmails, + StreamController> onProgressController + ) async* { + try { + yield Right(MarkAllAsStarredSelectionAllEmailsLoading()); + onProgressController.add(Right(MarkAllAsStarredSelectionAllEmailsLoading())); + + final listState = await Future.wait([ + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), + ], eagerError: true); + + final currentMailboxState = listState.first; + final currentEmailState = listState.last; + + final listEmailId = await _threadRepository.markAllAsStarredForSelectionAllEmails( + session, + accountId, + mailboxId, + totalEmails, + onProgressController); + + if (totalEmails == listEmailId.length) { + yield Right(MarkAllAsStarredSelectionAllEmailsAllSuccess( + currentEmailState: currentEmailState, + currentMailboxState: currentMailboxState)); + } else if (listEmailId.isNotEmpty) { + yield Right(MarkAllAsStarredSelectionAllEmailsHasSomeEmailFailure( + listEmailId.length, + currentEmailState: currentEmailState, + currentMailboxState: currentMailboxState)); + } else { + yield Left(MarkAllAsStarredSelectionAllEmailsAllFailure()); + } + } catch (e) { + yield Left(MarkAllAsStarredSelectionAllEmailsFailure(exception: e)); + } + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index a29b87d51b..0530d9d2a9 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -60,6 +60,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/get_email_by_id_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/load_more_emails_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_starred_selection_all_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_all_as_unread_selection_all_emails_state.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_star_multiple_email_state.dart'; @@ -411,6 +412,12 @@ class ThreadController extends BaseController with EmailActionController, PopupM } else if (success is DeleteAllPermanentlyEmailsSuccess) { cancelSelectEmail(); _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is MarkAllAsStarredSelectionAllEmailsAllSuccess) { + cancelSelectEmail(); + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is MarkAllAsStarredSelectionAllEmailsHasSomeEmailFailure) { + cancelSelectEmail(); + _refreshEmailChanges(currentEmailState: success.currentEmailState); } }); }); @@ -1494,6 +1501,15 @@ class ThreadController extends BaseController with EmailActionController, PopupM selectedMailbox, ); break; + case EmailActionType.markAllAsStarred: + mailboxDashBoardController.markAllAsStarredSelectionAllEmails( + _session!, + _accountId!, + selectedMailbox.mailboxId!, + selectedMailbox.getDisplayName(context), + selectedMailbox.countTotalEmails + ); + break; default: break; } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 89cd7bfcfe..ea02afca7d 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-04-29T18:26:15.565574", + "@@last_modified": "2024-04-29T19:54:23.924948", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3981,5 +3981,43 @@ "placeholders": { "reason": {} } + }, + "allStarred": "All starred", + "@allStarred": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageMarkAllAsStarredSelectionAllEmailsSuccess": "You’ve marked all mails as starred", + "@toastMessageMarkAllAsStarredSelectionAllEmailsSuccess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageMarkAllAsStarredSelectionAllEmailsHasSomeEmailFailure": "You’ve marked {count} mails as starred", + "@toastMessageMarkAllAsStarredSelectionAllEmailsHasSomeEmailFailure": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "toastMessageMarkAllAsStarredSelectionAllEmailsAllFailure": "All mails could not be marked as starred", + "@toastMessageMarkAllAsStarredSelectionAllEmailsAllFailure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageMarkAllAsStarredSelectionAllEmailsFailureWithReason": "All mails could not be marked as starred. Due \"{reason}\"", + "@toastMessageMarkAllAsStarredSelectionAllEmailsFailureWithReason": { + "type": "text", + "placeholders_order": [ + "reason" + ], + "placeholders": { + "reason": {} + } } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 7636200b30..562f037cca 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4111,4 +4111,38 @@ class AppLocalizations { args: [reason] ); } + + String get allStarred { + return Intl.message( + 'All starred', + name: 'allStarred', + ); + } + + String get toastMessageMarkAllAsStarredSelectionAllEmailsSuccess { + return Intl.message( + 'You’ve marked all mails as starred', + name: 'toastMessageMarkAllAsStarredSelectionAllEmailsSuccess'); + } + + String toastMessageMarkAllAsStarredSelectionAllEmailsHasSomeEmailFailure(int count) { + return Intl.message( + 'You’ve marked $count mails as starred', + name: 'toastMessageMarkAllAsStarredSelectionAllEmailsHasSomeEmailFailure', + args: [count]); + } + + String get toastMessageMarkAllAsStarredSelectionAllEmailsAllFailure { + return Intl.message( + 'All mails could not be marked as starred', + name: 'toastMessageMarkAllAsStarredSelectionAllEmailsAllFailure'); + } + + String toastMessageMarkAllAsStarredSelectionAllEmailsFailureWithReason(String reason) { + return Intl.message( + 'All mails could not be marked as starred. Due "$reason"', + name: 'toastMessageMarkAllAsStarredSelectionAllEmailsFailureWithReason', + args: [reason] + ); + } } \ No newline at end of file diff --git a/model/lib/email/email_action_type.dart b/model/lib/email/email_action_type.dart index 1b8d3a888e..bc30e8fcb5 100644 --- a/model/lib/email/email_action_type.dart +++ b/model/lib/email/email_action_type.dart @@ -33,4 +33,5 @@ enum EmailActionType { moveAll, moveAllToTrash, deleteAllPermanently, + markAllAsStarred, } \ No newline at end of file diff --git a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart index a44f46ecc3..47941a298e 100644 --- a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart +++ b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart @@ -81,6 +81,7 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/empty_trash_folder import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/load_more_emails_in_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/mark_all_as_starred_selection_all_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_all_as_unread_selection_all_emails_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart'; @@ -159,6 +160,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() { // mock mailbox dashboard controller direct dependencies @@ -244,6 +246,7 @@ void main() { final markAllAsUnreadSelectionAllEmailsInteractor = MockMarkAllAsUnreadSelectionAllEmailsInteractor(); final moveAllSelectionAllEmailsInteractor = MockMoveAllSelectionAllEmailsInteractor(); final deleteAllPermanentlyEmailsInteractor = MockDeleteAllPermanentlyEmailsInteractor(); + final markAllAsStarredSelectionAllEmailsInteractor = MockMarkAllAsStarredSelectionAllEmailsInteractor(); late MailboxController mailboxController; // mock thread controller direct dependencies @@ -300,6 +303,7 @@ void main() { Get.put(markAllAsUnreadSelectionAllEmailsInteractor); Get.put(moveAllSelectionAllEmailsInteractor); Get.put(deleteAllPermanentlyEmailsInteractor); + Get.put(markAllAsStarredSelectionAllEmailsInteractor); Get.testMode = true; PackageInfo.setMockInitialValues( @@ -347,6 +351,7 @@ void main() { markAllAsUnreadSelectionAllEmailsInteractor, moveAllSelectionAllEmailsInteractor, deleteAllPermanentlyEmailsInteractor, + markAllAsStarredSelectionAllEmailsInteractor, ); Get.put(mailboxDashboardController); mailboxDashboardController.onReady(); From 4a2ed6949c9ad302f0580ae11f97dd41742bff18 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 29 Apr 2024 20:21:43 +0700 Subject: [PATCH 12/15] TF-2646 Handle mark all as Spam/UnSpam for selection emails --- .../mailbox_dashboard_controller.dart | 46 ++++++++++++++++ .../widgets/top_bar_thread_selection.dart | 53 +++++++++++++------ .../presentation/thread_controller.dart | 16 ++++++ lib/main/localizations/app_localizations.dart | 14 +++++ model/lib/email/email_action_type.dart | 2 + 5 files changed, 116 insertions(+), 15 deletions(-) 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 f71ad5f3b7..38081a2a9a 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -2762,6 +2762,52 @@ class MailboxDashBoardController extends ReloadableController { failure: failure); } + Future maskAllAsSpamSelectionAllEmails( + BuildContext context, + Session session, + AccountId accountId, + PresentationMailbox currentMailbox + ) async { + final spamMailboxId = getMailboxIdByRole(PresentationMailbox.roleSpam); + + if (spamMailboxId == null) return; + + final spamMailboxPath = mapMailboxById[spamMailboxId]?.getDisplayName(context) ?? ''; + + consumeState(_moveAllSelectionAllEmailsInteractor.execute( + session, + accountId, + currentMailbox.id, + spamMailboxId, + spamMailboxPath, + currentMailbox.countTotalEmails, + _moveAllSelectionAllEmailsStreamController + )); + } + + Future allUnSpamSelectionAllEmails( + BuildContext context, + Session session, + AccountId accountId, + PresentationMailbox currentMailbox + ) async { + final inboxMailboxId = getMailboxIdByRole(PresentationMailbox.roleInbox); + + if (inboxMailboxId == null) return; + + final inboxMailboxPath = mapMailboxById[inboxMailboxId]?.getDisplayName(context) ?? ''; + + consumeState(_moveAllSelectionAllEmailsInteractor.execute( + session, + accountId, + currentMailbox.id, + inboxMailboxId, + inboxMailboxPath, + currentMailbox.countTotalEmails, + _moveAllSelectionAllEmailsStreamController + )); + } + @override void onClose() { _emailReceiveManager.closeEmailReceiveManagerStream(); diff --git a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart index 0b26533bbb..c1e0288e4b 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart @@ -95,24 +95,15 @@ class TopBarThreadSelection extends StatelessWidget{ ) ), TMailButtonWidget.fromIcon( - icon: isAllSpam ? imagePaths.icNotSpam : imagePaths.icSpam, + icon: _getIconForMoveToSpam(), backgroundColor: Colors.transparent, iconSize: 24, - tooltipMessage: isAllSpam - ? AppLocalizations.of(context).un_spam - : AppLocalizations.of(context).mark_as_spam, + tooltipMessage: _getTooltipMessageForMoveToSpam(context), onTapActionCallback: () { - if (isAllSpam) { - onEmailActionTypeAction?.call( - List.from(listEmail), - EmailActionType.unSpam - ); - } else { - onEmailActionTypeAction?.call( - List.from(listEmail.listEmailCanSpam(mapMailbox)), - EmailActionType.moveToSpam - ); - } + onEmailActionTypeAction?.call( + List.from(listEmail.listEmailCanSpam(mapMailbox)), + _getActionTypeForMoveToSpam() + ); } ) ], @@ -268,4 +259,36 @@ class TopBarThreadSelection extends StatelessWidget{ : EmailActionType.markAsStarred; } } + + String _getIconForMoveToSpam() { + if (isSelectAllEmailsEnabled) { + return selectedMailbox?.isSpam == true ? imagePaths.icNotSpam : imagePaths.icSpam; + } else { + return isAllSpam ? imagePaths.icNotSpam : imagePaths.icSpam; + } + } + + String _getTooltipMessageForMoveToSpam(BuildContext context) { + if (isSelectAllEmailsEnabled) { + return selectedMailbox?.isSpam == true + ? AppLocalizations.of(context).allUnSpam + : AppLocalizations.of(context).markAllAsSpam; + } else { + return isAllSpam + ? AppLocalizations.of(context).un_spam + : AppLocalizations.of(context).mark_as_spam; + } + } + + EmailActionType _getActionTypeForMoveToSpam() { + if (isSelectAllEmailsEnabled) { + return selectedMailbox?.isSpam == true + ? EmailActionType.allUnSpam + : EmailActionType.markAllAsSpam; + } else { + return isAllSpam + ? EmailActionType.unSpam + : EmailActionType.moveToSpam; + } + } } \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 0530d9d2a9..a356cbeb11 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -1510,6 +1510,22 @@ class ThreadController extends BaseController with EmailActionController, PopupM selectedMailbox.countTotalEmails ); break; + case EmailActionType.markAllAsSpam: + mailboxDashBoardController.maskAllAsSpamSelectionAllEmails( + context, + _session!, + _accountId!, + selectedMailbox, + ); + break; + case EmailActionType.allUnSpam: + mailboxDashBoardController.allUnSpamSelectionAllEmails( + context, + _session!, + _accountId!, + selectedMailbox, + ); + break; default: break; } diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 562f037cca..50f3d84837 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4145,4 +4145,18 @@ class AppLocalizations { args: [reason] ); } + + String get allUnSpam { + return Intl.message( + 'All UnSpam', + name: 'allUnSpam', + ); + } + + String get markAllAsSpam { + return Intl.message( + 'Mark all as spam', + name: 'markAllAsSpam', + ); + } } \ No newline at end of file diff --git a/model/lib/email/email_action_type.dart b/model/lib/email/email_action_type.dart index bc30e8fcb5..0e203b84d1 100644 --- a/model/lib/email/email_action_type.dart +++ b/model/lib/email/email_action_type.dart @@ -34,4 +34,6 @@ enum EmailActionType { moveAllToTrash, deleteAllPermanently, markAllAsStarred, + markAllAsSpam, + allUnSpam, } \ No newline at end of file From 06bb89c186de62207b790a66ba70d228d6592b14 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 29 Apr 2024 20:39:09 +0700 Subject: [PATCH 13/15] TF-2646 Disable selection all email when toggle select item or load more emails --- .../controller/mailbox_dashboard_controller.dart | 2 ++ .../widgets/top_bar_thread_selection.dart | 1 - .../thread/presentation/thread_controller.dart | 15 ++++++++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) 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 38081a2a9a..30088ee4e9 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -232,6 +232,7 @@ class MailboxDashBoardController extends ReloadableController { final isRecoveringDeletedMessage = RxBool(false); final localFileDraggableAppState = Rxn(); final isSelectAllEmailsEnabled = RxBool(false); + final isSelectAllPageEnabled = RxBool(false); final markAllAsUnreadSelectionAllEmailsViewState = Rx>(Right(UIState.idle)); final moveAllSelectionAllEmailsViewState = Rx>(Right(UIState.idle)); final deleteAllPermanentlyEmailsViewState = Rx>(Right(UIState.idle)); @@ -2203,6 +2204,7 @@ class MailboxDashBoardController extends ReloadableController { } void selectAllEmailAction() { + isSelectAllPageEnabled.value = true; dispatchAction(SelectionAllEmailAction()); } diff --git a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart index c1e0288e4b..5ec5437c69 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart @@ -249,7 +249,6 @@ class TopBarThreadSelection extends StatelessWidget{ } } - EmailActionType _getActionTypeForMarkAsStar() { if (isSelectAllEmailsEnabled) { return EmailActionType.markAllAsStarred; diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index a356cbeb11..7530b03b3c 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -729,6 +729,12 @@ class ThreadController extends BaseController with EmailActionController, PopupM } void selectEmail(BuildContext context, PresentationEmail presentationEmailSelected) { + if (mailboxDashBoardController.isSelectAllPageEnabled.value) { + mailboxDashBoardController.isSelectAllPageEnabled.value = false; + } + if (mailboxDashBoardController.isSelectAllEmailsEnabled.value) { + mailboxDashBoardController.isSelectAllEmailsEnabled.value = false; + } final emailsInCurrentMailbox = mailboxDashBoardController.emailsInCurrentMailbox; if (_rangeSelectionMode && latestEmailSelectedOrUnselected.value != null && latestEmailSelectedOrUnselected.value?.id != presentationEmailSelected.id) { @@ -784,6 +790,7 @@ class ThreadController extends BaseController with EmailActionController, PopupM .map((email) => email.toSelectedEmail(selectMode: SelectMode.INACTIVE)) .toList(); mailboxDashBoardController.isSelectAllEmailsEnabled.value = false; + mailboxDashBoardController.isSelectAllPageEnabled.value = false; mailboxDashBoardController.updateEmailList(newEmailList); mailboxDashBoardController.currentSelectMode.value = SelectMode.INACTIVE; mailboxDashBoardController.listEmailSelected.clear(); @@ -1362,6 +1369,12 @@ class ThreadController extends BaseController with EmailActionController, PopupM void handleLoadMoreEmailsRequest() { log('ThreadController::handleLoadMoreEmailsRequest:'); + if (mailboxDashBoardController.isSelectAllPageEnabled.value) { + mailboxDashBoardController.isSelectAllPageEnabled.value = false; + } + if (mailboxDashBoardController.isSelectAllEmailsEnabled.value) { + mailboxDashBoardController.isSelectAllEmailsEnabled.value = false; + } if (isSearchActive) { _searchMoreEmails(); } else { @@ -1370,7 +1383,7 @@ class ThreadController extends BaseController with EmailActionController, PopupM } bool validateToShowSelectionEmailsBanner() { - return isSelectionEnabled() + return mailboxDashBoardController.isSelectAllPageEnabled.isTrue && currentMailbox != null && currentMailbox!.countTotalEmails > ThreadConstants.maxCountEmails && mailboxDashBoardController.listEmailSelected.length < currentMailbox!.countTotalEmails; From 8d34f01fef326a28fa7e5978b4ae2056471fbae3 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 30 Apr 2024 00:29:00 +0700 Subject: [PATCH 14/15] TF-2646 Show the top bar thread button only when the email list is loaded --- .../base_mailbox_dashboard_view.dart | 3 +- .../mailbox_dashboard_controller.dart | 4 - .../mailbox_dashboard_view_web.dart | 161 ++-------------- .../app_bar/desktop_top_bar_thread_view.dart | 173 ++++++++++++++++++ .../top_bar_thread_selection.dart | 172 ++++++++--------- .../presentation/thread_controller.dart | 3 +- 6 files changed, 279 insertions(+), 237 deletions(-) create mode 100644 lib/features/mailbox_dashboard/presentation/widgets/app_bar/desktop_top_bar_thread_view.dart rename lib/features/mailbox_dashboard/presentation/widgets/{ => app_bar}/top_bar_thread_selection.dart (67%) diff --git a/lib/features/mailbox_dashboard/presentation/base_mailbox_dashboard_view.dart b/lib/features/mailbox_dashboard/presentation/base_mailbox_dashboard_view.dart index 344dd661b1..3e247ff8c5 100644 --- a/lib/features/mailbox_dashboard/presentation/base_mailbox_dashboard_view.dart +++ b/lib/features/mailbox_dashboard/presentation/base_mailbox_dashboard_view.dart @@ -2,11 +2,10 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mixin/filter_email_popup_menu_mixin.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mixin/user_setting_popup_menu_mixin.dart'; abstract class BaseMailboxDashBoardView extends GetWidget - with UserSettingPopupMenuMixin, FilterEmailPopupMenuMixin, + with UserSettingPopupMenuMixin, AppLoaderMixin { BaseMailboxDashBoardView({Key? key}) : super(key: key); } \ No newline at end of file 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 30088ee4e9..010466dd3e 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -1699,10 +1699,6 @@ class MailboxDashBoardController extends ReloadableController { consumeState(appGridDashboardController.showDashboardAction()); } - bool isAbleMarkAllAsRead(){ - return !searchController.isSearchEmailRunning && selectedMailbox.value != null && selectedMailbox.value!.isDrafts; - } - void refreshActionWhenBackToApp() { log('MailboxDashBoardController::refreshActionWhenBackToApp():'); _refreshActionEventController.add(RefreshActionViewEvent()); diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index bf59b8e6d1..bd375e12fa 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -4,7 +4,6 @@ import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:model/extensions/username_extension.dart'; -import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/base/widget/application_version_widget.dart'; import 'package:tmail_ui_user/features/base/widget/popup_item_no_icon_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_view_web.dart'; @@ -18,6 +17,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dash import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/app_bar/desktop_top_bar_thread_view.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/download/download_task_item_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/loading/delete_all_permanently_emails_loading_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/loading/mark_all_as_starred_selection_all_emails_loading_widget.dart'; @@ -26,14 +26,12 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/lo import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/loading/move_all_selection_all_emails_loading_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/recover_deleted_message_loading_banner_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/app_bar/top_bar_thread_selection.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; import 'package:tmail_ui_user/features/quotas/presentation/widget/quotas_banner_widget.dart'; import 'package:tmail_ui_user/features/search/email/presentation/search_email_view.dart'; import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_view.dart'; -import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/presentation/styles/banner_delete_all_spam_emails_styles.dart'; import 'package:tmail_ui_user/features/thread/presentation/styles/banner_empty_trash_styles.dart'; import 'package:tmail_ui_user/features/thread/presentation/thread_view.dart'; @@ -261,29 +259,22 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { Obx(() { final listEmailSelected = controller.listEmailSelected; if (controller.isSelectionEnabled() && listEmailSelected.isNotEmpty) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.5, horizontal: 16), - child: TopBarThreadSelection( - listEmail: listEmailSelected, - mapMailbox: controller.mapMailboxById, - isSelectAllEmailsEnabled: controller.isSelectAllEmailsEnabled.value, - selectedMailbox: controller.selectedMailbox.value, - onCancelSelection: () => - controller.dispatchAction(CancelSelectionAllEmailAction()), - onEmailActionTypeAction: (listEmails, actionType) => - controller.dispatchAction(HandleEmailActionTypeAction( - context, - listEmails, - actionType - )), - onMoreSelectedEmailAction: (position) => controller.dispatchAction(MoreSelectedEmailAction(context, position)), - ), + return TopBarThreadSelection( + listEmail: listEmailSelected, + mapMailbox: controller.mapMailboxById, + isSelectAllEmailsEnabled: controller.isSelectAllEmailsEnabled.value, + selectedMailbox: controller.selectedMailbox.value, + onCancelSelection: () => controller.dispatchAction(CancelSelectionAllEmailAction()), + onEmailActionTypeAction: (listEmails, actionType) => + controller.dispatchAction(HandleEmailActionTypeAction( + context, + listEmails, + actionType + )), + onMoreSelectedEmailAction: (position) => controller.dispatchAction(MoreSelectedEmailAction(context, position)), ); } else { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - child: _buildListButtonTopBar(context), - ); + return DesktopTopBarThreadView(); } }), const Divider(), @@ -368,126 +359,6 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { }); } - Widget _buildListButtonTopBar(BuildContext context) { - return Row(children: [ - Obx(() { - return controller.refreshingMailboxState.value.fold( - (failure) { - return TMailButtonWidget.fromIcon( - key: const Key('refresh_mailbox_button'), - icon: controller.imagePaths.icRefresh, - borderRadius: 10, - iconSize: 16, - onTapActionCallback: controller.refreshMailboxAction, - ); - }, - (success) { - if (success is RefreshAllEmailLoading) { - return const TMailContainerWidget( - borderRadius: 10, - padding: EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 8.5), - child: CupertinoLoadingWidget(size: 16)); - } else { - return TMailButtonWidget.fromIcon( - key: const Key('refresh_mailbox_button'), - icon: controller.imagePaths.icRefresh, - borderRadius: 10, - iconSize: 16, - onTapActionCallback: controller.refreshMailboxAction, - ); - } - } - ); - }), - const SizedBox(width: 16), - Tooltip( - message: AppLocalizations.of(context).selectAllMessagesOfThisPage, - child: ElevatedButton.icon( - onPressed: controller.selectAllEmailAction, - icon: SvgPicture.asset( - controller.imagePaths.icSelectAll, - width: 16, - height: 16, - fit: BoxFit.fill, - ), - label: Text( - AppLocalizations.of(context).selectAllMessagesOfThisPage, - maxLines: 1, - overflow: TextOverflow.ellipsis - ), - style: ElevatedButton.styleFrom( - backgroundColor: AppColor.colorButtonHeaderThread, - shadowColor: Colors.transparent, - padding: const EdgeInsetsDirectional.symmetric(horizontal: 12, vertical: 8), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - elevation: 0.0, - foregroundColor: AppColor.colorTextButtonHeaderThread, - maximumSize: const Size.fromWidth(250), - textStyle: const TextStyle(fontSize: 12), - ), - ), - ), - if (controller.isAbleMarkAllAsRead()) - Padding( - padding: const EdgeInsetsDirectional.only(start: 16), - child: TMailButtonWidget( - key: const Key('mark_as_read_emails_button'), - text: AppLocalizations.of(context).mark_all_as_read, - icon: controller.imagePaths.icSelectAll, - borderRadius: 10, - iconSize: 16, - padding: const EdgeInsetsDirectional.symmetric(horizontal: 12, vertical: 8), - onTapActionCallback: () => controller.markAsReadMailboxAction(context), - ), - ), - const SizedBox(width: 16), - Obx(() => TMailButtonWidget( - key: const Key('filter_emails_button'), - text: controller.filterMessageOption.value == FilterMessageOption.all - ? AppLocalizations.of(context).filter_messages - : controller.filterMessageOption.value.getTitle(context), - icon: controller.filterMessageOption.value.getIconSelected(controller.imagePaths), - borderRadius: 10, - iconSize: 16, - padding: const EdgeInsetsDirectional.symmetric(horizontal: 12, vertical: 8), - backgroundColor: controller.filterMessageOption.value.getBackgroundColor(), - textStyle: controller.filterMessageOption.value.getTextStyle(), - trailingIcon: controller.imagePaths.icArrowDown, - onTapActionAtPositionCallback: (position) { - return controller.openPopupMenuAction( - context, - position, - popupMenuFilterEmailActionTile( - context, - controller.filterMessageOption.value, - (option) => controller.dispatchAction(FilterMessageAction(context, option)), - isSearchEmailRunning: controller.searchController.isSearchEmailRunning - ) - ); - }, - )), - Obx(() { - final mailboxSelected = controller.selectedMailbox.value; - if (mailboxSelected != null && mailboxSelected.role == PresentationMailbox.roleTrash) { - return Padding( - padding: const EdgeInsetsDirectional.only(start: 16), - child: TMailButtonWidget.fromIcon( - key: const Key('recover_deleted_messages_button'), - icon: controller.imagePaths.icRecoverDeletedMessages, - borderRadius: 10, - iconSize: 16, - onTapActionCallback: () => controller.gotoEmailRecovery(), - ), - ); - } else { - return const SizedBox.shrink(); - } - }) - ]); - } - Widget _buildDownloadTaskStateWidget() { return Obx(() { if (controller.downloadController.notEmptyListDownloadTask) { diff --git a/lib/features/mailbox_dashboard/presentation/widgets/app_bar/desktop_top_bar_thread_view.dart b/lib/features/mailbox_dashboard/presentation/widgets/app_bar/desktop_top_bar_thread_view.dart new file mode 100644 index 0000000000..9ce67d6448 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/widgets/app_bar/desktop_top_bar_thread_view.dart @@ -0,0 +1,173 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/presentation/views/container/tmail_container_widget.dart'; +import 'package:core/presentation/views/loading/cupertino_loading_widget.dart'; +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/mailbox_dashboard/presentation/action/dashboard_action.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mixin/filter_email_popup_menu_mixin.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class DesktopTopBarThreadView extends StatelessWidget with FilterEmailPopupMenuMixin { + + final _dashboardController = Get.find(); + + DesktopTopBarThreadView({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + child: Row(children: [ + Obx(() { + return _dashboardController.refreshingMailboxState.value.fold( + (failure) { + return TMailButtonWidget.fromIcon( + key: const Key('refresh_mailbox_button'), + icon: _dashboardController.imagePaths.icRefresh, + borderRadius: 10, + iconSize: 16, + onTapActionCallback: _dashboardController.refreshMailboxAction, + ); + }, + (success) { + if (success is RefreshAllEmailLoading) { + return const TMailContainerWidget( + borderRadius: 10, + padding: EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 8.5), + child: CupertinoLoadingWidget(size: 16)); + } else { + return TMailButtonWidget.fromIcon( + key: const Key('refresh_mailbox_button'), + icon: _dashboardController.imagePaths.icRefresh, + borderRadius: 10, + iconSize: 16, + onTapActionCallback: _dashboardController.refreshMailboxAction, + ); + } + } + ); + }), + Obx(() { + if (_dashboardController.emailsInCurrentMailbox.isNotEmpty) { + return Padding( + padding: const EdgeInsetsDirectional.only(start: 16), + child: Tooltip( + message: AppLocalizations.of(context).selectAllMessagesOfThisPage, + child: ElevatedButton.icon( + onPressed: _dashboardController.selectAllEmailAction, + icon: SvgPicture.asset( + _dashboardController.imagePaths.icSelectAll, + width: 16, + height: 16, + fit: BoxFit.fill, + ), + label: Text( + AppLocalizations.of(context).selectAllMessagesOfThisPage, + maxLines: 1, + overflow: TextOverflow.ellipsis + ), + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.colorButtonHeaderThread, + shadowColor: Colors.transparent, + padding: const EdgeInsetsDirectional.symmetric(horizontal: 12, vertical: 8), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + elevation: 0.0, + foregroundColor: AppColor.colorTextButtonHeaderThread, + maximumSize: const Size.fromWidth(250), + textStyle: const TextStyle(fontSize: 12), + ), + ), + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (_isAbleMarkAllAsRead) { + return Padding( + padding: const EdgeInsetsDirectional.only(start: 16), + child: TMailButtonWidget( + key: const Key('mark_all_as_read_emails_button'), + text: AppLocalizations.of(context).mark_all_as_read, + icon: _dashboardController.imagePaths.icMarkAllAsRead, + borderRadius: 10, + iconSize: 16, + padding: const EdgeInsetsDirectional.symmetric(horizontal: 12, vertical: 8), + onTapActionCallback: () => _dashboardController.markAsReadMailboxAction(context), + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + final messageOption = _dashboardController.filterMessageOption.value; + if (_dashboardController.emailsInCurrentMailbox.isNotEmpty) { + return TMailButtonWidget( + key: const Key('filter_emails_button'), + text: messageOption == FilterMessageOption.all + ? AppLocalizations.of(context).filter_messages + : messageOption.getTitle(context), + icon: messageOption.getIconSelected(_dashboardController.imagePaths), + borderRadius: 10, + iconSize: 16, + margin: const EdgeInsetsDirectional.only(start: 16), + padding: const EdgeInsetsDirectional.symmetric(horizontal: 12, vertical: 8), + backgroundColor: messageOption.getBackgroundColor(), + textStyle: messageOption.getTextStyle(), + trailingIcon: _dashboardController.imagePaths.icArrowDown, + onTapActionAtPositionCallback: (position) { + return _dashboardController.openPopupMenuAction( + context, + position, + popupMenuFilterEmailActionTile( + context, + messageOption, + (option) => _dashboardController.dispatchAction(FilterMessageAction(context, option)), + isSearchEmailRunning: _dashboardController.searchController.isSearchEmailRunning + ) + ); + }, + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + final mailboxSelected = _dashboardController.selectedMailbox.value; + if (mailboxSelected != null && mailboxSelected.isTrash) { + return Padding( + padding: const EdgeInsetsDirectional.only(start: 16), + child: TMailButtonWidget.fromIcon( + key: const Key('recover_deleted_messages_button'), + icon: _dashboardController.imagePaths.icRecoverDeletedMessages, + borderRadius: 10, + iconSize: 16, + onTapActionCallback: _dashboardController.gotoEmailRecovery, + ), + ); + } else { + return const SizedBox.shrink(); + } + }) + ]), + ); + } + + bool get _isAbleMarkAllAsRead { + return !_dashboardController.searchController.isSearchEmailRunning + && _dashboardController.emailsInCurrentMailbox.isNotEmpty + && _dashboardController.selectedMailbox.value != null + && (!_dashboardController.selectedMailbox.value!.isDrafts && !_dashboardController.selectedMailbox.value!.isSpam); + } +} diff --git a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart b/lib/features/mailbox_dashboard/presentation/widgets/app_bar/top_bar_thread_selection.dart similarity index 67% rename from lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart rename to lib/features/mailbox_dashboard/presentation/widgets/app_bar/top_bar_thread_selection.dart index 5ec5437c69..229f863ce7 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/app_bar/top_bar_thread_selection.dart @@ -40,98 +40,100 @@ class TopBarThreadSelection extends StatelessWidget{ @override Widget build(BuildContext context) { - return Row(children: [ - TMailButtonWidget.fromIcon( - icon: imagePaths.icClose, - iconColor: AppColor.primaryColor, - tooltipMessage: AppLocalizations.of(context).cancel, - backgroundColor: Colors.transparent, - iconSize: 28, - padding: const EdgeInsets.all(3), - onTapActionCallback: onCancelSelection - ), - if (!isSelectAllEmailsEnabled) - Padding( - padding: const EdgeInsetsDirectional.only(end: 30), - child: Text( - AppLocalizations.of(context).count_email_selected(listEmail.length), - style: const TextStyle( - fontSize: 17, - fontWeight: FontWeight.w500, - color: AppColor.colorTextButton - ) - ), + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.5, horizontal: 16), + child: Row(children: [ + TMailButtonWidget.fromIcon( + icon: imagePaths.icClose, + iconColor: AppColor.primaryColor, + tooltipMessage: AppLocalizations.of(context).cancel, + backgroundColor: Colors.transparent, + iconSize: 28, + padding: const EdgeInsets.all(3), + onTapActionCallback: onCancelSelection ), - TMailButtonWidget.fromIcon( - icon: _getIconForMarkAsRead(), - tooltipMessage: _getTooltipMessageForMarkAsRead(context), - backgroundColor: Colors.transparent, - iconSize: 24, - onTapActionCallback: () => onEmailActionTypeAction?.call( - List.from(listEmail), - _getActionTypeForMarkAsRead() - ) - ), - TMailButtonWidget.fromIcon( - icon: _getIconForMarkAsStar(), - tooltipMessage: _getTooltipMessageForMarkAsStar(context), - backgroundColor: Colors.transparent, - iconSize: 24, - onTapActionCallback: () => onEmailActionTypeAction?.call( - List.from(listEmail), - _getActionTypeForMarkAsStar() - ) - ), - if (canSpamAndMove) - ...[ - TMailButtonWidget.fromIcon( - icon: imagePaths.icMove, - iconSize: 22, - tooltipMessage: _getTooltipMessageForMove(context), - backgroundColor: Colors.transparent, - onTapActionCallback: () => onEmailActionTypeAction?.call( - List.from(listEmail), - _getActionTypeForMove() - ) - ), - TMailButtonWidget.fromIcon( - icon: _getIconForMoveToSpam(), - backgroundColor: Colors.transparent, - iconSize: 24, - tooltipMessage: _getTooltipMessageForMoveToSpam(context), - onTapActionCallback: () { - onEmailActionTypeAction?.call( - List.from(listEmail.listEmailCanSpam(mapMailbox)), - _getActionTypeForMoveToSpam() - ); - } - ) - ], - if (isAllBelongToTheSameMailbox) + if (!isSelectAllEmailsEnabled) + Padding( + padding: const EdgeInsetsDirectional.only(end: 30), + child: Text( + AppLocalizations.of(context).count_email_selected(listEmail.length), + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: AppColor.colorTextButton + ) + ), + ), TMailButtonWidget.fromIcon( - icon: imagePaths.icDeleteComposer, + icon: _getIconForMarkAsRead(), + tooltipMessage: _getTooltipMessageForMarkAsRead(context), backgroundColor: Colors.transparent, - iconSize: 20, - iconColor: _getIconColorForMoveToTrash(), - tooltipMessage: _getTooltipMessageForMoveToTrash(context), - onTapActionCallback: () { - onEmailActionTypeAction?.call( - List.from(listEmail), - _getActionTypeForMoveToTrash() - ); - } + iconSize: 24, + onTapActionCallback: () => onEmailActionTypeAction?.call( + List.from(listEmail), + _getActionTypeForMarkAsRead() + ) ), - const Spacer(), - if (isSelectAllEmailsEnabled) TMailButtonWidget.fromIcon( - icon: imagePaths.icMoreVertical, - iconSize: 22, - iconColor: AppColor.primaryColor, - tooltipMessage: AppLocalizations.of(context).more, + icon: _getIconForMarkAsStar(), + tooltipMessage: _getTooltipMessageForMarkAsStar(context), backgroundColor: Colors.transparent, - onTapActionAtPositionCallback: onMoreSelectedEmailAction + iconSize: 24, + onTapActionCallback: () => onEmailActionTypeAction?.call( + List.from(listEmail), + _getActionTypeForMarkAsStar() + ) ), - ]); + if (canSpamAndMove) + ...[ + TMailButtonWidget.fromIcon( + icon: imagePaths.icMove, + iconSize: 22, + tooltipMessage: _getTooltipMessageForMove(context), + backgroundColor: Colors.transparent, + onTapActionCallback: () => onEmailActionTypeAction?.call( + List.from(listEmail), + _getActionTypeForMove() + ) + ), + TMailButtonWidget.fromIcon( + icon: _getIconForMoveToSpam(), + backgroundColor: Colors.transparent, + iconSize: 24, + tooltipMessage: _getTooltipMessageForMoveToSpam(context), + onTapActionCallback: () { + onEmailActionTypeAction?.call( + List.from(listEmail.listEmailCanSpam(mapMailbox)), + _getActionTypeForMoveToSpam() + ); + } + ) + ], + if (isAllBelongToTheSameMailbox) + TMailButtonWidget.fromIcon( + icon: imagePaths.icDeleteComposer, + backgroundColor: Colors.transparent, + iconSize: 20, + iconColor: _getIconColorForMoveToTrash(), + tooltipMessage: _getTooltipMessageForMoveToTrash(context), + onTapActionCallback: () { + onEmailActionTypeAction?.call( + List.from(listEmail), + _getActionTypeForMoveToTrash() + ); + } + ), + if (isSelectAllEmailsEnabled) + TMailButtonWidget.fromIcon( + icon: imagePaths.icMoreVertical, + iconSize: 22, + iconColor: AppColor.primaryColor, + tooltipMessage: AppLocalizations.of(context).more, + backgroundColor: Colors.transparent, + onTapActionAtPositionCallback: onMoreSelectedEmailAction + ), + ]), + ); } bool get canDeletePermanently => listEmail.isAllCanDeletePermanently(mapMailbox); diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 7530b03b3c..fb9d3a8c60 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -1393,7 +1393,8 @@ class ThreadController extends BaseController with EmailActionController, PopupM final listSelectionEmailActions = [ EmailActionType.markAllAsRead, EmailActionType.markAllAsUnread, - EmailActionType.moveAll, + if (currentMailbox == null || currentMailbox?.isDrafts == false) + EmailActionType.moveAll, if (currentMailbox?.isTrash == true || currentMailbox?.isSpam == true || currentMailbox?.isDrafts == true) EmailActionType.deleteAllPermanently else From 379171a0dbd9dc33fa4d5738e573c54791c6b699 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 2 May 2024 10:01:30 +0700 Subject: [PATCH 15/15] TF-2646 Add dependency `package_info_plus` & `file_picker` direct main to avoid warning import --- core/lib/presentation/resources/assets_paths.dart | 1 - core/lib/presentation/resources/image_paths.dart | 4 ---- pubspec.lock | 4 ++-- pubspec.yaml | 4 ++++ 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/core/lib/presentation/resources/assets_paths.dart b/core/lib/presentation/resources/assets_paths.dart index a8c7401165..40734a5628 100644 --- a/core/lib/presentation/resources/assets_paths.dart +++ b/core/lib/presentation/resources/assets_paths.dart @@ -1,5 +1,4 @@ class AssetsPaths { static const images = 'assets/images/'; - static const icons = 'assets/icons/'; static const configurationImages = 'configurations/icons/'; } \ No newline at end of file diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 92f9635004..322b6e6fd7 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -216,10 +216,6 @@ class ImagePaths { return AssetsPaths.images + imageName; } - String _getIconPath(String iconName) { - return AssetsPaths.icons + iconName; - } - String getConfigurationImagePath(String imageName) { return AssetsPaths.configurationImages + imageName; } diff --git a/pubspec.lock b/pubspec.lock index 847aeb3a3d..7ff92a8c9d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -505,7 +505,7 @@ packages: source: hosted version: "6.1.4" file_picker: - dependency: "direct overridden" + dependency: "direct main" description: path: "." ref: "email_supported_5.3.1" @@ -1256,7 +1256,7 @@ packages: source: hosted version: "2.1.0" package_info_plus: - dependency: transitive + dependency: "direct main" description: name: package_info_plus sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" diff --git a/pubspec.yaml b/pubspec.yaml index ac47e09245..cf9bccdc76 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -232,6 +232,10 @@ dependencies: future_loading_dialog: 0.3.0 + package_info_plus: 4.2.0 + + file_picker: 5.3.1 + dev_dependencies: flutter_test: sdk: flutter