From 705dd5d28c2284ef90939df2171de5084301e04e Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 9 Feb 2023 15:08:40 +0700 Subject: [PATCH] TF-1427 Create new mailbox search view on web --- assets/images/ic_clear_search_input.svg | 5 + assets/images/ic_hide_folder.svg | 11 + assets/images/ic_search_bar.svg | 6 +- .../extensions/color_extension.dart | 2 + .../presentation/resources/image_paths.dart | 2 + .../views/search/search_bar_view.dart | 17 +- .../presentation/action/dashboard_action.dart | 4 +- .../mailbox_dashboard_controller.dart | 13 +- .../controller/spam_report_controller.dart | 2 +- .../mailbox_dashboard_view_web.dart | 34 +- .../widgets/search_input_form_widget.dart | 16 +- .../presentation/search_mailbox_bindings.dart | 50 +++ .../search_mailbox_controller.dart | 383 ++++++++++++++++++ .../presentation/search_mailbox_view.dart | 293 ++++++++++++++ .../utils/search_mailbox_utils.dart | 16 + .../mailbox_searched_item_builder.dart | 181 +++++++++ .../widgets/search_app_bar_widget.dart | 15 +- lib/main/localizations/app_localizations.dart | 9 +- 18 files changed, 1021 insertions(+), 38 deletions(-) create mode 100644 assets/images/ic_clear_search_input.svg create mode 100644 assets/images/ic_hide_folder.svg create mode 100644 lib/features/search/mailbox/presentation/search_mailbox_bindings.dart create mode 100644 lib/features/search/mailbox/presentation/search_mailbox_controller.dart create mode 100644 lib/features/search/mailbox/presentation/search_mailbox_view.dart create mode 100644 lib/features/search/mailbox/presentation/utils/search_mailbox_utils.dart create mode 100644 lib/features/search/mailbox/presentation/widgets/mailbox_searched_item_builder.dart diff --git a/assets/images/ic_clear_search_input.svg b/assets/images/ic_clear_search_input.svg new file mode 100644 index 000000000..c74f42178 --- /dev/null +++ b/assets/images/ic_clear_search_input.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_hide_folder.svg b/assets/images/ic_hide_folder.svg new file mode 100644 index 000000000..87215898e --- /dev/null +++ b/assets/images/ic_hide_folder.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/assets/images/ic_search_bar.svg b/assets/images/ic_search_bar.svg index 6d1b7d605..eb9859029 100644 --- a/assets/images/ic_search_bar.svg +++ b/assets/images/ic_search_bar.svg @@ -1,3 +1,5 @@ - - + + diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index 55a77ab83..ef1fd8b78 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -172,6 +172,8 @@ extension AppColor on Color { static const colorCreateNewIdentityButton = Color(0xFFEBEDF0); static const colorSpamReportBox = Color(0xFFBFDEFF); static const colorSubtitle = Color(0xFF6D7885); + static const colorBackgroundSearchMailboxInput = Color(0xFFEBEDF0); + static const colorMailboxPath = Color(0xFF818C99); static const mapGradientColor = [ [Color(0xFF21D4FD), Color(0xFFB721FF)], diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 046f58032..32e7d2d7f 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -167,6 +167,8 @@ class ImagePaths { String get icInfoCircleOutline => _getImagePath('ic_info_circle_outline.svg'); String get icSpamReportDisable => _getImagePath('ic_spam_report_disable.svg'); String get icSpamReportEnable => _getImagePath('ic_spam_report_enable.svg'); + String get icClearSearchInput => _getImagePath('ic_clear_search_input.svg'); + String get icHideFolder => _getImagePath('ic_hide_folder.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; diff --git a/core/lib/presentation/views/search/search_bar_view.dart b/core/lib/presentation/views/search/search_bar_view.dart index bea4b30dd..3530586bf 100644 --- a/core/lib/presentation/views/search/search_bar_view.dart +++ b/core/lib/presentation/views/search/search_bar_view.dart @@ -49,15 +49,14 @@ class SearchBarView extends StatelessWidget { children: [ const SizedBox(width: 8), buildIconWeb( - splashRadius: 15, - minSize: 40, - iconPadding: EdgeInsets.zero, - icon: SvgPicture.asset( - _imagePaths.icSearchBar, - width: 16, - height: 16, - fit: BoxFit.fill), - onTap: onOpenSearchViewAction), + minSize: 40, + iconPadding: EdgeInsets.zero, + icon: SvgPicture.asset( + _imagePaths.icSearchBar, + fit: BoxFit.fill + ), + onTap: onOpenSearchViewAction + ), Expanded( child: Text( hintTextSearch ?? '', diff --git a/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart b/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart index cdd62b45a..787beaa8c 100644 --- a/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart +++ b/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart @@ -125,12 +125,12 @@ class SearchEmailByQueryAction extends DashBoardAction { @override List get props => [navigationRouter]; } -class OpenSpamMailboxAction extends DashBoardAction { +class OpenMailboxAction extends DashBoardAction { final BuildContext context; final PresentationMailbox presentationMailbox; - OpenSpamMailboxAction(this.context, this.presentationMailbox); + OpenMailboxAction(this.context, this.presentationMailbox); @override List get props => [context, presentationMailbox]; 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 d9e92e4a9..53bb401a8 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -159,6 +159,7 @@ class MailboxDashBoardController extends ReloadableController { final vacationResponse = Rxn(); final routerParameters = Rxn>(); final _isDraggingMailbox = RxBool(false); + final searchMailboxActivated = RxBool(false); Session? sessionCurrent; Map mapDefaultMailboxIdByRole = {}; Map mapMailboxById = {}; @@ -512,6 +513,14 @@ class MailboxDashBoardController extends ReloadableController { scaffoldKey.currentState?.openEndDrawer(); } + void hideMailboxMenuWhenScreenSizeChange(BuildContext context) { + if (isDrawerOpen && _responsiveUtils.isWebDesktop(context)) { + WidgetsBinding.instance.addPostFrameCallback((_) { + closeMailboxMenuDrawer(); + }); + } + } + bool get isDrawerOpen => scaffoldKey.currentState?.isDrawerOpen == true; bool isSelectionEnabled() => currentSelectMode.value == SelectMode.ACTIVE; @@ -1526,8 +1535,8 @@ class MailboxDashBoardController extends ReloadableController { emailsInCurrentMailbox.value = newEmailList; } - void openSpamMailboxAction(BuildContext context, PresentationMailbox presentationMailbox){ - dispatchAction(OpenSpamMailboxAction(context, presentationMailbox)); + void openMailboxAction(BuildContext context, PresentationMailbox presentationMailbox) { + dispatchAction(OpenMailboxAction(context, presentationMailbox)); } bool get enableSpamReport => spamReportController.enableSpamReport; 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 01fc3e0b9..e9e958ca6 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart @@ -80,7 +80,7 @@ class SpamReportController extends BaseController { void openMailbox(BuildContext context) { final _mailboxDashBoardController = Get.find(); dismissSpamReportAction(); - _mailboxDashBoardController.openSpamMailboxAction(context, _presentationSpamMailbox.value!); + _mailboxDashBoardController.openMailboxAction(context, _presentationSpamMailbox.value!); } void storeSpamReportStateAction(SpamReportState spamReportState) { 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 c01c95c6b..cfb5ea8bc 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -29,6 +29,7 @@ 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/quotas/presentation/widget/quotas_warning_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/presentation/thread_view.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -48,11 +49,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { @override Widget build(BuildContext context) { - if (controller.isDrawerOpen && responsiveUtils.isDesktop(context)) { - WidgetsBinding.instance.addPostFrameCallback((_) { - controller.closeMailboxMenuDrawer(); - }); - } + controller.hideMailboxMenuWhenScreenSizeChange(context); return Portal( child: Stack(children: [ @@ -108,7 +105,15 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { Column(children: [ _buildComposerButton(context), Expanded(child: SizedBox( - child: MailboxView(), + child: Obx(() { + if (controller.searchMailboxActivated.isTrue) { + return const SearchMailboxView( + backgroundColor: AppColor.colorBgDesktop + ); + } else { + return MailboxView(); + } + }), width: ResponsiveUtils.defaultSizeMenu )) ]), @@ -183,7 +188,12 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { ), Obx(() => controller.composerOverlayState.value == ComposerOverlayState.active ? ComposerView() - : const SizedBox.shrink()), + : const SizedBox.shrink() + ), + Obx(() => controller.searchMailboxActivated.value == true && !responsiveUtils.isWebDesktop(context) + ? const SearchMailboxView() + : const SizedBox.shrink() + ), _buildDownloadTaskStateWidget(), ]), ); @@ -193,11 +203,11 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { return Scaffold( key: controller.scaffoldKey, drawer: ResponsiveWidget( - responsiveUtils: responsiveUtils, - mobile: SizedBox(child: MailboxView(), width: ResponsiveUtils.defaultSizeDrawer), - tablet: SizedBox(child: MailboxView(), width: ResponsiveUtils.defaultSizeDrawer), - tabletLarge: SizedBox(child: MailboxView(), width: ResponsiveUtils.defaultSizeLeftMenuMobile), - desktop: const SizedBox.shrink()), + responsiveUtils: responsiveUtils, + mobile: SizedBox(child: MailboxView(), width: ResponsiveUtils.defaultSizeDrawer), + tabletLarge: SizedBox(child: MailboxView(), width: ResponsiveUtils.defaultSizeLeftMenuMobile), + desktop: const SizedBox.shrink() + ), body: body, ); } diff --git a/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart index 362d6dafa..ca843498a 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart @@ -85,12 +85,14 @@ class SearchInputFormWidget extends StatelessWidget with AppLoaderMixin { color: Colors.black, fontSize: 16.0) ), - leftButton: buildIconWeb( + leftButton: Padding( + padding: const EdgeInsets.only(left: 8), + child: buildIconWeb( + minSize: 40, + iconPadding: EdgeInsets.zero, icon: SvgPicture.asset( - imagePaths.icSearchBar, - width: 16, - height: 16, - fit: BoxFit.fill), + imagePaths.icSearchBar, + fit: BoxFit.fill), onTap: () { final keyword = controller.searchInputController.text.trim(); if (keyword.isNotEmpty) { @@ -99,7 +101,9 @@ class SearchInputFormWidget extends StatelessWidget with AppLoaderMixin { } else { dashBoardController.clearSearchEmail(); } - }), + } + ) + ), clearTextButton: buildIconWeb( icon: SvgPicture.asset( imagePaths.icClearTextSearch, diff --git a/lib/features/search/mailbox/presentation/search_mailbox_bindings.dart b/lib/features/search/mailbox/presentation/search_mailbox_bindings.dart new file mode 100644 index 000000000..50602863f --- /dev/null +++ b/lib/features/search/mailbox/presentation/search_mailbox_bindings.dart @@ -0,0 +1,50 @@ + +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/base_bindings.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/delete_multiple_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/move_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/refresh_all_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/rename_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/search_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree_builder.dart'; +import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart'; +import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_controller.dart'; + +class SearchMailboxBindings extends BaseBindings { + + @override + void bindingsController() { + Get.lazyPut(() => SearchMailboxController( + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find() + )); + } + + @override + void bindingsDataSource() {} + + @override + void bindingsDataSourceImpl() {} + + @override + void bindingsInteractor() { + Get.lazyPut(() => SearchMailboxInteractor()); + } + + @override + void bindingsRepository() {} + + @override + void bindingsRepositoryImpl() {} + + void disposeBindings() { + Get.delete(); + } +} \ 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 new file mode 100644 index 000000000..45896fdba --- /dev/null +++ b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart @@ -0,0 +1,383 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +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:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.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/mail/mailbox/mailbox.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/base/base_mailbox_controller.dart'; +import 'package:debounce_throttle/debounce_throttle.dart'; +import 'package:tmail_ui_user/features/base/mixin/mailbox_action_handler_mixin.dart'; +import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/move_mailbox_request.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/rename_mailbox_request.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/delete_multiple_mailbox_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/get_all_mailboxes_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/move_mailbox_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/refresh_changes_all_mailboxes_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/rename_mailbox_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/search_mailbox_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/delete_multiple_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/move_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/refresh_all_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/rename_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/search_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_action.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree_builder.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_utils.dart'; +import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.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/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; + +class SearchMailboxController extends BaseMailboxController with MailboxActionHandlerMixin { + + final GetAllMailboxInteractor _getAllMailboxInteractor; + final RefreshAllMailboxInteractor _refreshAllMailboxInteractor; + final SearchMailboxInteractor _searchMailboxInteractor; + final RenameMailboxInteractor _renameMailboxInteractor; + final MoveMailboxInteractor _moveMailboxInteractor; + final DeleteMultipleMailboxInteractor _deleteMultipleMailboxInteractor; + + final dashboardController = Get.find(); + final responsiveUtils = Get.find(); + final imagePaths = Get.find(); + final _appToast = Get.find(); + + final currentSearchQuery = RxString(''); + final listMailboxSearched = RxList(); + final textInputSearchController = TextEditingController(); + late Debouncer _deBouncerTime; + + SearchMailboxController( + this._getAllMailboxInteractor, + this._refreshAllMailboxInteractor, + this._searchMailboxInteractor, + this._renameMailboxInteractor, + this._moveMailboxInteractor, + this._deleteMultipleMailboxInteractor, + TreeBuilder treeBuilder, + VerifyNameInteractor verifyNameInteractor + ) : super(treeBuilder, verifyNameInteractor); + + @override + void onInit() { + super.onInit(); + _initializeDebounceTimeTextSearchChange(); + _getAllMailboxAction(); + } + + @override + void onDone() { + viewState.value.fold(_handleFailureViewState, _handleSuccessViewState); + } + + @override + void onData(Either newState) { + super.onData(newState); + newState.fold((failure) => null, (success) async { + if (success is GetAllMailboxSuccess) { + currentMailboxState = success.currentMailboxState; + buildTree(success.mailboxList); + } else if (success is RefreshChangesAllMailboxSuccess) { + currentMailboxState = success.currentMailboxState; + await refreshTree(success.mailboxList); + searchMailboxAction(); + } + }); + } + + void _handleFailureViewState(Failure failure) { + if (failure is SearchMailboxFailure) { + _handleSearchMailboxFailure(failure); + } + } + + void _handleSuccessViewState(Success success) { + if (success is SearchMailboxSuccess) { + _handleSearchMailboxSuccess(success); + } else if (success is MarkAsMailboxReadAllSuccess) { + refreshMailboxChanges(mailboxState: success.currentMailboxState); + } else if (success is MarkAsMailboxReadHasSomeEmailFailure) { + refreshMailboxChanges(mailboxState: success.currentMailboxState); + } else if (success is RenameMailboxSuccess) { + refreshMailboxChanges(mailboxState: success.currentMailboxState); + } else if (success is MoveMailboxSuccess) { + _moveMailboxSuccess(success); + }else if (success is DeleteMultipleMailboxAllSuccess) { + _deleteMultipleMailboxSuccess(success.listMailboxIdDeleted, success.currentMailboxState); + } else if (success is DeleteMultipleMailboxHasSomeSuccess) { + _deleteMultipleMailboxSuccess(success.listMailboxIdDeleted, success.currentMailboxState); + } + } + + void _initializeDebounceTimeTextSearchChange() { + _deBouncerTime = Debouncer( + const Duration(milliseconds: 300), + initialValue: '' + ); + + _deBouncerTime.values.listen((value) async { + log('SearchMailboxController::_initializeDebounceTimeTextSearchChange():query: $value'); + currentSearchQuery.value = value; + searchMailboxAction(); + }); + } + + void _getAllMailboxAction() { + final session = dashboardController.sessionCurrent; + final accountId = dashboardController.accountId.value; + if (session != null && accountId != null) { + consumeState(_getAllMailboxInteractor.execute(session, accountId)); + } + } + + void refreshMailboxChanges({jmap.State? mailboxState}) { + final newMailboxState = mailboxState ?? currentMailboxState; + dashboardController.dispatchMailboxUIAction(RefreshChangeMailboxAction(newMailboxState)); + final accountId = dashboardController.accountId.value; + if (accountId != null && newMailboxState != null) { + consumeState(_refreshAllMailboxInteractor.execute(accountId, newMailboxState)); + } + } + + void searchMailboxAction() { + if (currentSearchQuery.value.isNotEmpty) { + consumeState(_searchMailboxInteractor.execute( + allMailboxes, + SearchQuery(currentSearchQuery.value) + )); + } else { + listMailboxSearched.clear(); + } + } + + void _handleSearchMailboxSuccess(SearchMailboxSuccess success) { + final mailboxesSearchedWithPath = findMailboxPath(success.mailboxesSearched); + listMailboxSearched.value = mailboxesSearchedWithPath; + } + + void _handleSearchMailboxFailure(SearchMailboxFailure failure) { + listMailboxSearched.clear(); + } + + void onTextSearchChange(String text) { + _deBouncerTime.value = text; + } + + void setTextInputSearchForm(String value) { + textInputSearchController.text = value; + } + + void submitSearchAction(BuildContext context, String query) { + + } + + void handleMailboxAction(BuildContext context, MailboxActions actions, PresentationMailbox mailbox) { + popBack(); + + switch(actions) { + case MailboxActions.openInNewTab: + openMailboxInNewTabAction(mailbox); + break; + case MailboxActions.disableSpamReport: + case MailboxActions.enableSpamReport: + dashboardController.storeSpamReportStateAction(); + return; + case MailboxActions.markAsRead: + markAsReadMailboxAction(context, mailbox, dashboardController); + break; + case MailboxActions.rename: + openDialogRenameMailboxAction( + context, + mailbox, + responsiveUtils, + onRenameMailboxAction: _renameMailboxAction + ); + break; + case MailboxActions.move: + moveMailboxAction( + context, + mailbox, + dashboardController, + onMovingMailboxAction: _invokeMovingMailboxAction + ); + break; + case MailboxActions.delete: + openConfirmationDialogDeleteMailboxAction( + context, + responsiveUtils, + imagePaths, + mailbox, + onDeleteMailboxAction: _deleteMailboxAction + ); + break; + default: + break; + } + } + + void _renameMailboxAction(PresentationMailbox presentationMailbox, MailboxName newMailboxName) { + final accountId = dashboardController.accountId.value; + if (accountId != null) { + consumeState(_renameMailboxInteractor.execute( + accountId, + RenameMailboxRequest(presentationMailbox.id, newMailboxName) + )); + } + } + + void _invokeMovingMailboxAction(PresentationMailbox mailboxSelected, PresentationMailbox? destinationMailbox) { + final accountId = dashboardController.accountId.value; + if (accountId != null) { + _handleMovingMailbox( + accountId, + MoveAction.moving, + mailboxSelected, + destinationMailbox: destinationMailbox + ); + } + } + + void _handleMovingMailbox( + AccountId accountId, + MoveAction moveAction, + PresentationMailbox mailboxSelected, + {PresentationMailbox? destinationMailbox} + ) { + consumeState(_moveMailboxInteractor.execute( + accountId, + MoveMailboxRequest( + mailboxSelected.id, + moveAction, + destinationMailboxId: destinationMailbox?.id, + destinationMailboxName: destinationMailbox?.name, + parentId: mailboxSelected.parentId + ) + )); + } + + void _moveMailboxSuccess(MoveMailboxSuccess success) { + if (success.moveAction == MoveAction.moving && currentOverlayContext != null && currentContext != null) { + _appToast.showBottomToast( + currentOverlayContext!, + AppLocalizations.of(currentContext!).moved_to_mailbox(success.destinationMailboxName?.name ?? AppLocalizations.of(currentContext!).allMailboxes), + actionName: AppLocalizations.of(currentContext!).undo, + onActionClick: () { + _undoMovingMailbox(MoveMailboxRequest( + success.mailboxIdSelected, + MoveAction.undo, + destinationMailboxId: success.parentId, + parentId: success.destinationMailboxId) + ); + }, + leadingIcon: SvgPicture.asset( + imagePaths.icFolderMailbox, + width: 24, + height: 24, + color: Colors.white, + fit: BoxFit.fill), + backgroundColor: AppColor.toastSuccessBackgroundColor, + textColor: Colors.white, + textActionColor: Colors.white, + actionIcon: SvgPicture.asset(imagePaths.icUndo), + maxWidth: responsiveUtils.getMaxWidthToast(currentContext!) + ); + } + + refreshMailboxChanges(mailboxState: success.currentMailboxState); + } + + void _undoMovingMailbox(MoveMailboxRequest newMoveRequest) { + final accountId = dashboardController.accountId.value; + if (accountId != null) { + consumeState(_moveMailboxInteractor.execute(accountId, newMoveRequest)); + } + } + + void _deleteMailboxAction(PresentationMailbox presentationMailbox) { + final accountId = dashboardController.accountId.value; + final session = dashboardController.sessionCurrent; + + if (session != null && accountId != null) { + final tupleMap = MailboxUtils.generateMapDescendantIdsAndMailboxIdList( + [presentationMailbox], + defaultMailboxTree.value, + personalMailboxTree.value + ); + final mapDescendantIds = tupleMap.value1; + final listMailboxId = tupleMap.value2; + + consumeState(_deleteMultipleMailboxInteractor.execute( + session, + accountId, + mapDescendantIds, + listMailboxId + )); + } else { + _deleteMailboxFailure(DeleteMultipleMailboxFailure(null)); + } + + popBack(); + } + + void _deleteMultipleMailboxSuccess(List listMailboxIdDeleted, jmap.State? currentMailboxState) { + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastWithIcon( + currentOverlayContext!, + message: AppLocalizations.of(currentContext!).delete_mailboxes_successfully, + icon: imagePaths.icSelected + ); + } + + if (listMailboxIdDeleted.contains(dashboardController.selectedMailbox.value?.id)) { + dashboardController.selectedMailbox.value = null; + dashboardController.dispatchMailboxUIAction(SelectMailboxDefaultAction()); + } + + refreshMailboxChanges(mailboxState: currentMailboxState); + } + + void _deleteMailboxFailure(DeleteMultipleMailboxFailure failure) { + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastWithIcon( + currentOverlayContext!, + message: AppLocalizations.of(currentContext!).delete_mailboxes_failure, + icon: imagePaths.icDeleteToast + ); + } + } + + void clearAllTextInputSearchForm() { + textInputSearchController.clear(); + currentSearchQuery.value = ''; + searchMailboxAction(); + } + + void closeSearchView(BuildContext context) { + clearAllTextInputSearchForm(); + dashboardController.searchMailboxActivated.value = false; + SearchMailboxBindings().disposeBindings(); + } + + @override + void onClose() { + textInputSearchController.dispose(); + _deBouncerTime.cancel(); + super.onClose(); + } +} \ No newline at end of file diff --git a/lib/features/search/mailbox/presentation/search_mailbox_view.dart b/lib/features/search/mailbox/presentation/search_mailbox_view.dart new file mode 100644 index 000000000..0c86c5cdf --- /dev/null +++ b/lib/features/search/mailbox/presentation/search_mailbox_view.dart @@ -0,0 +1,293 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:core/presentation/views/text/text_field_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; +import 'package:tmail_ui_user/features/base/mixin/popup_menu_widget_mixin.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/search_mailbox_state.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/context_item_mailbox_action.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/mailbox_bottom_sheet_action_tile_builder.dart'; +import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_controller.dart'; +import 'package:tmail_ui_user/features/search/mailbox/presentation/utils/search_mailbox_utils.dart'; +import 'package:tmail_ui_user/features/search/mailbox/presentation/widgets/mailbox_searched_item_builder.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class SearchMailboxView extends GetWidget + with AppLoaderMixin, PopupMenuWidgetMixin { + + final Color? backgroundColor; + + const SearchMailboxView({ + Key? key, + this.backgroundColor + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Container( + color: backgroundColor ?? Colors.white, + child: Column(children: [ + Container( + color: Colors.transparent, + padding: SearchMailboxUtils.getPaddingAppBar(context, controller.responsiveUtils), + child: _buildSearchInputForm(context) + ), + if (!controller.responsiveUtils.isWebDesktop(context)) + const Divider(color: AppColor.colorDividerComposer, height: 1), + _buildLoadingView(), + Expanded(child: _buildMailboxListView()) + ]), + ), + ), + ); + } + + Widget _buildLoadingView() { + return Obx(() => controller.viewState.value.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is LoadingSearchMailbox) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: loadingWidget); + } else { + return const SizedBox.shrink(); + } + } + )); + } + + Widget _buildSearchInputForm(BuildContext context) { + return Row(children: [ + buildIconWeb( + minSize: 30, + iconPadding: EdgeInsets.zero, + splashRadius: 10, + icon: SvgPicture.asset( + controller.imagePaths.icBack, + color: AppColor.colorTextButton, + fit: BoxFit.fill + ), + tooltip: AppLocalizations.of(context).back, + onTap: () => controller.closeSearchView(context) + ), + Expanded(child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(controller.responsiveUtils.isWebDesktop(context) ? 12 : 0)), + color: controller.responsiveUtils.isWebDesktop(context) + ? AppColor.colorBackgroundSearchMailboxInput + : Colors.transparent + ), + alignment: Alignment.center, + child: Row(children: [ + Padding( + padding: const EdgeInsets.only(left: 5, right: 2), + child: buildIconWeb( + minSize: 40, + iconPadding: EdgeInsets.zero, + icon: SvgPicture.asset( + controller.imagePaths.icSearchBar, + color: AppColor.colorTextButton, + fit: BoxFit.fill + ), + tooltip: AppLocalizations.of(context).search, + onTap: controller.searchMailboxAction + ), + ), + Expanded(child: _buildTextFieldSearchInput(context)), + Obx(() { + if (controller.currentSearchQuery.isNotEmpty) { + return Padding( + padding: const EdgeInsets.only(right: 2), + child: buildIconWeb( + minSize: 40, + iconPadding: EdgeInsets.zero, + icon: SvgPicture.asset( + controller.imagePaths.icClearSearchInput, + width: 24, + height: 24, + fit: BoxFit.fill + ), + tooltip: AppLocalizations.of(context).clearAll, + onTap: controller.clearAllTextInputSearchForm), + ); + } else { + return const SizedBox.shrink(); + } + }) + ]) + )) + ]); + } + + Widget _buildTextFieldSearchInput(BuildContext context) { + return (TextFieldBuilder() + ..onChange(controller.onTextSearchChange) + ..textInputAction(TextInputAction.search) + ..addController(controller.textInputSearchController) + ..textStyle(const TextStyle( + color: Colors.black, + fontSize: 15, + fontWeight: FontWeight.normal)) + ..keyboardType(TextInputType.text) + ..onSubmitted((value) { + final query = value.trim(); + if (query.isNotEmpty) { + controller.submitSearchAction(context, query); + } + }) + ..maxLines(1) + ..textDecoration(InputDecoration( + contentPadding: EdgeInsets.zero, + hintText: AppLocalizations.of(context).searchForMailboxes, + hintStyle: const TextStyle( + color: AppColor.loginTextFieldHintColor, + fontSize: 15, + fontWeight: FontWeight.normal), + border: InputBorder.none + ) + )).build(); + } + + Widget _buildMailboxListView() { + return Obx(() { + return ListView.builder( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), + key: const Key('list_mailbox_searched'), + itemCount: controller.listMailboxSearched.length, + shrinkWrap: true, + primary: false, + itemBuilder: (context, index) { + return MailboxSearchedItemBuilder( + controller.imagePaths, + controller.responsiveUtils, + controller.listMailboxSearched[index], + onDragEmailToMailboxAccepted: controller.dashboardController.dragSelectedMultipleEmailToMailboxAction, + onClickOpenMailboxAction: (mailbox) => controller.dashboardController.openMailboxAction(context, mailbox), + onClickOpenMenuMailboxAction: (position, mailbox) => _openMailboxMenuAction(context, position, mailbox), + ); + } + ); + }); + } + + MailboxActions mailboxActionForSpam() { + return controller.dashboardController.enableSpamReport + ? MailboxActions.disableSpamReport + : MailboxActions.enableSpamReport; + } + + void _openMailboxMenuAction(BuildContext context, RelativeRect position, PresentationMailbox mailbox) { + final mailboxActionsSupported = [ + MailboxActions.openInNewTab, + if (mailbox.isPersonal && mailbox.isSpam) + mailboxActionForSpam(), + MailboxActions.markAsRead, + MailboxActions.move, + MailboxActions.rename, + MailboxActions.delete + ]; + + final listContextMenuItemAction = mailboxActionsSupported + .map((action) => ContextMenuItemMailboxAction(action, action.getContextMenuItemState(mailbox))) + .toList(); + + if (controller.responsiveUtils.isScreenWithShortestSide(context)) { + controller.openContextMenuAction( + context, + _bottomSheetMailboxActionTiles( + context, + mailbox, + listContextMenuItemAction + ) + ); + } else { + controller.openPopupMenuAction( + context, + position, + _popupMenuMailboxActionTiles( + context, + mailbox, + listContextMenuItemAction + ) + ); + } + } + + List _bottomSheetMailboxActionTiles( + BuildContext context, + PresentationMailbox mailbox, + List contextMenuActions + ) { + return contextMenuActions + .map((action) => _mailboxContextMenuActionTile(context, action, mailbox)) + .toList(); + } + + Widget _mailboxContextMenuActionTile( + BuildContext context, + ContextMenuItemMailboxAction contextMenuItem, + PresentationMailbox mailbox + ) { + return (MailboxBottomSheetActionTileBuilder( + Key('${contextMenuItem.action.name}_action'), + SvgPicture.asset( + contextMenuItem.action.getContextMenuIcon(controller.imagePaths), + color: contextMenuItem.action.getColorContextMenuIcon() + ), + contextMenuItem.action.getTitleContextMenu(context), + mailbox, + absorbing: !contextMenuItem.isActivated, + opacity: !contextMenuItem.isActivated) + ..actionTextStyle(textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black)) + ..onActionClick((mailbox) => controller.handleMailboxAction(context, contextMenuItem.action, mailbox)) + ).build(); + } + + List _popupMenuMailboxActionTiles( + BuildContext context, + PresentationMailbox mailbox, + List contextMenuActions + ) { + return contextMenuActions + .map((action) => _mailboxPopupMenuActionTile(context, action, mailbox)) + .toList(); + } + + PopupMenuItem _mailboxPopupMenuActionTile( + BuildContext context, + ContextMenuItemMailboxAction contextMenuItem, + PresentationMailbox mailbox + ) { + return PopupMenuItem( + padding: EdgeInsets.zero, + child: AbsorbPointer( + absorbing: !contextMenuItem.isActivated, + child: Opacity( + opacity: contextMenuItem.isActivated ? 1.0 : 0.3, + child: popupItem(contextMenuItem.action.getContextMenuIcon(controller.imagePaths), + contextMenuItem.action.getTitleContextMenu(context), + colorIcon: contextMenuItem.action.getColorContextMenuIcon(), + styleName: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + color: contextMenuItem.action.getColorContextMenuTitle() + ), + onCallbackAction: () => controller.handleMailboxAction(context, contextMenuItem.action, mailbox)) + , + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/search/mailbox/presentation/utils/search_mailbox_utils.dart b/lib/features/search/mailbox/presentation/utils/search_mailbox_utils.dart new file mode 100644 index 000000000..433cc4332 --- /dev/null +++ b/lib/features/search/mailbox/presentation/utils/search_mailbox_utils.dart @@ -0,0 +1,16 @@ +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/material.dart'; + +class SearchMailboxUtils { + static EdgeInsets getPaddingAppBar(BuildContext context, ResponsiveUtils responsiveUtils) { + if (responsiveUtils.isWebDesktop(context)) { + return const EdgeInsets.symmetric(vertical: 8, horizontal: 16); + } else { + if (responsiveUtils.isScreenWithShortestSide(context)) { + return const EdgeInsets.symmetric(horizontal: 10); + } else { + return const EdgeInsets.symmetric(horizontal: 32); + } + } + } +} \ No newline at end of file diff --git a/lib/features/search/mailbox/presentation/widgets/mailbox_searched_item_builder.dart b/lib/features/search/mailbox/presentation/widgets/mailbox_searched_item_builder.dart new file mode 100644 index 000000000..669152cf8 --- /dev/null +++ b/lib/features/search/mailbox/presentation/widgets/mailbox_searched_item_builder.dart @@ -0,0 +1,181 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/utils/build_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/extensions/presentation_mailbox_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_method_action_define.dart'; + +class MailboxSearchedItemBuilder extends StatefulWidget { + + final PresentationMailbox _presentationMailbox; + final ImagePaths _imagePaths; + final ResponsiveUtils _responsiveUtils; + final OnClickOpenMailboxAction? onClickOpenMailboxAction; + final OnClickOpenMenuMailboxAction? onClickOpenMenuMailboxAction; + final OnDragEmailToMailboxAccepted? onDragEmailToMailboxAccepted; + + const MailboxSearchedItemBuilder( + this._imagePaths, + this._responsiveUtils, + this._presentationMailbox, + { + Key? key, + this.onClickOpenMailboxAction, + this.onClickOpenMenuMailboxAction, + this.onDragEmailToMailboxAccepted + } + ) : super(key: key); + + @override + State createState() => _MailboxSearchedItemBuilderState(); +} + +class _MailboxSearchedItemBuilderState extends State { + bool isHoverItem = false; + + @override + Widget build(BuildContext context) { + if (BuildUtils.isWeb) { + return DragTarget>( + builder: (_, __, ___) { + return _buildMailboxItem(context); + }, + onAccept: (emails) { + widget.onDragEmailToMailboxAccepted?.call(emails, widget._presentationMailbox); + } + ); + } else { + return _buildMailboxItem(context); + } + } + + Widget _buildMailboxItem(BuildContext context) { + if (BuildUtils.isWeb) { + return InkWell( + onTap: _onTapMailboxAction, + onHover: (value) => setState(() => isHoverItem = value), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: getBackgroundColorItem(context) + ), + padding: const EdgeInsets.all(8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildMailboxIcon(), + Expanded(child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTitleItem(), + _buildSubtitleItem() + ] + ) + )), + if (widget._presentationMailbox.isSubscribed?.value == true) + _buildMenuIcon(context) + ] + ), + ) + ); + } else { + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + onTap: _onTapMailboxAction, + leading: _buildMailboxIcon(), + title: _buildTitleItem(), + subtitle: _buildSubtitleItem() + ); + } + } + + void _onTapMailboxAction() { + if (widget._presentationMailbox.isSubscribed?.value == true) { + widget.onClickOpenMailboxAction?.call(widget._presentationMailbox); + } + } + + Widget _buildMailboxIcon() { + return SvgPicture.asset( + widget._presentationMailbox.isSubscribed?.value == true + ? widget._presentationMailbox.getMailboxIcon(widget._imagePaths) + : widget._imagePaths.icHideFolder, + width: 20, + height: 20, + fit: BoxFit.fill + ); + } + + Widget _buildTitleItem() { + return Text( + widget._presentationMailbox.name?.name ?? '', + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: const TextStyle( + fontSize: 15, + color: Colors.black + ), + ); + } + + Widget _buildSubtitleItem() { + if (widget._presentationMailbox.mailboxPath?.isNotEmpty == true) { + return Text( + widget._presentationMailbox.mailboxPath ?? '', + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: const TextStyle( + fontSize: 11, + color: AppColor.colorMailboxPath + ), + ); + } else { + return const SizedBox.shrink(); + } + } + + Widget _buildMenuIcon(BuildContext context) { + if (isHoverItem) { + return InkWell( + onTapDown: (detail) { + final screenSize = MediaQuery.of(context).size; + final offset = detail.globalPosition; + final position = RelativeRect.fromLTRB( + offset.dx, + offset.dy, + screenSize.width - offset.dx, + screenSize.height - offset.dy, + ); + widget.onClickOpenMenuMailboxAction?.call(position, widget._presentationMailbox); + }, + child: SvgPicture.asset( + widget._imagePaths.icComposerMenu, + width: 20, + height: 20, + fit: BoxFit.fill + ) + ); + } else { + return const SizedBox.shrink(); + } + } + + Color getBackgroundColorItem(BuildContext context) { + if (isHoverItem) { + return AppColor.colorBgMailboxSelected; + } else { + return widget._responsiveUtils.isDesktop(context) + ? AppColor.colorBgDesktop + : Colors.transparent; + } + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/widgets/search_app_bar_widget.dart b/lib/features/thread/presentation/widgets/search_app_bar_widget.dart index b5c4709d3..c07ff6c6a 100644 --- a/lib/features/thread/presentation/widgets/search_app_bar_widget.dart +++ b/lib/features/thread/presentation/widgets/search_app_bar_widget.dart @@ -145,8 +145,17 @@ class SearchAppBarWidget { } Widget _buildSearchButton() { - return buildIconWeb( - icon: SvgPicture.asset(_imagePaths.icSearchBar, width: 16, height: 16, fit: BoxFit.fill), - onTap: () => _onSearchTextAction?.call(_searchInputController?.text ?? '')); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: buildIconWeb( + minSize: 40, + iconPadding: EdgeInsets.zero, + icon: SvgPicture.asset( + _imagePaths.icSearchBar, + fit: BoxFit.fill + ), + onTap: () => _onSearchTextAction?.call(_searchInputController?.text ?? '') + ), + ); } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index d45b14989..ffb91421f 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -2779,10 +2779,17 @@ class AppLocalizations { name: 'thisImageCannotBeAdded' ); } - + String get toastMsgHideMailboxSuccess { return Intl.message( 'This mailbox has been hidden from your primary mailbox', name: 'toastMsgHideMailboxSuccess'); } + + String get searchForMailboxes { + return Intl.message( + 'Search for mailboxes', + name: 'searchForMailboxes' + ); + } } \ No newline at end of file