From a12efc8d2b94c36d32ae694a8b9d6a5abd500200 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 21 Sep 2021 12:52:27 +0700 Subject: [PATCH] TF-80 Move multiple email to mailbox from thread view --- assets/images/empty_image_default.svg | 9 + core/lib/core.dart | 2 + .../presentation/resources/image_paths.dart | 1 + .../background/background_widget_builder.dart | 56 ++++++ .../presentation/views/list/tree_view.dart | 77 ++------ .../destination_picker_controller.dart | 28 ++- .../presentation/destination_picker_view.dart | 52 ++++-- .../data/datasource/email_datasource.dart | 4 +- .../email_datasource_impl.dart | 6 +- .../email/data/network/email_api.dart | 40 ++-- .../repository/email_repository_impl.dart | 6 +- .../email/domain/model/move_request.dart | 19 +- .../domain/repository/email_repository.dart | 4 +- .../state/mark_as_email_read_state.dart | 7 +- .../domain/state/move_to_mailbox_state.dart | 24 ++- .../mark_as_email_read_interactor.dart | 17 +- .../usecases/move_to_mailbox_interactor.dart | 13 +- .../email/presentation/email_controller.dart | 51 +++--- .../list_mailbox_node_extension.dart | 18 ++ .../presentation/mailbox_controller.dart | 24 ++- .../mailbox/presentation/mailbox_view.dart | 47 ++--- .../presentation/model/mailbox_node.dart | 63 ++++++- .../presentation/model/mailbox_tree.dart | 20 ++ .../widgets/mailbox_folder_tile_builder.dart | 43 ++++- .../mark_as_multiple_email_read_state.dart | 18 +- .../move_multiple_email_to_mailbox_state.dart | 69 +++++++ ...ark_as_multiple_email_read_interactor.dart | 43 ++--- ..._multiple_email_to_mailbox_interactor.dart | 42 +++++ .../thread/presentation/thread_bindings.dart | 7 +- .../presentation/thread_controller.dart | 159 ++++++++++------ .../thread/presentation/thread_view.dart | 173 ++++++------------ lib/l10n/intl_en.arb | 34 +++- lib/l10n/intl_fr.arb | 34 +++- lib/l10n/intl_messages.arb | 36 +++- lib/l10n/intl_ru.arb | 34 +++- lib/l10n/intl_vi.arb | 34 +++- lib/main/localizations/app_localizations.dart | 2 +- model/lib/extensions/email_extension.dart | 25 +++ .../extensions/list_email_id_extension.dart | 27 +++ .../presentation_email_extension.dart | 53 ++---- .../presentation_mailbox_extension.dart | 24 ++- model/lib/mailbox/presentation_mailbox.dart | 4 +- model/lib/model.dart | 1 + 43 files changed, 1007 insertions(+), 443 deletions(-) create mode 100644 assets/images/empty_image_default.svg create mode 100644 core/lib/presentation/views/background/background_widget_builder.dart create mode 100644 lib/features/mailbox/presentation/extensions/list_mailbox_node_extension.dart create mode 100644 lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart create mode 100644 lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart create mode 100644 model/lib/extensions/list_email_id_extension.dart diff --git a/assets/images/empty_image_default.svg b/assets/images/empty_image_default.svg new file mode 100644 index 000000000..fd0b26259 --- /dev/null +++ b/assets/images/empty_image_default.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/core/lib/core.dart b/core/lib/core.dart index 7c810c869..b4d3b0101 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -36,6 +36,8 @@ export 'presentation/views/context_menu/context_menu_builder.dart'; export 'presentation/views/context_menu/context_menu_header_builder.dart'; export 'presentation/views/context_menu/simple_context_menu_action_builder.dart'; export 'presentation/views/dialog/loading_dialog_builder.dart'; +export 'presentation/views/dialog/downloading_file_dialog_builder.dart'; +export 'presentation/views/background/background_widget_builder.dart'; // Resources export 'presentation/resources/assets_paths.dart'; diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 132831867..52960210a 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -41,6 +41,7 @@ class ImagePaths { String get icDownload => _getImagePath('ic_download.svg'); String get icMore => _getImagePath('ic_more.svg'); String get icPhotoLibrary => _getImagePath('ic_photo_library.svg'); + String get icEmptyImageDefault => _getImagePath('empty_image_default.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; diff --git a/core/lib/presentation/views/background/background_widget_builder.dart b/core/lib/presentation/views/background/background_widget_builder.dart new file mode 100644 index 000000000..dee90bc87 --- /dev/null +++ b/core/lib/presentation/views/background/background_widget_builder.dart @@ -0,0 +1,56 @@ + +import 'package:core/core.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class BackgroundWidgetBuilder { + Key? _key; + SvgPicture? _image; + String? _text; + + final BuildContext _context; + + BackgroundWidgetBuilder(this._context); + + void key(Key key) { + _key = key; + } + + void image(SvgPicture image) { + _image = image; + } + + void text(String text) { + _text = text; + } + + Widget build() { + return Center( + key: _key ?? Key('BackgroundWidgetBuilder'), + child: CustomScrollView( + slivers: [ + SliverFillRemaining( + child: Container( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _image ?? SizedBox.shrink(), + Padding( + padding: EdgeInsets.only(top: _image != null ? 16 : 0), + child: Text( + _text ?? '', + style: TextStyle(color: AppColor.baseTextColor, fontSize: 16), + textAlign: TextAlign.center, + ), + ), + ], + ), + height: MediaQuery.of(_context).size.height, + ), + ) + ] + ) + ); + } +} \ No newline at end of file diff --git a/core/lib/presentation/views/list/tree_view.dart b/core/lib/presentation/views/list/tree_view.dart index 1c9c55c60..9cd5bf7be 100644 --- a/core/lib/presentation/views/list/tree_view.dart +++ b/core/lib/presentation/views/list/tree_view.dart @@ -50,84 +50,43 @@ class _TreeViewData extends StatelessWidget { } } -class TreeViewChild extends StatefulWidget { - final bool? startExpanded; +class TreeViewChild { + final BuildContext context; + final bool? isExpanded; final Widget parent; final List children; final VoidCallback? onTap; - TreeViewChild({ - required this.parent, - required this.children, - this.startExpanded, - this.onTap, - Key? key, - }) : super(key: key); + TreeViewChild( + this.context, + { + required this.parent, + required this.children, + this.isExpanded, + this.onTap, + Key? key, + } + ); - @override - TreeViewChildState createState() => TreeViewChildState(); - - TreeViewChild copyWith( - TreeViewChild source, { - bool? startExpanded, - Widget? parent, - List? children, - VoidCallback? onTap, - }) { - return TreeViewChild( - parent: parent ?? source.parent, - children: children ?? source.children, - startExpanded: startExpanded ?? source.startExpanded, - onTap: onTap ?? source.onTap, - ); - } -} - -class TreeViewChildState extends State { - bool? isExpanded; - - @override - void initState() { - super.initState(); - isExpanded = widget.startExpanded; - } - - @override - void didChangeDependencies() { - isExpanded = widget.startExpanded ?? TreeView.of(context).startExpanded; - super.didChangeDependencies(); - } - - @override - Widget build(BuildContext context) { + Widget build() { return Column( mainAxisSize: MainAxisSize.min, children: [ GestureDetector( - child: widget.parent, + child: parent, onTap: () { - if (widget.onTap != null) { - widget.onTap!(); + if (onTap != null) { + onTap!(); } - toggleExpanded(); }, ), AnimatedContainer( duration: Duration(milliseconds: 400), child: isExpanded! - ? Column( - mainAxisSize: MainAxisSize.min, - children: widget.children, - ) + ? Column(mainAxisSize: MainAxisSize.min, children: children) : Offstage(), ), ], ); } - - void toggleExpanded() { - setState(() { - this.isExpanded = !this.isExpanded!; - }); - } } \ No newline at end of file diff --git a/lib/features/destination_picker/presentation/destination_picker_controller.dart b/lib/features/destination_picker/presentation/destination_picker_controller.dart index 2fb6d376e..667f79e07 100644 --- a/lib/features/destination_picker/presentation/destination_picker_controller.dart +++ b/lib/features/destination_picker/presentation/destination_picker_controller.dart @@ -9,6 +9,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/usecases/get_all_mailbox_i import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree_builder.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/list_mailbox_node_extension.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; @@ -17,7 +18,9 @@ class DestinationPickerController extends BaseController { final GetAllMailboxInteractor _getAllMailboxInteractor; final TreeBuilder _treeBuilder; - final folderMailboxTree = MailboxTree(MailboxNode.root()).obs; + MailboxTree folderMailboxTree = MailboxTree(MailboxNode.root()); + final defaultMailboxList = [].obs; + final folderMailboxNodeList = [].obs; DestinationPickerArguments? destinationPickerArguments; @@ -46,7 +49,13 @@ class DestinationPickerController extends BaseController { } @override - void onDone() {} + void onDone() { + viewState.value.map((success) { + if (success is GetAllMailboxSuccess) { + defaultMailboxList.value = success.defaultMailboxList; + } + }); + } @override void onError(error) {} @@ -59,7 +68,20 @@ class DestinationPickerController extends BaseController { } void _buildTree(List folderMailboxList) async { - folderMailboxTree.value = await _treeBuilder.generateMailboxTree(folderMailboxList); + folderMailboxTree = await _treeBuilder.generateMailboxTree(folderMailboxList); + folderMailboxNodeList.value = folderMailboxTree.root.childrenItems ?? []; + } + + void toggleMailboxFolder(MailboxNode selectedMailboxNode) { + final newExpandMode = selectedMailboxNode.expandMode == ExpandMode.COLLAPSE + ? ExpandMode.EXPAND + : ExpandMode.COLLAPSE; + + final newMailboxNodeList = folderMailboxNodeList.updateNode( + selectedMailboxNode.item.id, + selectedMailboxNode.copyWith(newExpandMode: newExpandMode)) ?? []; + + folderMailboxNodeList.value = newMailboxNodeList; } void moveEmailToMailboxAction(PresentationMailbox destinationMailbox) { diff --git a/lib/features/destination_picker/presentation/destination_picker_view.dart b/lib/features/destination_picker/presentation/destination_picker_view.dart index 956e7a736..b3be3a8de 100644 --- a/lib/features/destination_picker/presentation/destination_picker_view.dart +++ b/lib/features/destination_picker/presentation/destination_picker_view.dart @@ -4,6 +4,7 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:model/model.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/destination_picker_controller.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/widgets/app_bar_destination_picker_builder.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/get_all_mailboxes_state.dart'; @@ -87,7 +88,7 @@ class DestinationPickerView extends GetWidget { Widget _buildLoadingView() { return Obx(() => controller.viewState.value.fold( (failure) => SizedBox.shrink(), - (success) => success == UIState.loading + (success) => success is LoadingState ? Center(child: Padding( padding: EdgeInsets.only(top: 16), child: SizedBox( @@ -127,7 +128,8 @@ class DestinationPickerView extends GetWidget { _responsiveUtils, defaultMailboxList[index], mailboxDisplayed: MailboxDisplayed.destinationPicker) - ..onOpenMailboxAction((mailbox) => controller.moveEmailToMailboxAction(mailbox))) + ..onOpenMailboxAction((mailbox) => + controller.moveEmailToMailboxAction(mailbox.toPresentationMailboxWithMailboxPath(mailbox.name?.name ?? '')))) .build()); } else { return SizedBox.shrink(); @@ -137,7 +139,7 @@ class DestinationPickerView extends GetWidget { } Widget _buildFolderMailbox(BuildContext context) { - return Obx(() => controller.folderMailboxTree.value.root.hasChildren() + return Obx(() => controller.folderMailboxNodeList.isNotEmpty ? Transform( transform: Matrix4.translationValues(-12.0, 0.0, 0.0), child: Padding( @@ -145,7 +147,7 @@ class DestinationPickerView extends GetWidget { child: TreeView( startExpanded: false, key: Key('folder_mailbox_list'), - children: _buildListChildTileWidget(context, controller.folderMailboxTree.value.root.childrenItems!)))) + children: _buildListChildTileWidget(context, controller.folderMailboxNodeList)))) : SizedBox.shrink() ); } @@ -155,28 +157,40 @@ class DestinationPickerView extends GetWidget { .map((mailboxNode) => mailboxNode.hasChildren() ? Padding( padding: EdgeInsets.only(left: 20), - child: TreeViewChild( - key: Key('children_tree_mailbox_child'), - parent: MailBoxFolderTileBuilder( - context, - _imagePaths, - _responsiveUtils, - mailboxNode, - mailboxDisplayed: MailboxDisplayed.destinationPicker) - .build(), - children: _buildListChildTileWidget(context, mailboxNode.childrenItems!))) + child: TreeViewChild( + context, + key: Key('children_tree_mailbox_child'), + isExpanded: mailboxNode.expandMode == ExpandMode.EXPAND, + parent: (MailBoxFolderTileBuilder( + context, + _imagePaths, + _responsiveUtils, + mailboxNode, + mailboxDisplayed: MailboxDisplayed.destinationPicker) + ..addOnSelectMailboxFolderClick((mailboxNode) => controller.moveEmailToMailboxAction(mailboxNode.item.toPresentationMailboxWithMailboxPath( + mailboxNode.getPathMailboxNode( + controller.folderMailboxTree, + controller.defaultMailboxList, + )))) + ..addOnExpandFolderActionClick((mailboxNode) => controller.toggleMailboxFolder(mailboxNode))) + .build(), + children: _buildListChildTileWidget(context, mailboxNode.childrenItems!) + ).build()) : Padding( padding: EdgeInsets.only(left: 20), - child: GestureDetector( - onTap: () => controller.moveEmailToMailboxAction(mailboxNode.item), - child: MailBoxFolderTileBuilder( + child: (MailBoxFolderTileBuilder( context, _imagePaths, _responsiveUtils, mailboxNode, mailboxDisplayed: MailboxDisplayed.destinationPicker) - .build(), - ))) + ..addOnSelectMailboxFolderClick((mailboxNode) => controller.moveEmailToMailboxAction(mailboxNode.item.toPresentationMailboxWithMailboxPath( + mailboxNode.getPathMailboxNode( + controller.folderMailboxTree, + controller.defaultMailboxList, + ))))) + .build(), + )) .toList(); } } \ No newline at end of file diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index d2f932f4c..a5fa066a8 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -11,7 +11,7 @@ abstract class EmailDataSource { Future sendEmail(AccountId accountId, EmailRequest emailRequest); - Future markAsRead(AccountId accountId, EmailId emailId, ReadActions readActions); + Future> markAsRead(AccountId accountId, List emails, ReadActions readActions); Future> downloadAttachments( List attachments, @@ -28,5 +28,5 @@ abstract class EmailDataSource { CancelToken cancelToken ); - Future moveToMailbox(AccountId accountId, MoveRequest moveRequest); + Future> moveToMailbox(AccountId accountId, MoveRequest moveRequest); } \ No newline at end of file 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 2fbf04712..2731397da 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -32,9 +32,9 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future markAsRead(AccountId accountId, EmailId emailId, ReadActions readActions) { + Future> markAsRead(AccountId accountId, List emails, ReadActions readActions) { return Future.sync(() async { - return await emailAPI.markAsRead(accountId, emailId, readActions); + return await emailAPI.markAsRead(accountId, emails, readActions); }).catchError((error) { throw error; }); @@ -70,7 +70,7 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future moveToMailbox(AccountId accountId, MoveRequest moveRequest) { + Future> moveToMailbox(AccountId accountId, MoveRequest moveRequest) { return Future.sync(() async { return await emailAPI.moveToMailbox(accountId, moveRequest); }).catchError((error) { diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index b35bce6b2..2fac78fa6 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -15,7 +15,6 @@ import 'package:jmap_dart_client/jmap/jmap_request.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.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/keyword_identifier.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/email/submission/address.dart'; @@ -113,28 +112,33 @@ class EmailAPI { }); } - Future markAsRead(AccountId accountId, EmailId emailId, ReadActions readAction) async { + Future> markAsRead(AccountId accountId, List emails, ReadActions readActions) async { + final emailIds = emails.map((email) => email.id).toList(); + final setEmailMethod = SetEmailMethod(accountId) - ..addUpdates({ - emailId.id: KeyWordIdentifier.emailSeen.generateReadActionPath(readAction) - }); + ..addUpdates(emailIds.generateMapUpdateObjectMarkAsRead(readActions)); + + final getEmailMethod = GetEmailMethod(accountId) + ..addIds(emailIds.toIds().toSet()) + ..addProperties(Properties({'keywords'})); final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + requestBuilder.invocation(setEmailMethod); + + final getEmailInvocation = requestBuilder.invocation(getEmailMethod); final response = await (requestBuilder ..usings(setEmailMethod.requiredCapabilities)) .build() .execute(); - final setEmailResponse = response.parse( - setEmailInvocation.methodCallId, - SetEmailResponse.deserialize); + final getEmailResponse = response.parse( + getEmailInvocation.methodCallId, + GetEmailResponse.deserialize); return Future.sync(() async { - final emailUpdated = setEmailResponse!.updated![emailId.id]; - return emailUpdated == null; + return getEmailResponse!.list; }).catchError((error) { throw error; }); @@ -188,12 +192,10 @@ class EmailAPI { cancelToken: cancelToken); } - Future moveToMailbox(AccountId accountId, MoveRequest moveRequest) async { + Future> moveToMailbox(AccountId accountId, MoveRequest moveRequest) async { final setEmailMethod = SetEmailMethod(accountId) - ..addUpdates({ - moveRequest.emailId.id: moveRequest.currentMailboxId - .generateMoveToMailboxActionPath(moveRequest.destinationMailboxId) - }); + ..addUpdates(moveRequest.emailIds + .generateMapUpdateObjectMoveToMailbox(moveRequest.currentMailboxId, moveRequest.destinationMailboxId)); final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); @@ -209,8 +211,10 @@ class EmailAPI { SetEmailResponse.deserialize); return Future.sync(() async { - final emailUpdated = setEmailResponse!.updated![moveRequest.emailId.id]; - return emailUpdated == null; + final mapUpdated = setEmailResponse!.updated!; + return moveRequest.emailIds + .where((emailId) => mapUpdated.containsKey(emailId.id)) + .toList(); }).catchError((error) { throw error; }); diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index 3299f99b1..568944b06 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -25,8 +25,8 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future markAsRead(AccountId accountId, EmailId emailId, ReadActions readAction) { - return emailDataSource.markAsRead(accountId, emailId, readAction); + Future> markAsRead(AccountId accountId, List emails, ReadActions readActions) { + return emailDataSource.markAsRead(accountId, emails, readActions); } @override @@ -56,7 +56,7 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future moveToMailbox(AccountId accountId, MoveRequest moveRequest) { + Future> moveToMailbox(AccountId accountId, MoveRequest moveRequest) { return emailDataSource.moveToMailbox(accountId, moveRequest); } } \ No newline at end of file diff --git a/lib/features/email/domain/model/move_request.dart b/lib/features/email/domain/model/move_request.dart index e90f6013d..2290db693 100644 --- a/lib/features/email/domain/model/move_request.dart +++ b/lib/features/email/domain/model/move_request.dart @@ -6,29 +6,28 @@ import 'package:model/model.dart'; class MoveRequest with EquatableMixin { - final EmailId emailId; + final List emailIds; final MailboxId currentMailboxId; - final MailboxName currentMailboxName; final MailboxId destinationMailboxId; - final MailboxName destinationMailboxName; final MoveAction moveAction; + final String? destinationPath; MoveRequest( - this.emailId, + this.emailIds, this.currentMailboxId, - this.currentMailboxName, this.destinationMailboxId, - this.destinationMailboxName, this.moveAction, + { + this.destinationPath, + } ); @override List get props => [ - emailId, + emailIds, currentMailboxId, - currentMailboxName, destinationMailboxId, - destinationMailboxName, - moveAction + moveAction, + destinationPath, ]; } \ No newline at end of file diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart index dccd09415..046e91362 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -10,7 +10,7 @@ abstract class EmailRepository { Future sendEmail(AccountId accountId, EmailRequest emailRequest); - Future markAsRead(AccountId accountId, EmailId emailId, ReadActions readAction); + Future> markAsRead(AccountId accountId, List emails, ReadActions readActions); Future> downloadAttachments( List attachments, @@ -27,5 +27,5 @@ abstract class EmailRepository { CancelToken cancelToken ); - Future moveToMailbox(AccountId accountId, MoveRequest moveRequest); + Future> moveToMailbox(AccountId accountId, MoveRequest moveRequest); } \ No newline at end of file diff --git a/lib/features/email/domain/state/mark_as_email_read_state.dart b/lib/features/email/domain/state/mark_as_email_read_state.dart index 7103efffa..4f5612f51 100644 --- a/lib/features/email/domain/state/mark_as_email_read_state.dart +++ b/lib/features/email/domain/state/mark_as_email_read_state.dart @@ -1,15 +1,16 @@ import 'package:core/core.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/email/read_actions.dart'; +import 'package:model/model.dart'; class MarkAsEmailReadSuccess extends UIState { - final EmailId emailId; + final Email updatedEmail; final ReadActions readActions; - MarkAsEmailReadSuccess(this.emailId, this.readActions); + MarkAsEmailReadSuccess(this.updatedEmail, this.readActions); @override - List get props => [emailId, readActions]; + List get props => [updatedEmail, readActions]; } class MarkAsEmailReadFailure extends FeatureFailure { diff --git a/lib/features/email/domain/state/move_to_mailbox_state.dart b/lib/features/email/domain/state/move_to_mailbox_state.dart index 06371470b..0beea9b0b 100644 --- a/lib/features/email/domain/state/move_to_mailbox_state.dart +++ b/lib/features/email/domain/state/move_to_mailbox_state.dart @@ -1,13 +1,29 @@ import 'package:core/core.dart'; -import 'package:tmail_ui_user/features/email/domain/model/move_request.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/model.dart'; class MoveToMailboxSuccess extends UIState { - final MoveRequest moveRequest; + final EmailId emailId; + final MailboxId currentMailboxId; + final MailboxId destinationMailboxId; + final MoveAction moveAction; + final String? destinationPath; - MoveToMailboxSuccess(this.moveRequest); + MoveToMailboxSuccess( + this.emailId, + this.currentMailboxId, + this.destinationMailboxId, + this.moveAction, + this.destinationPath); @override - List get props => [moveRequest]; + List get props => [ + emailId, + currentMailboxId, + destinationMailboxId, + moveAction, + destinationPath]; } class MoveToMailboxFailure extends FeatureFailure { diff --git a/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart b/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart index 6bcb0dc8c..858b2d3cf 100644 --- a/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart +++ b/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart @@ -2,19 +2,24 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:model/email/read_actions.dart'; +import 'package:model/model.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; class MarkAsEmailReadInteractor { - final EmailRepository emailRepository; + final EmailRepository _emailRepository; - MarkAsEmailReadInteractor(this.emailRepository); + MarkAsEmailReadInteractor(this._emailRepository); - Stream> execute(AccountId accountId, EmailId emailId, ReadActions readAction) async* { + Stream> execute(AccountId accountId, Email email, ReadActions readAction) async* { try { - final result = await emailRepository.markAsRead(accountId, emailId, readAction); - yield result ? Right(MarkAsEmailReadSuccess(emailId, readAction)) : Left(MarkAsEmailReadFailure(null, readAction)); + final result = await _emailRepository.markAsRead(accountId, [email], readAction); + if (result.isNotEmpty) { + final updatedEmail = email.updatedEmail(newKeywords: result.first.keywords); + yield Right(MarkAsEmailReadSuccess(updatedEmail, readAction)); + } else { + yield Left(MarkAsEmailReadFailure(null, readAction)); + } } catch (e) { yield Left(MarkAsEmailReadFailure(e, readAction)); } diff --git a/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart b/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart index af47f7f6d..0919193d2 100644 --- a/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart +++ b/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart @@ -13,9 +13,16 @@ class MoveToMailboxInteractor { Stream> execute(AccountId accountId, MoveRequest moveRequest) async* { try { final result = await emailRepository.moveToMailbox(accountId, moveRequest); - yield result - ? Right(MoveToMailboxSuccess(moveRequest)) - : Left(MoveToMailboxFailure(null)); + if (result.isNotEmpty) { + yield Right(MoveToMailboxSuccess( + result.first, + moveRequest.currentMailboxId, + moveRequest.destinationMailboxId, + moveRequest.moveAction, + moveRequest.destinationPath)); + } else { + yield Left(MoveToMailboxFailure(null)); + } } catch (e) { yield Left(MoveToMailboxFailure(e)); } diff --git a/lib/features/email/presentation/email_controller.dart b/lib/features/email/presentation/email_controller.dart index 5dbfd6e0f..2edeea9a7 100644 --- a/lib/features/email/presentation/email_controller.dart +++ b/lib/features/email/presentation/email_controller.dart @@ -8,7 +8,6 @@ import 'package:flutter/widgets.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; @@ -47,6 +46,7 @@ class EmailController extends BaseController { final emailAddressExpandMode = ExpandMode.COLLAPSE.obs; final attachmentsExpandMode = ExpandMode.COLLAPSE.obs; final emailContent = Rxn(); + EmailId? _currentEmailId; EmailController( this._getEmailContentInteractor, @@ -62,12 +62,15 @@ class EmailController extends BaseController { void onReady() { super.onReady(); mailboxDashBoardController.selectedEmail.listen((presentationEmail) { - _clearEmailContent(); - final accountId = mailboxDashBoardController.accountId.value; - if (accountId != null && presentationEmail != null) { - _getEmailContentAction(accountId, presentationEmail.id); - if (presentationEmail.isUnReadEmail()) { - markAsEmailRead(presentationEmail, ReadActions.markAsRead); + if (_currentEmailId != presentationEmail?.id) { + _currentEmailId = presentationEmail?.id; + _clearEmailContent(); + final accountId = mailboxDashBoardController.accountId.value; + if (accountId != null && presentationEmail != null) { + _getEmailContentAction(accountId, presentationEmail.id); + if (presentationEmail.isUnReadEmail()) { + markAsEmailRead(presentationEmail, ReadActions.markAsRead); + } } } }); @@ -131,11 +134,14 @@ class EmailController extends BaseController { final accountId = mailboxDashBoardController.accountId.value; final mailboxCurrent = mailboxDashBoardController.selectedMailbox.value; if (accountId != null && mailboxCurrent != null) { - consumeState(_markAsEmailReadInteractor.execute(accountId, presentationEmail.id, readActions)); + consumeState(_markAsEmailReadInteractor.execute(accountId, presentationEmail.toEmail(), readActions)); } } void _markAsEmailReadSuccess(Success success) { + if (success is MarkAsEmailReadSuccess) { + mailboxDashBoardController.setSelectedEmail(success.updatedEmail.toPresentationEmail(selectMode: SelectMode.ACTIVE)); + } mailboxDashBoardController.dispatchState(Right(success)); if (success is MarkAsEmailReadSuccess && success.readActions == ReadActions.markAsUnread) { @@ -144,7 +150,9 @@ class EmailController extends BaseController { } void _markAsEmailReadFailure(Failure failure) { - backToThreadView(); + if (failure is MarkAsEmailReadFailure && failure.readActions == ReadActions.markAsUnread) { + backToThreadView(); + } } void toggleDisplayAttachmentsAction() { @@ -244,19 +252,18 @@ class EmailController extends BaseController { final accountId = mailboxDashBoardController.accountId.value; if (currentMailbox != null && accountId != null) { - final mailboxDestination = await push( + final destinationMailbox = await push( AppRoutes.DESTINATION_PICKER, arguments: DestinationPickerArguments(accountId, [email.id], currentMailbox) ); - if (mailboxDestination != null && mailboxDestination is PresentationMailbox) { + if (destinationMailbox != null && destinationMailbox is PresentationMailbox) { _moveToMailbox(accountId, MoveRequest( - email.id, + [email.id], currentMailbox.id, - currentMailbox.name ?? MailboxName(''), - mailboxDestination.id, - mailboxDestination.name ?? MailboxName(''), - MoveAction.moveTo)); + destinationMailbox.id, + MoveAction.moveTo, + destinationPath: destinationMailbox.mailboxPath)); } } } @@ -269,19 +276,17 @@ class EmailController extends BaseController { mailboxDashBoardController.dispatchState(Right(success)); if (success is MoveToMailboxSuccess - && success.moveRequest.moveAction == MoveAction.moveTo + && success.moveAction == MoveAction.moveTo && Get.context != null && Get.overlayContext != null) { _appToast.showToastWithAction( Get.overlayContext!, - AppLocalizations.of(Get.context!).moved_to_mailbox(success.moveRequest.destinationMailboxName.name), + AppLocalizations.of(Get.context!).moved_to_mailbox(success.destinationPath ?? ''), AppLocalizations.of(Get.context!).undo_action, () { final newMoveRequest = MoveRequest( - success.moveRequest.emailId, - success.moveRequest.destinationMailboxId, - success.moveRequest.destinationMailboxName, - success.moveRequest.currentMailboxId, - success.moveRequest.currentMailboxName, + [success.emailId], + success.destinationMailboxId, + success.currentMailboxId, MoveAction.undo); _undoMoveToMailbox(newMoveRequest); } diff --git a/lib/features/mailbox/presentation/extensions/list_mailbox_node_extension.dart b/lib/features/mailbox/presentation/extensions/list_mailbox_node_extension.dart new file mode 100644 index 000000000..79ddcaeb0 --- /dev/null +++ b/lib/features/mailbox/presentation/extensions/list_mailbox_node_extension.dart @@ -0,0 +1,18 @@ + +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; + +extension ListMailboxNodeExtensions on List { + List? updateNode(MailboxId currentMailboxId, MailboxNode newNode) { + return map((MailboxNode child) { + if (child.item.id == currentMailboxId) { + return newNode; + } else { + if (child.hasChildren()) { + return child.copyWith(mailboxNodes: child.updateNode(currentMailboxId, newNode)); + } + return child; + } + }).toList(); + } +} \ 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 646aaa322..b1ccf6147 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -11,10 +11,11 @@ import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_i import 'package:tmail_ui_user/features/mailbox/domain/state/get_all_mailboxes_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree_builder.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/list_mailbox_node_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.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/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; @@ -26,8 +27,8 @@ class MailboxController extends BaseController { final TreeBuilder _treeBuilder; final ResponsiveUtils responsiveUtils; - final folderMailboxTree = MailboxTree(MailboxNode.root()).obs; final defaultMailboxList = [].obs; + final folderMailboxNodeList = [].obs; MailboxController( this._getAllMailboxInteractor, @@ -51,6 +52,10 @@ class MailboxController extends BaseController { success is MarkAsMultipleEmailReadAllSuccess || success is MarkAsMultipleEmailReadHasSomeEmailFailure) { refreshGetAllMailboxAction(); + } else if (success is MoveMultipleEmailToMailboxAllSuccess + || success is MoveMultipleEmailToMailboxHasSomeEmailFailure) { + mailboxDashBoardController.clearState(); + refreshGetAllMailboxAction(); } }); }); @@ -97,7 +102,20 @@ class MailboxController extends BaseController { void onError(error) {} void _buildTree(List folderMailboxList) async { - folderMailboxTree.value = await _treeBuilder.generateMailboxTree(folderMailboxList); + final _folderMailboxTree = await _treeBuilder.generateMailboxTree(folderMailboxList); + folderMailboxNodeList.value = _folderMailboxTree.root.childrenItems ?? []; + } + + void toggleMailboxFolder(MailboxNode mailboxNode) { + final newExpandMode = mailboxNode.expandMode == ExpandMode.COLLAPSE + ? ExpandMode.EXPAND + : ExpandMode.COLLAPSE; + + final newMailboxNodeList = folderMailboxNodeList.updateNode( + mailboxNode.item.id, + mailboxNode.copyWith(newExpandMode: newExpandMode)) ?? []; + + folderMailboxNodeList.value = newMailboxNodeList; } void _setUpMapMailboxIdDefault(List defaultMailboxList) { diff --git a/lib/features/mailbox/presentation/mailbox_view.dart b/lib/features/mailbox/presentation/mailbox_view.dart index 24e24650a..b138ef769 100644 --- a/lib/features/mailbox/presentation/mailbox_view.dart +++ b/lib/features/mailbox/presentation/mailbox_view.dart @@ -1,7 +1,4 @@ import 'package:core/core.dart'; -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/resources/image_paths.dart'; -import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; @@ -174,7 +171,7 @@ class MailboxView extends GetWidget { } Widget _buildFolderMailbox(BuildContext context) { - return Obx(() => controller.folderMailboxTree.value.root.hasChildren() + return Obx(() => controller.folderMailboxNodeList.isNotEmpty ? Transform( transform: Matrix4.translationValues(responsiveUtils.isMobile(context) ? 0.0 : -15.0, 0.0, 0.0), child: Padding( @@ -182,7 +179,7 @@ class MailboxView extends GetWidget { child: TreeView( startExpanded: false, key: Key('folder_mailbox_list'), - children: _buildListChildTileWidget(context, controller.folderMailboxTree.value.root.childrenItems!))) + children: _buildListChildTileWidget(context, controller.folderMailboxNodeList))) ) : SizedBox.shrink() ); @@ -192,24 +189,27 @@ class MailboxView extends GetWidget { return listMailboxNode .map((mailboxNode) => mailboxNode.hasChildren() ? Padding( - padding: EdgeInsets.only(left: 16), + padding: EdgeInsets.only(left: 20), child: TreeViewChild( - key: Key('children_tree_mailbox_child'), - parent: Obx(() => MailBoxFolderTileBuilder( - context, - imagePaths, - responsiveUtils, - mailboxNode, - selectMode: controller.getSelectMode( - mailboxNode.item, - controller.mailboxDashBoardController.selectedMailbox.value)) - .build()), - children: _buildListChildTileWidget(context, mailboxNode.childrenItems!))) + context, + key: Key('children_tree_mailbox_child'), + isExpanded: mailboxNode.expandMode == ExpandMode.EXPAND, + parent: Obx(() => (MailBoxFolderTileBuilder( + context, + imagePaths, + responsiveUtils, + mailboxNode, + selectMode: controller.getSelectMode( + mailboxNode.item, + controller.mailboxDashBoardController.selectedMailbox.value)) + ..addOnSelectMailboxFolderClick((mailboxNode) => controller.selectMailbox(context, mailboxNode.item)) + ..addOnExpandFolderActionClick((mailboxNode) => controller.toggleMailboxFolder(mailboxNode))) + .build()), + children: _buildListChildTileWidget(context, mailboxNode.childrenItems!) + ).build()) : Padding( - padding: EdgeInsets.only(left: 16), - child: Obx(() => GestureDetector( - onTap: () => controller.selectMailbox(context, mailboxNode.item), - child: MailBoxFolderTileBuilder( + padding: EdgeInsets.only(left: 20), + child: Obx(() => (MailBoxFolderTileBuilder( context, imagePaths, responsiveUtils, @@ -217,8 +217,9 @@ class MailboxView extends GetWidget { selectMode: controller.getSelectMode( mailboxNode.item, controller.mailboxDashBoardController.selectedMailbox.value)) - .build(), - )))) + ..addOnSelectMailboxFolderClick((mailboxNode) => controller.selectMailbox(context, mailboxNode.item))) + .build(), + ))) .toList(); } diff --git a/lib/features/mailbox/presentation/model/mailbox_node.dart b/lib/features/mailbox/presentation/model/mailbox_node.dart index 790218ada..dc4cfad87 100644 --- a/lib/features/mailbox/presentation/model/mailbox_node.dart +++ b/lib/features/mailbox/presentation/model/mailbox_node.dart @@ -1,19 +1,24 @@ import 'package:equatable/equatable.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/mailbox/expand_mode.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree.dart'; class MailboxNode with EquatableMixin{ static PresentationMailbox _root = PresentationMailbox(MailboxId(Id('root'))); PresentationMailbox item; List? childrenItems; + ExpandMode expandMode; factory MailboxNode.root() => MailboxNode(_root); + static PresentationMailbox rootItem() => _root; + bool hasChildren() => childrenItems?.isNotEmpty ?? false; - MailboxNode(this.item, {this.childrenItems}); + MailboxNode(this.item, {this.childrenItems, this.expandMode = ExpandMode.COLLAPSE}); void addChildNode(MailboxNode node) { if (childrenItems == null) { @@ -22,6 +27,60 @@ class MailboxNode with EquatableMixin{ childrenItems?.add(node); } + String getPathMailboxNode(MailboxTree mailboxTree, List defaultMailboxList) { + String path = '${item.name?.name}'; + + var parentId = item.parentId; + + while(parentId != null) { + var parent = mailboxTree.findNode(parentId); + if (parent == null) { + try { + final parentItem = defaultMailboxList.firstWhere((mailbox) => mailbox.id == parentId); + parent = MailboxNode(parentItem); + } catch(e) {} + } + + if (parent != null) { + path = '${parent.item.name?.name}/$path'; + parentId = parent.item.parentId; + } else { + break; + } + } + + return path; + } + + List? updateNode(MailboxId mailboxId, MailboxNode newNode, {MailboxNode? parent}) { + List? _children = parent == null ? this.childrenItems : parent.childrenItems; + return _children?.map((MailboxNode child) { + if (child.item.id == mailboxId) { + return newNode; + } else { + if (child.hasChildren()) { + return child.copyWith( + mailboxNodes: updateNode( + mailboxId, + newNode, + parent: child, + ), + ); + } + return child; + } + }).toList(); + } + @override List get props => [item, childrenItems]; -} \ No newline at end of file +} + +extension MailboxNodeExtension on MailboxNode { + MailboxNode copyWith({ExpandMode? newExpandMode, List? mailboxNodes}) { + return MailboxNode( + item, + childrenItems: mailboxNodes ?? childrenItems, + expandMode: newExpandMode ?? expandMode); + } +} diff --git a/lib/features/mailbox/presentation/model/mailbox_tree.dart b/lib/features/mailbox/presentation/model/mailbox_tree.dart index 32461a07f..ce89f2e87 100644 --- a/lib/features/mailbox/presentation/model/mailbox_tree.dart +++ b/lib/features/mailbox/presentation/model/mailbox_tree.dart @@ -1,5 +1,8 @@ +import 'dart:collection'; + import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'mailbox_node.dart'; @@ -7,6 +10,23 @@ class MailboxTree with EquatableMixin { MailboxNode root; MailboxTree(this.root); + MailboxNode? findNode(MailboxId? mailboxId) { + var result; + final queue = ListQueue(); + queue.addLast(root); + while (queue.isNotEmpty && mailboxId != null) { + final currentNode = queue.removeFirst(); + if (mailboxId == currentNode.item.id) { + result = currentNode; + break; + } + currentNode.childrenItems?.forEach((child) { + queue.addLast(child); + }); + } + return result; + } + @override List get props => [root]; } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/widgets/mailbox_folder_tile_builder.dart b/lib/features/mailbox/presentation/widgets/mailbox_folder_tile_builder.dart index 49044483a..982dee3f6 100644 --- a/lib/features/mailbox/presentation/widgets/mailbox_folder_tile_builder.dart +++ b/lib/features/mailbox/presentation/widgets/mailbox_folder_tile_builder.dart @@ -3,9 +3,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:model/mailbox/select_mode.dart'; +import 'package:model/model.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_displayed.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; +typedef OnExpandFolderActionClick = void Function(MailboxNode mailboxNode); +typedef OnSelectMailboxFolderClick = void Function(MailboxNode mailboxNode); + class MailBoxFolderTileBuilder { final MailboxNode _mailboxNode; @@ -15,6 +19,9 @@ class MailBoxFolderTileBuilder { final SelectMode selectMode; final MailboxDisplayed mailboxDisplayed; + OnExpandFolderActionClick? _onExpandFolderActionClick; + OnSelectMailboxFolderClick? _onSelectMailboxFolderClick; + MailBoxFolderTileBuilder( this._context, this._imagePaths, @@ -26,6 +33,14 @@ class MailBoxFolderTileBuilder { } ); + void addOnExpandFolderActionClick(OnExpandFolderActionClick onExpandFolderActionClick) { + this._onExpandFolderActionClick = onExpandFolderActionClick; + } + + void addOnSelectMailboxFolderClick(OnSelectMailboxFolderClick onSelectMailboxFolderClick) { + this._onSelectMailboxFolderClick = onSelectMailboxFolderClick; + } + Widget build() { return Container( key: Key('mailbox_folder_tile'), @@ -39,6 +54,11 @@ class MailBoxFolderTileBuilder { child: MediaQuery( data: MediaQueryData(padding: EdgeInsets.zero), child: ListTile( + onTap: () { + if (_onSelectMailboxFolderClick != null) { + _onSelectMailboxFolderClick!(_mailboxNode); + } + }, contentPadding: EdgeInsets.zero, leading: Padding( padding: EdgeInsets.only(left: mailboxDisplayed == MailboxDisplayed.mailbox @@ -72,14 +92,21 @@ class MailBoxFolderTileBuilder { : 36, left: mailboxDisplayed == MailboxDisplayed.mailbox ? 16 : 16), child: _mailboxNode.hasChildren() - ? SvgPicture.asset( - _imagePaths.icFolderArrow, - width: 12, - height: 12, - color: selectMode == SelectMode.ACTIVE - ? AppColor.mailboxSelectedIconColor - : AppColor.mailboxIconColor, - fit: BoxFit.fill) + ? GestureDetector( + onTap: () { + if (_onExpandFolderActionClick != null) { + _onExpandFolderActionClick!(_mailboxNode); + } + }, + child: SvgPicture.asset( + _mailboxNode.expandMode == ExpandMode.EXPAND ? _imagePaths.icExpandFolder : _imagePaths.icFolderArrow, + width: _mailboxNode.expandMode == ExpandMode.EXPAND ? 8 : 12, + height: _mailboxNode.expandMode == ExpandMode.EXPAND ? 8 : 12, + color: selectMode == SelectMode.ACTIVE + ? AppColor.mailboxSelectedIconColor + : AppColor.mailboxIconColor, + fit: BoxFit.fill), + ) : SizedBox.shrink()) ) ) diff --git a/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart b/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart index e01d61953..cefe9b79e 100644 --- a/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart +++ b/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart @@ -1,35 +1,33 @@ import 'package:core/core.dart'; -import 'package:dartz/dartz.dart'; import 'package:model/email/read_actions.dart'; class MarkAsMultipleEmailReadAllSuccess extends UIState { - final List> resultList; + final int countMarkAsReadSuccess; final ReadActions readActions; - MarkAsMultipleEmailReadAllSuccess(this.resultList, this.readActions); + MarkAsMultipleEmailReadAllSuccess(this.countMarkAsReadSuccess, this.readActions); @override - List get props => [resultList, readActions]; + List get props => [countMarkAsReadSuccess, readActions]; } class MarkAsMultipleEmailReadAllFailure extends FeatureFailure { - final List> resultList; final ReadActions readActions; - MarkAsMultipleEmailReadAllFailure(this.resultList, this.readActions); + MarkAsMultipleEmailReadAllFailure(this.readActions); @override - List get props => [resultList, readActions]; + List get props => [readActions]; } class MarkAsMultipleEmailReadHasSomeEmailFailure extends UIState { - final List> resultList; + final int countMarkAsReadSuccess; final ReadActions readActions; - MarkAsMultipleEmailReadHasSomeEmailFailure(this.resultList, this.readActions); + MarkAsMultipleEmailReadHasSomeEmailFailure(this.countMarkAsReadSuccess, this.readActions); @override - List get props => [resultList, readActions]; + List get props => [countMarkAsReadSuccess, readActions]; } class MarkAsMultipleEmailReadFailure extends FeatureFailure { diff --git a/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart b/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart new file mode 100644 index 000000000..5c8cb2813 --- /dev/null +++ b/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart @@ -0,0 +1,69 @@ +import 'package:core/core.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/model.dart'; + +class MoveMultipleEmailToMailboxAllSuccess extends UIState { + final List movedListEmailId; + final MailboxId currentMailboxId; + final MailboxId destinationMailboxId; + final MoveAction moveAction; + final String? destinationPath; + + MoveMultipleEmailToMailboxAllSuccess( + this.movedListEmailId, + this.currentMailboxId, + this.destinationMailboxId, + this.moveAction, + this.destinationPath); + + @override + List get props => [ + movedListEmailId, + currentMailboxId, + destinationMailboxId, + moveAction, + destinationPath]; +} + +class MoveMultipleEmailToMailboxAllFailure extends FeatureFailure { + final MoveAction moveAction; + + MoveMultipleEmailToMailboxAllFailure(this.moveAction); + + @override + List get props => [moveAction]; +} + +class MoveMultipleEmailToMailboxHasSomeEmailFailure extends UIState { + final List movedListEmailId; + final MailboxId currentMailboxId; + final MailboxId destinationMailboxId; + final MoveAction moveAction; + final String? destinationPath; + + MoveMultipleEmailToMailboxHasSomeEmailFailure( + this.movedListEmailId, + this.currentMailboxId, + this.destinationMailboxId, + this.moveAction, + this.destinationPath); + + @override + List get props => [ + movedListEmailId, + currentMailboxId, + destinationMailboxId, + moveAction, + destinationPath]; +} + +class MoveMultipleEmailToMailboxFailure extends FeatureFailure { + final exception; + final MoveAction moveAction; + + MoveMultipleEmailToMailboxFailure(this.exception, this.moveAction); + + @override + List get props => [exception, moveAction]; +} \ No newline at end of file diff --git a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart index 758efe375..2a0006987 100644 --- a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart +++ b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart @@ -2,40 +2,37 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:model/email/read_actions.dart'; -import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart'; +import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; class MarkAsMultipleEmailReadInteractor { - final MarkAsEmailReadInteractor markAsEmailReadInteractor; + final EmailRepository _emailRepository; - MarkAsMultipleEmailReadInteractor(this.markAsEmailReadInteractor); + MarkAsMultipleEmailReadInteractor(this._emailRepository); Stream> execute( AccountId accountId, - List listEmailId, + List emails, ReadActions readAction ) async* { try { - final listResult = await Future.wait(listEmailId.map((emailId) async { - final result = await markAsEmailReadInteractor.execute(accountId, emailId, readAction).toList(); - return result.first; - })); - if (listResult.length == 1) { - yield listResult.first; + yield Right(LoadingState()); + + final listEmailNeedMarkAsRead = emails + .where((email) => readAction == ReadActions.markAsUnread ? email.isReadEmail() : email.isUnReadEmail()) + .toList(); + + final result = await _emailRepository.markAsRead(accountId, listEmailNeedMarkAsRead, readAction); + + if (listEmailNeedMarkAsRead.length == result.length) { + final countMarkAsReadSuccess = emails.length; + yield Right(MarkAsMultipleEmailReadAllSuccess(countMarkAsReadSuccess, readAction)); + } else if (result.isEmpty) { + yield Left(MarkAsMultipleEmailReadAllFailure(readAction)); } else { - var failedFileCount = 0; - listResult.forEach((element) { - if (element is Left) { - failedFileCount++; - } - }); - if (failedFileCount == 0) { - yield Right(MarkAsMultipleEmailReadAllSuccess(listResult, readAction)); - } else if (failedFileCount == listResult.length) { - yield Left(MarkAsMultipleEmailReadAllFailure(listResult, readAction)); - } - yield Right(MarkAsMultipleEmailReadHasSomeEmailFailure(listResult, readAction)); + final countMarkAsReadSuccess = emails.length - (listEmailNeedMarkAsRead.length - result.length); + yield Right(MarkAsMultipleEmailReadHasSomeEmailFailure(countMarkAsReadSuccess, readAction)); } } catch (e) { yield Left(MarkAsMultipleEmailReadFailure(e, readAction)); diff --git a/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart b/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart new file mode 100644 index 000000000..8e5237bc4 --- /dev/null +++ b/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:core/core.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:tmail_ui_user/features/email/domain/model/move_request.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; + +class MoveMultipleEmailToMailboxInteractor { + final EmailRepository _emailRepository; + + MoveMultipleEmailToMailboxInteractor(this._emailRepository); + + Stream> execute(AccountId accountId, MoveRequest moveRequest) async* { + try { + yield Right(LoadingState()); + + final result = await _emailRepository.moveToMailbox(accountId, moveRequest); + + if (moveRequest.emailIds.length == result.length) { + yield Right(MoveMultipleEmailToMailboxAllSuccess( + result, + moveRequest.currentMailboxId, + moveRequest.destinationMailboxId, + moveRequest.moveAction, + moveRequest.destinationPath)); + } else if (result.isEmpty) { + yield Left(MoveMultipleEmailToMailboxAllFailure(moveRequest.moveAction)); + } else { + yield Right(MoveMultipleEmailToMailboxHasSomeEmailFailure( + result, + moveRequest.currentMailboxId, + moveRequest.destinationMailboxId, + moveRequest.moveAction, + moveRequest.destinationPath)); + } + } catch (e) { + yield Left(MoveMultipleEmailToMailboxFailure(e, moveRequest.moveAction)); + } + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_bindings.dart b/lib/features/thread/presentation/thread_bindings.dart index 68e3dd20a..a8cb87cf0 100644 --- a/lib/features/thread/presentation/thread_bindings.dart +++ b/lib/features/thread/presentation/thread_bindings.dart @@ -8,6 +8,7 @@ import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/email/data/repository/email_repository_impl.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/data/datasource/thread_datasource.dart'; import 'package:tmail_ui_user/features/thread/data/datasource_impl/thread_datasource_impl.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; @@ -15,6 +16,7 @@ import 'package:tmail_ui_user/features/thread/data/repository/thread_repository_ import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.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/mark_as_multiple_email_read_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/thread_controller.dart'; class ThreadBindings extends Bindings { @@ -31,13 +33,16 @@ class ThreadBindings extends Bindings { Get.lazyPut(() => EmailRepositoryImpl(Get.find())); Get.lazyPut(() => Get.find()); Get.lazyPut(() => MarkAsEmailReadInteractor(Get.find())); - Get.lazyPut(() => MarkAsMultipleEmailReadInteractor(Get.find())); + Get.lazyPut(() => MarkAsMultipleEmailReadInteractor(Get.find())); + Get.lazyPut(() => MoveToMailboxInteractor(Get.find())); + Get.lazyPut(() => MoveMultipleEmailToMailboxInteractor(Get.find())); Get.put(ThreadController( Get.find(), Get.find(), Get.find(), Get.find(), Get.find(), + Get.find(), )); } } \ 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 ddefe9bc1..b77e3b392 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -16,6 +16,7 @@ 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/destination_picker/presentation/model/destination_picker_arguments.dart'; +import 'package:tmail_ui_user/features/email/domain/model/move_request.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; @@ -23,8 +24,10 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_da import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_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/features/thread/domain/usecases/get_emails_in_mailbox_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/move_multiple_email_to_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/load_more_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; @@ -38,6 +41,7 @@ class ThreadController extends BaseController { final AppToast _appToast; final ResponsiveUtils responsiveUtils; final ScrollController listEmailController; + final MoveMultipleEmailToMailboxInteractor _moveMultipleEmailToMailboxInteractor; final _properties = Properties({ 'id', 'subject', 'from', 'to', 'cc', 'bcc', 'keywords', 'receivedAt', @@ -58,6 +62,7 @@ class ThreadController extends BaseController { this.listEmailController, this._markAsMultipleEmailReadInteractor, this._appToast, + this._moveMultipleEmailToMailboxInteractor, ); @override @@ -73,20 +78,17 @@ class ThreadController extends BaseController { if (_currentMailboxId != selectedMailbox?.id) { _currentMailboxId = selectedMailbox?.id; refreshGetAllEmailAction(); - } else { - mailboxDashBoardController.viewState.value.map((success) { - if (success is MarkAsEmailReadSuccess || - success is MarkAsMultipleEmailReadAllSuccess || - success is MarkAsMultipleEmailReadHasSomeEmailFailure) { - _refreshListEmail(); - } - }); } }); mailboxDashBoardController.viewState.listen((state) { state.map((success) { - if (success is MoveToMailboxSuccess) { + if (success is MarkAsEmailReadSuccess + || success is MarkAsMultipleEmailReadAllSuccess + || success is MarkAsMultipleEmailReadHasSomeEmailFailure) { + _refreshListEmail(); + mailboxDashBoardController.clearState(); + } else if (success is MoveToMailboxSuccess) { _refreshListEmail(); mailboxDashBoardController.clearState(); } @@ -108,19 +110,20 @@ class ThreadController extends BaseController { (failure) { if (failure is GetAllEmailFailure) { _resetPositionCurrentAndLoadMoreState(); - } else if (failure is MarkAsEmailReadFailure || - failure is MarkAsMultipleEmailReadAllFailure || - failure is MarkAsMultipleEmailReadFailure) { + } else if (failure is MarkAsMultipleEmailReadAllFailure + || failure is MarkAsMultipleEmailReadFailure) { _markAsSelectedEmailReadFailure(failure); } }, (success) { if (success is GetAllEmailSuccess) { _getAllEmailSuccess(success); - } else if (success is MarkAsEmailReadSuccess || - success is MarkAsMultipleEmailReadAllSuccess || - success is MarkAsMultipleEmailReadHasSomeEmailFailure) { + } else if (success is MarkAsMultipleEmailReadAllSuccess + || success is MarkAsMultipleEmailReadHasSomeEmailFailure) { _markAsSelectedEmailReadSuccess(success); + } else if (success is MoveMultipleEmailToMailboxAllSuccess + || success is MoveMultipleEmailToMailboxHasSomeEmailFailure) { + _moveSelectedMultipleEmailToMailboxSuccess(success); } } ); @@ -177,7 +180,8 @@ class ThreadController extends BaseController { void refreshGetAllEmailAction() { loadMoreState.value = LoadMoreState.IDLE; positionCurrent = 0; - emailList.clear(); + dispatchState(Right(LoadingState())); + emailList.value = []; final accountId = mailboxDashBoardController.accountId.value; @@ -245,9 +249,11 @@ class ThreadController extends BaseController { } void _refreshListEmail() { + currentSelectMode.value = SelectMode.INACTIVE; final newLimit = emailList.isNotEmpty ? UnsignedInt(emailList.length) : ThreadConstants.defaultLimit; loadMoreState.value = LoadMoreState.IDLE; - emailList.clear(); + dispatchState(Right(LoadingState())); + emailList.value = []; final accountId = mailboxDashBoardController.accountId.value; @@ -262,61 +268,42 @@ class ThreadController extends BaseController { } } - void markAsSelectedEmailRead(List listEmail, {bool fromContextMenuAction = false}) { + void markAsSelectedEmailRead(List listPresentationEmail, {bool fromContextMenuAction = false}) { if (fromContextMenuAction) { popBack(); } - final readAction = isEmailAllRead(listEmail) ? ReadActions.markAsUnread : ReadActions.markAsRead; - - final listEmailId = listEmail - .where((email) => readAction == ReadActions.markAsUnread ? email.isReadEmail() : email.isUnReadEmail()) - .map((email) => email.id) - .toList(); + final readAction = isEmailAllRead(listPresentationEmail) ? ReadActions.markAsUnread : ReadActions.markAsRead; final accountId = mailboxDashBoardController.accountId.value; final mailboxCurrent = mailboxDashBoardController.selectedMailbox.value; if (accountId != null && mailboxCurrent != null) { - consumeState(_markAsMultipleEmailReadInteractor.execute(accountId, listEmailId, readAction)); + final listEmail = listPresentationEmail.map((presentationEmail) => presentationEmail.toEmail()).toList(); + consumeState(_markAsMultipleEmailReadInteractor.execute(accountId, listEmail, readAction)); } } void _markAsSelectedEmailReadSuccess(Success success) { cancelSelectEmail(); - List listEmailId = []; - ReadActions? readActions; + mailboxDashBoardController.dispatchState(Right(success)); - if (success is MarkAsEmailReadSuccess) { - listEmailId.add(success.emailId); - readActions = success.readActions; - } else if (success is MarkAsMultipleEmailReadAllSuccess) { - success.resultList.forEach((either) { - either.map((success) { - if (success is MarkAsEmailReadSuccess) { - listEmailId.add(success.emailId); - } - }); - }); + ReadActions? readActions; + int countMarkAsReadSuccess = 0; + + if (success is MarkAsMultipleEmailReadAllSuccess) { readActions = success.readActions; + countMarkAsReadSuccess = success.countMarkAsReadSuccess; } else if (success is MarkAsMultipleEmailReadHasSomeEmailFailure) { - success.resultList.forEach((either) { - either.map((success) { - if (success is MarkAsEmailReadSuccess) { - listEmailId.add(success.emailId); - } - }); - }); readActions = success.readActions; + countMarkAsReadSuccess = success.countMarkAsReadSuccess; } if (Get.context != null && readActions != null) { _appToast.showSuccessToast(readActions == ReadActions.markAsUnread - ? AppLocalizations.of(Get.context!).marked_multiple_item_as_unread(listEmailId.length) - : AppLocalizations.of(Get.context!).marked_multiple_item_as_read(listEmailId.length)); + ? AppLocalizations.of(Get.context!).marked_multiple_item_as_unread(countMarkAsReadSuccess) + : AppLocalizations.of(Get.context!).marked_multiple_item_as_read(countMarkAsReadSuccess)); } - - mailboxDashBoardController.dispatchState(Right(success)); } void _markAsSelectedEmailReadFailure(Failure failure) { @@ -330,17 +317,87 @@ class ThreadController extends BaseController { .build(); } - void moveSelectedMultipleEmailToMailboxAction(List listEmail) { + void moveSelectedMultipleEmailToMailboxAction(List listEmail) async { final currentMailbox = mailboxDashBoardController.selectedMailbox.value; final accountId = mailboxDashBoardController.accountId.value; if (currentMailbox != null && accountId != null) { popBack(); final listEmailIds = listEmail.map((email) => email.id).toList(); - push( + final destinationMailbox = await push( AppRoutes.DESTINATION_PICKER, arguments: DestinationPickerArguments(accountId, listEmailIds, currentMailbox) ); + + if (destinationMailbox != null && destinationMailbox is PresentationMailbox) { + _moveSelectedEmailMultipleToMailbox( + accountId, + MoveRequest( + listEmailIds, + currentMailbox.id, + destinationMailbox.id, + MoveAction.moveTo, + destinationPath: destinationMailbox.mailboxPath)); + } + } + } + + void _moveSelectedEmailMultipleToMailbox(AccountId accountId, MoveRequest moveRequest) { + consumeState(_moveMultipleEmailToMailboxInteractor.execute(accountId, moveRequest)); + } + + void _moveSelectedMultipleEmailToMailboxSuccess(Success success) { + mailboxDashBoardController.dispatchState(Right(success)); + + String? destinationPath; + List movedEmailIds = []; + MailboxId? currentMailboxId; + MailboxId? destinationMailboxId; + MoveAction? moveAction; + + if (success is MoveMultipleEmailToMailboxAllSuccess) { + destinationPath = success.destinationPath; + movedEmailIds = success.movedListEmailId; + currentMailboxId = success.currentMailboxId; + destinationMailboxId = success.destinationMailboxId; + moveAction = success.moveAction; + } else if (success is MoveMultipleEmailToMailboxHasSomeEmailFailure) { + destinationPath = success.destinationPath; + movedEmailIds = success.movedListEmailId; + currentMailboxId = success.currentMailboxId; + destinationMailboxId = success.destinationMailboxId; + moveAction = success.moveAction; + } + + if (Get.context != null && Get.overlayContext != null + && destinationPath != null && moveAction == MoveAction.moveTo) { + _appToast.showToastWithAction( + Get.overlayContext!, + AppLocalizations.of(Get.context!).moved_to_mailbox(destinationPath), + AppLocalizations.of(Get.context!).undo_action, + () { + final newCurrentMailboxId = destinationMailboxId; + final newDestinationMailboxId = currentMailboxId; + if (newCurrentMailboxId != null && newDestinationMailboxId != null) { + _undoMoveSelectedMultipleEmailToMailbox(MoveRequest( + movedEmailIds, + newCurrentMailboxId, + newDestinationMailboxId, + MoveAction.undo, + destinationPath: destinationPath)); + } + } + ); + } + + _refreshListEmail(); + } + + void _undoMoveSelectedMultipleEmailToMailbox(MoveRequest moveRequest) { + final accountId = mailboxDashBoardController.accountId.value; + + if (accountId != null) { + _moveSelectedEmailMultipleToMailbox(accountId, moveRequest); } } diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index 4788d0489..a803ed816 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -106,73 +106,14 @@ class ThreadView extends GetWidget { .build(); } - Widget _moveToTrashAction(BuildContext context, List listEmail) { - return (EmailContextMenuActionBuilder( - Key('move_to_trash_context_menu_action'), - SvgPicture.asset(imagePaths.icTrash, width: 24, height: 24, fit: BoxFit.fill), - AppLocalizations.of(context).move_to_trash, listEmail) - ..onActionClick((data) => {})) - .build(); - } - - Widget _moveToMailboxAction(BuildContext context, List listEmail) { - return (EmailContextMenuActionBuilder( - Key('move_to_mailbox_context_menu_action'), - SvgPicture.asset(imagePaths.icFolder, width: 24, height: 24, fit: BoxFit.fill), - AppLocalizations.of(context).move_to_mailbox, listEmail) - ..onActionClick((data) => {})) - .build(); - } - - Widget _markAsFlagAction(BuildContext context, List listEmail) { - return (EmailContextMenuActionBuilder( - Key('mark_as_flag_context_menu_action'), - SvgPicture.asset(imagePaths.icFlag, width: 24, height: 24, fit: BoxFit.fill), - AppLocalizations.of(context).mark_as_flag, listEmail) - ..onActionClick((data) => {})) - .build(); - } - - Widget _moveToSpamAction(BuildContext context, List listEmail) { - return (EmailContextMenuActionBuilder( - Key('move_to_spam_context_menu_action'), - SvgPicture.asset(imagePaths.icMailboxSpam, width: 24, height: 24, fit: BoxFit.fill), - AppLocalizations.of(context).move_to_spam, listEmail) - ..onActionClick((data) => {})) - .build(); - } - - List _contextMenuActionList(BuildContext context, List listEmail) { - return [ - _moveToTrashAction(context, listEmail), - _moveToMailboxAction(context, listEmail), - _markAsReadAction(context, listEmail), - _markAsFlagAction(context, listEmail), - _moveToSpamAction(context, listEmail), - SizedBox(height: 30), - ]; - } - - Widget _markAsReadAction(BuildContext context, List listEmail) { - return (EmailContextMenuActionBuilder( - Key('mark_as_read_context_menu_action'), - SvgPicture.asset(imagePaths.icEyeDisable, width: 24, height: 24, fit: BoxFit.fill), - controller.isEmailAllRead(listEmail) - ? AppLocalizations.of(context).mark_as_unread - : AppLocalizations.of(context).mark_as_read, - listEmail) - ..onActionClick((data) => controller.markAsSelectedEmailRead(data, fromContextMenuAction: true))) - .build(); - } - Widget _moveToTrashAction(BuildContext context, List listEmail) { return (EmailContextMenuActionBuilder( Key('move_to_trash_context_menu_action'), SvgPicture.asset(imagePaths.icTrash, width: 24, height: 24, fit: BoxFit.fill), AppLocalizations.of(context).move_to_trash, listEmail) - ..onActionClick((data) => {})) - .build(); + ..onActionClick((data) => {})) + .build(); } Widget _moveToMailboxAction(BuildContext context, List listEmail) { @@ -181,8 +122,8 @@ class ThreadView extends GetWidget { SvgPicture.asset(imagePaths.icFolder, width: 24, height: 24, fit: BoxFit.fill), AppLocalizations.of(context).move_to_mailbox, listEmail) - ..onActionClick((emails) => controller.moveSelectedMultipleEmailToMailboxAction(emails))) - .build(); + ..onActionClick((data) => controller.moveSelectedMultipleEmailToMailboxAction(data))) + .build(); } Widget _markAsFlagAction(BuildContext context, List listEmail) { @@ -191,8 +132,8 @@ class ThreadView extends GetWidget { SvgPicture.asset(imagePaths.icFlag, width: 24, height: 24, fit: BoxFit.fill), AppLocalizations.of(context).mark_as_flag, listEmail) - ..onActionClick((data) => {})) - .build(); + ..onActionClick((data) => {})) + .build(); } Widget _moveToSpamAction(BuildContext context, List listEmail) { @@ -201,8 +142,8 @@ class ThreadView extends GetWidget { SvgPicture.asset(imagePaths.icMailboxSpam, width: 24, height: 24, fit: BoxFit.fill), AppLocalizations.of(context).move_to_spam, listEmail) - ..onActionClick((data) => {})) - .build(); + ..onActionClick((data) => {})) + .build(); } Widget _buildLoadingView() { @@ -238,63 +179,59 @@ class ThreadView extends GetWidget { padding: EdgeInsets.zero, color: responsiveUtils.isMobile(context) ? AppColor.bgMailboxListMail : Colors.white, child: Obx(() => controller.currentSelectMode.value == SelectMode.INACTIVE - ? RefreshIndicator( - color: AppColor.primaryColor, - onRefresh: () async => controller.refreshGetAllEmailAction(), - child: _buildListEmailBody(context)) - : _buildListEmailBody(context))); + ? controller.emailList.isNotEmpty + ? RefreshIndicator( + color: AppColor.primaryColor, + onRefresh: () async => controller.refreshGetAllEmailAction(), + child: _buildListEmailBody(context, controller.emailList)) + : RefreshIndicator( + color: AppColor.primaryColor, + onRefresh: () async => controller.refreshGetAllEmailAction(), + child: _buildEmptyEmail(context)) + : controller.emailList.isNotEmpty + ? _buildListEmailBody(context, controller.emailList) + : _buildEmptyEmail(context))); } - Widget _buildListEmailBody(BuildContext context) { - return controller.emailList.isNotEmpty - ? _buildListEmailSuccess(context, controller.emailList) - : _buildEmptyEmail(context); - } - Widget _buildListEmailSuccess(BuildContext context, List listPresentationEmail) { - if (listPresentationEmail.isEmpty) { - return _buildEmptyEmail(context); - } else { - return NotificationListener( - onNotification: (ScrollNotification scrollInfo) { - if (controller.loadMoreState.value == LoadMoreState.IDLE - && scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) { - controller.loadMoreEmailAction(); - return true; - } - return false; - }, - child: ListView.builder( - controller: controller.listEmailController, - padding: EdgeInsets.only(top: 16), - key: Key('presentation_email_list'), - itemCount: listPresentationEmail.length, - itemBuilder: (context, index) => - Obx(() => (EmailTileBuilder( - context, - imagePaths, - controller.getSelectMode(listPresentationEmail[index], controller.mailboxDashBoardController.selectedEmail.value), - listPresentationEmail[index], - responsiveUtils, - controller.currentSelectMode.value) - ..onOpenEmailAction((selectedEmail) => controller.previewEmail(context, selectedEmail)) - ..onSelectEmailAction((selectedEmail) => controller.selectEmail(context, selectedEmail))) - .build()), - )); - } + Widget _buildListEmailBody(BuildContext context, List listPresentationEmail) { + return NotificationListener( + onNotification: (ScrollNotification scrollInfo) { + if (scrollInfo is ScrollEndNotification + && controller.loadMoreState.value == LoadMoreState.IDLE + && scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) { + controller.loadMoreEmailAction(); + } + return false; + }, + child: ListView.builder( + controller: controller.listEmailController, + physics: AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.only(top: 16), + key: Key('presentation_email_list'), + itemCount: listPresentationEmail.length, + itemBuilder: (context, index) => Obx(() => (EmailTileBuilder( + context, + imagePaths, + controller.getSelectMode(listPresentationEmail[index], controller.mailboxDashBoardController.selectedEmail.value), + listPresentationEmail[index], + responsiveUtils, + controller.currentSelectMode.value) + ..onOpenEmailAction((selectedEmail) => controller.previewEmail(context, selectedEmail)) + ..onSelectEmailAction((selectedEmail) => controller.selectEmail(context, selectedEmail))) + .build()), + )); } Widget _buildEmptyEmail(BuildContext context) { return Obx(() => controller.viewState.value.fold( - (failure) => Text( - AppLocalizations.of(context).no_emails, - maxLines: 1, - style: TextStyle(fontSize: 25, color: AppColor.mailboxTextColor, fontWeight: FontWeight.bold)), - (success) => success is LoadingState - ? SizedBox.shrink() - : Text( - AppLocalizations.of(context).no_emails, - maxLines: 1, - style: TextStyle(fontSize: 25, color: AppColor.mailboxTextColor, fontWeight: FontWeight.bold))) + (failure) => SizedBox.shrink(), + (success) => !(success is LoadingState) + ? (BackgroundWidgetBuilder(context) + ..key(Key('empty_email_background')) + ..image(SvgPicture.asset(imagePaths.icEmptyImageDefault, width: 120, height: 120, fit: BoxFit.fill)) + ..text(AppLocalizations.of(context).no_emails)) + .build() + : SizedBox.shrink()) ); } } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 6d7add0c9..561bdc89e 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -90,7 +90,7 @@ "placeholders_order": [], "placeholders": {} }, - "no_emails": "No emails", + "no_emails": "No emails in this mailbox", "@no_emails": { "type": "text", "placeholders_order": [], @@ -339,5 +339,37 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "you_need_to_grant_files_permission_to_download_attachments": "You need to grant files permission to download attachments", + "@you_need_to_grant_files_permission_to_download_attachments": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "count_attachment": "{count} attachments", + "@count_attachment": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "moved_to_mailbox": "Moved to {destinationMailboxPath}", + "@moved_to_mailbox": { + "type": "text", + "placeholders_order": [ + "destinationMailboxPath" + ], + "placeholders": { + "destinationMailboxPath": {} + } + }, + "undo_action": "UNDO", + "@undo_action": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 6d7add0c9..561bdc89e 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -90,7 +90,7 @@ "placeholders_order": [], "placeholders": {} }, - "no_emails": "No emails", + "no_emails": "No emails in this mailbox", "@no_emails": { "type": "text", "placeholders_order": [], @@ -339,5 +339,37 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "you_need_to_grant_files_permission_to_download_attachments": "You need to grant files permission to download attachments", + "@you_need_to_grant_files_permission_to_download_attachments": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "count_attachment": "{count} attachments", + "@count_attachment": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "moved_to_mailbox": "Moved to {destinationMailboxPath}", + "@moved_to_mailbox": { + "type": "text", + "placeholders_order": [ + "destinationMailboxPath" + ], + "placeholders": { + "destinationMailboxPath": {} + } + }, + "undo_action": "UNDO", + "@undo_action": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index ccdfaffc3..ad31ae84c 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2021-09-17T17:37:57.805293", + "@@last_modified": "2021-09-21T12:00:21.345356", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -90,7 +90,7 @@ "placeholders_order": [], "placeholders": {} }, - "no_emails": "No emails", + "no_emails": "No emails in this mailbox", "@no_emails": { "type": "text", "placeholders_order": [], @@ -339,5 +339,37 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "you_need_to_grant_files_permission_to_download_attachments": "You need to grant files permission to download attachments", + "@you_need_to_grant_files_permission_to_download_attachments": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "count_attachment": "{count} attachments", + "@count_attachment": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "moved_to_mailbox": "Moved to {destinationMailboxPath}", + "@moved_to_mailbox": { + "type": "text", + "placeholders_order": [ + "destinationMailboxPath" + ], + "placeholders": { + "destinationMailboxPath": {} + } + }, + "undo_action": "UNDO", + "@undo_action": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 6d7add0c9..561bdc89e 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -90,7 +90,7 @@ "placeholders_order": [], "placeholders": {} }, - "no_emails": "No emails", + "no_emails": "No emails in this mailbox", "@no_emails": { "type": "text", "placeholders_order": [], @@ -339,5 +339,37 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "you_need_to_grant_files_permission_to_download_attachments": "You need to grant files permission to download attachments", + "@you_need_to_grant_files_permission_to_download_attachments": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "count_attachment": "{count} attachments", + "@count_attachment": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "moved_to_mailbox": "Moved to {destinationMailboxPath}", + "@moved_to_mailbox": { + "type": "text", + "placeholders_order": [ + "destinationMailboxPath" + ], + "placeholders": { + "destinationMailboxPath": {} + } + }, + "undo_action": "UNDO", + "@undo_action": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_vi.arb b/lib/l10n/intl_vi.arb index 6d7add0c9..561bdc89e 100644 --- a/lib/l10n/intl_vi.arb +++ b/lib/l10n/intl_vi.arb @@ -90,7 +90,7 @@ "placeholders_order": [], "placeholders": {} }, - "no_emails": "No emails", + "no_emails": "No emails in this mailbox", "@no_emails": { "type": "text", "placeholders_order": [], @@ -339,5 +339,37 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "you_need_to_grant_files_permission_to_download_attachments": "You need to grant files permission to download attachments", + "@you_need_to_grant_files_permission_to_download_attachments": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "count_attachment": "{count} attachments", + "@count_attachment": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "moved_to_mailbox": "Moved to {destinationMailboxPath}", + "@moved_to_mailbox": { + "type": "text", + "placeholders_order": [ + "destinationMailboxPath" + ], + "placeholders": { + "destinationMailboxPath": {} + } + }, + "undo_action": "UNDO", + "@undo_action": { + "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 301d49aa3..4ce80bc94 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -110,7 +110,7 @@ class AppLocalizations { String get no_emails { return Intl.message( - 'No emails', + 'No emails in this mailbox', name: 'no_emails', ); } diff --git a/model/lib/extensions/email_extension.dart b/model/lib/extensions/email_extension.dart index 22564c5dd..b7d2bc1ea 100644 --- a/model/lib/extensions/email_extension.dart +++ b/model/lib/extensions/email_extension.dart @@ -1,9 +1,16 @@ import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:model/model.dart'; extension EmailExtension on Email { + bool isUnReadEmail() => !(keywords?.containsKey(KeyWordIdentifier.emailSeen) == true); + + bool isReadEmail() => keywords?.containsKey(KeyWordIdentifier.emailSeen) == true; + + bool isFlaggedEmail() => keywords?.containsKey(KeyWordIdentifier.emailFlagged) == true; + Set getRecipientEmailAddressList() { final listEmailAddress = Set(); final listToAddress = to.getListAddress() ?? []; @@ -22,6 +29,24 @@ extension EmailExtension on Email { ); } + Email updatedEmail({Map? newKeywords}) { + return Email( + id, + keywords: newKeywords ?? keywords, + size: size, + receivedAt: receivedAt, + hasAttachment: hasAttachment, + preview: preview, + subject: subject, + sentAt: sentAt, + from: from, + to: to, + cc: cc, + bcc: bcc, + replyTo: replyTo + ); + } + PresentationEmail toPresentationEmail({SelectMode selectMode = SelectMode.INACTIVE}) { return PresentationEmail( id, diff --git a/model/lib/extensions/list_email_id_extension.dart b/model/lib/extensions/list_email_id_extension.dart new file mode 100644 index 000000000..79c28479d --- /dev/null +++ b/model/lib/extensions/list_email_id_extension.dart @@ -0,0 +1,27 @@ +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/patch_object.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.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/model.dart'; + +extension ListEmailIdExtension on List { + + List toIds() => map((emailId) => emailId.id).toList(); + + Map generateMapUpdateObjectMarkAsRead(ReadActions readActions) { + final Map maps = {}; + forEach((emailId) { + maps[emailId.id] = KeyWordIdentifier.emailSeen.generateReadActionPath(readActions); + }); + return maps; + } + + Map generateMapUpdateObjectMoveToMailbox(MailboxId currentMailboxId, MailboxId destinationMailboxId) { + final Map maps = {}; + forEach((emailId) { + maps[emailId.id] = currentMailboxId.generateMoveToMailboxActionPath(destinationMailboxId); + }); + return maps; + } +} \ No newline at end of file diff --git a/model/lib/extensions/presentation_email_extension.dart b/model/lib/extensions/presentation_email_extension.dart index b6ebfb3b9..1c8410122 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -1,5 +1,5 @@ import 'package:core/core.dart'; -import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; extension PresentationEmailExtension on PresentationEmail { @@ -14,39 +14,6 @@ extension PresentationEmailExtension on PresentationEmail { return ''; } - PresentationEmail markAsReadPresentationEmail(ReadActions readActions) { - var newKeyWord = keywords; - - if (readActions == ReadActions.markAsUnread) { - newKeyWord?.removeWhere((key, value) => key == KeyWordIdentifier.emailSeen); - } else { - if (newKeyWord == null) { - newKeyWord = { - KeyWordIdentifier.emailSeen: true - }; - } else { - newKeyWord[KeyWordIdentifier.emailSeen] = true; - } - } - - return PresentationEmail( - id, - keywords: newKeyWord, - size: size, - receivedAt: receivedAt, - hasAttachment: hasAttachment, - preview: preview, - subject: subject, - sentAt: sentAt, - from: from, - to: to, - cc: cc, - bcc: bcc, - replyTo: replyTo, - selectMode: selectMode - ); - } - PresentationEmail toggleSelect() { return PresentationEmail( id, @@ -84,4 +51,22 @@ extension PresentationEmailExtension on PresentationEmail { selectMode: selectMode ); } + + Email toEmail() { + return Email( + id, + keywords: keywords, + size: size, + receivedAt: receivedAt, + hasAttachment: hasAttachment, + preview: preview, + subject: subject, + sentAt: sentAt, + from: from, + to: to, + cc: cc, + bcc: bcc, + replyTo: replyTo + ); + } } \ 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 1ffbf6aea..9e63bfba6 100644 --- a/model/lib/extensions/presentation_mailbox_extension.dart +++ b/model/lib/extensions/presentation_mailbox_extension.dart @@ -1,10 +1,9 @@ -import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; extension PresentationMailboxExtension on PresentationMailbox { - PresentationMailbox toPresentationMailbox({required UnsignedInt countUnRead}) { + PresentationMailbox toPresentationMailboxWithMailboxPath(String mailboxPath) { return PresentationMailbox( id, name: name, @@ -12,12 +11,29 @@ extension PresentationMailboxExtension on PresentationMailbox { role: role, sortOrder: sortOrder, totalEmails: totalEmails, - unreadEmails: UnreadEmails(countUnRead), + unreadEmails: unreadEmails, totalThreads: totalThreads, unreadThreads: unreadThreads, myRights: myRights, isSubscribed: isSubscribed, - selectMode: selectMode + selectMode: selectMode, + mailboxPath: mailboxPath + ); + } + + Mailbox toMailbox() { + return Mailbox( + id, + name, + parentId, + role, + sortOrder, + totalEmails, + unreadEmails, + totalThreads, + unreadThreads, + myRights, + isSubscribed ); } } \ No newline at end of file diff --git a/model/lib/mailbox/presentation_mailbox.dart b/model/lib/mailbox/presentation_mailbox.dart index 54fbcff81..80f279151 100644 --- a/model/lib/mailbox/presentation_mailbox.dart +++ b/model/lib/mailbox/presentation_mailbox.dart @@ -23,6 +23,7 @@ class PresentationMailbox with EquatableMixin { final MailboxRights? myRights; final IsSubscribed? isSubscribed; final SelectMode selectMode; + final String? mailboxPath; PresentationMailbox( this.id, @@ -37,7 +38,8 @@ class PresentationMailbox with EquatableMixin { this.unreadThreads, this.myRights, this.isSubscribed, - this.selectMode = SelectMode.INACTIVE + this.selectMode = SelectMode.INACTIVE, + this.mailboxPath, } ); diff --git a/model/lib/model.dart b/model/lib/model.dart index 4dac31ce4..785c1e623 100644 --- a/model/lib/model.dart +++ b/model/lib/model.dart @@ -38,6 +38,7 @@ export 'extensions/keyword_identifier_extension.dart'; export 'extensions/presentation_mailbox_extension.dart'; export 'extensions/email_body_part_extension.dart'; export 'extensions/attachment_extension.dart'; +export 'extensions/list_email_id_extension.dart'; export 'extensions/mailbox_id_extension.dart'; // Download