TF-3911 Support mail keyboard shortcut actions in email searched list view on web mobile

This commit is contained in:
dab246
2025-08-04 12:45:02 +07:00
committed by Dat H. Pham
parent d61468e73b
commit 31549ebad3
3 changed files with 194 additions and 93 deletions
@@ -0,0 +1,71 @@
import 'dart:async';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
import 'package:model/extensions/list_presentation_email_extension.dart';
import 'package:rxdart/rxdart.dart';
import 'package:tmail_ui_user/features/base/shortcut/app_shortcut_manager.dart';
import 'package:tmail_ui_user/features/base/shortcut/mail/mail_list_action_shortcut_type.dart';
import 'package:tmail_ui_user/features/search/email/presentation/model/mail_searched_list_shortcut_action_view_event.dart';
import 'package:tmail_ui_user/features/search/email/presentation/search_email_controller.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/mail_list_shortcut_action_view_event.dart';
extension HandleKeyboardShortcutActionsExtension on SearchEmailController {
void onKeyboardShortcutInit() {
if (PlatformInfo.isWeb) {
keyboardFocusNode = FocusNode();
shortcutActionEventController =
StreamController<MailListShortcutActionViewEvent>();
shortcutActionEventSubscription = shortcutActionEventController
?.stream
.debounceTime(const Duration(milliseconds: 300))
.listen(_onShortcutActionViewEvent);
}
}
void _onShortcutActionViewEvent(MailListShortcutActionViewEvent event) {
handleSelectionEmailAction(event.actionType, event.listPresentationEmail);
}
void onKeyboardShortcutDispose() {
if (PlatformInfo.isWeb) {
keyboardFocusNode?.dispose();
shortcutActionEventSubscription?.cancel();
shortcutActionEventController?.close();
}
}
void clearMailShortcutFocus() {
if (keyboardFocusNode?.hasFocus == true) {
keyboardFocusNode?.unfocus();
}
}
void refocusMailShortcutFocus() {
if (keyboardFocusNode?.hasFocus == false) {
keyboardFocusNode?.requestFocus();
}
}
void onKeyDownEventAction(KeyEvent event) {
final shortcutType = AppShortcutManager.getMailListActionFromEvent(event);
log('$runtimeType::onKeyDownEventAction:🔥Shortcut triggered: $shortcutType');
if (shortcutType == null) return;
handleMailSearchListShortcutAction(shortcutType);
}
void handleMailSearchListShortcutAction(MailListActionShortcutType shortcutType) {
log('$runtimeType::handleMailSearchListShortcutAction: 🔥 Shortcut triggered: $shortcutType');
final selectedEmails = listResultSearch.listEmailSelected;
final emailActionType = shortcutType.getEmailActionTypeBySearch(
selectedEmails: selectedEmails,
);
if (emailActionType == null) return;
shortcutActionEventController?.add(
MailSearchedListShortcutActionViewEvent(emailActionType, selectedEmails),
);
}
}
@@ -1,8 +1,9 @@
import 'dart:async';
import 'package:core/presentation/extensions/either_view_state_extension.dart'; import 'package:core/presentation/extensions/either_view_state_extension.dart';
import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/state/success.dart';
import 'package:core/presentation/utils/keyboard_utils.dart';
import 'package:core/utils/app_logger.dart'; import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart'; import 'package:core/utils/platform_info.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
@@ -59,6 +60,7 @@ import 'package:tmail_ui_user/features/push_notification/presentation/websocket/
import 'package:tmail_ui_user/features/push_notification/presentation/websocket/web_socket_queue_handler.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/websocket/web_socket_queue_handler.dart';
import 'package:tmail_ui_user/features/search/email/domain/state/refresh_changes_search_email_state.dart'; import 'package:tmail_ui_user/features/search/email/domain/state/refresh_changes_search_email_state.dart';
import 'package:tmail_ui_user/features/search/email/domain/usecases/refresh_changes_search_email_interactor.dart'; import 'package:tmail_ui_user/features/search/email/domain/usecases/refresh_changes_search_email_interactor.dart';
import 'package:tmail_ui_user/features/search/email/presentation/extension/handle_keyboard_shortcut_actions_extension.dart';
import 'package:tmail_ui_user/features/search/email/presentation/model/search_more_state.dart'; import 'package:tmail_ui_user/features/search/email/presentation/model/search_more_state.dart';
import 'package:tmail_ui_user/features/search/email/presentation/search_email_bindings.dart'; import 'package:tmail_ui_user/features/search/email/presentation/search_email_bindings.dart';
import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart';
@@ -71,6 +73,7 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/search_more_email_
import 'package:tmail_ui_user/features/thread/presentation/extensions/list_presentation_email_extensions.dart'; import 'package:tmail_ui_user/features/thread/presentation/extensions/list_presentation_email_extensions.dart';
import 'package:tmail_ui_user/features/thread/presentation/mixin/email_action_controller.dart'; import 'package:tmail_ui_user/features/thread/presentation/mixin/email_action_controller.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/mail_list_shortcut_action_view_event.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart';
import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/dialog_router.dart';
@@ -114,6 +117,9 @@ class SearchEmailController extends BaseController
late SearchMoreState searchMoreState; late SearchMoreState searchMoreState;
late bool canSearchMore; late bool canSearchMore;
WebSocketQueueHandler? _webSocketQueueHandler; WebSocketQueueHandler? _webSocketQueueHandler;
FocusNode? keyboardFocusNode;
StreamController<MailListShortcutActionViewEvent>? shortcutActionEventController;
StreamSubscription<MailListShortcutActionViewEvent>? shortcutActionEventSubscription;
PresentationMailbox? get currentMailbox => mailboxDashBoardController.selectedMailbox.value; PresentationMailbox? get currentMailbox => mailboxDashBoardController.selectedMailbox.value;
@@ -157,6 +163,7 @@ class SearchEmailController extends BaseController
_initializeTextInputFocus(); _initializeTextInputFocus();
_initWorkerListener(); _initWorkerListener();
_initWebSocketQueueHandler(); _initWebSocketQueueHandler();
onKeyboardShortcutInit();
} }
@override @override
@@ -446,7 +453,6 @@ class SearchEmailController extends BaseController
} }
void _searchEmailAction(BuildContext context) { void _searchEmailAction(BuildContext context) {
KeyboardUtils.hideKeyboard(context);
textInputSearchFocus.unfocus(); textInputSearchFocus.unfocus();
if (session == null || accountId == null) { if (session == null || accountId == null) {
@@ -513,6 +519,10 @@ class SearchEmailController extends BaseController
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
curve: Curves.fastOutSlowIn); curve: Curves.fastOutSlowIn);
} }
if (PlatformInfo.isWeb) {
refocusMailShortcutFocus();
}
} }
void _searchEmailsFailure(SearchEmailFailure failure) { void _searchEmailsFailure(SearchEmailFailure failure) {
@@ -825,6 +835,8 @@ class SearchEmailController extends BaseController
listContactSuggestionSearch.clear(); listContactSuggestionSearch.clear();
if (requestFocus) { if (requestFocus) {
textInputSearchFocus.requestFocus(); textInputSearchFocus.requestFocus();
} else {
textInputSearchFocus.unfocus();
} }
} }
@@ -843,9 +855,6 @@ class SearchEmailController extends BaseController
void closeSearchView({BuildContext? context}) { void closeSearchView({BuildContext? context}) {
clearAllTextInputSearchForm(); clearAllTextInputSearchForm();
clearAllResultSearch(); clearAllResultSearch();
if (context != null) {
KeyboardUtils.hideKeyboard(context);
}
mailboxDashBoardController.searchController.disableAllSearchEmail(); mailboxDashBoardController.searchController.disableAllSearchEmail();
mailboxDashBoardController.dispatchRoute(DashboardRoutes.thread); mailboxDashBoardController.dispatchRoute(DashboardRoutes.thread);
if (PlatformInfo.isWeb) { if (PlatformInfo.isWeb) {
@@ -927,6 +936,10 @@ class SearchEmailController extends BaseController
selectionMode.value = SelectMode.ACTIVE; selectionMode.value = SelectMode.ACTIVE;
} }
} }
if (PlatformInfo.isWeb) {
refocusMailShortcutFocus();
}
} }
void cancelSelectionMode() { void cancelSelectionMode() {
@@ -934,6 +947,9 @@ class SearchEmailController extends BaseController
.map((email) => email.toSelectedEmail(selectMode: SelectMode.INACTIVE)) .map((email) => email.toSelectedEmail(selectMode: SelectMode.INACTIVE))
.toList(); .toList();
selectionMode.value = SelectMode.INACTIVE; selectionMode.value = SelectMode.INACTIVE;
if (PlatformInfo.isWeb) {
clearMailShortcutFocus();
}
} }
void setSelectAllEmailAction() { void setSelectAllEmailAction() {
@@ -1107,6 +1123,7 @@ class SearchEmailController extends BaseController
emailUIActionWorker.dispose(); emailUIActionWorker.dispose();
dashBoardActionWorker.dispose(); dashBoardActionWorker.dispose();
_webSocketQueueHandler?.dispose(); _webSocketQueueHandler?.dispose();
onKeyboardShortcutDispose();
super.onClose(); super.onClose();
} }
} }
@@ -14,6 +14,7 @@ import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:model/model.dart'; import 'package:model/model.dart';
import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart';
import 'package:tmail_ui_user/features/base/widget/keyboard/keyboard_handler_wrapper.dart';
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart'; import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart';
import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.dart';
@@ -25,6 +26,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/qu
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/quick_search/recent_search_item_tile_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/quick_search/recent_search_item_tile_widget.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/search_filters/search_filter_button.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/search_filters/search_filter_button.dart';
import 'package:tmail_ui_user/features/search/email/presentation/extension/handle_email_more_action_extension.dart'; import 'package:tmail_ui_user/features/search/email/presentation/extension/handle_email_more_action_extension.dart';
import 'package:tmail_ui_user/features/search/email/presentation/extension/handle_keyboard_shortcut_actions_extension.dart';
import 'package:tmail_ui_user/features/search/email/presentation/extension/handle_press_email_selection_action.dart'; import 'package:tmail_ui_user/features/search/email/presentation/extension/handle_press_email_selection_action.dart';
import 'package:tmail_ui_user/features/search/email/presentation/model/context_item_receive_time_type_action.dart'; import 'package:tmail_ui_user/features/search/email/presentation/model/context_item_receive_time_type_action.dart';
import 'package:tmail_ui_user/features/search/email/presentation/model/context_item_sort_order_type_action.dart'; import 'package:tmail_ui_user/features/search/email/presentation/model/context_item_sort_order_type_action.dart';
@@ -59,101 +61,112 @@ class SearchEmailView extends GetWidget<SearchEmailController>
}); });
} }
return Scaffold( final bodyWidget = Column(children: [
body: GestureDetector( PopScope(
onTap: () => FocusScope.of(context).unfocus(), canPop: false,
onPopInvokedWithResult: (_, __) {
if (!PlatformInfo.isAndroid) return;
controller.closeSearchView(context: context);
},
child: Container( child: Container(
color: Colors.white, height: 52,
child: Column(children: [ color: Colors.white,
PopScope( padding: SearchEmailViewStyle.getAppBarPadding(
canPop: false, context,
onPopInvokedWithResult: (_, __) { controller.responsiveUtils
if (!PlatformInfo.isAndroid) return;
controller.closeSearchView(context: context);
},
child: Container(
height: 52,
color: Colors.white,
padding: SearchEmailViewStyle.getAppBarPadding(
context,
controller.responsiveUtils
),
child: Obx(() {
if (controller.selectionMode.value == SelectMode.ACTIVE) {
final mapMailboxById = controller.mailboxDashBoardController.mapMailboxById;
final selectedEmails = controller.listResultSearch.listEmailSelected;
final actionTypes = _createEmailSelectionActionTypes(
mapMailboxById,
selectedEmails,
);
return SelectionMobileAppBarThreadWidget(
imagePaths: controller.imagePaths,
responsiveUtils: controller.responsiveUtils,
selectedEmails: selectedEmails,
padding: EdgeInsets.zero,
onCancelSelectionAction: controller.cancelSelectionMode,
emailSelectionActionTypes: actionTypes,
onPressEmailSelectionActionClick: (type, emails) =>
controller.handlePressEmailSelectionAction(
context,
type,
emails,
mapMailboxById,
),
);
} else {
return _buildSearchInputForm(context);
}
})
),
), ),
const Divider(color: AppColor.colorDividerComposer, height: 1), child: Obx(() {
_buildListSearchFilterAction(context), if (controller.selectionMode.value == SelectMode.ACTIVE) {
Obx(() => SearchEmailLoadingBarWidget( final mapMailboxById = controller.mailboxDashBoardController.mapMailboxById;
suggestionViewState: controller.suggestionSearchViewState.value, final selectedEmails = controller.listResultSearch.listEmailSelected;
resultSearchViewState: controller.resultSearchViewState.value,
)), final actionTypes = _createEmailSelectionActionTypes(
Expanded(child: Obx(() { mapMailboxById,
if (controller.searchIsRunning.isFalse) { selectedEmails,
return SingleChildScrollView( );
physics: const ClampingScrollPhysics(),
child: Column(children: [ return SelectionMobileAppBarThreadWidget(
if (controller.currentSearchText.value.isNotEmpty) imagePaths: controller.imagePaths,
_buildShowAllResultSearchButton(context, controller.currentSearchText.value), responsiveUtils: controller.responsiveUtils,
if (controller.listContactSuggestionSearch.isNotEmpty selectedEmails: selectedEmails,
&& controller.currentSearchText.isNotEmpty) padding: EdgeInsets.zero,
_buildListContactSuggestionSearch(context, controller.listContactSuggestionSearch), onCancelSelectionAction: controller.cancelSelectionMode,
if (controller.listContactSuggestionSearch.isNotEmpty emailSelectionActionTypes: actionTypes,
&& controller.listSuggestionSearch.isNotEmpty) onPressEmailSelectionActionClick: (type, emails) =>
const Divider(), controller.handlePressEmailSelectionAction(
if (controller.listSuggestionSearch.isNotEmpty && controller.currentSearchText.isNotEmpty) context,
_buildListSuggestionSearch(context, controller.listSuggestionSearch) type,
else if (controller.listRecentSearch.isNotEmpty && controller.listSuggestionSearch.isEmpty) emails,
_buildListRecentSearch(context, controller.listRecentSearch) mapMailboxById,
]), ),
); );
} else { } else {
if (controller.listResultSearch.isNotEmpty) { return _buildSearchInputForm(context);
return _buildListEmailBody(
context,
controller.listResultSearch
);
} else {
return Obx(() => EmptySearchEmailWidget(
suggestionViewState: controller.suggestionSearchViewState.value,
resultSearchViewState: controller.resultSearchViewState.value,
isNetworkConnectionAvailable: controller.networkConnectionController.isNetworkConnectionAvailable(),
));
}
} }
})), })
_buildLoadingViewLoadMore(),
]),
), ),
), ),
); const Divider(color: AppColor.colorDividerComposer, height: 1),
_buildListSearchFilterAction(context),
Obx(() => SearchEmailLoadingBarWidget(
suggestionViewState: controller.suggestionSearchViewState.value,
resultSearchViewState: controller.resultSearchViewState.value,
)),
Expanded(child: Obx(() {
if (controller.searchIsRunning.isFalse) {
return SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: Column(children: [
if (controller.currentSearchText.value.isNotEmpty)
_buildShowAllResultSearchButton(context, controller.currentSearchText.value),
if (controller.listContactSuggestionSearch.isNotEmpty
&& controller.currentSearchText.isNotEmpty)
_buildListContactSuggestionSearch(context, controller.listContactSuggestionSearch),
if (controller.listContactSuggestionSearch.isNotEmpty
&& controller.listSuggestionSearch.isNotEmpty)
const Divider(),
if (controller.listSuggestionSearch.isNotEmpty && controller.currentSearchText.isNotEmpty)
_buildListSuggestionSearch(context, controller.listSuggestionSearch)
else if (controller.listRecentSearch.isNotEmpty && controller.listSuggestionSearch.isEmpty)
_buildListRecentSearch(context, controller.listRecentSearch)
]),
);
} else {
if (controller.listResultSearch.isNotEmpty) {
return _buildListEmailBody(
context,
controller.listResultSearch
);
} else {
return Obx(() => EmptySearchEmailWidget(
suggestionViewState: controller.suggestionSearchViewState.value,
resultSearchViewState: controller.resultSearchViewState.value,
isNetworkConnectionAvailable: controller.networkConnectionController.isNetworkConnectionAvailable(),
));
}
}
})),
_buildLoadingViewLoadMore(),
]);
if (PlatformInfo.isWeb && controller.keyboardFocusNode != null) {
return KeyboardHandlerWrapper(
onKeyDownEventAction: controller.onKeyDownEventAction,
focusNode: controller.keyboardFocusNode!,
child: Scaffold(
backgroundColor: Colors.white,
body: bodyWidget,
),
);
} else {
return Scaffold(
backgroundColor: Colors.white,
body: GestureDetector(
onTap: controller.textInputSearchFocus.unfocus,
child: bodyWidget,
),
);
}
} }
List<EmailSelectionActionType> _createEmailSelectionActionTypes( List<EmailSelectionActionType> _createEmailSelectionActionTypes(