TF-3911 Support mail keyboard shortcut actions in email detailed view on web
This commit is contained in:
@@ -51,4 +51,8 @@ class LoadThreadDetailAfterSelectedEmailAction extends ThreadDetailUIAction {
|
||||
|
||||
@override
|
||||
List<Object?> get props => [threadId, isSentMailbox];
|
||||
}
|
||||
}
|
||||
|
||||
class ReclaimMailViewKeyboardShortcutFocusAction extends ThreadDetailUIAction {}
|
||||
|
||||
class ClearMailViewKeyboardShortcutFocusAction extends ThreadDetailUIAction {}
|
||||
@@ -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 {
|
||||
|
||||
+9
-2
@@ -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();
|
||||
|
||||
+104
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -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]
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ extension ThreadDetailOnSelectedEmailUpdated on ThreadDetailController {
|
||||
BuildContext? context,
|
||||
) {
|
||||
if (selectedEmail?.id == null) {
|
||||
closeThreadDetailAction(context);
|
||||
closeThreadDetailAction();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import 'package:core/presentation/state/success.dart';
|
||||
import 'package:model/email/email_action_type.dart';
|
||||
import 'package:model/email/presentation_email.dart';
|
||||
|
||||
class MailViewShortcutActionViewEvent extends ViewEvent {
|
||||
final EmailActionType actionType;
|
||||
final PresentationEmail presentationEmail;
|
||||
|
||||
MailViewShortcutActionViewEvent(this.actionType, this.presentationEmail);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [actionType, presentationEmail];
|
||||
}
|
||||
@@ -50,9 +50,11 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/extension/hand
|
||||
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_get_emails_by_ids_success.dart';
|
||||
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_get_thread_by_id_failure.dart';
|
||||
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_mark_multiple_emails_read_success.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/handle_refresh_thread_detail_action.dart';
|
||||
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/initialize_thread_detail_emails.dart';
|
||||
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated.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_manager.dart';
|
||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
||||
@@ -124,6 +126,9 @@ class ThreadDetailController extends BaseController {
|
||||
CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor;
|
||||
bool loadThreadOnThreadChanged = false;
|
||||
bool isDisplayAllAttachments = false;
|
||||
FocusNode? keyboardShortcutFocusNode;
|
||||
StreamController<MailViewShortcutActionViewEvent>? shortcutActionEventController;
|
||||
StreamSubscription<MailViewShortcutActionViewEvent>? shortcutActionEventSubscription;
|
||||
|
||||
AccountId? get accountId => mailboxDashBoardController.accountId.value;
|
||||
Session? get session => mailboxDashBoardController.sessionCurrent;
|
||||
@@ -197,6 +202,10 @@ class ThreadDetailController extends BaseController {
|
||||
threadId: action.threadId,
|
||||
isSentMailbox: action.isSentMailbox,
|
||||
);
|
||||
} else if (action is ReclaimMailViewKeyboardShortcutFocusAction) {
|
||||
refocusMailShortcutFocus();
|
||||
} else if (action is ClearMailViewKeyboardShortcutFocusAction) {
|
||||
clearMailShortcutFocus();
|
||||
}
|
||||
// Reset [threadDetailUIAction] to original value
|
||||
mailboxDashBoardController.dispatchThreadDetailUIAction(
|
||||
@@ -212,6 +221,8 @@ class ThreadDetailController extends BaseController {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
onKeyboardShortcutInit();
|
||||
}
|
||||
|
||||
bool _validateLoadThread(ThreadId? threadId) {
|
||||
@@ -295,4 +306,10 @@ class ThreadDetailController extends BaseController {
|
||||
}
|
||||
super.handleFailureViewState(failure);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
onKeyboardShortcutDispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import 'package:dartz/dartz.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:model/mailbox/presentation_mailbox.dart';
|
||||
import 'package:tmail_ui_user/features/base/widget/keyboard/keyboard_handler_wrapper.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/styles/email_view_app_bar_widget_styles.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_bottom_bar_widget.dart';
|
||||
@@ -17,6 +18,7 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/extension/clos
|
||||
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart';
|
||||
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_details_email_views.dart';
|
||||
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/on_thread_detail_action_click.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/on_thread_page_changed.dart';
|
||||
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/parsing_email_opened_properties_extension.dart';
|
||||
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
|
||||
@@ -209,7 +211,7 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
|
||||
}
|
||||
|
||||
if (controller.responsiveUtils.isDesktop(context)) {
|
||||
return Container(
|
||||
bodyWidget = Container(
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
color: Colors.white,
|
||||
@@ -219,7 +221,17 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
|
||||
child: bodyWidget,
|
||||
);
|
||||
} else {
|
||||
return ColoredBox(color: Colors.white, child: bodyWidget);
|
||||
bodyWidget = ColoredBox(color: Colors.white, child: bodyWidget);
|
||||
}
|
||||
|
||||
if (PlatformInfo.isWeb && controller.keyboardShortcutFocusNode != null) {
|
||||
return KeyboardHandlerWrapper(
|
||||
onKeyDownEventAction: controller.onKeyDownEventAction,
|
||||
focusNode: controller.keyboardShortcutFocusNode!,
|
||||
child: bodyWidget,
|
||||
);
|
||||
} else {
|
||||
return bodyWidget;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class ThreadDetailAppBar extends StatelessWidget {
|
||||
final ResponsiveUtils responsiveUtils;
|
||||
final ImagePaths imagePaths;
|
||||
final bool isSearchRunning;
|
||||
final void Function(BuildContext context) closeThreadDetailAction;
|
||||
final VoidCallback closeThreadDetailAction;
|
||||
final bool threadActionReady;
|
||||
final bool threadDetailIsStarred;
|
||||
final bool isThreadDetailEnabled;
|
||||
@@ -99,7 +99,7 @@ class ThreadDetailAppBar extends StatelessWidget {
|
||||
builder: (_, constraints) {
|
||||
Widget backButton = EmailViewBackButton(
|
||||
imagePaths: imagePaths,
|
||||
onBackAction: () => closeThreadDetailAction(context),
|
||||
onBackAction: closeThreadDetailAction,
|
||||
mailboxContain: mailboxContain,
|
||||
isSearchActivated: isSearchRunning,
|
||||
maxWidth: constraints.maxWidth,
|
||||
@@ -164,7 +164,7 @@ class ThreadDetailAppBar extends StatelessWidget {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (_, __) {
|
||||
closeThreadDetailAction(context);
|
||||
closeThreadDetailAction();
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user