diff --git a/core/lib/core.dart b/core/lib/core.dart index d9a84ed79..d05cc3f27 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -106,6 +106,7 @@ export 'presentation/views/button/multi_click_widget.dart'; export 'presentation/views/semantics/checkbox_semantics.dart'; export 'presentation/views/semantics/text_field_semantics.dart'; export 'presentation/views/semantics/icon_semantics.dart'; +export 'presentation/views/shortcut/key_shortcut.dart'; // Resources export 'presentation/resources/assets_paths.dart'; diff --git a/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart b/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart index 13db0016a..36fdc97d0 100644 --- a/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart +++ b/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart @@ -4,6 +4,7 @@ import 'dart:math' as math; import 'package:core/presentation/constants/constants_ui.dart'; import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/views/shortcut/key_shortcut.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/html/html_interaction.dart'; import 'package:core/utils/html/html_template.dart'; @@ -14,6 +15,7 @@ import 'package:universal_html/html.dart' as html; typedef OnClickHyperLinkAction = Function(Uri?); typedef OnMailtoClicked = void Function(Uri? uri); +typedef OnIFrameKeyboardShortcutAction = void Function(KeyShortcut keyShortcut); class HtmlContentViewerOnWeb extends StatefulWidget { @@ -30,6 +32,8 @@ class HtmlContentViewerOnWeb extends StatefulWidget { final OnClickHyperLinkAction? onClickHyperLinkAction; + final OnIFrameKeyboardShortcutAction? onIFrameKeyboardShortcutAction; + // if widthContent is bigger than width of htmlContent, set this to true let widget able to resize to width of htmlContent final bool allowResizeToDocumentSize; @@ -66,6 +70,7 @@ class HtmlContentViewerOnWeb extends StatefulWidget { this.htmlContentMinWidth = ConstantsUI.htmlContentMinWidth, this.offsetHtmlContentHeight = ConstantsUI.htmlContentOffsetHeight, this.viewMaxHeight, + this.onIFrameKeyboardShortcutAction, }) : super(key: key); @override @@ -116,6 +121,9 @@ class _HtmlContentViewerOnWebState extends State } else if (_isScrollingIsAvailable && _isScrollEndEventTriggered(type)) { _handleIframeOnScrollEndListener(data, widget.scrollController!); return; + } else if (_isIframeKeyboardEventTriggered(type)) { + _handleOnIFrameKeyboardEvent(data); + return; } if (data['message'] == iframeOnLoadMessage) { @@ -257,6 +265,21 @@ class _HtmlContentViewerOnWebState extends State } } + bool _isIframeKeyboardEventTriggered(String? type) { + return widget.onIFrameKeyboardShortcutAction != null && + type?.contains('toDart: iframeKeydown') == true; + } + + void _handleOnIFrameKeyboardEvent(dynamic data) { + final shortcut = KeyShortcut( + key: data['key'] as String, + code: data['code'] as String, + shift: data['shift'] == true, + ); + log('_HtmlContentViewerOnWebState::_handleOnIFrameKeyboardEvent:📥 Shortcut pressed: $shortcut'); + widget.onIFrameKeyboardShortcutAction?.call(shortcut); + } + @override void didUpdateWidget(covariant HtmlContentViewerOnWeb oldWidget) { super.didUpdateWidget(oldWidget); @@ -397,6 +420,8 @@ class _HtmlContentViewerOnWebState extends State viewId: _createdViewId, onScrollChangedEvent: onScrollChangedEvent, ), + if (widget.onIFrameKeyboardShortcutAction != null) + HtmlInteraction.scriptHandleIframeKeyboardListener(_createdViewId), ].join(); final htmlTemplate = HtmlUtils.generateHtmlDocument( diff --git a/core/lib/presentation/views/shortcut/key_shortcut.dart b/core/lib/presentation/views/shortcut/key_shortcut.dart new file mode 100644 index 000000000..c89501b2c --- /dev/null +++ b/core/lib/presentation/views/shortcut/key_shortcut.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; + +class KeyShortcut with EquatableMixin{ + final String key; + final String code; + final bool shift; + + KeyShortcut({ + required this.key, + required this.code, + this.shift = false, + }); + + bool matches(String expectedKey, {bool shift = false}) { + return key.toLowerCase() == expectedKey.toLowerCase() && + this.shift == shift; + } + + @override + List get props => [key, code, shift]; +} diff --git a/core/lib/utils/html/html_interaction.dart b/core/lib/utils/html/html_interaction.dart index b3141baf1..87a99abaa 100644 --- a/core/lib/utils/html/html_interaction.dart +++ b/core/lib/utils/html/html_interaction.dart @@ -60,7 +60,7 @@ class HtmlInteraction { '''; static const String scriptsHandleContentSizeChanged = ''' - '''; + + static String scriptHandleIframeKeyboardListener(String viewId) => ''' + + '''; } \ No newline at end of file diff --git a/lib/features/base/extensions/logical_key_set_helper.dart b/lib/features/base/extensions/logical_key_set_helper.dart new file mode 100644 index 000000000..b7395026d --- /dev/null +++ b/lib/features/base/extensions/logical_key_set_helper.dart @@ -0,0 +1,13 @@ +import 'package:flutter/services.dart'; + +extension LogicalKeySetHelper on Set { + bool isKey(LogicalKeyboardKey key) => contains(key); + + bool isOnly(LogicalKeyboardKey key) => length == 1 && contains(key); + + bool isShift() => + contains(LogicalKeyboardKey.shiftLeft) || + contains(LogicalKeyboardKey.shiftRight); + + bool isShiftPlus(LogicalKeyboardKey key) => isShift() && contains(key); +} diff --git a/lib/features/base/shortcut/app_shortcut_manager.dart b/lib/features/base/shortcut/app_shortcut_manager.dart new file mode 100644 index 000000000..059e01892 --- /dev/null +++ b/lib/features/base/shortcut/app_shortcut_manager.dart @@ -0,0 +1,27 @@ +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/services.dart'; +import 'package:tmail_ui_user/features/base/extensions/logical_key_set_helper.dart'; +import 'package:tmail_ui_user/features/base/shortcut/mail/mail_view_action_shortcut_type.dart'; + +class AppShortcutManager { + static MailViewActionShortcutType? getMailViewActionFromEvent(KeyEvent event) { + final keysPressed = HardwareKeyboard.instance.logicalKeysPressed; + log('AppShortcutManager::getMailViewActionFromEvent: Keys pressed: $keysPressed'); + if (keysPressed.isOnly(LogicalKeyboardKey.keyR)) { + return MailViewActionShortcutType.reply; + } else if (keysPressed.isShiftPlus(LogicalKeyboardKey.keyR)) { + return MailViewActionShortcutType.replyAll; + } else if (keysPressed.isOnly(LogicalKeyboardKey.keyF)) { + return MailViewActionShortcutType.forward; + } else if (keysPressed.isOnly(LogicalKeyboardKey.delete) || + keysPressed.isOnly(LogicalKeyboardKey.backspace)) { + return MailViewActionShortcutType.delete; + } else if (keysPressed.isOnly(LogicalKeyboardKey.keyN)) { + return MailViewActionShortcutType.newMessage; + } else if (keysPressed.isOnly(LogicalKeyboardKey.keyU)) { + return MailViewActionShortcutType.markAsUnread; + } else { + return null; + } + } +} diff --git a/lib/features/base/shortcut/mail/mail_view_action_shortcut_type.dart b/lib/features/base/shortcut/mail/mail_view_action_shortcut_type.dart new file mode 100644 index 000000000..08ba43cb2 --- /dev/null +++ b/lib/features/base/shortcut/mail/mail_view_action_shortcut_type.dart @@ -0,0 +1,39 @@ + +import 'package:model/email/email_action_type.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/extensions/presentation_email_extension.dart'; + +enum MailViewActionShortcutType { + reply, + replyAll, + forward, + delete, + newMessage, + markAsUnread; + + EmailActionType? getEmailActionType({ + required PresentationEmail currentEmail, + required String ownerEmailAddress, + }) { + switch(this) { + case MailViewActionShortcutType.reply: + return EmailActionType.reply; + case MailViewActionShortcutType.replyAll: + if (currentEmail.isReplyAllEnabled(ownerEmailAddress)) { + return EmailActionType.replyAll; + } else { + return null; + } + case MailViewActionShortcutType.forward: + return EmailActionType.forward; + case MailViewActionShortcutType.delete: + return currentEmail.isDeletePermanentlyEnabled + ? EmailActionType.deletePermanently + : EmailActionType.moveToTrash; + case MailViewActionShortcutType.newMessage: + return EmailActionType.compose; + case MailViewActionShortcutType.markAsUnread: + return EmailActionType.markAsUnread; + } + } +} \ No newline at end of file diff --git a/lib/features/base/widget/keyboard/keyboard_handler_wrapper.dart b/lib/features/base/widget/keyboard/keyboard_handler_wrapper.dart new file mode 100644 index 000000000..27735259f --- /dev/null +++ b/lib/features/base/widget/keyboard/keyboard_handler_wrapper.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +typedef OnHandleKeyEventAction = void Function(KeyEvent event); + +class KeyboardHandlerWrapper extends StatelessWidget { + final Widget child; + final FocusNode focusNode; + final OnHandleKeyEventAction onKeyDownEventAction; + final OnHandleKeyEventAction? onKeyUpEventAction; + + const KeyboardHandlerWrapper({ + Key? key, + required this.child, + required this.focusNode, + required this.onKeyDownEventAction, + this.onKeyUpEventAction, + }) : super(key: key); + + void _handleKeyEvent(KeyEvent event) { + if (event is KeyDownEvent) { + onKeyDownEventAction(event); + } else if (event is KeyUpEvent) { + onKeyUpEventAction?.call(event); + } + } + + @override + Widget build(BuildContext context) { + return KeyboardListener( + focusNode: focusNode, + autofocus: true, + onKeyEvent: _handleKeyEvent, + child: child, + ); + } +} diff --git a/lib/features/email/presentation/action/email_ui_action.dart b/lib/features/email/presentation/action/email_ui_action.dart index 549ae4d71..da067d3bf 100644 --- a/lib/features/email/presentation/action/email_ui_action.dart +++ b/lib/features/email/presentation/action/email_ui_action.dart @@ -105,4 +105,14 @@ class OpenAttachmentListAction extends EmailUIAction { isDisplayAllAttachments, screenHeight, ]; +} + +class TriggerMailViewKeyboardShortcutAction extends EmailUIAction { + TriggerMailViewKeyboardShortcutAction(this.actionType, this.email); + + final EmailActionType actionType; + final PresentationEmail email; + + @override + List get props => [actionType, email]; } \ No newline at end of file diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 5af9dace6..7c821189d 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -97,6 +97,7 @@ import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_ import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_attendee_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_organizer_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/handle_open_attachment_list_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/handle_mail_action_by_shortcut_action_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/update_attendance_status_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/model/blob_calendar_event.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; @@ -441,6 +442,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { isDisplayAllAttachments: action.isDisplayAllAttachments, ); mailboxDashBoardController.clearEmailUIAction(); + } else if (action is TriggerMailViewKeyboardShortcutAction) { + handleMailActionByShortcutAction( + actionType: action.actionType, + email: action.email, + ); + mailboxDashBoardController.clearEmailUIAction(); } })); @@ -1255,7 +1262,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void moveToMailbox(BuildContext context, PresentationEmail email) async { + void moveToMailbox(PresentationEmail email) async { if (session != null && accountId != null) { final moveActionRequest = await emailActionReactor.moveToMailbox( session!, @@ -1267,7 +1274,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { ), isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning, ); - if (!context.mounted || moveActionRequest == null) return; + if (moveActionRequest == null) return; mailboxDashBoardController.moveToMailbox( session!, accountId!, @@ -1275,12 +1282,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { moveActionRequest.emailIdsWithReadStatus, ); if (_threadDetailController?.emailIdsPresentation.length == 1) { - _threadDetailController?.closeThreadDetailAction(context); + _threadDetailController?.closeThreadDetailAction(); } } } - void moveToTrash(BuildContext context, PresentationEmail email) { + void moveToTrash(PresentationEmail email) { if (session != null && accountId != null) { final moveActionRequest = emailActionReactor.moveToTrash( email, @@ -1289,7 +1296,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning, mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole, ); - if (!context.mounted || moveActionRequest == null) return; + if (moveActionRequest == null) return; mailboxDashBoardController.moveToMailbox( session!, accountId!, @@ -1297,12 +1304,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { moveActionRequest.emailIdsWithReadStatus, ); if (_threadDetailController?.emailIdsPresentation.length == 1) { - _threadDetailController?.closeThreadDetailAction(context); + _threadDetailController?.closeThreadDetailAction(); } } } - void moveToSpam(BuildContext context, PresentationEmail email) { + void moveToSpam(PresentationEmail email) { if (session != null && accountId != null) { final moveActionRequest = emailActionReactor.moveToSpam( email, @@ -1311,7 +1318,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning, mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole, ); - if (!context.mounted || moveActionRequest == null) return; + if (moveActionRequest == null) return; mailboxDashBoardController.moveToMailbox( session!, accountId!, @@ -1319,12 +1326,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { moveActionRequest.emailIdsWithReadStatus, ); if (_threadDetailController?.emailIdsPresentation.length == 1) { - _threadDetailController?.closeThreadDetailAction(context); + _threadDetailController?.closeThreadDetailAction(); } } } - void unSpam(BuildContext context, PresentationEmail email) { + void unSpam(PresentationEmail email) { if (session != null && accountId != null) { final moveActionRequest = emailActionReactor.unSpam( email, @@ -1333,7 +1340,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning, mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole, ); - if (!context.mounted || moveActionRequest == null) return; + if (moveActionRequest == null) return; mailboxDashBoardController.moveToMailbox( session!, accountId!, @@ -1341,7 +1348,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { moveActionRequest.emailIdsWithReadStatus, ); if (_threadDetailController?.emailIdsPresentation.length == 1) { - _threadDetailController?.closeThreadDetailAction(context); + _threadDetailController?.closeThreadDetailAction(); } } } @@ -1370,7 +1377,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin { toastManager.showMessageSuccess(success); } - void handleEmailAction(BuildContext context, PresentationEmail presentationEmail, EmailActionType actionType) { + void handleEmailAction( + PresentationEmail presentationEmail, + EmailActionType actionType, + ) { switch(actionType) { case EmailActionType.markAsUnread: if (session != null && accountId != null) { @@ -1403,31 +1413,31 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } break; case EmailActionType.moveToMailbox: - moveToMailbox(context, presentationEmail); + moveToMailbox(presentationEmail); break; case EmailActionType.moveToTrash: - moveToTrash(context, presentationEmail); + moveToTrash(presentationEmail); break; case EmailActionType.deletePermanently: - deleteEmailPermanently(context, presentationEmail); + deleteEmailPermanently(presentationEmail); break; case EmailActionType.moveToSpam: - moveToSpam(context, presentationEmail); + moveToSpam(presentationEmail); break; case EmailActionType.unSpam: - unSpam(context, presentationEmail); + unSpam(presentationEmail); break; case EmailActionType.createRule: - quickCreatingRule(context, presentationEmail.from!.first); + quickCreatingRule(presentationEmail.from!.first); break; case EmailActionType.unsubscribe: - _unsubscribeEmail(context, presentationEmail); + _unsubscribeEmail(presentationEmail); break; case EmailActionType.archiveMessage: - archiveMessage(context, presentationEmail); + archiveMessage(presentationEmail); break; case EmailActionType.printAll: - _printEmail(context, presentationEmail); + _printEmail(presentationEmail); break; case EmailActionType.downloadMessageAsEML: _downloadMessageAsEML(presentationEmail); @@ -1441,6 +1451,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { case EmailActionType.replyAll: case EmailActionType.replyToList: case EmailActionType.forward: + case EmailActionType.compose: pressEmailAction(actionType, presentationEmail); break; default: @@ -1488,14 +1499,14 @@ class SingleEmailController extends BaseController with AppLoaderMixin { ); } - void deleteEmailPermanently(BuildContext context, PresentationEmail email) { + void deleteEmailPermanently(PresentationEmail email) { emailActionReactor.deleteEmailPermanently( email, onDeleteEmailRequest: (email) { popBack(); mailboxDashBoardController.deleteEmailPermanently(email); if (_threadDetailController?.emailIdsPresentation.length == 1) { - _threadDetailController?.closeThreadDetailAction(context); + _threadDetailController?.closeThreadDetailAction(); } }, responsiveUtils: responsiveUtils, @@ -1689,7 +1700,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void quickCreatingRule(BuildContext context, EmailAddress emailAddress) async { + void quickCreatingRule(EmailAddress emailAddress) { popBack(); if (accountId != null && session != null) { @@ -1782,7 +1793,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { threadDetailController?.isDisplayAllAttachments = false; } - void _unsubscribeEmail(BuildContext context, PresentationEmail presentationEmail) { + void _unsubscribeEmail(PresentationEmail presentationEmail) { emailActionReactor.unsubscribeEmail( presentationEmail, emailUnsubscribe: emailUnsubscribe.value, @@ -1800,16 +1811,14 @@ class SingleEmailController extends BaseController with AppLoaderMixin { ); } - void archiveMessage(BuildContext context, PresentationEmail email) { + void archiveMessage(PresentationEmail email) { emailActionReactor.archiveMessage( email, - onArchiveEmailRequest: (presentationEmail) { - mailboxDashBoardController.archiveMessage(context, presentationEmail); - }, + onArchiveEmailRequest: mailboxDashBoardController.archiveMessage, ); } - void _printEmail(BuildContext context, PresentationEmail email) { + void _printEmail(PresentationEmail email) { if (_printEmailButtonState == ButtonState.disabled) { log('SingleEmailController::_printEmail: Print email started'); return; diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 8f83fde7d..941b3fef1 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -51,6 +51,7 @@ class EmailView extends GetWidget { this.isFirstEmailInThreadDetail = false, this.threadSubject, this.onToggleThreadDetailCollapseExpand, + this.onIFrameKeyboardShortcutAction, this.scrollController, }); @@ -59,6 +60,7 @@ class EmailView extends GetWidget { final bool isFirstEmailInThreadDetail; final String? threadSubject; final VoidCallback? onToggleThreadDetailCollapseExpand; + final OnIFrameKeyboardShortcutAction? onIFrameKeyboardShortcutAction; final ScrollController? scrollController; @override @@ -83,7 +85,7 @@ class EmailView extends GetWidget { mailboxContain: _getMailboxContain(currentEmail), isSearchActivated: controller.mailboxDashBoardController.searchController.isSearchEmailRunning, onBackAction: () => controller.closeEmailView(context: context), - onEmailActionClick: (email, action) => controller.handleEmailAction(context, email, action), + onEmailActionClick: controller.handleEmailAction, onMoreActionClick: (presentationEmail, position) { return controller.emailActionReactor.handleMoreEmailAction( mailboxContain: controller.getMailboxContain(presentationEmail), @@ -92,7 +94,7 @@ class EmailView extends GetWidget { responsiveUtils: controller.responsiveUtils, imagePaths: controller.imagePaths, ownEmailAddress: controller.ownEmailAddress, - handleEmailAction: (email, action) => controller.handleEmailAction(context, email, action), + handleEmailAction: controller.handleEmailAction, additionalActions: [], emailIsRead: presentationEmail.hasRead, openBottomSheetContextMenu: controller.mailboxDashBoardController.openBottomSheetContextMenu, @@ -256,7 +258,7 @@ class EmailView extends GetWidget { maxBodyHeight: bodyConstraints.maxHeight, isInsideThreadCollapsed: false, openEmailAddressDetailAction: (_, emailAddress) => controller.openEmailAddressDialog(emailAddress), - onEmailActionClick: (presentationEmail, actionType) => controller.handleEmailAction(context, presentationEmail, actionType), + onEmailActionClick: controller.handleEmailAction, isInsideThreadDetailView: isInsideThreadDetailView, emailLoaded: controller.currentEmailLoaded.value, onMoreActionClick: (presentationEmail, position) => controller.emailActionReactor.handleMoreEmailAction( @@ -266,7 +268,7 @@ class EmailView extends GetWidget { responsiveUtils: controller.responsiveUtils, imagePaths: controller.imagePaths, ownEmailAddress: controller.ownEmailAddress, - handleEmailAction: (email, action) => controller.handleEmailAction(context, email, action), + handleEmailAction: controller.handleEmailAction, additionalActions: [ EmailActionType.reply, EmailActionType.forward, @@ -360,6 +362,7 @@ class EmailView extends GetWidget { scrollController: scrollController, enableQuoteToggle: true, fontSize: isMobileResponsive ? 16 : 14, + onIFrameKeyboardShortcutAction: onIFrameKeyboardShortcutAction, ), ); } else if (PlatformInfo.isIOS) { diff --git a/lib/features/email/presentation/extensions/handle_mail_action_by_shortcut_action_extension.dart b/lib/features/email/presentation/extensions/handle_mail_action_by_shortcut_action_extension.dart new file mode 100644 index 000000000..7f1eb209d --- /dev/null +++ b/lib/features/email/presentation/extensions/handle_mail_action_by_shortcut_action_extension.dart @@ -0,0 +1,14 @@ +import 'package:core/utils/app_logger.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; + +extension HandleMailActionByShortcutActionExtension on SingleEmailController { + void handleMailActionByShortcutAction({ + required EmailActionType actionType, + required PresentationEmail email, + }) { + log('$runtimeType::handleMailActionByShortcutAction:🔥Email action type: $actionType'); + handleEmailAction(email, actionType); + } +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart b/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart index 88695497b..657489b68 100644 --- a/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart +++ b/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart @@ -155,4 +155,6 @@ class SynchronizeEmailSortOrderAction extends DashBoardAction { @override List get props => [emailSortOrderType]; -} \ No newline at end of file +} + +class ReclaimMailListKeyboardShortcutFocusAction extends DashBoardAction {} \ No newline at end of file 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 0c3a39511..5701b18c5 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -121,6 +121,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_clear_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_create_new_rule_filter.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_preferences_setting_extension.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_reactive_obx_variable_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_save_email_as_draft_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_store_email_sort_order_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/initialize_app_language.dart'; @@ -310,6 +311,8 @@ class MailboxDashBoardController extends ReloadableController int minInputLengthAutocomplete = AppConfig.defaultMinInputLengthAutocomplete; EmailSortOrderType currentSortOrder = SearchEmailFilter.defaultSortOrder; PaywallController? paywallController; + Worker? advancedSearchVisibleWorker; + Worker? searchInputFocusWorker; final StreamController> _progressStateController = StreamController>.broadcast(); @@ -376,6 +379,7 @@ class MailboxDashBoardController extends ReloadableController if (PlatformInfo.isWeb) { listSearchFilterScrollController = ScrollController(); twakeAppManager.setExecutingBeforeReconnect(false); + registerReactiveObxVariableListener(); } if (PlatformInfo.isIOS) { _registerPendingCurrentEmailIdInNotification(); @@ -3039,18 +3043,17 @@ class MailboxDashBoardController extends ReloadableController return false; } - void archiveMessage(BuildContext context, PresentationEmail email) { + void archiveMessage(PresentationEmail email) { final mailboxContain = email.findMailboxContain(mapMailboxById); if (mailboxContain != null) { final archiveMailboxId = getMailboxIdByRole(PresentationMailbox.roleArchive); - final archiveMailboxPath = mapMailboxById[archiveMailboxId]?.getDisplayName(context); if (archiveMailboxId != null) { final moveToArchiveMailboxRequest = MoveToMailboxRequest( {mailboxContain.id: [email.id!]}, archiveMailboxId, MoveAction.moving, EmailActionType.moveToMailbox, - destinationPath: archiveMailboxPath + destinationPath: getMailboxNameById(archiveMailboxId), ); moveToMailbox( sessionCurrent!, @@ -3062,6 +3065,15 @@ class MailboxDashBoardController extends ReloadableController } } + String getMailboxNameById(MailboxId mailboxId) { + final mailbox = mapMailboxById[mailboxId]; + if (currentContext != null) { + return mailbox?.getDisplayName(currentContext!) ?? ''; + } else { + return mailbox?.name?.name ?? ''; + } + } + void _handleRestoreDeletedMessageSuccess(EmailRecoveryActionId emailRecoveryActionId) async { log('MailboxDashBoardController::_handleRestoreDeletedMessageSuccess():emailRecoveryActionId: $emailRecoveryActionId'); _getRestoredDeletedMessage(emailRecoveryActionId); @@ -3271,10 +3283,14 @@ class MailboxDashBoardController extends ReloadableController } } + bool get isEmailOpened => + dashboardRoute.value == DashboardRoutes.threadDetailed; + @override void onClose() { if (PlatformInfo.isWeb) { listSearchFilterScrollController?.dispose(); + disposeReactiveObxVariableListener(); } if (PlatformInfo.isIOS) { _iosNotificationManager?.dispose(); diff --git a/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart index dab5a15eb..07be9e129 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart @@ -47,6 +47,7 @@ class SearchController extends BaseController with DateRangePickerMixin { final listFilterOnSuggestionForm = RxList(); final simpleSearchIsActivated = RxBool(false); final advancedSearchIsActivated = RxBool(false); + final isSearchInputFocused = RxBool(false); SearchQuery? get searchQuery => searchEmailFilter.value.text; @@ -59,6 +60,17 @@ class SearchController extends BaseController with DateRangePickerMixin { this._getAllRecentSearchLatestInteractor, ); + @override + void onInit() { + super.onInit(); + searchFocus.addListener(_onSearchFocusChanged); + } + + void _onSearchFocusChanged() { + log('SearchController::_onSearchFocusChanged: ${searchFocus.hasFocus}'); + isSearchInputFocused.value = searchFocus.hasFocus; + } + void openAdvanceSearch() { isAdvancedSearchViewOpen.value = true; } @@ -322,6 +334,7 @@ class SearchController extends BaseController with DateRangePickerMixin { @override void onClose() { searchInputController.dispose(); + searchFocus.removeListener(_onSearchFocusChanged); searchFocus.dispose(); super.onClose(); } diff --git a/lib/features/mailbox_dashboard/presentation/extensions/handle_reactive_obx_variable_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/handle_reactive_obx_variable_extension.dart new file mode 100644 index 000000000..407aa85b0 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/extensions/handle_reactive_obx_variable_extension.dart @@ -0,0 +1,49 @@ + +import 'package:core/utils/app_logger.dart'; +import 'package:get/get_rx/src/rx_workers/rx_workers.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/action/thread_detail_ui_action.dart'; + +extension HandleReactiveObxVariableExtension on MailboxDashBoardController { + + void registerReactiveObxVariableListener() { + advancedSearchVisibleWorker = ever( + searchController.isAdvancedSearchViewOpen, + _onAdvancedSearchVisibleChanged + ); + + searchInputFocusWorker = ever( + searchController.isSearchInputFocused, + _onSearchInputFocusChanged + ); + } + + void _onAdvancedSearchVisibleChanged(bool visible) { + log('$runtimeType::_onAdvancedSearchVisibleChanged: visible is $visible | isEmailOpened = $isEmailOpened'); + if (isEmailOpened) { + _performThreadDetailUIActionWhenEmailOpened(visible); + } + } + + void _onSearchInputFocusChanged(bool focused) { + log('$runtimeType::_onSearchInputFocusChanged: focused is $focused | isEmailOpened = $isEmailOpened'); + if (isEmailOpened) { + _performThreadDetailUIActionWhenEmailOpened(focused); + } + } + + void _performThreadDetailUIActionWhenEmailOpened(bool focused) { + final emailAction = focused + ? ClearMailViewKeyboardShortcutFocusAction() + : ReclaimMailViewKeyboardShortcutFocusAction(); + + dispatchThreadDetailUIAction(emailAction); + } + + void disposeReactiveObxVariableListener() { + advancedSearchVisibleWorker?.dispose(); + advancedSearchVisibleWorker = null; + searchInputFocusWorker?.dispose(); + searchInputFocusWorker = null; + } +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/extensions/open_and_close_composer_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/open_and_close_composer_extension.dart index 0bf967b5a..60f55dbaa 100644 --- a/lib/features/mailbox_dashboard/presentation/extensions/open_and_close_composer_extension.dart +++ b/lib/features/mailbox_dashboard/presentation/extensions/open_and_close_composer_extension.dart @@ -11,8 +11,10 @@ import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart' import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/composer_arguments_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/action/thread_detail_ui_action.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; @@ -111,6 +113,9 @@ extension OpenAndCloseComposerExtension on MailboxDashBoardController { twakeAppManager.setHasComposer(false); } + dispatchThreadDetailUIAction(ReclaimMailViewKeyboardShortcutFocusAction()); + dispatchAction(ReclaimMailListKeyboardShortcutFocusAction()); + _handleResultAfterCloseComposer(result); if (composerId != null) { 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 47ebbf151..77a2fb662 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -68,137 +68,141 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { ResponsiveWidget( responsiveUtils: controller.responsiveUtils, desktop: Scaffold( - body: GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), - child: Container( - color: AppColor.colorBgDesktop, - child: Column(children: [ - FutureBuilder( - future: CozyConfigManager().isInsideCozy, - builder: (context, snapshot) { - if (snapshot.data == true) { - return const SizedBox.shrink(); + backgroundColor: AppColor.colorBgDesktop, + body: Column( + children: [ + FutureBuilder( + future: CozyConfigManager().isInsideCozy, + builder: (context, snapshot) { + if (snapshot.data == true) { + return const SizedBox.shrink(); + } + + return Obx(() { + final accountId = controller.accountId.value; + String accountDisplayName = controller.ownEmailAddress.value; + final contactSupportCapability = accountId != null + ? controller.sessionCurrent?.getContactSupportCapability(accountId) + : null; + if (accountDisplayName.trim().isEmpty) { + accountDisplayName = controller + .sessionCurrent + ?.getOwnEmailAddressOrUsername() ?? ''; } - return Obx(() { - final accountId = controller.accountId.value; - String accountDisplayName = controller.ownEmailAddress.value; - final contactSupportCapability = accountId != null - ? controller.sessionCurrent?.getContactSupportCapability(accountId) - : null; - if (accountDisplayName.trim().isEmpty) { - accountDisplayName = controller - .sessionCurrent - ?.getOwnEmailAddressOrUsername() ?? ''; - } - - return NavigationBarWidget( - imagePaths: controller.imagePaths, - accountId: accountId, - ownEmailAddress: accountDisplayName, - contactSupportCapability: contactSupportCapability, - searchForm: SearchInputFormWidget(), - appGridController: - controller.appGridDashboardController, - settingActionTypes: ProfileSettingActionType.values, - onTapApplicationLogoAction: - controller.redirectToInboxAction, - onTapContactSupportAction: (contactSupport) => - controller.onGetHelpOrReportBug(contactSupport), - onProfileSettingActionTypeClick: (actionType) => - controller.handleProfileSettingActionTypeClick( - context: context, - actionType: actionType, - ), - ); - }); - }, - ), - Expanded(child: Row(children: [ - Column(children: [ - ComposeButtonWidget( + return NavigationBarWidget( imagePaths: controller.imagePaths, - onTapAction: () => - controller.openComposer(ComposerArguments()), + accountId: accountId, + ownEmailAddress: accountDisplayName, + contactSupportCapability: contactSupportCapability, + searchForm: SearchInputFormWidget(), + appGridController: + controller.appGridDashboardController, + settingActionTypes: ProfileSettingActionType.values, + onTapApplicationLogoAction: + controller.redirectToInboxAction, + onTapContactSupportAction: (contactSupport) => + controller.onGetHelpOrReportBug(contactSupport), + onProfileSettingActionTypeClick: (actionType) => + controller.handleProfileSettingActionTypeClick( + context: context, + actionType: actionType, + ), + ); + }); + }, + ), + Expanded( + child: Row( + children: [ + Column( + children: [ + ComposeButtonWidget( + imagePaths: controller.imagePaths, + onTapAction: () => + controller.openComposer(ComposerArguments()), + ), + Expanded(child: SizedBox( + width: ResponsiveUtils.defaultSizeMenu, + child: Obx(() { + if (controller.searchMailboxActivated.isTrue) { + return const SearchMailboxView( + backgroundColor: AppColor.colorBgDesktop + ); + } else { + return MailboxView(); + } + }) + )) + ], ), - Expanded(child: SizedBox( - width: ResponsiveUtils.defaultSizeMenu, - child: Obx(() { - if (controller.searchMailboxActivated.isTrue) { - return const SearchMailboxView( - backgroundColor: AppColor.colorBgDesktop - ); - } else { - return MailboxView(); - } - }) - )) - ]), - Expanded(child: Column(children: [ - const SizedBox(height: 16), - FutureBuilder( - future: CozyConfigManager().isInsideCozy, - builder: (context, snapshot) { - if (snapshot.data != true) { - return const SizedBox.shrink(); - } + Expanded( + child: Column( + children: [ + const SizedBox(height: 16), + FutureBuilder( + future: CozyConfigManager().isInsideCozy, + builder: (context, snapshot) { + if (snapshot.data != true) { + return const SizedBox.shrink(); + } - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Row( - children: [ - SizedBox( - width: MediaQuery.sizeOf(context).width * 0.4, - height: 44, - child: SearchInputFormWidget( - fontSize: 15, - contentPadding: const EdgeInsets.only(bottom: 4), - ), - ), - const Spacer(), - Obx(() { - String accountDisplayName = - controller.ownEmailAddress.value; - if (accountDisplayName.trim().isEmpty) { - accountDisplayName = controller - .sessionCurrent - ?.getOwnEmailAddressOrUsername() ?? ''; - } - return ProfileSettingIcon( - ownEmailAddress: accountDisplayName, - settingActionTypes: ProfileSettingActionType.values, - onProfileSettingActionTypeClick: (actionType) => - controller.handleProfileSettingActionTypeClick( - context: context, - actionType: actionType, + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + children: [ + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.4, + height: 44, + child: SearchInputFormWidget( + fontSize: 15, + contentPadding: const EdgeInsets.only(bottom: 4), + ), ), - isInsideCozy: true, - ); - }), - ], + const Spacer(), + Obx(() { + String accountDisplayName = + controller.ownEmailAddress.value; + if (accountDisplayName.trim().isEmpty) { + accountDisplayName = controller + .sessionCurrent + ?.getOwnEmailAddressOrUsername() ?? ''; + } + return ProfileSettingIcon( + ownEmailAddress: accountDisplayName, + settingActionTypes: ProfileSettingActionType.values, + onProfileSettingActionTypeClick: (actionType) => + controller.handleProfileSettingActionTypeClick( + context: context, + actionType: actionType, + ), + isInsideCozy: true, + ); + }), + ], + ), + ); + } ), - ); - } - ), - Obx(() => RecoverDeletedMessageLoadingBannerWidget( - isLoading: controller.isRecoveringDeletedMessage.value, - horizontalLoadingWidget: horizontalLoadingWidget, - responsiveUtils: controller.responsiveUtils, - )), - Obx(() => MarkMailboxAsReadLoadingBanner( - viewState: controller.viewStateMailboxActionProgress.value, - )), - Obx(() { - final spamController = controller.spamReportController; + Obx(() => RecoverDeletedMessageLoadingBannerWidget( + isLoading: controller.isRecoveringDeletedMessage.value, + horizontalLoadingWidget: horizontalLoadingWidget, + responsiveUtils: controller.responsiveUtils, + )), + Obx(() => MarkMailboxAsReadLoadingBanner( + viewState: controller.viewStateMailboxActionProgress.value, + )), + Obx(() { + final spamController = controller.spamReportController; - final isSpamReportDisabled = spamController.spamReportState.value == SpamReportState.disabled; + final isSpamReportDisabled = spamController.spamReportState.value == SpamReportState.disabled; - final isSpamFolderSelected = controller - .selectedMailbox - .value - ?.isSpam == true; + final isSpamFolderSelected = controller + .selectedMailbox + .value + ?.isSpam == true; - final isPresentationSpamMailboxIsNull = spamController.presentationSpamMailbox.value == null; + final isPresentationSpamMailboxIsNull = spamController.presentationSpamMailbox.value == null; final isEmailOpened = controller.dashboardRoute.value == DashboardRoutes.threadDetailed; @@ -209,91 +213,96 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { return const SizedBox.shrink(); } - return ReportMessageBanner( - imagePaths: controller.imagePaths, - message: AppLocalizations - .of(context) - .countMessageInSpam( - spamController.numberOfUnreadSpamEmails, - ), - positiveName: AppLocalizations.of(context).view, - isDesktop: controller - .responsiveUtils - .isDesktop(context), - margin: SpamReportBannerWebStyles.bannerMargin, - onPositiveAction: spamController.openMailbox, - onNegativeAction: () => - spamController.dismissSpamReportAction(context), - ); - }), - QuotasBannerWidget(), - _buildVacationNotificationMessage(context), - Obx(() { - final selectedMailbox = controller - .selectedMailbox - .value; + return ReportMessageBanner( + imagePaths: controller.imagePaths, + message: AppLocalizations + .of(context) + .countMessageInSpam( + spamController.numberOfUnreadSpamEmails, + ), + positiveName: AppLocalizations.of(context).view, + isDesktop: controller + .responsiveUtils + .isDesktop(context), + margin: SpamReportBannerWebStyles.bannerMargin, + onPositiveAction: spamController.openMailbox, + onNegativeAction: () => + spamController.dismissSpamReportAction(context), + ); + }), + QuotasBannerWidget(), + _buildVacationNotificationMessage(context), + Obx(() { + final selectedMailbox = controller + .selectedMailbox + .value; - bool showTrashBanner = controller - .isEmptyTrashBannerEnabledOnWeb( - context, - selectedMailbox, - ); - bool showSpamBanner = controller - .isEmptySpamBannerEnabledOnWeb( - context, - selectedMailbox, - ); + bool showTrashBanner = controller + .isEmptyTrashBannerEnabledOnWeb( + context, + selectedMailbox, + ); + bool showSpamBanner = controller + .isEmptySpamBannerEnabledOnWeb( + context, + selectedMailbox, + ); - if (showTrashBanner) { - return CleanMessagesBanner( - responsiveUtils: controller.responsiveUtils, - message: AppLocalizations - .of(context) - .message_delete_all_email_in_trash_button, - positiveAction: AppLocalizations - .of(context) - .empty_trash_now, - onPositiveAction: controller.emptyTrashAction, - margin: const EdgeInsetsDirectional.only( - bottom: 8, - end: 16, - ), - ); - } else if (showSpamBanner) { - return CleanMessagesBanner( - responsiveUtils: controller.responsiveUtils, - message: AppLocalizations - .of(context) - .bannerDeleteAllSpamEmailsMessage, - positiveAction: AppLocalizations - .of(context) - .deleteAllSpamEmailsNow, - onPositiveAction: () => - controller.openDialogEmptySpamFolder(context), - margin: const EdgeInsetsDirectional.only( - bottom: 8, - end: 16, - ), - ); - } else { - return const SizedBox.shrink(); - } - }), - _buildListButtonQuickSearchFilter(context), - Expanded(child: Obx(() { - switch(controller.dashboardRoute.value) { - case DashboardRoutes.thread: - return _buildThreadViewForWebDesktop(context); - case DashboardRoutes.threadDetailed: - return const ThreadDetailView(); - default: - return const SizedBox.shrink(); - } - })) - ])) - ])) - ]), - ), + if (showTrashBanner) { + return CleanMessagesBanner( + responsiveUtils: controller.responsiveUtils, + message: AppLocalizations + .of(context) + .message_delete_all_email_in_trash_button, + positiveAction: AppLocalizations + .of(context) + .empty_trash_now, + onPositiveAction: controller.emptyTrashAction, + margin: const EdgeInsetsDirectional.only( + bottom: 8, + end: 16, + ), + ); + } else if (showSpamBanner) { + return CleanMessagesBanner( + responsiveUtils: controller.responsiveUtils, + message: AppLocalizations + .of(context) + .bannerDeleteAllSpamEmailsMessage, + positiveAction: AppLocalizations + .of(context) + .deleteAllSpamEmailsNow, + onPositiveAction: () => + controller.openDialogEmptySpamFolder(context), + margin: const EdgeInsetsDirectional.only( + bottom: 8, + end: 16, + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + _buildListButtonQuickSearchFilter(context), + Expanded( + child: Obx(() { + switch(controller.dashboardRoute.value) { + case DashboardRoutes.thread: + return _buildThreadViewForWebDesktop(context); + case DashboardRoutes.threadDetailed: + return const ThreadDetailView(); + default: + return const SizedBox.shrink(); + } + }), + ) + ], + ), + ) + ], + ), + ) + ], ), ), tabletLarge: Obx(() { diff --git a/lib/features/thread/presentation/mixin/email_action_controller.dart b/lib/features/thread/presentation/mixin/email_action_controller.dart index 20b6119cd..74ec894f8 100644 --- a/lib/features/thread/presentation/mixin/email_action_controller.dart +++ b/lib/features/thread/presentation/mixin/email_action_controller.dart @@ -341,8 +341,8 @@ mixin EmailActionController { AppUtils.launchLink(email.routeWebAsString); } - void archiveMessage(BuildContext context, PresentationEmail email) { - mailboxDashBoardController.archiveMessage(context, email); + void archiveMessage(PresentationEmail email) { + mailboxDashBoardController.archiveMessage(email); } bool hasArchiveMailbox() { diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 8b3b03c09..6798ca1eb 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -1198,9 +1198,7 @@ class ThreadController extends BaseController with EmailActionController { openEmailInNewTabAction(selectedEmail); break; case EmailActionType.archiveMessage: - if (currentContext != null) { - archiveMessage(currentContext!, selectedEmail); - } + archiveMessage(selectedEmail); break; case EmailActionType.editAsNewEmail: editAsNewEmail(selectedEmail); @@ -1376,7 +1374,7 @@ class ThreadController extends BaseController with EmailActionController { ReadActions readActions = !email.hasRead ? ReadActions.markAsRead : ReadActions.markAsUnread; markAsEmailRead(email, readActions, MarkReadAction.swipeOnThread); } else if (direction == DismissDirection.endToStart) { - archiveMessage(context, email); + archiveMessage(email); } return false; } diff --git a/lib/features/thread_detail/presentation/action/thread_detail_ui_action.dart b/lib/features/thread_detail/presentation/action/thread_detail_ui_action.dart index 0f7752c7f..a6df6e89d 100644 --- a/lib/features/thread_detail/presentation/action/thread_detail_ui_action.dart +++ b/lib/features/thread_detail/presentation/action/thread_detail_ui_action.dart @@ -51,4 +51,8 @@ class LoadThreadDetailAfterSelectedEmailAction extends ThreadDetailUIAction { @override List get props => [threadId, isSentMailbox]; -} \ No newline at end of file +} + +class ReclaimMailViewKeyboardShortcutFocusAction extends ThreadDetailUIAction {} + +class ClearMailViewKeyboardShortcutFocusAction extends ThreadDetailUIAction {} \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/close_thread_detail_action.dart b/lib/features/thread_detail/presentation/extension/close_thread_detail_action.dart index 3c7e0ca4f..11d58f48d 100644 --- a/lib/features/thread_detail/presentation/extension/close_thread_detail_action.dart +++ b/lib/features/thread_detail/presentation/extension/close_thread_detail_action.dart @@ -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 { diff --git a/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart b/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart index ed3630cbe..416c58029 100644 --- a/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart +++ b/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart @@ -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(); diff --git a/lib/features/thread_detail/presentation/extension/handle_mail_shortcut_actions_extension.dart b/lib/features/thread_detail/presentation/extension/handle_mail_shortcut_actions_extension.dart new file mode 100644 index 000000000..236880ad6 --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/handle_mail_shortcut_actions_extension.dart @@ -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(); + 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); + } +} \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/handle_mark_multiple_emails_read_success.dart b/lib/features/thread_detail/presentation/extension/handle_mark_multiple_emails_read_success.dart index b1862a14e..681575d85 100644 --- a/lib/features/thread_detail/presentation/extension/handle_mark_multiple_emails_read_success.dart +++ b/lib/features/thread_detail/presentation/extension/handle_mark_multiple_emails_read_success.dart @@ -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; } diff --git a/lib/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart b/lib/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart index c7f681e43..454e0a5a8 100644 --- a/lib/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart +++ b/lib/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart @@ -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; } diff --git a/lib/features/thread_detail/presentation/extension/key_shortcut_extension.dart b/lib/features/thread_detail/presentation/extension/key_shortcut_extension.dart new file mode 100644 index 000000000..6e072c510 --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/key_shortcut_extension.dart @@ -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; + } +} \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/mark_collapsed_email_unread_success.dart b/lib/features/thread_detail/presentation/extension/mark_collapsed_email_unread_success.dart index aeae43b09..1531f5eb4 100644 --- a/lib/features/thread_detail/presentation/extension/mark_collapsed_email_unread_success.dart +++ b/lib/features/thread_detail/presentation/extension/mark_collapsed_email_unread_success.dart @@ -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] diff --git a/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart b/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart index 96fa5ed2d..baf84e621 100644 --- a/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart +++ b/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart @@ -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, diff --git a/lib/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated.dart b/lib/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated.dart index 8a9d3f730..adf34d843 100644 --- a/lib/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated.dart +++ b/lib/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated.dart @@ -20,7 +20,7 @@ extension ThreadDetailOnSelectedEmailUpdated on ThreadDetailController { BuildContext? context, ) { if (selectedEmail?.id == null) { - closeThreadDetailAction(context); + closeThreadDetailAction(); return; } diff --git a/lib/features/thread_detail/presentation/model/mail_view_shortcut_action_view_event.dart b/lib/features/thread_detail/presentation/model/mail_view_shortcut_action_view_event.dart new file mode 100644 index 000000000..9cb8eb922 --- /dev/null +++ b/lib/features/thread_detail/presentation/model/mail_view_shortcut_action_view_event.dart @@ -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 get props => [actionType, presentationEmail]; +} \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/thread_detail_controller.dart b/lib/features/thread_detail/presentation/thread_detail_controller.dart index f55a507cd..1c8f6642d 100644 --- a/lib/features/thread_detail/presentation/thread_detail_controller.dart +++ b/lib/features/thread_detail/presentation/thread_detail_controller.dart @@ -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? shortcutActionEventController; + StreamSubscription? 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(); + } } \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/thread_detail_view.dart b/lib/features/thread_detail/presentation/thread_detail_view.dart index 0b5e0479e..8c0c93d84 100644 --- a/lib/features/thread_detail/presentation/thread_detail_view.dart +++ b/lib/features/thread_detail/presentation/thread_detail_view.dart @@ -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 { } 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 { 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; } } diff --git a/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart b/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart index e6c341022..a24f48fe5 100644 --- a/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart +++ b/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart @@ -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, ); diff --git a/model/lib/extensions/list_presentation_email_extension.dart b/model/lib/extensions/list_presentation_email_extension.dart index 5eb8726c0..da712ea7a 100644 --- a/model/lib/extensions/list_presentation_email_extension.dart +++ b/model/lib/extensions/list_presentation_email_extension.dart @@ -7,6 +7,8 @@ import 'package:model/model.dart'; extension ListPresentationEmailExtension on List { bool get isAllEmailRead => every((email) => email.hasRead); + bool get isAllEmailUnread => every((email) => !email.hasRead); + bool get isAllEmailStarred => every((email) => email.hasStarred); bool get isAllSelectionInActive { diff --git a/model/lib/extensions/presentation_email_extension.dart b/model/lib/extensions/presentation_email_extension.dart index a760800d2..404bc245b 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -15,6 +15,7 @@ import 'package:model/email/eml_attachment.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:model/extensions/list_email_address_extension.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/extensions/utc_date_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:model/mailbox/select_mode.dart'; @@ -207,4 +208,13 @@ extension PresentationEmailExtension on PresentationEmail { keywords?.addAll(newKeywords); keywords?.removeWhere((key, value) => !value); } + + bool get isDeletePermanentlyEnabled { + return mailboxContain?.isTrash ?? mailboxContain?.isSpam ?? false; + } + + bool isReplyAllEnabled(String ownerEmailAddress) { + final countMailAddress = getCountMailAddressWithoutMe(ownerEmailAddress); + return countMailAddress > 1; + } } \ No newline at end of file