TF-3911 Support mail keyboard shortcut actions in email detailed view on web

This commit is contained in:
dab246
2025-07-29 02:00:32 +07:00
committed by Dat H. Pham
parent 1c7778d6cc
commit b2989521d9
36 changed files with 763 additions and 270 deletions
@@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
extension CloseThreadDetailAction on ThreadDetailController {
void closeThreadDetailAction(BuildContext? context) {
void closeThreadDetailAction() {
if (isSearchRunning) {
mailboxDashBoardController.dispatchRoute(DashboardRoutes.searchEmail);
} else {
@@ -1,12 +1,13 @@
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
import 'package:model/email/email_in_thread_status.dart';
import 'package:model/extensions/presentation_email_extension.dart';
import 'package:tmail_ui_user/features/email/presentation/email_view.dart';
import 'package:model/email/email_in_thread_status.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_mail_shortcut_actions_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/load_more_thread_detail_emails.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_load_more_segments.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_open_email_address_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_load_more_segments.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/toggle_thread_detail_collapse_expand.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart';
@@ -78,6 +79,9 @@ extension GetThreadDetailEmailViews on ThreadDetailController {
onToggleThreadDetailCollapseExpand: () {
toggleThreadDetailCollapseExpand(presentationEmail);
},
onIFrameKeyboardShortcutAction: keyboardShortcutFocusNode != null
? onIFrameKeyboardShortcutAction
: null,
scrollController: scrollController,
);
}
@@ -91,6 +95,9 @@ extension GetThreadDetailEmailViews on ThreadDetailController {
onToggleThreadDetailCollapseExpand: () {
toggleThreadDetailCollapseExpand(presentationEmail);
},
onIFrameKeyboardShortcutAction: keyboardShortcutFocusNode != null
? onIFrameKeyboardShortcutAction
: null,
scrollController: scrollController,
);
}).toList();
@@ -0,0 +1,104 @@
import 'dart:async';
import 'package:core/presentation/views/shortcut/key_shortcut.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
import 'package:model/email/presentation_email.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_view_action_shortcut_type.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/key_shortcut_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/model/mail_view_shortcut_action_view_event.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
extension HandleMailShortcutActionsExtension on ThreadDetailController {
void onKeyDownEventAction(KeyEvent event) {
final shortcutType = AppShortcutManager.getMailViewActionFromEvent(event);
log('$runtimeType::onKeyDownEventAction:🔥Shortcut triggered: $shortcutType');
if (shortcutType == null) return;
handleMailShortcutAction(shortcutType);
}
PresentationEmail? get currentExpandedEmail {
final expandedEmailId = currentExpandedEmailId.value;
if (expandedEmailId == null) return null;
final expandedPresentationEmail = emailIdsPresentation[expandedEmailId];
if (expandedPresentationEmail == null) return null;
return expandedPresentationEmail;
}
void onKeyboardShortcutInit() {
if (PlatformInfo.isWeb) {
keyboardShortcutFocusNode = FocusNode();
keyboardShortcutFocusNode?.addListener(() {
log('$runtimeType::onInit:keyboardShortcutFocusNode: ${keyboardShortcutFocusNode?.hasFocus}');
});
shortcutActionEventController =
StreamController<MailViewShortcutActionViewEvent>();
shortcutActionEventSubscription = shortcutActionEventController
?.stream
.debounceTime(const Duration(milliseconds: 300))
.listen(_onShortcutActionViewEvent);
}
}
void _onShortcutActionViewEvent(MailViewShortcutActionViewEvent event) {
mailboxDashBoardController.dispatchEmailUIAction(
TriggerMailViewKeyboardShortcutAction(
event.actionType,
event.presentationEmail,
),
);
}
void onKeyboardShortcutDispose() {
if (PlatformInfo.isWeb) {
keyboardShortcutFocusNode?.dispose();
shortcutActionEventSubscription?.cancel();
shortcutActionEventController?.close();
}
}
void handleMailShortcutAction(MailViewActionShortcutType shortcutType) {
log('$runtimeType::handleMailShortcutAction:🔥ShortcutType: $shortcutType');
final expandedEmail = currentExpandedEmail;
if (expandedEmail == null) return;
final emailActionType = shortcutType.getEmailActionType(
currentEmail: expandedEmail,
ownerEmailAddress: mailboxDashBoardController.ownEmailAddress.value,
);
log('$runtimeType::onKeyDownEventAction:🔥EmailActionType: $emailActionType');
if (emailActionType == null) return;
shortcutActionEventController?.add(
MailViewShortcutActionViewEvent(emailActionType, expandedEmail),
);
}
void clearMailShortcutFocus() {
if (keyboardShortcutFocusNode?.hasFocus == true) {
keyboardShortcutFocusNode?.unfocus();
}
}
void refocusMailShortcutFocus() {
if (keyboardShortcutFocusNode?.hasFocus == false) {
keyboardShortcutFocusNode?.requestFocus();
}
}
void onIFrameKeyboardShortcutAction(KeyShortcut keyShortcut) {
log('$runtimeType::onIFrameKeyboardShortcutAction: ${keyShortcut.toString()}');
final shortcutType = keyShortcut.mailViewActionShortcutType;
log('$runtimeType::onIFrameKeyboardShortcutAction:🔥MailViewActionShortcutType: $shortcutType');
if (shortcutType == null) return;
handleMailShortcutAction(shortcutType);
}
}
@@ -5,7 +5,6 @@ import 'package:model/email/read_actions.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/toggle_thread_detail_collapse_expand.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
extension HandleMarkMultipleEmailsReadSuccess on ThreadDetailController {
void handleMarkMultipleEmailsReadSuccess(
@@ -17,7 +16,7 @@ extension HandleMarkMultipleEmailsReadSuccess on ThreadDetailController {
if (readActions == ReadActions.markAsRead) return;
if (emailsInThreadDetailInfo.length == 1) {
closeThreadDetailAction(currentContext);
closeThreadDetailAction();
return;
}
@@ -14,7 +14,6 @@ import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
extension HandleRefreshThreadDetailAction on ThreadDetailController {
void handleRefreshThreadDetailAction(
@@ -86,7 +85,7 @@ extension HandleRefreshThreadDetailAction on ThreadDetailController {
);
if (afterRefreshedEmailIds.isEmpty) {
closeThreadDetailAction(currentContext);
closeThreadDetailAction();
return;
}
@@ -0,0 +1,22 @@
import 'package:core/presentation/views/shortcut/key_shortcut.dart';
import 'package:tmail_ui_user/features/base/shortcut/mail/mail_view_action_shortcut_type.dart';
extension KeyShortcutExtension on KeyShortcut {
MailViewActionShortcutType? get mailViewActionShortcutType {
if (matches('r')) {
return MailViewActionShortcutType.reply;
} else if (matches('r', shift: true)) {
return MailViewActionShortcutType.replyAll;
} else if (matches('f')) {
return MailViewActionShortcutType.forward;
} else if (matches('u')) {
return MailViewActionShortcutType.markAsUnread;
} else if (matches('n')) {
return MailViewActionShortcutType.newMessage;
} else if (matches('backspace') || matches('delete')) {
return MailViewActionShortcutType.delete;
}
return null;
}
}
@@ -5,7 +5,6 @@ import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_sta
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
extension MarkCollapsedEmailReadSuccess on ThreadDetailController {
void markCollapsedEmailReadSuccess(MarkAsEmailReadSuccess success) {
@@ -41,7 +40,7 @@ extension MarkCollapsedEmailReadSuccess on ThreadDetailController {
) {
if (success.readActions == ReadActions.markAsRead) return;
if (emailIdsPresentation.length == 1) {
closeThreadDetailAction(currentContext);
closeThreadDetailAction();
} else {
emailIdsPresentation[success.emailId] = emailIdsPresentation
[success.emailId]
@@ -72,7 +72,7 @@ extension OnThreadDetailActionClick on ThreadDetailController {
case EmailActionType.deletePermanently:
mailboxDashBoardController.permanentDeleteMultipleEmailInThreadDetail(
emailsInThreadDetailInfo,
onConfirm: () => closeThreadDetailAction(currentContext),
onConfirm: closeThreadDetailAction,
);
break;
case EmailActionType.moveToMailbox:
@@ -183,7 +183,7 @@ extension OnThreadDetailActionClick on ThreadDetailController {
}
void _moveToMailbox(MailboxId mailboxId, EmailActionType emailActionType) {
closeThreadDetailAction(currentContext);
closeThreadDetailAction();
mailboxDashBoardController.moveMultipleEmailInThreadDetail(
emailsInThreadDetailInfo,
destinationMailboxId: mailboxId,
@@ -20,7 +20,7 @@ extension ThreadDetailOnSelectedEmailUpdated on ThreadDetailController {
BuildContext? context,
) {
if (selectedEmail?.id == null) {
closeThreadDetailAction(context);
closeThreadDetailAction();
return;
}